feat: add AnyTLS protocol support across all layers

Implement end-to-end AnyTLS protocol support including domain value
objects, repository persistence, use case handling, subscription
formatting (Base64/Clash/Surge), and HTTP handler bindings. Add
migration script for the anytls_configs table and wire protocol-specific
config loading in parallel with existing protocols.

Also add PlanType filter to plan repository list query.
This commit is contained in:
orris-inc
2026-02-11 18:30:46 +08:00
parent cacc3a265b
commit e33acff06a
25 changed files with 1282 additions and 44 deletions

View File

@@ -19,7 +19,7 @@ import (
// Compatible with sing-box inbound configuration
type NodeConfigResponse struct {
NodeSID string `json:"node_id" binding:"required"` // Node SID (Stripe-style: node_xxx)
Protocol string `json:"protocol" binding:"required,oneof=shadowsocks trojan vless vmess hysteria2 tuic"` // Protocol type
Protocol string `json:"protocol" binding:"required,oneof=shadowsocks trojan vless vmess hysteria2 tuic anytls"` // Protocol type
ServerHost string `json:"server_host" binding:"required"` // Server hostname or IP address
ServerPort int `json:"server_port" binding:"required,min=1,max=65535"` // Server port number
EncryptionMethod string `json:"encryption_method,omitempty"` // Encryption method for Shadowsocks
@@ -65,6 +65,12 @@ type NodeConfigResponse struct {
TUICUDPRelayMode string `json:"tuic_udp_relay_mode,omitempty"` // UDP relay mode (native, quic)
TUICAlpn string `json:"tuic_alpn,omitempty"` // ALPN protocols
TUICDisableSNI bool `json:"tuic_disable_sni,omitempty"` // Disable SNI
// AnyTLS specific fields
AnyTLSFingerprint string `json:"anytls_fingerprint,omitempty"` // TLS fingerprint for AnyTLS
AnyTLSIdleSessionCheckInterval string `json:"anytls_idle_session_check_interval,omitempty"` // Idle session check interval
AnyTLSIdleSessionTimeout string `json:"anytls_idle_session_timeout,omitempty"` // Idle session timeout
AnyTLSMinIdleSession int `json:"anytls_min_idle_session,omitempty"` // Minimum idle sessions
}
// RouteConfigDTO represents the routing configuration for sing-box
@@ -144,6 +150,12 @@ type OutboundDTO struct {
// TUIC specific fields
TUICCongestionControl string `json:"congestion_control,omitempty"` // Congestion control algorithm
TUICUDPRelayMode string `json:"udp_relay_mode,omitempty"` // UDP relay mode
// AnyTLS specific fields
AnyTLSFingerprint string `json:"anytls_fingerprint,omitempty"` // TLS fingerprint
AnyTLSIdleSessionCheckInterval string `json:"anytls_idle_session_check_interval,omitempty"` // Idle session check interval
AnyTLSIdleSessionTimeout string `json:"anytls_idle_session_timeout,omitempty"` // Idle session timeout
AnyTLSMinIdleSession int `json:"anytls_min_idle_session,omitempty"` // Minimum idle sessions
}
// OutboundTLSDTO represents TLS configuration for outbound.
@@ -382,6 +394,25 @@ func ToNodeConfigResponse(n *node.Node, referencedNodes []*node.Node, serverKeyF
// TUIC uses QUIC transport implicitly
config.TransportProtocol = "quic"
}
case n.Protocol().IsAnyTLS():
config.Protocol = "anytls"
// Extract AnyTLS-specific configuration
if n.AnyTLSConfig() != nil {
ac := n.AnyTLSConfig()
config.SNI = ac.SNI()
config.AllowInsecure = ac.AllowInsecure()
// AnyTLS specific fields
config.AnyTLSFingerprint = ac.Fingerprint()
config.AnyTLSIdleSessionCheckInterval = ac.IdleSessionCheckInterval()
config.AnyTLSIdleSessionTimeout = ac.IdleSessionTimeout()
config.AnyTLSMinIdleSession = ac.MinIdleSession()
// AnyTLS uses TLS transport
config.TransportProtocol = "tcp"
}
}
// Convert route configuration if present
@@ -664,6 +695,27 @@ func ToOutboundDTO(n *node.Node, serverKey string) *OutboundDTO {
dto.TLS.ALPN = []string{tc.ALPN()}
}
}
case n.Protocol().IsAnyTLS():
dto.Type = "anytls"
dto.Password = serverKey // For AnyTLS, serverKey is the password
if n.AnyTLSConfig() != nil {
ac := n.AnyTLSConfig()
// TLS configuration (AnyTLS always uses TLS)
dto.TLS = &OutboundTLSDTO{
Enabled: true,
ServerName: ac.SNI(),
Insecure: ac.AllowInsecure(),
}
// AnyTLS specific fields
dto.AnyTLSFingerprint = ac.Fingerprint()
dto.AnyTLSIdleSessionCheckInterval = ac.IdleSessionCheckInterval()
dto.AnyTLSIdleSessionTimeout = ac.IdleSessionTimeout()
dto.AnyTLSMinIdleSession = ac.MinIdleSession()
}
}
return dto

View File

@@ -14,7 +14,7 @@ type NodeDTO struct {
ServerAddress string `json:"server_address" example:"proxy.example.com" description:"Server hostname or IP address"`
AgentPort uint16 `json:"agent_port" example:"8388" description:"Port for agent connections"`
SubscriptionPort *uint16 `json:"subscription_port,omitempty" example:"8389" description:"Port for client subscriptions (if null, uses agent_port)"`
Protocol string `json:"protocol" example:"shadowsocks" enums:"shadowsocks,trojan" description:"Proxy protocol type"`
Protocol string `json:"protocol" example:"shadowsocks" enums:"shadowsocks,trojan,vless,vmess,hysteria2,tuic,anytls" description:"Proxy protocol type"`
EncryptionMethod string `json:"encryption_method" example:"aes-256-gcm" enums:"aes-256-gcm,aes-128-gcm,chacha20-ietf-poly1305" description:"Encryption method for the proxy connection"`
Plugin string `json:"plugin,omitempty" example:"obfs-local" description:"Optional plugin name"`
PluginOpts map[string]string `json:"plugin_opts,omitempty" example:"obfs:http,obfs-host:example.com" description:"Plugin configuration options"`
@@ -71,9 +71,18 @@ type NodeDTO struct {
TUICUDPRelayMode string `json:"tuic_udp_relay_mode,omitempty" example:"native" enums:"native,quic" description:"TUIC UDP relay mode"`
TUICAlpn string `json:"tuic_alpn,omitempty" description:"TUIC ALPN protocols"`
TUICSni string `json:"tuic_sni,omitempty" description:"TUIC TLS SNI"`
TUICAllowInsecure bool `json:"tuic_allow_insecure,omitempty" description:"TUIC allow insecure TLS"`
TUICDisableSNI bool `json:"tuic_disable_sni,omitempty" description:"TUIC disable SNI"`
IsOnline bool `json:"is_online" example:"true" description:"Indicates if the node agent is online (reported within 5 minutes)"`
TUICAllowInsecure bool `json:"tuic_allow_insecure,omitempty" description:"TUIC allow insecure TLS"`
TUICDisableSNI bool `json:"tuic_disable_sni,omitempty" description:"TUIC disable SNI"`
// AnyTLS specific fields
AnyTLSSni string `json:"anytls_sni,omitempty" description:"AnyTLS TLS SNI"`
AnyTLSAllowInsecure bool `json:"anytls_allow_insecure,omitempty" description:"AnyTLS allow insecure TLS"`
AnyTLSFingerprint string `json:"anytls_fingerprint,omitempty" description:"AnyTLS TLS fingerprint"`
AnyTLSIdleSessionCheckInterval string `json:"anytls_idle_session_check_interval,omitempty" description:"AnyTLS idle session check interval"`
AnyTLSIdleSessionTimeout string `json:"anytls_idle_session_timeout,omitempty" description:"AnyTLS idle session timeout"`
AnyTLSMinIdleSession int `json:"anytls_min_idle_session,omitempty" description:"AnyTLS minimum idle sessions"`
IsOnline bool `json:"is_online" example:"true" description:"Indicates if the node agent is online (reported within 5 minutes)"`
LastSeenAt *time.Time `json:"last_seen_at,omitempty" example:"2024-01-15T14:20:00Z" description:"Last time the node agent reported status"`
ExpiresAt *string `json:"expires_at,omitempty" example:"2025-12-31T23:59:59Z" description:"Expiration time in ISO8601 format (null = never expires)"`
CostLabel string `json:"cost_label,omitempty" example:"35$/m" description:"Cost label for display (e.g., '35$/m', '35¥/y')"`
@@ -274,6 +283,16 @@ func ToNodeDTO(n *node.Node) *NodeDTO {
dto.TUICDisableSNI = n.TUICConfig().DisableSNI()
}
// Map AnyTLS specific fields
if n.AnyTLSConfig() != nil {
dto.AnyTLSSni = n.AnyTLSConfig().SNI()
dto.AnyTLSAllowInsecure = n.AnyTLSConfig().AllowInsecure()
dto.AnyTLSFingerprint = n.AnyTLSConfig().Fingerprint()
dto.AnyTLSIdleSessionCheckInterval = n.AnyTLSConfig().IdleSessionCheckInterval()
dto.AnyTLSIdleSessionTimeout = n.AnyTLSConfig().IdleSessionTimeout()
dto.AnyTLSMinIdleSession = n.AnyTLSConfig().MinIdleSession()
}
metadata := n.Metadata()
if metadata.Region() != "" {
dto.Region = metadata.Region()
@@ -302,7 +321,7 @@ type UserNodeDTO struct {
ServerAddress string `json:"server_address" example:"proxy.example.com" description:"Server hostname or IP address"`
AgentPort uint16 `json:"agent_port" example:"8388" description:"Port for agent connections"`
SubscriptionPort *uint16 `json:"subscription_port,omitempty" example:"8389" description:"Port for client subscriptions"`
Protocol string `json:"protocol" example:"shadowsocks" enums:"shadowsocks,trojan,vless,vmess,hysteria2,tuic" description:"Proxy protocol type"`
Protocol string `json:"protocol" example:"shadowsocks" enums:"shadowsocks,trojan,vless,vmess,hysteria2,tuic,anytls" description:"Proxy protocol type"`
EncryptionMethod string `json:"encryption_method,omitempty" example:"aes-256-gcm" description:"Encryption method (Shadowsocks only)"`
Status string `json:"status" example:"active" enums:"active,inactive,maintenance" description:"Current operational status"`
IsOnline bool `json:"is_online" example:"true" description:"Indicates if the node agent is online"`
@@ -357,6 +376,14 @@ type UserNodeDTO struct {
TUICAllowInsecure bool `json:"tuic_allow_insecure,omitempty" description:"TUIC allow insecure TLS"`
TUICDisableSNI bool `json:"tuic_disable_sni,omitempty" description:"TUIC disable SNI"`
// AnyTLS specific fields
AnyTLSSni string `json:"anytls_sni,omitempty" description:"AnyTLS TLS SNI"`
AnyTLSAllowInsecure bool `json:"anytls_allow_insecure,omitempty" description:"AnyTLS allow insecure TLS"`
AnyTLSFingerprint string `json:"anytls_fingerprint,omitempty" description:"AnyTLS TLS fingerprint"`
AnyTLSIdleSessionCheckInterval string `json:"anytls_idle_session_check_interval,omitempty" description:"AnyTLS idle session check interval"`
AnyTLSIdleSessionTimeout string `json:"anytls_idle_session_timeout,omitempty" description:"AnyTLS idle session timeout"`
AnyTLSMinIdleSession int `json:"anytls_min_idle_session,omitempty" description:"AnyTLS minimum idle sessions"`
CreatedAt time.Time `json:"created_at" example:"2024-01-15T10:30:00Z" description:"Timestamp when the node was created"`
UpdatedAt time.Time `json:"updated_at" example:"2024-01-15T14:20:00Z" description:"Timestamp when the node was last updated"`
}
@@ -442,6 +469,16 @@ func ToUserNodeDTO(n *node.Node) *UserNodeDTO {
dto.TUICDisableSNI = n.TUICConfig().DisableSNI()
}
// Map AnyTLS specific fields
if n.AnyTLSConfig() != nil {
dto.AnyTLSSni = n.AnyTLSConfig().SNI()
dto.AnyTLSAllowInsecure = n.AnyTLSConfig().AllowInsecure()
dto.AnyTLSFingerprint = n.AnyTLSConfig().Fingerprint()
dto.AnyTLSIdleSessionCheckInterval = n.AnyTLSConfig().IdleSessionCheckInterval()
dto.AnyTLSIdleSessionTimeout = n.AnyTLSConfig().IdleSessionTimeout()
dto.AnyTLSMinIdleSession = n.AnyTLSConfig().MinIdleSession()
}
return dto
}

View File

@@ -3,6 +3,7 @@ package usecases
import (
"context"
"fmt"
"time"
"github.com/orris-inc/orris/internal/application/node/dto"
"github.com/orris-inc/orris/internal/domain/node"
@@ -79,6 +80,14 @@ type CreateNodeCommand struct {
TUICSni string
TUICAllowInsecure bool
TUICDisableSNI bool
// AnyTLS specific fields
AnyTLSSni string
AnyTLSAllowInsecure bool
AnyTLSFingerprint string
AnyTLSIdleSessionCheckInterval string
AnyTLSIdleSessionTimeout string
AnyTLSMinIdleSession int
}
type CreateNodeResult struct {
@@ -162,6 +171,7 @@ func (uc *CreateNodeUseCase) Execute(ctx context.Context, cmd CreateNodeCommand)
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
if protocol.IsShadowsocks() {
encryptionConfig, err = vo.NewEncryptionConfig(cmd.Method)
@@ -334,6 +344,21 @@ func (uc *CreateNodeUseCase) Execute(ctx context.Context, cmd CreateNodeCommand)
return nil, err
}
tuicConfig = &tc
} else if protocol.IsAnyTLS() {
// Create AnyTLS config
ac, err := vo.NewAnyTLSConfig(
"placeholder", // Password will be replaced by subscription UUID
cmd.AnyTLSSni,
cmd.AnyTLSAllowInsecure,
cmd.AnyTLSFingerprint,
cmd.AnyTLSIdleSessionCheckInterval,
cmd.AnyTLSIdleSessionTimeout,
cmd.AnyTLSMinIdleSession,
)
if err != nil {
return nil, err
}
anytlsConfig = &ac
}
// Create metadata
@@ -367,6 +392,7 @@ func (uc *CreateNodeUseCase) Execute(ctx context.Context, cmd CreateNodeCommand)
vmessConfig,
hysteria2Config,
tuicConfig,
anytlsConfig,
metadata,
cmd.SortOrder,
routeConfig,
@@ -519,6 +545,13 @@ func (uc *CreateNodeUseCase) validateCommand(cmd CreateNodeCommand) error {
}
}
// Validate AnyTLS-specific requirements
if cmd.Protocol == "anytls" {
if err := uc.validateAnyTLSCommand(cmd); err != nil {
return err
}
}
return nil
}
@@ -655,6 +688,25 @@ func (uc *CreateNodeUseCase) validateTUICCommand(cmd CreateNodeCommand) error {
return nil
}
// validateAnyTLSCommand validates AnyTLS protocol specific requirements
func (uc *CreateNodeUseCase) validateAnyTLSCommand(cmd CreateNodeCommand) error {
// Validate idle session check interval (must be valid Go duration if non-empty)
if cmd.AnyTLSIdleSessionCheckInterval != "" {
if _, err := time.ParseDuration(cmd.AnyTLSIdleSessionCheckInterval); err != nil {
return errors.NewValidationError("invalid AnyTLS idle session check interval (must be a valid duration, e.g. '30s', '1m')")
}
}
// Validate idle session timeout (must be valid Go duration if non-empty)
if cmd.AnyTLSIdleSessionTimeout != "" {
if _, err := time.ParseDuration(cmd.AnyTLSIdleSessionTimeout); err != nil {
return errors.NewValidationError("invalid AnyTLS idle session timeout (must be a valid duration, e.g. '30s', '1m')")
}
}
return nil
}
// validateProtocolMethodCompatibility validates that the encryption method matches the protocol type
func (uc *CreateNodeUseCase) validateProtocolMethodCompatibility(protocol vo.Protocol, method string) error {
// Shadowsocks encryption methods

View File

@@ -3,6 +3,7 @@ package usecases
import (
"context"
"fmt"
"time"
"github.com/orris-inc/orris/internal/domain/node"
vo "github.com/orris-inc/orris/internal/domain/node/valueobjects"
@@ -71,6 +72,14 @@ type CreateUserNodeCommand struct {
TUICSni string
TUICAllowInsecure bool
TUICDisableSNI bool
// AnyTLS specific fields
AnyTLSSni string
AnyTLSAllowInsecure bool
AnyTLSFingerprint string
AnyTLSIdleSessionCheckInterval string
AnyTLSIdleSessionTimeout string
AnyTLSMinIdleSession int
}
type CreateUserNodeResult struct {
@@ -164,6 +173,7 @@ func (uc *CreateUserNodeUseCase) Execute(ctx context.Context, cmd CreateUserNode
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
if protocol.IsShadowsocks() {
encryptionConfig, err = vo.NewEncryptionConfig(cmd.Method)
@@ -346,6 +356,22 @@ func (uc *CreateUserNodeUseCase) Execute(ctx context.Context, cmd CreateUserNode
return nil, err
}
tuicConfig = &tc
} else if protocol.IsAnyTLS() {
// Create AnyTLS config
ac, err := vo.NewAnyTLSConfig(
"placeholder", // Password will be replaced by subscription UUID
cmd.AnyTLSSni,
cmd.AnyTLSAllowInsecure,
cmd.AnyTLSFingerprint,
cmd.AnyTLSIdleSessionCheckInterval,
cmd.AnyTLSIdleSessionTimeout,
cmd.AnyTLSMinIdleSession,
)
if err != nil {
uc.logger.Errorw("invalid AnyTLS config", "error", err)
return nil, err
}
anytlsConfig = &ac
}
// Create metadata (user nodes don't have region/tags/description)
@@ -365,6 +391,7 @@ func (uc *CreateUserNodeUseCase) Execute(ctx context.Context, cmd CreateUserNode
vmessConfig,
hysteria2Config,
tuicConfig,
anytlsConfig,
metadata,
0, // sortOrder not used for user nodes
nil, // routeConfig - can be set later via UpdateRouteConfig
@@ -485,6 +512,13 @@ func (uc *CreateUserNodeUseCase) validateCommand(cmd CreateUserNodeCommand) erro
}
}
// Validate AnyTLS-specific requirements
if cmd.Protocol == "anytls" {
if err := uc.validateAnyTLSCommand(cmd); err != nil {
return err
}
}
return nil
}
@@ -621,6 +655,25 @@ func (uc *CreateUserNodeUseCase) validateTUICCommand(cmd CreateUserNodeCommand)
return nil
}
// validateAnyTLSCommand validates AnyTLS protocol specific requirements
func (uc *CreateUserNodeUseCase) validateAnyTLSCommand(cmd CreateUserNodeCommand) error {
// Validate idle session check interval (must be valid Go duration if non-empty)
if cmd.AnyTLSIdleSessionCheckInterval != "" {
if _, err := time.ParseDuration(cmd.AnyTLSIdleSessionCheckInterval); err != nil {
return errors.NewValidationError("invalid AnyTLS idle session check interval (must be a valid duration, e.g. '30s', '1m')")
}
}
// Validate idle session timeout (must be valid Go duration if non-empty)
if cmd.AnyTLSIdleSessionTimeout != "" {
if _, err := time.ParseDuration(cmd.AnyTLSIdleSessionTimeout); err != nil {
return errors.NewValidationError("invalid AnyTLS idle session timeout (must be a valid duration, e.g. '30s', '1m')")
}
}
return nil
}
// validateProtocolMethodCompatibility validates that the encryption method matches the protocol type
func (uc *CreateUserNodeUseCase) validateProtocolMethodCompatibility(protocol vo.Protocol, method string) error {
// Shadowsocks encryption methods

View File

@@ -247,6 +247,7 @@ func (uc *GenerateSubscriptionUseCase) createInfoNode(template *Node, name strin
VMessConfig: template.VMessConfig,
Hysteria2Config: template.Hysteria2Config,
TUICConfig: template.TUICConfig,
AnyTLSConfig: template.AnyTLSConfig,
SortOrder: sortOrder,
}
}
@@ -450,6 +451,7 @@ type Node struct {
VMessConfig *valueobjects.VMessConfig
Hysteria2Config *valueobjects.Hysteria2Config
TUICConfig *valueobjects.TUICConfig
AnyTLSConfig *valueobjects.AnyTLSConfig
// Sorting field for subscription output ordering
SortOrder int
}

View File

@@ -92,6 +92,10 @@ func (uc *GetNodeConfigUseCase) Execute(ctx context.Context, cmd GetNodeConfigCo
if refNode.Protocol().IsTrojan() {
return vo.GenerateTrojanServerPassword(refNode.TokenHash())
}
// For AnyTLS, generate password from token hash for node-to-node forwarding
if refNode.Protocol().IsAnyTLS() {
return vo.GenerateAnyTLSServerPassword(refNode.TokenHash())
}
return ""
}

View File

@@ -93,6 +93,10 @@ func (f *Base64Formatter) FormatWithPassword(nodes []*Node, password string) (st
// For TUIC, use password as both uuid and password (derived from subscription)
link = node.TUICConfig.ToURI(node.ServerAddress, node.SubscriptionPort, node.Name, password, password)
}
case vo.ProtocolAnyTLS:
if node.AnyTLSConfig != nil {
link = node.AnyTLSConfig.ToURI(node.ServerAddress, node.SubscriptionPort, node.Name, password)
}
default:
// Shadowsocks: adjust password for SS2022 methods
nodePassword := adjustPasswordForMethod(password, node.EncryptionMethod, node.TokenHash)
@@ -170,6 +174,10 @@ type clashProxy struct {
UDPRelayMode string `yaml:"udp-relay-mode,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
DisableSNI bool `yaml:"disable-sni,omitempty"`
// AnyTLS specific fields
IdleSessionCheckInterval string `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout string `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
type clashWSOpts struct {
@@ -259,6 +267,11 @@ func (f *ClashFormatter) FormatWithPassword(nodes []*Node, password string) (str
proxy = f.buildTUICProxy(node, password)
}
case vo.ProtocolAnyTLS:
if node.AnyTLSConfig != nil {
proxy = f.buildAnyTLSProxy(node, password)
}
default:
// Shadowsocks: adjust password for SS2022 methods
nodePassword := adjustPasswordForMethod(password, node.EncryptionMethod, node.TokenHash)
@@ -493,6 +506,42 @@ func (f *ClashFormatter) buildTUICProxy(node *Node, password string) clashProxy
return proxy
}
// buildAnyTLSProxy builds a Clash Meta AnyTLS proxy configuration
// password is the subscription-derived credential
func (f *ClashFormatter) buildAnyTLSProxy(node *Node, password string) clashProxy {
cfg := node.AnyTLSConfig
proxy := clashProxy{
Name: node.Name,
Type: "anytls",
Server: node.ServerAddress,
Port: node.SubscriptionPort,
Password: password,
SkipCertVerify: cfg.AllowInsecure(),
}
if cfg.SNI() != "" {
proxy.SNI = cfg.SNI()
}
if cfg.Fingerprint() != "" {
proxy.Fingerprint = cfg.Fingerprint()
}
if cfg.IdleSessionCheckInterval() != "" {
proxy.IdleSessionCheckInterval = cfg.IdleSessionCheckInterval()
}
if cfg.IdleSessionTimeout() != "" {
proxy.IdleSessionTimeout = cfg.IdleSessionTimeout()
}
if cfg.MinIdleSession() > 0 {
proxy.MinIdleSession = cfg.MinIdleSession()
}
return proxy
}
func (f *ClashFormatter) ContentType() string {
return "text/yaml; charset=utf-8"
}
@@ -706,6 +755,10 @@ func (f *SurgeFormatter) FormatWithPassword(nodes []*Node, password string) (str
line = f.buildTUICLine(node, password)
}
case vo.ProtocolAnyTLS:
// Surge does not natively support AnyTLS, skip
continue
default:
// Shadowsocks: adjust password for SS2022 methods
nodePassword := adjustPasswordForMethod(password, node.EncryptionMethod, node.TokenHash)

View File

@@ -84,6 +84,14 @@ type UpdateNodeCommand struct {
TUICAllowInsecure *bool
TUICDisableSNI *bool
// AnyTLS specific fields
AnyTLSSni *string
AnyTLSAllowInsecure *bool
AnyTLSFingerprint *string
AnyTLSIdleSessionCheckInterval *string
AnyTLSIdleSessionTimeout *string
AnyTLSMinIdleSession *int
// Expiration and cost label fields
ExpiresAt *time.Time // nil: no update, set to update expiration time
ClearExpiresAt bool // true: clear expiration time
@@ -457,6 +465,11 @@ func (uc *UpdateNodeUseCase) applyUpdates(n *node.Node, cmd UpdateNodeCommand) e
return err
}
// Update AnyTLS config (only for AnyTLS protocol nodes)
if err := uc.applyAnyTLSUpdates(n, cmd); err != nil {
return err
}
// Update route config
if cmd.ClearRoute {
n.ClearRouteConfig()
@@ -518,6 +531,10 @@ func (uc *UpdateNodeUseCase) validateCommand(cmd UpdateNodeCommand) error {
// TUIC fields
cmd.TUICCongestionControl != nil || cmd.TUICUDPRelayMode != nil || cmd.TUICAlpn != nil ||
cmd.TUICSni != nil || cmd.TUICAllowInsecure != nil || cmd.TUICDisableSNI != nil ||
// AnyTLS fields
cmd.AnyTLSSni != nil || cmd.AnyTLSAllowInsecure != nil || cmd.AnyTLSFingerprint != nil ||
cmd.AnyTLSIdleSessionCheckInterval != nil || cmd.AnyTLSIdleSessionTimeout != nil ||
cmd.AnyTLSMinIdleSession != nil ||
// Expiration and cost label fields
cmd.ExpiresAt != nil || cmd.ClearExpiresAt || cmd.CostLabel != nil || cmd.ClearCostLabel
@@ -1025,6 +1042,81 @@ func (uc *UpdateNodeUseCase) applyTUICUpdates(n *node.Node, cmd UpdateNodeComman
return nil
}
// applyAnyTLSUpdates applies AnyTLS-specific configuration updates
func (uc *UpdateNodeUseCase) applyAnyTLSUpdates(n *node.Node, cmd UpdateNodeCommand) error {
// Check if any AnyTLS fields need updating
hasAnyTLSUpdate := cmd.AnyTLSSni != nil || cmd.AnyTLSAllowInsecure != nil ||
cmd.AnyTLSFingerprint != nil || cmd.AnyTLSIdleSessionCheckInterval != nil ||
cmd.AnyTLSIdleSessionTimeout != nil || cmd.AnyTLSMinIdleSession != nil
if !hasAnyTLSUpdate {
return nil
}
// Validate protocol is AnyTLS
if !n.Protocol().IsAnyTLS() {
return errors.NewValidationError("cannot update AnyTLS config for non-AnyTLS protocol node")
}
// Get current AnyTLS config or use defaults
currentConfig := n.AnyTLSConfig()
var password, sni, fingerprint, idleCheckInterval, idleTimeout string
var allowInsecure bool
var minIdleSession int
if currentConfig != nil {
password = currentConfig.Password()
sni = currentConfig.SNI()
allowInsecure = currentConfig.AllowInsecure()
fingerprint = currentConfig.Fingerprint()
idleCheckInterval = currentConfig.IdleSessionCheckInterval()
idleTimeout = currentConfig.IdleSessionTimeout()
minIdleSession = currentConfig.MinIdleSession()
} else {
password = "placeholder"
allowInsecure = true
}
if cmd.AnyTLSSni != nil {
sni = *cmd.AnyTLSSni
}
if cmd.AnyTLSAllowInsecure != nil {
allowInsecure = *cmd.AnyTLSAllowInsecure
}
if cmd.AnyTLSFingerprint != nil {
fingerprint = *cmd.AnyTLSFingerprint
}
if cmd.AnyTLSIdleSessionCheckInterval != nil {
idleCheckInterval = *cmd.AnyTLSIdleSessionCheckInterval
}
if cmd.AnyTLSIdleSessionTimeout != nil {
idleTimeout = *cmd.AnyTLSIdleSessionTimeout
}
if cmd.AnyTLSMinIdleSession != nil {
minIdleSession = *cmd.AnyTLSMinIdleSession
}
newConfig, err := vo.NewAnyTLSConfig(
password,
sni,
allowInsecure,
fingerprint,
idleCheckInterval,
idleTimeout,
minIdleSession,
)
if err != nil {
return errors.NewValidationError("invalid AnyTLS configuration: " + err.Error())
}
if err := n.UpdateAnyTLSConfig(&newConfig); err != nil {
return errors.NewValidationError("failed to update AnyTLS config: " + err.Error())
}
return nil
}
// validateProtocolMethodCompatibility validates that the encryption method matches the protocol type
func (uc *UpdateNodeUseCase) validateProtocolMethodCompatibility(protocol vo.Protocol, method string) error {
// Shadowsocks encryption methods

View File

@@ -31,6 +31,7 @@ type Node struct {
vmessConfig *vo.VMessConfig
hysteria2Config *vo.Hysteria2Config
tuicConfig *vo.TUICConfig
anytlsConfig *vo.AnyTLSConfig
status vo.NodeStatus
metadata vo.NodeMetadata
groupIDs []uint // resource group IDs
@@ -70,6 +71,7 @@ func NewNode(
vmessConfig *vo.VMessConfig,
hysteria2Config *vo.Hysteria2Config,
tuicConfig *vo.TUICConfig,
anytlsConfig *vo.AnyTLSConfig,
metadata vo.NodeMetadata,
sortOrder int,
routeConfig *vo.RouteConfig,
@@ -104,6 +106,9 @@ func NewNode(
if protocol.IsTUIC() && tuicConfig == nil {
return nil, fmt.Errorf("tuic config is required for TUIC protocol")
}
if protocol.IsAnyTLS() && anytlsConfig == nil {
return nil, fmt.Errorf("anytls config is required for AnyTLS protocol")
}
// Validate route config if provided
if routeConfig != nil {
@@ -139,6 +144,7 @@ func NewNode(
vmessConfig: vmessConfig,
hysteria2Config: hysteria2Config,
tuicConfig: tuicConfig,
anytlsConfig: anytlsConfig,
status: vo.NodeStatusInactive,
metadata: metadata,
apiToken: plainToken,
@@ -170,6 +176,7 @@ func ReconstructNode(
vmessConfig *vo.VMessConfig,
hysteria2Config *vo.Hysteria2Config,
tuicConfig *vo.TUICConfig,
anytlsConfig *vo.AnyTLSConfig,
status vo.NodeStatus,
metadata vo.NodeMetadata,
groupIDs []uint,
@@ -225,6 +232,7 @@ func ReconstructNode(
vmessConfig: vmessConfig,
hysteria2Config: hysteria2Config,
tuicConfig: tuicConfig,
anytlsConfig: anytlsConfig,
status: status,
metadata: metadata,
groupIDs: groupIDs,
@@ -330,6 +338,11 @@ func (n *Node) TUICConfig() *vo.TUICConfig {
return n.tuicConfig
}
// AnyTLSConfig returns the AnyTLS configuration
func (n *Node) AnyTLSConfig() *vo.AnyTLSConfig {
return n.anytlsConfig
}
// Status returns the node status
func (n *Node) Status() vo.NodeStatus {
return n.status
@@ -700,6 +713,22 @@ func (n *Node) UpdateTUICConfig(config *vo.TUICConfig) error {
return nil
}
// UpdateAnyTLSConfig updates the AnyTLS configuration
func (n *Node) UpdateAnyTLSConfig(config *vo.AnyTLSConfig) error {
if !n.protocol.IsAnyTLS() {
return fmt.Errorf("cannot update anytls config for non-anytls protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.anytlsConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateMetadata updates the node metadata
func (n *Node) UpdateMetadata(metadata vo.NodeMetadata) error {
n.mu.Lock()
@@ -969,6 +998,9 @@ func (n *Node) Validate() error {
if n.protocol.IsTUIC() && n.tuicConfig == nil {
return fmt.Errorf("tuic config is required for TUIC protocol")
}
if n.protocol.IsAnyTLS() && n.anytlsConfig == nil {
return fmt.Errorf("anytls config is required for AnyTLS protocol")
}
if n.status == vo.NodeStatusMaintenance && n.maintenanceReason == nil {
return fmt.Errorf("maintenance reason is required when in maintenance mode")
}
@@ -995,6 +1027,12 @@ func (n *Node) GenerateSubscriptionURI(password string, remarks string) (string,
trojanConfig := vo.NewTrojanProtocolConfig(*n.trojanConfig)
return factory.GenerateSubscriptionURI(n.protocol, trojanConfig, serverAddr, port, password, remarks)
case vo.ProtocolAnyTLS:
if n.anytlsConfig == nil {
return "", fmt.Errorf("anytls config is required for AnyTLS protocol")
}
return n.anytlsConfig.ToURI(serverAddr, port, remarks, password), nil
default:
return "", fmt.Errorf("unsupported protocol: %s", n.protocol)
}

View File

@@ -43,6 +43,7 @@ func newShadowsocksNode(t *testing.T) *Node {
nil, // vmessConfig
nil, // hysteria2Config
nil, // tuicConfig
nil, // anytlsConfig
meta,
0, // sortOrder
nil, // routeConfig
@@ -76,6 +77,7 @@ func newTrojanNode(t *testing.T) *Node {
nil,
nil,
nil,
nil, // anytlsConfig
meta,
0,
nil,
@@ -118,6 +120,7 @@ func reconstructedNode(t *testing.T, status vo.NodeStatus) *Node {
nil, // vmessConfig
nil, // hysteria2Config
nil, // tuicConfig
nil, // anytlsConfig
status, // status
meta,
[]uint{1, 2}, // groupIDs
@@ -162,7 +165,7 @@ func TestNewNode_ValidInput_Shadowsocks(t *testing.T) {
nil,
vo.ProtocolShadowsocks,
enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
meta,
10,
nil,
@@ -210,7 +213,7 @@ func TestNewNode_ValidInput_Trojan(t *testing.T) {
&subPort,
vo.ProtocolTrojan,
vo.EncryptionConfig{},
nil, &trojanCfg, nil, nil, nil, nil,
nil, &trojanCfg, nil, nil, nil, nil, nil,
meta,
0,
nil,
@@ -256,7 +259,7 @@ func TestNewNode_ValidInput_VLESS(t *testing.T) {
nil,
vo.ProtocolVLESS,
vo.EncryptionConfig{},
nil, nil, &vlessCfg, nil, nil, nil,
nil, nil, &vlessCfg, nil, nil, nil, nil,
meta,
0,
nil,
@@ -295,7 +298,7 @@ func TestNewNode_ValidInput_VMess(t *testing.T) {
nil,
vo.ProtocolVMess,
vo.EncryptionConfig{},
nil, nil, nil, &vmessCfg, nil, nil,
nil, nil, nil, &vmessCfg, nil, nil, nil,
meta,
0,
nil,
@@ -334,7 +337,7 @@ func TestNewNode_ValidInput_Hysteria2(t *testing.T) {
nil,
vo.ProtocolHysteria2,
vo.EncryptionConfig{},
nil, nil, nil, nil, &hy2Cfg, nil,
nil, nil, nil, nil, &hy2Cfg, nil, nil,
meta,
0,
nil,
@@ -372,7 +375,7 @@ func TestNewNode_ValidInput_TUIC(t *testing.T) {
nil,
vo.ProtocolTUIC,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, &tuicCfg,
nil, nil, nil, nil, nil, &tuicCfg, nil,
meta,
0,
nil,
@@ -396,7 +399,7 @@ func TestNewNode_InvalidProtocol(t *testing.T) {
nil,
vo.Protocol("wireguard"), // not a valid protocol
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -421,7 +424,7 @@ func TestNewNode_MissingName(t *testing.T) {
nil,
vo.ProtocolShadowsocks,
enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -446,7 +449,7 @@ func TestNewNode_MissingAgentPort(t *testing.T) {
nil,
vo.ProtocolShadowsocks,
enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -468,7 +471,7 @@ func TestNewNode_ShadowsocksMissingEncryption(t *testing.T) {
nil,
vo.ProtocolShadowsocks,
vo.EncryptionConfig{}, // empty encryption config
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -490,7 +493,7 @@ func TestNewNode_TrojanMissingConfig(t *testing.T) {
nil,
vo.ProtocolTrojan,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil, // trojanConfig = nil
nil, nil, nil, nil, nil, nil, nil, // trojanConfig = nil, anytlsConfig = nil
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -512,7 +515,7 @@ func TestNewNode_VLESSMissingConfig(t *testing.T) {
nil,
vo.ProtocolVLESS,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil, // vlessConfig = nil
nil, nil, nil, nil, nil, nil, nil, // vlessConfig = nil, anytlsConfig = nil
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -534,7 +537,7 @@ func TestNewNode_VMessMissingConfig(t *testing.T) {
nil,
vo.ProtocolVMess,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -556,7 +559,7 @@ func TestNewNode_Hysteria2MissingConfig(t *testing.T) {
nil,
vo.ProtocolHysteria2,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -578,7 +581,7 @@ func TestNewNode_TUICMissingConfig(t *testing.T) {
nil,
vo.ProtocolTUIC,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
@@ -589,6 +592,66 @@ func TestNewNode_TUICMissingConfig(t *testing.T) {
assert.Contains(t, err.Error(), "tuic config is required for TUIC protocol")
}
func TestNewNode_ValidInput_AnyTLS(t *testing.T) {
addr, err := vo.NewServerAddress("anytls.example.com")
require.NoError(t, err)
anytlsCfg, err := vo.NewAnyTLSConfig("securepass123", "anytls.example.com", false, "chrome", "30s", "30s", 2)
require.NoError(t, err)
meta := vo.NewNodeMetadata("ap-east", nil, "anytls node")
n, err := NewNode(
"test-anytls-node",
addr,
443,
nil,
vo.ProtocolAnyTLS,
vo.EncryptionConfig{},
nil, // pluginConfig
nil, // trojanConfig
nil, // vlessConfig
nil, // vmessConfig
nil, // hysteria2Config
nil, // tuicConfig
&anytlsCfg, // anytlsConfig
meta,
0,
nil,
fakeSIDGenerator("node_anytls789"),
)
require.NoError(t, err)
assert.Equal(t, "test-anytls-node", n.Name())
assert.True(t, n.Protocol().IsAnyTLS())
require.NotNil(t, n.AnyTLSConfig())
assert.Equal(t, "anytls.example.com", n.AnyTLSConfig().SNI())
assert.Equal(t, "chrome", n.AnyTLSConfig().Fingerprint())
assert.Equal(t, 2, n.AnyTLSConfig().MinIdleSession())
}
func TestNewNode_AnyTLSMissingConfig(t *testing.T) {
addr, err := vo.NewServerAddress("1.2.3.4")
require.NoError(t, err)
_, err = NewNode(
"anytls-no-cfg",
addr,
443,
nil,
vo.ProtocolAnyTLS,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil, nil, // anytlsConfig = nil
vo.NewNodeMetadata("", nil, ""),
0,
nil,
fakeSIDGenerator("node_noanytls"),
)
require.Error(t, err)
assert.Contains(t, err.Error(), "anytls config is required for AnyTLS protocol")
}
func TestNewNode_WithSubscriptionPort(t *testing.T) {
n := newShadowsocksNode(t)
// Default: no subscription port, effective = agent port
@@ -620,7 +683,7 @@ func TestReconstructNode_ZeroID(t *testing.T) {
_, err = ReconstructNode(
0, "node_x", "name", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -642,7 +705,7 @@ func TestReconstructNode_EmptySID(t *testing.T) {
_, err = ReconstructNode(
1, "", "name", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -664,7 +727,7 @@ func TestReconstructNode_EmptyTokenHash(t *testing.T) {
_, err = ReconstructNode(
1, "node_x", "name", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1117,7 +1180,7 @@ func TestNode_IsOnline(t *testing.T) {
n2, err := ReconstructNode(
2, "node_online001", "online-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1141,7 +1204,7 @@ func TestNode_IsOnline(t *testing.T) {
n, err := ReconstructNode(
3, "node_stale001", "stale-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1322,7 +1385,7 @@ func TestNode_EffectiveServerAddress(t *testing.T) {
n, err := ReconstructNode(
4, "node_fb001", "fallback-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1345,7 +1408,7 @@ func TestNode_EffectiveServerAddress(t *testing.T) {
n, err := ReconstructNode(
5, "node_empty001", "empty-addr-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1472,6 +1535,53 @@ func TestNode_UpdateTUICConfig_ProtocolMismatch(t *testing.T) {
assert.Contains(t, err.Error(), "cannot update tuic config for non-tuic protocol")
}
func TestNode_UpdateAnyTLSConfig_ProtocolMismatch(t *testing.T) {
n := newShadowsocksNode(t) // Shadowsocks node
anytlsCfg, err := vo.NewAnyTLSConfig("securepass123", "example.com", false, "chrome", "", "", 0)
require.NoError(t, err)
err = n.UpdateAnyTLSConfig(&anytlsCfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot update anytls config for non-anytls protocol")
}
func TestNode_UpdateAnyTLSConfig_CorrectProtocol(t *testing.T) {
addr, err := vo.NewServerAddress("anytls.example.com")
require.NoError(t, err)
anytlsCfg, err := vo.NewAnyTLSConfig("securepass123", "anytls.example.com", false, "chrome", "", "", 0)
require.NoError(t, err)
n, err := NewNode(
"test-anytls-update",
addr,
443,
nil,
vo.ProtocolAnyTLS,
vo.EncryptionConfig{},
nil, nil, nil, nil, nil, nil, &anytlsCfg,
vo.NewNodeMetadata("", nil, ""),
0,
nil,
fakeSIDGenerator("node_anytlsupd"),
)
require.NoError(t, err)
initialVersion := n.Version()
newCfg, err := vo.NewAnyTLSConfig("newlongpassword1", "new.example.com", true, "firefox", "60s", "120s", 5)
require.NoError(t, err)
err = n.UpdateAnyTLSConfig(&newCfg)
require.NoError(t, err)
assert.Equal(t, initialVersion+1, n.Version())
require.NotNil(t, n.AnyTLSConfig())
assert.Equal(t, "new.example.com", n.AnyTLSConfig().SNI())
assert.Equal(t, "firefox", n.AnyTLSConfig().Fingerprint())
assert.Equal(t, 5, n.AnyTLSConfig().MinIdleSession())
assert.True(t, n.AnyTLSConfig().AllowInsecure())
}
func TestNode_UpdateTrojanConfig_CorrectProtocol(t *testing.T) {
n := newTrojanNode(t)
initialVersion := n.Version()
@@ -1506,7 +1616,7 @@ func TestNode_Validate(t *testing.T) {
n, err := ReconstructNode(
10, "node_val001", "val-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusMaintenance, // maintenance status
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1566,7 +1676,7 @@ func TestNode_AgentInfo(t *testing.T) {
n, err := ReconstructNode(
6, "node_agent001", "agent-info-node", addr, 8388, nil,
vo.ProtocolShadowsocks, enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NodeStatusActive,
vo.NewNodeMetadata("", nil, ""),
nil, nil,
@@ -1647,7 +1757,7 @@ func TestNewNode_EmptyServerAddress(t *testing.T) {
nil,
vo.ProtocolShadowsocks,
enc,
nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil,
vo.NewNodeMetadata("", nil, ""),
0,
nil,

View File

@@ -0,0 +1,210 @@
package valueobjects
import (
"fmt"
"net/url"
"strings"
"time"
)
// validAnyTLSFingerprints defines the valid TLS fingerprints for AnyTLS
var validAnyTLSFingerprints = map[string]bool{
"chrome": true,
"firefox": true,
"safari": true,
"ios": true,
"android": true,
"edge": true,
"360": true,
"qq": true,
"random": true,
"randomized": true,
"": true, // allow empty (use default)
}
// AnyTLSConfig represents the AnyTLS protocol configuration.
// This is an immutable value object following DDD principles.
type AnyTLSConfig struct {
password string
sni string
allowInsecure bool
fingerprint string
idleSessionCheckInterval string // duration string, e.g. "30s"
idleSessionTimeout string // duration string, e.g. "30s"
minIdleSession int
}
// NewAnyTLSConfig creates a new AnyTLSConfig with validation
func NewAnyTLSConfig(
password string,
sni string,
allowInsecure bool,
fingerprint string,
idleSessionCheckInterval string,
idleSessionTimeout string,
minIdleSession int,
) (AnyTLSConfig, error) {
// Validate password
if len(password) < 8 {
return AnyTLSConfig{}, fmt.Errorf("password must be at least 8 characters long")
}
// Validate fingerprint
if !validAnyTLSFingerprints[fingerprint] {
return AnyTLSConfig{}, fmt.Errorf("unsupported TLS fingerprint: %s", fingerprint)
}
// Validate duration strings
if idleSessionCheckInterval != "" {
if _, err := time.ParseDuration(idleSessionCheckInterval); err != nil {
return AnyTLSConfig{}, fmt.Errorf("invalid idle_session_check_interval: %w", err)
}
}
if idleSessionTimeout != "" {
if _, err := time.ParseDuration(idleSessionTimeout); err != nil {
return AnyTLSConfig{}, fmt.Errorf("invalid idle_session_timeout: %w", err)
}
}
// Validate minIdleSession
if minIdleSession < 0 {
return AnyTLSConfig{}, fmt.Errorf("min_idle_session must be non-negative")
}
return AnyTLSConfig{
password: password,
sni: sni,
allowInsecure: allowInsecure,
fingerprint: fingerprint,
idleSessionCheckInterval: idleSessionCheckInterval,
idleSessionTimeout: idleSessionTimeout,
minIdleSession: minIdleSession,
}, nil
}
// Password returns the AnyTLS password
func (c AnyTLSConfig) Password() string {
return c.password
}
// SNI returns the Server Name Indication
func (c AnyTLSConfig) SNI() string {
return c.sni
}
// AllowInsecure returns whether to allow insecure connections
func (c AnyTLSConfig) AllowInsecure() bool {
return c.allowInsecure
}
// Fingerprint returns the TLS fingerprint
func (c AnyTLSConfig) Fingerprint() string {
return c.fingerprint
}
// IdleSessionCheckInterval returns the idle session check interval
func (c AnyTLSConfig) IdleSessionCheckInterval() string {
return c.idleSessionCheckInterval
}
// IdleSessionTimeout returns the idle session timeout
func (c AnyTLSConfig) IdleSessionTimeout() string {
return c.idleSessionTimeout
}
// MinIdleSession returns the minimum idle session count
func (c AnyTLSConfig) MinIdleSession() int {
return c.minIdleSession
}
// ToURI generates an AnyTLS URI string for subscription
// Format: anytls://password@host:port?security=tls&sni=xxx&allowInsecure=1&fp=chrome#remarks
// Password is passed externally as it's derived from the subscription UUID, not stored in config.
func (c AnyTLSConfig) ToURI(serverAddr string, serverPort uint16, remarks string, password string) string {
uri := fmt.Sprintf("anytls://%s@%s:%d", password, serverAddr, serverPort)
var params []string
// Add security parameter (AnyTLS requires TLS)
params = append(params, "security=tls")
// Add allowInsecure parameter
if c.allowInsecure {
params = append(params, "allowInsecure=1")
}
// Add SNI if provided
if c.sni != "" {
params = append(params, "sni="+url.QueryEscape(c.sni))
}
// Add fingerprint if provided
if c.fingerprint != "" {
params = append(params, "fp="+url.QueryEscape(c.fingerprint))
}
// Add idle session parameters if provided
if c.idleSessionCheckInterval != "" {
params = append(params, "idle-session-check-interval="+url.QueryEscape(c.idleSessionCheckInterval))
}
if c.idleSessionTimeout != "" {
params = append(params, "idle-session-timeout="+url.QueryEscape(c.idleSessionTimeout))
}
if c.minIdleSession > 0 {
params = append(params, fmt.Sprintf("min-idle-session=%d", c.minIdleSession))
}
// Append query parameters
if len(params) > 0 {
uri += "?" + strings.Join(params, "&")
}
// Add remarks if provided
if remarks != "" {
uri += "#" + url.QueryEscape(remarks)
}
return uri
}
// String returns a string representation of the config
func (c AnyTLSConfig) String() string {
var parts []string
if c.sni != "" {
parts = append(parts, fmt.Sprintf("sni=%s", c.sni))
}
if c.allowInsecure {
parts = append(parts, "allowInsecure=true")
}
if c.fingerprint != "" {
parts = append(parts, fmt.Sprintf("fingerprint=%s", c.fingerprint))
}
if c.idleSessionCheckInterval != "" {
parts = append(parts, fmt.Sprintf("idle_check=%s", c.idleSessionCheckInterval))
}
if c.idleSessionTimeout != "" {
parts = append(parts, fmt.Sprintf("idle_timeout=%s", c.idleSessionTimeout))
}
if c.minIdleSession > 0 {
parts = append(parts, fmt.Sprintf("min_idle=%d", c.minIdleSession))
}
return strings.Join(parts, ", ")
}
// Equals checks if two AnyTLSConfig instances are equal
func (c AnyTLSConfig) Equals(other AnyTLSConfig) bool {
return c.password == other.password &&
c.sni == other.sni &&
c.allowInsecure == other.allowInsecure &&
c.fingerprint == other.fingerprint &&
c.idleSessionCheckInterval == other.idleSessionCheckInterval &&
c.idleSessionTimeout == other.idleSessionTimeout &&
c.minIdleSession == other.minIdleSession
}

View File

@@ -166,3 +166,20 @@ func GenerateTrojanServerPassword(tokenHash string) string {
// Return as hex string (64 chars, common Trojan password format)
return fmt.Sprintf("%x", keyMaterial)
}
// GenerateAnyTLSServerPassword derives an AnyTLS password from node token hash.
// This is used for node-to-node forwarding (outbound) scenarios.
// Returns a hex-encoded 32-byte password derived using HMAC-SHA256.
func GenerateAnyTLSServerPassword(tokenHash string) string {
if tokenHash == "" {
return ""
}
// Derive password using HMAC-SHA256 with a fixed salt
mac := hmac.New(sha256.New, []byte("anytls-server-password"))
mac.Write([]byte(tokenHash))
keyMaterial := mac.Sum(nil) // 32 bytes
// Return as hex string (64 chars)
return fmt.Sprintf("%x", keyMaterial)
}

View File

@@ -16,6 +16,8 @@ const (
ProtocolHysteria2 Protocol = "hysteria2"
// ProtocolTUIC represents the TUIC protocol
ProtocolTUIC Protocol = "tuic"
// ProtocolAnyTLS represents the AnyTLS protocol
ProtocolAnyTLS Protocol = "anytls"
)
var validProtocols = map[Protocol]bool{
@@ -25,6 +27,7 @@ var validProtocols = map[Protocol]bool{
ProtocolVMess: true,
ProtocolHysteria2: true,
ProtocolTUIC: true,
ProtocolAnyTLS: true,
}
// String returns the string representation of the protocol
@@ -71,3 +74,8 @@ func (p Protocol) IsHysteria2() bool {
func (p Protocol) IsTUIC() bool {
return p == ProtocolTUIC
}
// IsAnyTLS checks if the protocol is AnyTLS
func (p Protocol) IsAnyTLS() bool {
return p == ProtocolAnyTLS
}

View File

@@ -0,0 +1,19 @@
-- +goose Up
CREATE TABLE node_anytls_configs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
node_id BIGINT UNSIGNED NOT NULL,
sni VARCHAR(255),
allow_insecure TINYINT(1) NOT NULL DEFAULT 1,
fingerprint VARCHAR(100),
idle_session_check_interval VARCHAR(20),
idle_session_timeout VARCHAR(20),
min_idle_session INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
UNIQUE INDEX idx_node_anytls_configs_node_id (node_id),
INDEX idx_node_anytls_configs_deleted_at (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- +goose Down
DROP TABLE IF EXISTS node_anytls_configs;

View File

@@ -0,0 +1,70 @@
package mappers
import (
"fmt"
vo "github.com/orris-inc/orris/internal/domain/node/valueobjects"
"github.com/orris-inc/orris/internal/infrastructure/persistence/models"
)
// AnyTLSConfigMapper handles the conversion between AnyTLSConfig value objects and persistence models
type AnyTLSConfigMapper interface {
// ToValueObject converts a persistence model to a domain value object
ToValueObject(model *models.AnyTLSConfigModel, password string) (*vo.AnyTLSConfig, error)
// ToModel converts a domain value object to a persistence model
ToModel(nodeID uint, config *vo.AnyTLSConfig) (*models.AnyTLSConfigModel, error)
}
// AnyTLSConfigMapperImpl is the concrete implementation of AnyTLSConfigMapper
type AnyTLSConfigMapperImpl struct{}
// NewAnyTLSConfigMapper creates a new AnyTLS config mapper
func NewAnyTLSConfigMapper() AnyTLSConfigMapper {
return &AnyTLSConfigMapperImpl{}
}
// ToValueObject converts a persistence model to a domain value object
// Password is passed separately as it's derived from subscription UUID, not stored in DB
func (m *AnyTLSConfigMapperImpl) ToValueObject(model *models.AnyTLSConfigModel, password string) (*vo.AnyTLSConfig, error) {
if model == nil {
return nil, nil
}
// Use placeholder password if not provided (for node entity reconstruction)
if password == "" {
password = PlaceholderPassword
}
config, err := vo.NewAnyTLSConfig(
password,
model.SNI,
model.AllowInsecure,
model.Fingerprint,
model.IdleSessionCheckInterval,
model.IdleSessionTimeout,
model.MinIdleSession,
)
if err != nil {
return nil, fmt.Errorf("failed to create anytls config value object: %w", err)
}
return &config, nil
}
// ToModel converts a domain value object to a persistence model
func (m *AnyTLSConfigMapperImpl) ToModel(nodeID uint, config *vo.AnyTLSConfig) (*models.AnyTLSConfigModel, error) {
if config == nil {
return nil, nil
}
return &models.AnyTLSConfigModel{
NodeID: nodeID,
SNI: config.SNI(),
AllowInsecure: config.AllowInsecure(),
Fingerprint: config.Fingerprint(),
IdleSessionCheckInterval: config.IdleSessionCheckInterval(),
IdleSessionTimeout: config.IdleSessionTimeout(),
MinIdleSession: config.MinIdleSession(),
}, nil
}

View File

@@ -21,7 +21,7 @@ import (
type NodeMapper interface {
// ToEntity converts a persistence model to a domain entity
// Protocol-specific configs are loaded separately from their respective tables
ToEntity(model *models.NodeModel, encryptionConfig vo.EncryptionConfig, pluginConfig *vo.PluginConfig, trojanConfig *vo.TrojanConfig, vlessConfig *vo.VLESSConfig, vmessConfig *vo.VMessConfig, hysteria2Config *vo.Hysteria2Config, tuicConfig *vo.TUICConfig) (*node.Node, error)
ToEntity(model *models.NodeModel, encryptionConfig vo.EncryptionConfig, pluginConfig *vo.PluginConfig, trojanConfig *vo.TrojanConfig, vlessConfig *vo.VLESSConfig, vmessConfig *vo.VMessConfig, hysteria2Config *vo.Hysteria2Config, tuicConfig *vo.TUICConfig, anytlsConfig *vo.AnyTLSConfig) (*node.Node, error)
// ToModel converts a domain entity to a persistence model
// Note: Protocol-specific configs are handled separately via their respective mappers
@@ -34,7 +34,8 @@ type NodeMapper interface {
// vmessConfigs is a map of nodeID -> VMessConfig
// hysteria2Configs is a map of nodeID -> Hysteria2Config
// tuicConfigs is a map of nodeID -> TUICConfig
ToEntities(models []*models.NodeModel, ssConfigs map[uint]*ShadowsocksConfigData, trojanConfigs map[uint]*vo.TrojanConfig, vlessConfigs map[uint]*vo.VLESSConfig, vmessConfigs map[uint]*vo.VMessConfig, hysteria2Configs map[uint]*vo.Hysteria2Config, tuicConfigs map[uint]*vo.TUICConfig) ([]*node.Node, error)
// anytlsConfigs is a map of nodeID -> AnyTLSConfig
ToEntities(models []*models.NodeModel, ssConfigs map[uint]*ShadowsocksConfigData, trojanConfigs map[uint]*vo.TrojanConfig, vlessConfigs map[uint]*vo.VLESSConfig, vmessConfigs map[uint]*vo.VMessConfig, hysteria2Configs map[uint]*vo.Hysteria2Config, tuicConfigs map[uint]*vo.TUICConfig, anytlsConfigs map[uint]*vo.AnyTLSConfig) ([]*node.Node, error)
// ToModels converts multiple domain entities to persistence models
ToModels(entities []*node.Node) ([]*models.NodeModel, error)
@@ -81,7 +82,7 @@ func NewNodeMapper() NodeMapper {
// ToEntity converts a persistence model to a domain entity
// Protocol-specific configs are loaded separately and passed in
func (m *NodeMapperImpl) ToEntity(model *models.NodeModel, encryptionConfig vo.EncryptionConfig, pluginConfig *vo.PluginConfig, trojanConfig *vo.TrojanConfig, vlessConfig *vo.VLESSConfig, vmessConfig *vo.VMessConfig, hysteria2Config *vo.Hysteria2Config, tuicConfig *vo.TUICConfig) (*node.Node, error) {
func (m *NodeMapperImpl) ToEntity(model *models.NodeModel, encryptionConfig vo.EncryptionConfig, pluginConfig *vo.PluginConfig, trojanConfig *vo.TrojanConfig, vlessConfig *vo.VLESSConfig, vmessConfig *vo.VMessConfig, hysteria2Config *vo.Hysteria2Config, tuicConfig *vo.TUICConfig, anytlsConfig *vo.AnyTLSConfig) (*node.Node, error) {
if model == nil {
return nil, nil
}
@@ -162,6 +163,7 @@ func (m *NodeMapperImpl) ToEntity(model *models.NodeModel, encryptionConfig vo.E
vmessConfig,
hysteria2Config,
tuicConfig,
anytlsConfig,
nodeStatus,
metadata,
groupIDs,
@@ -277,7 +279,7 @@ func (m *NodeMapperImpl) ToModel(entity *node.Node) (*models.NodeModel, error) {
// vmessConfigs is a map of nodeID -> VMessConfig
// hysteria2Configs is a map of nodeID -> Hysteria2Config
// tuicConfigs is a map of nodeID -> TUICConfig
func (m *NodeMapperImpl) ToEntities(nodeModels []*models.NodeModel, ssConfigs map[uint]*ShadowsocksConfigData, trojanConfigs map[uint]*vo.TrojanConfig, vlessConfigs map[uint]*vo.VLESSConfig, vmessConfigs map[uint]*vo.VMessConfig, hysteria2Configs map[uint]*vo.Hysteria2Config, tuicConfigs map[uint]*vo.TUICConfig) ([]*node.Node, error) {
func (m *NodeMapperImpl) ToEntities(nodeModels []*models.NodeModel, ssConfigs map[uint]*ShadowsocksConfigData, trojanConfigs map[uint]*vo.TrojanConfig, vlessConfigs map[uint]*vo.VLESSConfig, vmessConfigs map[uint]*vo.VMessConfig, hysteria2Configs map[uint]*vo.Hysteria2Config, tuicConfigs map[uint]*vo.TUICConfig, anytlsConfigs map[uint]*vo.AnyTLSConfig) ([]*node.Node, error) {
entities := make([]*node.Node, 0, len(nodeModels))
for _, model := range nodeModels {
@@ -289,6 +291,7 @@ func (m *NodeMapperImpl) ToEntities(nodeModels []*models.NodeModel, ssConfigs ma
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
switch model.Protocol {
case "shadowsocks":
@@ -318,9 +321,13 @@ func (m *NodeMapperImpl) ToEntities(nodeModels []*models.NodeModel, ssConfigs ma
if tuicConfigs != nil {
tuicConfig = tuicConfigs[model.ID]
}
case "anytls":
if anytlsConfigs != nil {
anytlsConfig = anytlsConfigs[model.ID]
}
}
entity, err := m.ToEntity(model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig)
entity, err := m.ToEntity(model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig, anytlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to map model ID %d: %w", model.ID, err)
}

View File

@@ -0,0 +1,29 @@
package models
import (
"time"
"gorm.io/gorm"
"github.com/orris-inc/orris/internal/shared/constants"
)
// AnyTLSConfigModel represents the database persistence model for AnyTLS protocol configuration
type AnyTLSConfigModel struct {
ID uint `gorm:"primarykey"`
NodeID uint `gorm:"uniqueIndex;not null"`
SNI string `gorm:"size:255"`
AllowInsecure bool `gorm:"not null;default:true"`
Fingerprint string `gorm:"size:100"`
IdleSessionCheckInterval string `gorm:"size:20"`
IdleSessionTimeout string `gorm:"size:20"`
MinIdleSession int `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// TableName specifies the table name for GORM
func (AnyTLSConfigModel) TableName() string {
return constants.TableNodeAnyTLSConfigs
}

View File

@@ -0,0 +1,189 @@
package repository
import (
"context"
"fmt"
"gorm.io/gorm"
vo "github.com/orris-inc/orris/internal/domain/node/valueobjects"
"github.com/orris-inc/orris/internal/infrastructure/persistence/mappers"
"github.com/orris-inc/orris/internal/infrastructure/persistence/models"
"github.com/orris-inc/orris/internal/shared/logger"
)
// AnyTLSConfigRepository handles persistence operations for AnyTLSConfig
type AnyTLSConfigRepository struct {
db *gorm.DB
mapper mappers.AnyTLSConfigMapper
logger logger.Interface
}
// NewAnyTLSConfigRepository creates a new AnyTLSConfigRepository
func NewAnyTLSConfigRepository(db *gorm.DB, logger logger.Interface) *AnyTLSConfigRepository {
return &AnyTLSConfigRepository{
db: db,
mapper: mappers.NewAnyTLSConfigMapper(),
logger: logger,
}
}
// Create creates a new AnyTLSConfig record for a node
func (r *AnyTLSConfigRepository) Create(ctx context.Context, nodeID uint, config *vo.AnyTLSConfig) error {
if config == nil {
return nil
}
model, err := r.mapper.ToModel(nodeID, config)
if err != nil {
return fmt.Errorf("failed to map anytls config to model: %w", err)
}
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
r.logger.Errorw("failed to create anytls config", "node_id", nodeID, "error", err)
return fmt.Errorf("failed to create anytls config: %w", err)
}
r.logger.Infow("anytls config created", "node_id", nodeID, "id", model.ID)
return nil
}
// GetByNodeID retrieves AnyTLSConfig for a specific node
func (r *AnyTLSConfigRepository) GetByNodeID(ctx context.Context, nodeID uint) (*vo.AnyTLSConfig, error) {
var model models.AnyTLSConfigModel
if err := r.db.WithContext(ctx).Where("node_id = ?", nodeID).First(&model).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
r.logger.Errorw("failed to get anytls config", "node_id", nodeID, "error", err)
return nil, fmt.Errorf("failed to get anytls config: %w", err)
}
return r.mapper.ToValueObject(&model, mappers.PlaceholderPassword)
}
// GetByNodeIDs retrieves AnyTLSConfigs for multiple nodes
// Returns a map of nodeID -> AnyTLSConfig
func (r *AnyTLSConfigRepository) GetByNodeIDs(ctx context.Context, nodeIDs []uint) (map[uint]*vo.AnyTLSConfig, error) {
if len(nodeIDs) == 0 {
return make(map[uint]*vo.AnyTLSConfig), nil
}
var anytlsModels []models.AnyTLSConfigModel
if err := r.db.WithContext(ctx).
Select("node_id", "sni", "allow_insecure", "fingerprint", "idle_session_check_interval", "idle_session_timeout", "min_idle_session").
Where("node_id IN ?", nodeIDs).
Find(&anytlsModels).Error; err != nil {
r.logger.Errorw("failed to get anytls configs by node IDs", "node_ids", nodeIDs, "error", err)
return nil, fmt.Errorf("failed to get anytls configs: %w", err)
}
result := make(map[uint]*vo.AnyTLSConfig)
for _, model := range anytlsModels {
config, err := r.mapper.ToValueObject(&model, mappers.PlaceholderPassword)
if err != nil {
r.logger.Warnw("failed to map anytls config", "node_id", model.NodeID, "error", err)
continue
}
result[model.NodeID] = config
}
return result, nil
}
// Update updates the AnyTLSConfig for a node
func (r *AnyTLSConfigRepository) Update(ctx context.Context, nodeID uint, config *vo.AnyTLSConfig) error {
if config == nil {
return r.DeleteByNodeID(ctx, nodeID)
}
var existing models.AnyTLSConfigModel
err := r.db.WithContext(ctx).Where("node_id = ?", nodeID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
return r.Create(ctx, nodeID, config)
}
if err != nil {
return fmt.Errorf("failed to check existing anytls config: %w", err)
}
model, err := r.mapper.ToModel(nodeID, config)
if err != nil {
return fmt.Errorf("failed to map anytls config to model: %w", err)
}
model.ID = existing.ID
model.CreatedAt = existing.CreatedAt
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
r.logger.Errorw("failed to update anytls config", "node_id", nodeID, "error", err)
return fmt.Errorf("failed to update anytls config: %w", err)
}
r.logger.Infow("anytls config updated", "node_id", nodeID)
return nil
}
// DeleteByNodeID deletes the AnyTLSConfig for a node
func (r *AnyTLSConfigRepository) DeleteByNodeID(ctx context.Context, nodeID uint) error {
result := r.db.WithContext(ctx).Where("node_id = ?", nodeID).Delete(&models.AnyTLSConfigModel{})
if result.Error != nil {
r.logger.Errorw("failed to delete anytls config", "node_id", nodeID, "error", result.Error)
return fmt.Errorf("failed to delete anytls config: %w", result.Error)
}
if result.RowsAffected > 0 {
r.logger.Infow("anytls config deleted", "node_id", nodeID)
}
return nil
}
// CreateInTx creates an AnyTLSConfig record within a transaction
func (r *AnyTLSConfigRepository) CreateInTx(tx *gorm.DB, nodeID uint, config *vo.AnyTLSConfig) error {
if config == nil {
return nil
}
model, err := r.mapper.ToModel(nodeID, config)
if err != nil {
return fmt.Errorf("failed to map anytls config to model: %w", err)
}
if err := tx.Create(model).Error; err != nil {
return fmt.Errorf("failed to create anytls config: %w", err)
}
return nil
}
// UpdateInTx updates an AnyTLSConfig record within a transaction
func (r *AnyTLSConfigRepository) UpdateInTx(tx *gorm.DB, nodeID uint, config *vo.AnyTLSConfig) error {
if config == nil {
return r.DeleteInTx(tx, nodeID)
}
var existing models.AnyTLSConfigModel
err := tx.Where("node_id = ?", nodeID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
return r.CreateInTx(tx, nodeID, config)
}
if err != nil {
return fmt.Errorf("failed to check existing anytls config: %w", err)
}
model, err := r.mapper.ToModel(nodeID, config)
if err != nil {
return fmt.Errorf("failed to map anytls config to model: %w", err)
}
model.ID = existing.ID
model.CreatedAt = existing.CreatedAt
if err := tx.Save(model).Error; err != nil {
return fmt.Errorf("failed to update anytls config: %w", err)
}
return nil
}
// DeleteInTx deletes an AnyTLSConfig record within a transaction
func (r *AnyTLSConfigRepository) DeleteInTx(tx *gorm.DB, nodeID uint) error {
return tx.Where("node_id = ?", nodeID).Delete(&models.AnyTLSConfigModel{}).Error
}

View File

@@ -47,6 +47,7 @@ type NodeRepositoryImpl struct {
vmessConfigRepo *VMessConfigRepository
hysteria2ConfigRepo *Hysteria2ConfigRepository
tuicConfigRepo *TUICConfigRepository
anytlsConfigRepo *AnyTLSConfigRepository
logger logger.Interface
}
@@ -61,6 +62,7 @@ func NewNodeRepository(db *gorm.DB, logger logger.Interface) node.NodeRepository
vmessConfigRepo: NewVMessConfigRepository(db, logger),
hysteria2ConfigRepo: NewHysteria2ConfigRepository(db, logger),
tuicConfigRepo: NewTUICConfigRepository(db, logger),
anytlsConfigRepo: NewAnyTLSConfigRepository(db, logger),
logger: logger,
}
}
@@ -126,6 +128,12 @@ func (r *NodeRepositoryImpl) Create(ctx context.Context, nodeEntity *node.Node)
return fmt.Errorf("failed to create tuic config: %w", err)
}
}
case vo.ProtocolAnyTLS:
if nodeEntity.AnyTLSConfig() != nil {
if err := r.anytlsConfigRepo.CreateInTx(tx, model.ID, nodeEntity.AnyTLSConfig()); err != nil {
return fmt.Errorf("failed to create anytls config: %w", err)
}
}
}
return nil
@@ -165,6 +173,7 @@ func (r *NodeRepositoryImpl) GetByID(ctx context.Context, id uint) (*node.Node,
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
switch model.Protocol {
case "shadowsocks":
@@ -209,9 +218,16 @@ func (r *NodeRepositoryImpl) GetByID(ctx context.Context, id uint) (*node.Node,
r.logger.Errorw("failed to get tuic config", "node_id", id, "error", err)
return nil, fmt.Errorf("failed to get tuic config: %w", err)
}
case "anytls":
var err error
anytlsConfig, err = r.anytlsConfigRepo.GetByNodeID(ctx, id)
if err != nil {
r.logger.Errorw("failed to get anytls config", "node_id", id, "error", err)
return nil, fmt.Errorf("failed to get anytls config: %w", err)
}
}
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig)
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig, anytlsConfig)
if err != nil {
r.logger.Errorw("failed to map node model to entity", "id", id, "error", err)
return nil, fmt.Errorf("failed to map node: %w", err)
@@ -240,6 +256,7 @@ func (r *NodeRepositoryImpl) GetBySID(ctx context.Context, sid string) (*node.No
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
switch model.Protocol {
case "shadowsocks":
@@ -284,9 +301,16 @@ func (r *NodeRepositoryImpl) GetBySID(ctx context.Context, sid string) (*node.No
r.logger.Errorw("failed to get tuic config", "node_id", model.ID, "error", err)
return nil, fmt.Errorf("failed to get tuic config: %w", err)
}
case "anytls":
var err error
anytlsConfig, err = r.anytlsConfigRepo.GetByNodeID(ctx, model.ID)
if err != nil {
r.logger.Errorw("failed to get anytls config", "node_id", model.ID, "error", err)
return nil, fmt.Errorf("failed to get anytls config: %w", err)
}
}
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig)
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig, anytlsConfig)
if err != nil {
r.logger.Errorw("failed to map node model to entity", "sid", sid, "error", err)
return nil, fmt.Errorf("failed to map node: %w", err)
@@ -364,8 +388,15 @@ func (r *NodeRepositoryImpl) GetBySIDs(ctx context.Context, sids []string) ([]*n
tuicConfigs = make(map[uint]*vo.TUICConfig)
}
// Load anytls configs
anytlsConfigs, err := r.anytlsConfigRepo.GetByNodeIDs(ctx, nodeIDs)
if err != nil {
r.logger.Warnw("failed to load anytls configs", "error", err)
anytlsConfigs = make(map[uint]*vo.AnyTLSConfig)
}
// Convert to entities
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs)
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs, anytlsConfigs)
if err != nil {
r.logger.Errorw("failed to map node models to entities", "error", err)
return nil, fmt.Errorf("failed to map nodes: %w", err)
@@ -443,8 +474,15 @@ func (r *NodeRepositoryImpl) GetByIDs(ctx context.Context, ids []uint) ([]*node.
tuicConfigs = make(map[uint]*vo.TUICConfig)
}
// Load anytls configs
anytlsConfigs, err := r.anytlsConfigRepo.GetByNodeIDs(ctx, nodeIDs)
if err != nil {
r.logger.Warnw("failed to load anytls configs", "error", err)
anytlsConfigs = make(map[uint]*vo.AnyTLSConfig)
}
// Convert to entities
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs)
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs, anytlsConfigs)
if err != nil {
r.logger.Errorw("failed to map node models to entities", "error", err)
return nil, fmt.Errorf("failed to map nodes: %w", err)
@@ -473,6 +511,7 @@ func (r *NodeRepositoryImpl) GetByToken(ctx context.Context, tokenHash string) (
var vmessConfig *vo.VMessConfig
var hysteria2Config *vo.Hysteria2Config
var tuicConfig *vo.TUICConfig
var anytlsConfig *vo.AnyTLSConfig
switch model.Protocol {
case "shadowsocks":
@@ -517,9 +556,16 @@ func (r *NodeRepositoryImpl) GetByToken(ctx context.Context, tokenHash string) (
r.logger.Errorw("failed to get tuic config", "node_id", model.ID, "error", err)
return nil, fmt.Errorf("failed to get tuic config: %w", err)
}
case "anytls":
var err error
anytlsConfig, err = r.anytlsConfigRepo.GetByNodeID(ctx, model.ID)
if err != nil {
r.logger.Errorw("failed to get anytls config", "node_id", model.ID, "error", err)
return nil, fmt.Errorf("failed to get anytls config: %w", err)
}
}
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig)
entity, err := r.mapper.ToEntity(&model, encryptionConfig, pluginConfig, trojanConfig, vlessConfig, vmessConfig, hysteria2Config, tuicConfig, anytlsConfig)
if err != nil {
r.logger.Errorw("failed to map node model to entity", "token_hash", tokenHash, "error", err)
return nil, fmt.Errorf("failed to map node: %w", err)
@@ -602,6 +648,9 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolTrojan:
if err := r.trojanConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.TrojanConfig()); err != nil {
return fmt.Errorf("failed to update trojan config: %w", err)
@@ -622,6 +671,9 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolVLESS:
if err := r.vlessConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.VLESSConfig()); err != nil {
return fmt.Errorf("failed to update vless config: %w", err)
@@ -642,6 +694,9 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolVMess:
if err := r.vmessConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.VMessConfig()); err != nil {
return fmt.Errorf("failed to update vmess config: %w", err)
@@ -662,6 +717,9 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolHysteria2:
if err := r.hysteria2ConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.Hysteria2Config()); err != nil {
return fmt.Errorf("failed to update hysteria2 config: %w", err)
@@ -682,6 +740,9 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolTUIC:
if err := r.tuicConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.TUICConfig()); err != nil {
return fmt.Errorf("failed to update tuic config: %w", err)
@@ -702,6 +763,32 @@ func (r *NodeRepositoryImpl) Update(ctx context.Context, nodeEntity *node.Node)
if err := r.hysteria2ConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete hysteria2 config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
case vo.ProtocolAnyTLS:
if err := r.anytlsConfigRepo.UpdateInTx(tx, model.ID, nodeEntity.AnyTLSConfig()); err != nil {
return fmt.Errorf("failed to update anytls config: %w", err)
}
// Delete other protocol configs if they exist (protocol changed)
if err := r.shadowsocksConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete shadowsocks config: %w", err)
}
if err := r.trojanConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete trojan config: %w", err)
}
if err := r.vlessConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete vless config: %w", err)
}
if err := r.vmessConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete vmess config: %w", err)
}
if err := r.hysteria2ConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete hysteria2 config: %w", err)
}
if err := r.tuicConfigRepo.DeleteInTx(tx, model.ID); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
}
return nil
@@ -738,6 +825,9 @@ func (r *NodeRepositoryImpl) Delete(ctx context.Context, id uint) error {
if err := r.tuicConfigRepo.DeleteInTx(tx, id); err != nil {
return fmt.Errorf("failed to delete tuic config: %w", err)
}
if err := r.anytlsConfigRepo.DeleteInTx(tx, id); err != nil {
return fmt.Errorf("failed to delete anytls config: %w", err)
}
// Hard delete node using Unscoped() to bypass soft delete
result := tx.Unscoped().Delete(&models.NodeModel{}, id)
@@ -829,6 +919,7 @@ func (r *NodeRepositoryImpl) List(ctx context.Context, filter node.NodeFilter) (
vmessNodeIDs := make([]uint, 0, protoCapacity)
hysteria2NodeIDs := make([]uint, 0, protoCapacity)
tuicNodeIDs := make([]uint, 0, protoCapacity)
anytlsNodeIDs := make([]uint, 0, protoCapacity)
for _, m := range nodeModels {
switch m.Protocol {
case "shadowsocks":
@@ -843,6 +934,8 @@ func (r *NodeRepositoryImpl) List(ctx context.Context, filter node.NodeFilter) (
hysteria2NodeIDs = append(hysteria2NodeIDs, m.ID)
case "tuic":
tuicNodeIDs = append(tuicNodeIDs, m.ID)
case "anytls":
anytlsNodeIDs = append(anytlsNodeIDs, m.ID)
}
}
@@ -856,6 +949,7 @@ func (r *NodeRepositoryImpl) List(ctx context.Context, filter node.NodeFilter) (
vmessConfigs map[uint]*vo.VMessConfig
hysteria2Configs map[uint]*vo.Hysteria2Config
tuicConfigs map[uint]*vo.TUICConfig
anytlsConfigs map[uint]*vo.AnyTLSConfig
)
g, gctx := errgroup.WithContext(ctx)
@@ -938,6 +1032,19 @@ func (r *NodeRepositoryImpl) List(ctx context.Context, filter node.NodeFilter) (
})
}
// AnyTLS configs
if len(anytlsNodeIDs) > 0 {
g.Go(func() error {
configs, err := r.anytlsConfigRepo.GetByNodeIDs(gctx, anytlsNodeIDs)
if err != nil {
r.logger.Errorw("failed to get anytls configs", "error", err)
return fmt.Errorf("failed to get anytls configs: %w", err)
}
anytlsConfigs = configs
return nil
})
}
// Wait for all goroutines to complete
if err := g.Wait(); err != nil {
return nil, 0, err
@@ -953,7 +1060,7 @@ func (r *NodeRepositoryImpl) List(ctx context.Context, filter node.NodeFilter) (
}
// Convert models to entities
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs)
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs, anytlsConfigs)
if err != nil {
r.logger.Errorw("failed to map node models to entities", "error", err)
return nil, 0, fmt.Errorf("failed to map nodes: %w", err)
@@ -1530,6 +1637,7 @@ func (r *NodeRepositoryImpl) loadProtocolConfigsAndConvert(ctx context.Context,
vmessNodeIDs := make([]uint, 0, protoCapacity)
hysteria2NodeIDs := make([]uint, 0, protoCapacity)
tuicNodeIDs := make([]uint, 0, protoCapacity)
anytlsNodeIDs := make([]uint, 0, protoCapacity)
for _, m := range nodeModels {
switch m.Protocol {
case "shadowsocks":
@@ -1544,6 +1652,8 @@ func (r *NodeRepositoryImpl) loadProtocolConfigsAndConvert(ctx context.Context,
hysteria2NodeIDs = append(hysteria2NodeIDs, m.ID)
case "tuic":
tuicNodeIDs = append(tuicNodeIDs, m.ID)
case "anytls":
anytlsNodeIDs = append(anytlsNodeIDs, m.ID)
}
}
@@ -1557,6 +1667,7 @@ func (r *NodeRepositoryImpl) loadProtocolConfigsAndConvert(ctx context.Context,
vmessConfigs map[uint]*vo.VMessConfig
hysteria2Configs map[uint]*vo.Hysteria2Config
tuicConfigs map[uint]*vo.TUICConfig
anytlsConfigs map[uint]*vo.AnyTLSConfig
)
g, gctx := errgroup.WithContext(ctx)
@@ -1621,6 +1732,16 @@ func (r *NodeRepositoryImpl) loadProtocolConfigsAndConvert(ctx context.Context,
return nil
})
}
if len(anytlsNodeIDs) > 0 {
g.Go(func() error {
configs, err := r.anytlsConfigRepo.GetByNodeIDs(gctx, anytlsNodeIDs)
if err != nil {
return fmt.Errorf("failed to get anytls configs: %w", err)
}
anytlsConfigs = configs
return nil
})
}
if err := g.Wait(); err != nil {
r.logger.Errorw("failed to load protocol configs", "error", err)
@@ -1636,7 +1757,7 @@ func (r *NodeRepositoryImpl) loadProtocolConfigsAndConvert(ctx context.Context,
}
}
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs)
entities, err := r.mapper.ToEntities(nodeModels, ssConfigs, trojanConfigs, vlessConfigs, vmessConfigs, hysteria2Configs, tuicConfigs, anytlsConfigs)
if err != nil {
r.logger.Errorw("failed to map node models to entities", "error", err)
return nil, fmt.Errorf("failed to map nodes: %w", err)

View File

@@ -206,6 +206,10 @@ func (r *PlanRepositoryImpl) List(ctx context.Context, filter subscription.PlanF
query = query.Where("is_public = ?", *filter.IsPublic)
}
if filter.PlanType != nil && *filter.PlanType != "" {
query = query.Where("plan_type = ?", *filter.PlanType)
}
var total int64
if err := query.Count(&total).Error; err != nil {
r.logger.Errorw("failed to count subscription plans", "error", err)

View File

@@ -29,6 +29,7 @@ type ProtocolConfigs struct {
VMess map[uint]*models.VMessConfigModel
Hysteria2 map[uint]*models.Hysteria2ConfigModel
TUIC map[uint]*models.TUICConfigModel
AnyTLS map[uint]*models.AnyTLSConfigModel
}
// NewProtocolConfigs creates an empty ProtocolConfigs instance.
@@ -40,6 +41,7 @@ func NewProtocolConfigs() ProtocolConfigs {
VMess: make(map[uint]*models.VMessConfigModel),
Hysteria2: make(map[uint]*models.Hysteria2ConfigModel),
TUIC: make(map[uint]*models.TUICConfigModel),
AnyTLS: make(map[uint]*models.AnyTLSConfigModel),
}
}
@@ -85,6 +87,8 @@ func ApplyProtocolConfig(node *usecases.Node, protocol string, nodeID uint, conf
applyHysteria2Config(node, nodeID, configs.Hysteria2)
case "tuic":
applyTUICConfig(node, nodeID, configs.TUIC)
case "anytls":
applyAnyTLSConfig(node, nodeID, configs.AnyTLS)
}
}
@@ -198,6 +202,21 @@ func applyTUICConfig(node *usecases.Node, nodeID uint, configs map[uint]*models.
node.TUICConfig = config
}
// applyAnyTLSConfig applies AnyTLS-specific configuration to a node.
func applyAnyTLSConfig(node *usecases.Node, nodeID uint, configs map[uint]*models.AnyTLSConfigModel) {
ac, ok := configs[nodeID]
if !ok {
return
}
mapper := mappers.NewAnyTLSConfigMapper()
config, err := mapper.ToValueObject(ac, mappers.PlaceholderPassword)
if err != nil {
return
}
node.AnyTLSConfig = config
}
// ResolveServerAddress returns the effective server address for subscription.
// If server address is configured, use it; otherwise fall back to agent's reported public IP.
func ResolveServerAddress(configuredAddr string, publicIPv4, publicIPv6 *string) string {
@@ -251,4 +270,5 @@ func CopyProtocolFieldsFromNode(dst, src *usecases.Node) {
dst.VMessConfig = src.VMessConfig
dst.Hysteria2Config = src.Hysteria2Config
dst.TUICConfig = src.TUICConfig
dst.AnyTLSConfig = src.AnyTLSConfig
}

View File

@@ -37,6 +37,7 @@ func (l *ConfigLoader) LoadProtocolConfigs(ctx context.Context, nodeModels []mod
l.loadConfigsIntoMap(ctx, nodeIDsByProtocol["vmess"], &configs.VMess)
l.loadConfigsIntoMap(ctx, nodeIDsByProtocol["hysteria2"], &configs.Hysteria2)
l.loadConfigsIntoMap(ctx, nodeIDsByProtocol["tuic"], &configs.TUIC)
l.loadConfigsIntoMap(ctx, nodeIDsByProtocol["anytls"], &configs.AnyTLS)
return configs
}
@@ -75,6 +76,8 @@ func (l *ConfigLoader) loadConfigsIntoMap(ctx context.Context, nodeIDs []uint, t
l.loadHysteria2Configs(ctx, nodeIDs, m)
case *map[uint]*models.TUICConfigModel:
l.loadTUICConfigs(ctx, nodeIDs, m)
case *map[uint]*models.AnyTLSConfigModel:
l.loadAnyTLSConfigs(ctx, nodeIDs, m)
}
}
@@ -161,3 +164,17 @@ func (l *ConfigLoader) loadTUICConfigs(ctx context.Context, nodeIDs []uint, targ
(*targetMap)[configs[i].NodeID] = &configs[i]
}
}
// loadAnyTLSConfigs loads AnyTLS configs into the provided map.
func (l *ConfigLoader) loadAnyTLSConfigs(ctx context.Context, nodeIDs []uint, targetMap *map[uint]*models.AnyTLSConfigModel) {
var configs []models.AnyTLSConfigModel
if err := l.db.WithContext(ctx).
Where("node_id IN ?", nodeIDs).
Find(&configs).Error; err != nil {
l.logger.Warnw("failed to query AnyTLS configs", "error", err)
return
}
for i := range configs {
(*targetMap)[configs[i].NodeID] = &configs[i]
}
}

View File

@@ -280,7 +280,7 @@ type CreateNodeRequest struct {
ServerAddress string `json:"server_address,omitempty" example:"1.2.3.4"`
AgentPort uint16 `json:"agent_port" binding:"required" example:"8388" comment:"Port for agent connections"`
SubscriptionPort *uint16 `json:"subscription_port,omitempty" example:"8389" comment:"Port for client subscriptions (if null, uses agent_port)"`
Protocol string `json:"protocol" binding:"required,oneof=shadowsocks trojan vless vmess hysteria2 tuic" example:"shadowsocks" comment:"Protocol type"`
Protocol string `json:"protocol" binding:"required,oneof=shadowsocks trojan vless vmess hysteria2 tuic anytls" example:"shadowsocks" comment:"Protocol type"`
EncryptionMethod string `json:"encryption_method,omitempty" example:"aes-256-gcm" comment:"Encryption method (for Shadowsocks)"`
Plugin *string `json:"plugin,omitempty" example:"obfs-local"`
PluginOpts map[string]string `json:"plugin_opts,omitempty"`
@@ -341,6 +341,14 @@ type CreateNodeRequest struct {
TUICSni string `json:"tuic_sni,omitempty" comment:"TUIC TLS SNI"`
TUICAllowInsecure bool `json:"tuic_allow_insecure,omitempty" comment:"TUIC allow insecure TLS"`
TUICDisableSNI bool `json:"tuic_disable_sni,omitempty" comment:"TUIC disable SNI"`
// AnyTLS specific fields
AnyTLSSni string `json:"anytls_sni,omitempty" comment:"AnyTLS TLS SNI"`
AnyTLSAllowInsecure bool `json:"anytls_allow_insecure,omitempty" comment:"AnyTLS allow insecure TLS"`
AnyTLSFingerprint string `json:"anytls_fingerprint,omitempty" comment:"AnyTLS TLS fingerprint"`
AnyTLSIdleSessionCheckInterval string `json:"anytls_idle_session_check_interval,omitempty" comment:"AnyTLS idle session check interval"`
AnyTLSIdleSessionTimeout string `json:"anytls_idle_session_timeout,omitempty" comment:"AnyTLS idle session timeout"`
AnyTLSMinIdleSession int `json:"anytls_min_idle_session,omitempty" comment:"AnyTLS minimum idle sessions"`
}
func (r *CreateNodeRequest) ToCommand() usecases.CreateNodeCommand {
@@ -404,6 +412,13 @@ func (r *CreateNodeRequest) ToCommand() usecases.CreateNodeCommand {
TUICSni: r.TUICSni,
TUICAllowInsecure: r.TUICAllowInsecure,
TUICDisableSNI: r.TUICDisableSNI,
// AnyTLS
AnyTLSSni: r.AnyTLSSni,
AnyTLSAllowInsecure: r.AnyTLSAllowInsecure,
AnyTLSFingerprint: r.AnyTLSFingerprint,
AnyTLSIdleSessionCheckInterval: r.AnyTLSIdleSessionCheckInterval,
AnyTLSIdleSessionTimeout: r.AnyTLSIdleSessionTimeout,
AnyTLSMinIdleSession: r.AnyTLSMinIdleSession,
}
}
@@ -476,6 +491,14 @@ type UpdateNodeRequest struct {
TUICAllowInsecure *bool `json:"tuic_allow_insecure,omitempty" comment:"TUIC allow insecure TLS"`
TUICDisableSNI *bool `json:"tuic_disable_sni,omitempty" comment:"TUIC disable SNI"`
// AnyTLS specific fields
AnyTLSSni *string `json:"anytls_sni,omitempty" comment:"AnyTLS TLS SNI"`
AnyTLSAllowInsecure *bool `json:"anytls_allow_insecure,omitempty" comment:"AnyTLS allow insecure TLS"`
AnyTLSFingerprint *string `json:"anytls_fingerprint,omitempty" comment:"AnyTLS TLS fingerprint"`
AnyTLSIdleSessionCheckInterval *string `json:"anytls_idle_session_check_interval,omitempty" comment:"AnyTLS idle session check interval"`
AnyTLSIdleSessionTimeout *string `json:"anytls_idle_session_timeout,omitempty" comment:"AnyTLS idle session timeout"`
AnyTLSMinIdleSession *int `json:"anytls_min_idle_session,omitempty" comment:"AnyTLS minimum idle sessions"`
// Expiration and cost label fields
ExpiresAt *string `json:"expires_at,omitempty" example:"2025-12-31T23:59:59Z" comment:"Expiration time in ISO8601 format (empty string to clear, omit to keep unchanged)"`
CostLabel *string `json:"cost_label,omitempty" example:"35$/m" comment:"Cost label for display (empty string to clear, omit to keep unchanged)"`
@@ -545,6 +568,13 @@ func (r *UpdateNodeRequest) ToCommand(sid string) usecases.UpdateNodeCommand {
TUICSni: r.TUICSni,
TUICAllowInsecure: r.TUICAllowInsecure,
TUICDisableSNI: r.TUICDisableSNI,
// AnyTLS
AnyTLSSni: r.AnyTLSSni,
AnyTLSAllowInsecure: r.AnyTLSAllowInsecure,
AnyTLSFingerprint: r.AnyTLSFingerprint,
AnyTLSIdleSessionCheckInterval: r.AnyTLSIdleSessionCheckInterval,
AnyTLSIdleSessionTimeout: r.AnyTLSIdleSessionTimeout,
AnyTLSMinIdleSession: r.AnyTLSMinIdleSession,
}
// Note: ExpiresAt is handled by the handler layer after ToCommand returns.

View File

@@ -276,6 +276,9 @@ func (h *UserNodeHandler) GetInstallScript(c *gin.Context) {
// CreateUserNodeRequest represents the request body for creating a user node
// TODO: Protocol binding only allows shadowsocks/trojan. CreateUserNodeCommand already supports
// all 7 protocols (vless, vmess, hysteria2, tuic, anytls). Need to add protocol-specific fields
// and update ToCommand() mapping if user nodes should support all protocols.
type CreateUserNodeRequest struct {
Name string `json:"name" binding:"required,min=2,max=100" example:"My-Node-01"`
ServerAddress string `json:"server_address,omitempty" example:"1.2.3.4"`

View File

@@ -48,6 +48,7 @@ const (
TablePasskeyCredentials = "passkey_credentials"
TableUSDTAmountSuffixes = "usdt_amount_suffixes"
TableUserAnnouncementReads = "user_announcement_reads"
TableNodeAnyTLSConfigs = "node_anytls_configs"
// Default values
DefaultCurrency = "CNY"