mirror of
https://github.com/orris-inc/orris.git
synced 2026-05-06 21:44:01 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
210
internal/domain/node/valueobjects/anytlsconfig.go
Normal file
210
internal/domain/node/valueobjects/anytlsconfig.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
189
internal/infrastructure/repository/anytlsconfigrepository.go
Normal file
189
internal/infrastructure/repository/anytlsconfigrepository.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -48,6 +48,7 @@ const (
|
||||
TablePasskeyCredentials = "passkey_credentials"
|
||||
TableUSDTAmountSuffixes = "usdt_amount_suffixes"
|
||||
TableUserAnnouncementReads = "user_announcement_reads"
|
||||
TableNodeAnyTLSConfigs = "node_anytls_configs"
|
||||
|
||||
// Default values
|
||||
DefaultCurrency = "CNY"
|
||||
|
||||
Reference in New Issue
Block a user