Files
orris/internal/domain/node/node.go
orris-inc e33acff06a 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.
2026-02-11 18:30:46 +08:00

1042 lines
26 KiB
Go

// Package node provides domain models and business logic for node management.
// It includes the Node aggregate root, node groups, traffic tracking, and token validation.
package node
import (
"crypto/subtle"
"fmt"
"sync"
"time"
vo "github.com/orris-inc/orris/internal/domain/node/valueobjects"
"github.com/orris-inc/orris/internal/domain/shared"
"github.com/orris-inc/orris/internal/domain/shared/services"
"github.com/orris-inc/orris/internal/shared/biztime"
)
// Node represents the node aggregate root
type Node struct {
mu sync.RWMutex // protects concurrent access to mutable fields
id uint
sid string // external API identifier (Stripe-style)
name string
serverAddress vo.ServerAddress
agentPort uint16 // port for agent connections (required)
subscriptionPort *uint16 // port for client subscriptions (if nil, use agentPort)
protocol vo.Protocol
encryptionConfig vo.EncryptionConfig
pluginConfig *vo.PluginConfig
trojanConfig *vo.TrojanConfig
vlessConfig *vo.VLESSConfig
vmessConfig *vo.VMessConfig
hysteria2Config *vo.Hysteria2Config
tuicConfig *vo.TUICConfig
anytlsConfig *vo.AnyTLSConfig
status vo.NodeStatus
metadata vo.NodeMetadata
groupIDs []uint // resource group IDs
userID *uint // owner user ID (nil = admin created, non-nil = user created)
apiToken string
tokenHash string
sortOrder int
muteNotification bool // mute online/offline notifications for this node
maintenanceReason *string
routeConfig *vo.RouteConfig // routing configuration for traffic splitting
lastSeenAt *time.Time // last time the node agent reported status
publicIPv4 *string // public IPv4 address reported by agent
publicIPv6 *string // public IPv6 address reported by agent
agentVersion *string // agent software version (e.g., "1.2.3")
platform *string // OS platform (linux, darwin, windows)
arch *string // CPU architecture (amd64, arm64, arm, 386)
expiresAt *time.Time // expiration time (nil = never expires)
costLabel *string // cost label for display (e.g., "35$/m", "35¥/y")
version int
originalVersion int // version when loaded from database, for optimistic locking
createdAt time.Time
updatedAt time.Time
tokenGenerator services.TokenGenerator
}
// NewNode creates a new node aggregate
func NewNode(
name string,
serverAddress vo.ServerAddress,
agentPort uint16,
subscriptionPort *uint16,
protocol vo.Protocol,
encryptionConfig vo.EncryptionConfig,
pluginConfig *vo.PluginConfig,
trojanConfig *vo.TrojanConfig,
vlessConfig *vo.VLESSConfig,
vmessConfig *vo.VMessConfig,
hysteria2Config *vo.Hysteria2Config,
tuicConfig *vo.TUICConfig,
anytlsConfig *vo.AnyTLSConfig,
metadata vo.NodeMetadata,
sortOrder int,
routeConfig *vo.RouteConfig,
sidGenerator func() (string, error),
) (*Node, error) {
if name == "" {
return nil, fmt.Errorf("node name is required")
}
if agentPort == 0 {
return nil, fmt.Errorf("agent port is required")
}
if !protocol.IsValid() {
return nil, fmt.Errorf("invalid protocol: %s", protocol)
}
// Validate protocol-specific configurations
if protocol.IsShadowsocks() && encryptionConfig.Method() == "" {
return nil, fmt.Errorf("encryption config is required for Shadowsocks protocol")
}
if protocol.IsTrojan() && trojanConfig == nil {
return nil, fmt.Errorf("trojan config is required for Trojan protocol")
}
if protocol.IsVLESS() && vlessConfig == nil {
return nil, fmt.Errorf("vless config is required for VLESS protocol")
}
if protocol.IsVMess() && vmessConfig == nil {
return nil, fmt.Errorf("vmess config is required for VMess protocol")
}
if protocol.IsHysteria2() && hysteria2Config == nil {
return nil, fmt.Errorf("hysteria2 config is required for Hysteria2 protocol")
}
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 {
if err := routeConfig.Validate(); err != nil {
return nil, fmt.Errorf("invalid route config: %w", err)
}
}
tokenGen := services.NewTokenGenerator()
plainToken, tokenHash, err := tokenGen.GenerateAPIToken("node")
if err != nil {
return nil, fmt.Errorf("failed to generate API token: %w", err)
}
// Generate SID for external API use
sid, err := sidGenerator()
if err != nil {
return nil, fmt.Errorf("failed to generate SID: %w", err)
}
now := biztime.NowUTC()
n := &Node{
sid: sid,
name: name,
serverAddress: serverAddress,
agentPort: agentPort,
subscriptionPort: subscriptionPort,
protocol: protocol,
encryptionConfig: encryptionConfig,
pluginConfig: pluginConfig,
trojanConfig: trojanConfig,
vlessConfig: vlessConfig,
vmessConfig: vmessConfig,
hysteria2Config: hysteria2Config,
tuicConfig: tuicConfig,
anytlsConfig: anytlsConfig,
status: vo.NodeStatusInactive,
metadata: metadata,
apiToken: plainToken,
tokenHash: tokenHash,
sortOrder: sortOrder,
routeConfig: routeConfig,
version: 1,
createdAt: now,
updatedAt: now,
tokenGenerator: tokenGen,
}
return n, nil
}
// ReconstructNode reconstructs a node from persistence
func ReconstructNode(
id uint,
sid string,
name string,
serverAddress vo.ServerAddress,
agentPort uint16,
subscriptionPort *uint16,
protocol vo.Protocol,
encryptionConfig vo.EncryptionConfig,
pluginConfig *vo.PluginConfig,
trojanConfig *vo.TrojanConfig,
vlessConfig *vo.VLESSConfig,
vmessConfig *vo.VMessConfig,
hysteria2Config *vo.Hysteria2Config,
tuicConfig *vo.TUICConfig,
anytlsConfig *vo.AnyTLSConfig,
status vo.NodeStatus,
metadata vo.NodeMetadata,
groupIDs []uint,
userID *uint,
tokenHash string,
apiToken string,
sortOrder int,
muteNotification bool,
maintenanceReason *string,
routeConfig *vo.RouteConfig,
lastSeenAt *time.Time,
publicIPv4 *string,
publicIPv6 *string,
agentVersion *string,
platform *string,
arch *string,
expiresAt *time.Time,
costLabel *string,
version int,
createdAt, updatedAt time.Time,
) (*Node, error) {
if id == 0 {
return nil, fmt.Errorf("node ID cannot be zero")
}
if sid == "" {
return nil, fmt.Errorf("node SID is required")
}
if name == "" {
return nil, fmt.Errorf("node name is required")
}
if agentPort == 0 {
return nil, fmt.Errorf("agent port is required")
}
if tokenHash == "" {
return nil, fmt.Errorf("token hash is required")
}
if !protocol.IsValid() {
return nil, fmt.Errorf("invalid protocol: %s", protocol)
}
return &Node{
id: id,
sid: sid,
name: name,
serverAddress: serverAddress,
agentPort: agentPort,
subscriptionPort: subscriptionPort,
protocol: protocol,
encryptionConfig: encryptionConfig,
pluginConfig: pluginConfig,
trojanConfig: trojanConfig,
vlessConfig: vlessConfig,
vmessConfig: vmessConfig,
hysteria2Config: hysteria2Config,
tuicConfig: tuicConfig,
anytlsConfig: anytlsConfig,
status: status,
metadata: metadata,
groupIDs: groupIDs,
userID: userID,
tokenHash: tokenHash,
apiToken: apiToken,
sortOrder: sortOrder,
muteNotification: muteNotification,
maintenanceReason: maintenanceReason,
routeConfig: routeConfig,
lastSeenAt: lastSeenAt,
publicIPv4: publicIPv4,
publicIPv6: publicIPv6,
agentVersion: agentVersion,
platform: platform,
arch: arch,
expiresAt: expiresAt,
costLabel: costLabel,
version: version,
originalVersion: version, // preserve original version for optimistic locking
createdAt: createdAt,
updatedAt: updatedAt,
tokenGenerator: services.NewTokenGenerator(),
}, nil
}
// ID returns the node ID
func (n *Node) ID() uint {
return n.id
}
// SID returns the external API identifier
func (n *Node) SID() string {
return n.sid
}
// Name returns the node name
func (n *Node) Name() string {
return n.name
}
// ServerAddress returns the server address
func (n *Node) ServerAddress() vo.ServerAddress {
return n.serverAddress
}
// AgentPort returns the agent connection port
func (n *Node) AgentPort() uint16 {
return n.agentPort
}
// SubscriptionPort returns the subscription port (may be nil)
func (n *Node) SubscriptionPort() *uint16 {
return n.subscriptionPort
}
// EffectiveSubscriptionPort returns the port to use for subscriptions
// If subscriptionPort is nil, returns agentPort
func (n *Node) EffectiveSubscriptionPort() uint16 {
if n.subscriptionPort != nil {
return *n.subscriptionPort
}
return n.agentPort
}
// Protocol returns the protocol type
func (n *Node) Protocol() vo.Protocol {
return n.protocol
}
// EncryptionConfig returns the encryption configuration
func (n *Node) EncryptionConfig() vo.EncryptionConfig {
return n.encryptionConfig
}
// PluginConfig returns the plugin configuration
func (n *Node) PluginConfig() *vo.PluginConfig {
return n.pluginConfig
}
// TrojanConfig returns the trojan configuration
func (n *Node) TrojanConfig() *vo.TrojanConfig {
return n.trojanConfig
}
// VLESSConfig returns the VLESS configuration
func (n *Node) VLESSConfig() *vo.VLESSConfig {
return n.vlessConfig
}
// VMessConfig returns the VMess configuration
func (n *Node) VMessConfig() *vo.VMessConfig {
return n.vmessConfig
}
// Hysteria2Config returns the Hysteria2 configuration
func (n *Node) Hysteria2Config() *vo.Hysteria2Config {
return n.hysteria2Config
}
// TUICConfig returns the TUIC configuration
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
}
// Metadata returns the node metadata
func (n *Node) Metadata() vo.NodeMetadata {
return n.metadata
}
// GroupIDs returns the resource group IDs
func (n *Node) GroupIDs() []uint {
return n.groupIDs
}
// UserID returns the owner user ID (nil for admin-created nodes)
func (n *Node) UserID() *uint {
return n.userID
}
// SetUserID sets the owner user ID
func (n *Node) SetUserID(userID *uint) {
n.mu.Lock()
defer n.mu.Unlock()
n.userID = userID
n.updatedAt = biztime.NowUTC()
n.version++
}
// SetGroupIDs sets the resource group IDs
func (n *Node) SetGroupIDs(groupIDs []uint) {
n.mu.Lock()
defer n.mu.Unlock()
n.groupIDs = groupIDs
n.updatedAt = biztime.NowUTC()
n.version++
}
// AddGroupID adds a resource group ID if not already present
func (n *Node) AddGroupID(groupID uint) bool {
n.mu.Lock()
defer n.mu.Unlock()
newIDs, added := shared.AddToGroupIDs(n.groupIDs, groupID)
if added {
n.groupIDs = newIDs
n.updatedAt = biztime.NowUTC()
n.version++
}
return added
}
// RemoveGroupID removes a resource group ID
func (n *Node) RemoveGroupID(groupID uint) bool {
n.mu.Lock()
defer n.mu.Unlock()
newIDs, removed := shared.RemoveFromGroupIDs(n.groupIDs, groupID)
if removed {
n.groupIDs = newIDs
n.updatedAt = biztime.NowUTC()
n.version++
}
return removed
}
// HasGroupID checks if the node belongs to a specific resource group
func (n *Node) HasGroupID(groupID uint) bool {
n.mu.RLock()
defer n.mu.RUnlock()
return shared.HasGroupID(n.groupIDs, groupID)
}
// TokenHash returns the API token hash
func (n *Node) TokenHash() string {
return n.tokenHash
}
// SortOrder returns the sort order
func (n *Node) SortOrder() int {
return n.sortOrder
}
// MaintenanceReason returns the maintenance reason
func (n *Node) MaintenanceReason() *string {
return n.maintenanceReason
}
// RouteConfig returns the routing configuration
func (n *Node) RouteConfig() *vo.RouteConfig {
return n.routeConfig
}
// Version returns the aggregate version for optimistic locking
func (n *Node) Version() int {
return n.version
}
// OriginalVersion returns the version when the entity was loaded from database.
// This is used for optimistic locking to detect concurrent modifications.
func (n *Node) OriginalVersion() int {
return n.originalVersion
}
// CreatedAt returns when the node was created
func (n *Node) CreatedAt() time.Time {
return n.createdAt
}
// UpdatedAt returns when the node was last updated
func (n *Node) UpdatedAt() time.Time {
return n.updatedAt
}
// SetID sets the node ID (only for persistence layer use)
func (n *Node) SetID(id uint) error {
if n.id != 0 {
return fmt.Errorf("node ID is already set")
}
if id == 0 {
return fmt.Errorf("node ID cannot be zero")
}
n.id = id
return nil
}
// Activate activates the node
func (n *Node) Activate() error {
n.mu.Lock()
defer n.mu.Unlock()
if n.status == vo.NodeStatusActive {
return nil
}
if !n.status.CanTransitionTo(vo.NodeStatusActive) {
return fmt.Errorf("cannot activate node with status %s", n.status)
}
n.status = vo.NodeStatusActive
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// Deactivate deactivates the node
func (n *Node) Deactivate() error {
n.mu.Lock()
defer n.mu.Unlock()
if n.status == vo.NodeStatusInactive {
return nil
}
if !n.status.CanTransitionTo(vo.NodeStatusInactive) {
return fmt.Errorf("cannot deactivate node with status %s", n.status)
}
n.status = vo.NodeStatusInactive
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// EnterMaintenance puts the node into maintenance mode
func (n *Node) EnterMaintenance(reason string) error {
if reason == "" {
return fmt.Errorf("maintenance reason is required")
}
n.mu.Lock()
defer n.mu.Unlock()
if n.status == vo.NodeStatusMaintenance {
return nil
}
if !n.status.CanTransitionTo(vo.NodeStatusMaintenance) {
return fmt.Errorf("cannot enter maintenance mode from status %s", n.status)
}
n.status = vo.NodeStatusMaintenance
n.maintenanceReason = &reason
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// ExitMaintenance exits maintenance mode and returns to active status
func (n *Node) ExitMaintenance() error {
n.mu.Lock()
defer n.mu.Unlock()
if n.status != vo.NodeStatusMaintenance {
return fmt.Errorf("node is not in maintenance mode")
}
n.status = vo.NodeStatusActive
n.maintenanceReason = nil
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateServerAddress updates the server address
func (n *Node) UpdateServerAddress(address vo.ServerAddress) error {
n.mu.Lock()
defer n.mu.Unlock()
if n.serverAddress.Value() == address.Value() {
return nil
}
n.serverAddress = address
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateAgentPort updates the agent connection port
func (n *Node) UpdateAgentPort(port uint16) error {
if port == 0 {
return fmt.Errorf("agent port cannot be zero")
}
n.mu.Lock()
defer n.mu.Unlock()
if n.agentPort == port {
return nil
}
n.agentPort = port
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateSubscriptionPort updates the subscription port
func (n *Node) UpdateSubscriptionPort(port *uint16) error {
if port != nil && *port == 0 {
return fmt.Errorf("subscription port cannot be zero")
}
n.mu.Lock()
defer n.mu.Unlock()
// Check if values are equal
if n.subscriptionPort == nil && port == nil {
return nil
}
if n.subscriptionPort != nil && port != nil && *n.subscriptionPort == *port {
return nil
}
n.subscriptionPort = port
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateEncryption updates the encryption configuration
func (n *Node) UpdateEncryption(config vo.EncryptionConfig) error {
n.mu.Lock()
defer n.mu.Unlock()
n.encryptionConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdatePlugin updates the plugin configuration
func (n *Node) UpdatePlugin(config *vo.PluginConfig) error {
n.mu.Lock()
defer n.mu.Unlock()
n.pluginConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateTrojanConfig updates the trojan configuration
func (n *Node) UpdateTrojanConfig(config *vo.TrojanConfig) error {
if !n.protocol.IsTrojan() {
return fmt.Errorf("cannot update trojan config for non-trojan protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.trojanConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateVLESSConfig updates the VLESS configuration
func (n *Node) UpdateVLESSConfig(config *vo.VLESSConfig) error {
if !n.protocol.IsVLESS() {
return fmt.Errorf("cannot update vless config for non-vless protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.vlessConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateVMessConfig updates the VMess configuration
func (n *Node) UpdateVMessConfig(config *vo.VMessConfig) error {
if !n.protocol.IsVMess() {
return fmt.Errorf("cannot update vmess config for non-vmess protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.vmessConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateHysteria2Config updates the Hysteria2 configuration
func (n *Node) UpdateHysteria2Config(config *vo.Hysteria2Config) error {
if !n.protocol.IsHysteria2() {
return fmt.Errorf("cannot update hysteria2 config for non-hysteria2 protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.hysteria2Config = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateTUICConfig updates the TUIC configuration
func (n *Node) UpdateTUICConfig(config *vo.TUICConfig) error {
if !n.protocol.IsTUIC() {
return fmt.Errorf("cannot update tuic config for non-tuic protocol")
}
n.mu.Lock()
defer n.mu.Unlock()
n.tuicConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
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()
defer n.mu.Unlock()
n.metadata = metadata
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateName updates the node name
func (n *Node) UpdateName(name string) error {
if name == "" {
return fmt.Errorf("node name cannot be empty")
}
n.mu.Lock()
defer n.mu.Unlock()
if n.name == name {
return nil
}
n.name = name
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// UpdateSortOrder updates the sort order
func (n *Node) UpdateSortOrder(order int) error {
n.mu.Lock()
defer n.mu.Unlock()
n.sortOrder = order
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// MuteNotification returns whether notifications are muted for this node
func (n *Node) MuteNotification() bool {
return n.muteNotification
}
// SetMuteNotification sets the mute notification flag
func (n *Node) SetMuteNotification(mute bool) {
n.mu.Lock()
defer n.mu.Unlock()
n.muteNotification = mute
n.updatedAt = biztime.NowUTC()
n.version++
}
// UpdateRouteConfig updates the routing configuration
func (n *Node) UpdateRouteConfig(config *vo.RouteConfig) error {
if config != nil {
if err := config.Validate(); err != nil {
return fmt.Errorf("invalid route config: %w", err)
}
}
n.mu.Lock()
defer n.mu.Unlock()
n.routeConfig = config
n.updatedAt = biztime.NowUTC()
n.version++
return nil
}
// ClearRouteConfig removes the routing configuration
func (n *Node) ClearRouteConfig() {
n.mu.Lock()
defer n.mu.Unlock()
n.routeConfig = nil
n.updatedAt = biztime.NowUTC()
n.version++
}
// HasRouteConfig checks if the node has a routing configuration
func (n *Node) HasRouteConfig() bool {
return n.routeConfig != nil
}
func (n *Node) GenerateAPIToken() (string, error) {
if n.tokenGenerator == nil {
n.tokenGenerator = services.NewTokenGenerator()
}
plainToken, tokenHash, err := n.tokenGenerator.GenerateAPIToken("node")
if err != nil {
return "", fmt.Errorf("failed to generate API token: %w", err)
}
n.mu.Lock()
defer n.mu.Unlock()
n.apiToken = plainToken
n.tokenHash = tokenHash
n.updatedAt = biztime.NowUTC()
n.version++
return plainToken, nil
}
func (n *Node) VerifyAPIToken(plainToken string) bool {
if n.tokenGenerator == nil {
n.tokenGenerator = services.NewTokenGenerator()
}
computedHash := n.tokenGenerator.HashToken(plainToken)
return subtle.ConstantTimeCompare([]byte(n.tokenHash), []byte(computedHash)) == 1
}
// IsAvailable checks if node is available for use
func (n *Node) IsAvailable() bool {
return n.status == vo.NodeStatusActive
}
// IsUserOwned returns true if this node is owned by a user (not admin-created)
func (n *Node) IsUserOwned() bool {
return n.userID != nil
}
// IsOwnedBy checks if the node is owned by the specified user
func (n *Node) IsOwnedBy(userID uint) bool {
return n.userID != nil && *n.userID == userID
}
// LastSeenAt returns the last time the node agent reported status
func (n *Node) LastSeenAt() *time.Time {
return n.lastSeenAt
}
// IsOnline checks if node agent is online (reported within 5 minutes)
func (n *Node) IsOnline() bool {
return shared.IsOnline(n.lastSeenAt)
}
// PublicIPv4 returns the public IPv4 address reported by agent
func (n *Node) PublicIPv4() *string {
return n.publicIPv4
}
// PublicIPv6 returns the public IPv6 address reported by agent
func (n *Node) PublicIPv6() *string {
return n.publicIPv6
}
// AgentVersion returns the agent software version
func (n *Node) AgentVersion() *string {
return n.agentVersion
}
// AgentPlatform returns the OS platform
func (n *Node) AgentPlatform() *string {
return n.platform
}
// AgentArch returns the CPU architecture
func (n *Node) AgentArch() *string {
return n.arch
}
// EffectiveServerAddress returns the server address to use for outbound connections.
// If serverAddress is configured, it returns that; otherwise, it falls back to publicIPv4.
// Returns empty string if neither is available.
func (n *Node) EffectiveServerAddress() string {
if n.serverAddress.Value() != "" {
return n.serverAddress.Value()
}
if n.publicIPv4 != nil && *n.publicIPv4 != "" {
return *n.publicIPv4
}
return ""
}
// GetAPIToken returns the plain API token (only available after creation)
func (n *Node) GetAPIToken() string {
return n.apiToken
}
// ClearAPIToken clears the plain API token from memory
func (n *Node) ClearAPIToken() {
n.mu.Lock()
defer n.mu.Unlock()
n.apiToken = ""
}
// ExpiresAt returns the expiration time (nil means never expires)
func (n *Node) ExpiresAt() *time.Time {
return n.expiresAt
}
// SetExpiresAt sets the expiration time (nil to clear)
func (n *Node) SetExpiresAt(t *time.Time) {
n.mu.Lock()
defer n.mu.Unlock()
n.expiresAt = t
n.updatedAt = biztime.NowUTC()
n.version++
}
// CostLabel returns the cost label for display (nil means not set)
func (n *Node) CostLabel() *string {
return n.costLabel
}
// SetCostLabel sets the cost label (nil to clear)
func (n *Node) SetCostLabel(label *string) {
n.mu.Lock()
defer n.mu.Unlock()
n.costLabel = label
n.updatedAt = biztime.NowUTC()
n.version++
}
// IsExpired checks if the node has expired
func (n *Node) IsExpired() bool {
return shared.IsExpired(n.expiresAt)
}
// IsExpiringSoon checks if the node will expire within the specified number of days
func (n *Node) IsExpiringSoon(days int) bool {
return shared.IsExpiringSoon(n.expiresAt, days)
}
// Validate performs domain-level validation
func (n *Node) Validate() error {
if n.name == "" {
return fmt.Errorf("node name is required")
}
if n.agentPort == 0 {
return fmt.Errorf("agent port is required")
}
if n.tokenHash == "" {
return fmt.Errorf("token hash is required")
}
if !n.protocol.IsValid() {
return fmt.Errorf("invalid protocol: %s", n.protocol)
}
if n.protocol.IsShadowsocks() && n.encryptionConfig.Method() == "" {
return fmt.Errorf("encryption config is required for Shadowsocks protocol")
}
if n.protocol.IsTrojan() && n.trojanConfig == nil {
return fmt.Errorf("trojan config is required for Trojan protocol")
}
if n.protocol.IsVLESS() && n.vlessConfig == nil {
return fmt.Errorf("vless config is required for VLESS protocol")
}
if n.protocol.IsVMess() && n.vmessConfig == nil {
return fmt.Errorf("vmess config is required for VMess protocol")
}
if n.protocol.IsHysteria2() && n.hysteria2Config == nil {
return fmt.Errorf("hysteria2 config is required for Hysteria2 protocol")
}
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")
}
return nil
}
// GenerateSubscriptionURI generates a subscription URI for this node
// The password parameter should be the subscription UUID
// Uses EffectiveSubscriptionPort() for the port (subscriptionPort if set, otherwise agentPort)
func (n *Node) GenerateSubscriptionURI(password string, remarks string) (string, error) {
factory := vo.NewProtocolConfigFactory()
serverAddr := n.serverAddress.Value()
port := n.EffectiveSubscriptionPort()
switch n.protocol {
case vo.ProtocolShadowsocks:
ssConfig := vo.NewShadowsocksProtocolConfig(n.encryptionConfig, n.pluginConfig)
return factory.GenerateSubscriptionURI(n.protocol, ssConfig, serverAddr, port, password, remarks)
case vo.ProtocolTrojan:
if n.trojanConfig == nil {
return "", fmt.Errorf("trojan config is required for Trojan protocol")
}
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)
}
}
// generateAPIToken generates a new API token and its hash