package node import ( "encoding/json" "fmt" "strings" "time" "github.com/perfect-panel/server/pkg/logger" "github.com/pkg/errors" "gorm.io/gorm" ) type Server struct { Id int64 `gorm:"primary_key"` Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"` Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` City string `gorm:"type:varchar(128);not null;default:'';comment:City"` //Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` Sort int `gorm:"type:int;not null;default:0;comment:Sort"` Protocols string `gorm:"type:text;default:null;comment:Protocol"` LastReportedAt *time.Time `gorm:"comment:Last Reported Time"` Longitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Longitude"` Latitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Latitude"` LongitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Longitude"` LatitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Latitude"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } func (*Server) TableName() string { return "servers" } func (m *Server) BeforeCreate(tx *gorm.DB) error { if m.Sort == 0 { var maxSort int if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { return err } m.Sort = maxSort + 1 } return nil } func (m *Server) BeforeDelete(tx *gorm.DB) error { if err := tx.Exec("UPDATE `servers` SET sort = sort - 1 WHERE sort > ?", m.Sort).Error; err != nil { return err } return nil } func (m *Server) BeforeUpdate(tx *gorm.DB) error { var count int64 if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). Where("sort = ? AND id != ?", m.Sort, m.Id).Count(&count).Error; err != nil { return err } if count > 1 { // reorder sort if err := reorderSortWithServer(tx); err != nil { logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) return err } // get max sort var maxSort int if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { return err } m.Sort = maxSort + 1 } return nil } // MarshalProtocols Marshal server protocols to json. // Uniqueness is scoped to type+port (a protocol entry is one listener instance), // which allows the same protocol on multiple ports, e.g. simnet:443 and simnet:8443. func (m *Server) MarshalProtocols(list []Protocol) error { var validate = make(map[string]bool) for _, protocol := range list { if strings.TrimSpace(protocol.Type) == "" { return errors.New("protocol type is required") } key := fmt.Sprintf("%s:%d", strings.ToLower(strings.TrimSpace(protocol.Type)), protocol.Port) if _, exists := validate[key]; exists { return errors.New("duplicate protocol type+port: " + key) } validate[key] = true } data, err := json.Marshal(list) if err != nil { return err } m.Protocols = string(data) return nil } // UnmarshalProtocols Unmarshal server protocols from json func (m *Server) UnmarshalProtocols() ([]Protocol, error) { var list []Protocol if m.Protocols == "" { return list, nil } err := json.Unmarshal([]byte(m.Protocols), &list) if err != nil { return nil, err } return list, nil } type Protocol struct { Type string `json:"type"` Port uint16 `json:"port"` Enable bool `json:"enable"` Security string `json:"security,omitempty"` SNI string `json:"sni,omitempty"` AllowInsecure bool `json:"allow_insecure,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` RealityServerAddr string `json:"reality_server_addr,omitempty"` RealityServerPort int `json:"reality_server_port,omitempty"` RealityPrivateKey string `json:"reality_private_key,omitempty"` RealityPublicKey string `json:"reality_public_key,omitempty"` RealityShortId string `json:"reality_short_id,omitempty"` Transport string `json:"transport,omitempty"` Host string `json:"host,omitempty"` Path string `json:"path,omitempty"` ServiceName string `json:"service_name,omitempty"` Cipher string `json:"cipher,omitempty"` ServerKey string `json:"server_key,omitempty"` Flow string `json:"flow,omitempty"` HopPorts string `json:"hop_ports,omitempty"` HopInterval int `json:"hop_interval,omitempty"` ObfsPassword string `json:"obfs_password,omitempty"` DisableSNI bool `json:"disable_sni,omitempty"` ReduceRtt bool `json:"reduce_rtt,omitempty"` UDPRelayMode string `json:"udp_relay_mode,omitempty"` CongestionController string `json:"congestion_controller,omitempty"` Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme UpMbps int `json:"up_mbps,omitempty"` // upload speed limit DownMbps int `json:"down_mbps,omitempty"` // download speed limit Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' ObfsHost string `json:"obfs_host,omitempty"` // obfs host ObfsPath string `json:"obfs_path,omitempty"` // obfs path XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate CertDNSEnv string `json:"cert_dns_env"` // Environment for DNS provider // Simnet protocol fields. Field set and json tags mirror the Pro reference // (NPanel-backend internal/model/server/protocol.go) so OmnXT/SlagClient // see an identical wire contract. SimnetPsk string `json:"simnet_psk,omitempty"` // server-side PSK (key_id=0), never sent to normal users SimnetKeyID int `json:"simnet_key_id,omitempty"` // server key id (0) SimnetTicketID string `json:"simnet_ticket_id,omitempty"` SimnetPath string `json:"simnet_path,omitempty"` SimnetCarrier string `json:"simnet_carrier,omitempty"` SimnetAfEnabled bool `json:"simnet_af_enabled,omitempty"` SimnetAfPathMode string `json:"simnet_af_path_mode,omitempty"` SimnetAfPathPrefix string `json:"simnet_af_path_prefix,omitempty"` SimnetAfPathSuffix string `json:"simnet_af_path_suffix,omitempty"` SimnetAfMagicMode string `json:"simnet_af_magic_mode,omitempty"` SimnetAfResponseJitterMs int `json:"simnet_af_response_jitter_ms,omitempty"` SimnetAfHandshakePolymorphism bool `json:"simnet_af_handshake_polymorphism,omitempty"` SimnetAfSettingsJitter bool `json:"simnet_af_settings_jitter,omitempty"` SimnetAfFakeHeaderInjection bool `json:"simnet_af_fake_header_injection,omitempty"` SimnetReverseEnabled bool `json:"simnet_reverse_enabled,omitempty"` SimnetReverseListenAddr string `json:"simnet_reverse_listen_addr,omitempty"` SimnetReverseListenPort int `json:"simnet_reverse_listen_port,omitempty"` SimnetReverseTargetHost string `json:"simnet_reverse_target_host,omitempty"` SimnetReverseTargetPort int `json:"simnet_reverse_target_port,omitempty"` SimnetFallbackEnabled bool `json:"simnet_fallback_enabled,omitempty"` SimnetFallbackTargetScheme string `json:"simnet_fallback_target_scheme,omitempty"` SimnetFallbackTargetHost string `json:"simnet_fallback_target_host,omitempty"` SimnetFallbackTargetPort int `json:"simnet_fallback_target_port,omitempty"` SimnetFallbackHostHeader string `json:"simnet_fallback_host_header,omitempty"` SimnetFallbackTLSSNI string `json:"simnet_fallback_tls_sni,omitempty"` SimnetInboundMaxStreamsPerSession int `json:"simnet_inbound_max_streams_per_session,omitempty"` SimnetInboundMaxUDPStreamsPerSession int `json:"simnet_inbound_max_udp_streams_per_session,omitempty"` SimnetInboundMaxHandlerTasksPerSession int `json:"simnet_inbound_max_handler_tasks_per_session,omitempty"` SimnetStreamEventChannelCapacity int `json:"simnet_stream_event_channel_capacity,omitempty"` SimnetStreamDataChannelCapacity int `json:"simnet_stream_data_channel_capacity,omitempty"` SimnetTargetDialTimeoutMs int `json:"simnet_target_dial_timeout_ms,omitempty"` SimnetTargetMaxConcurrentDials int `json:"simnet_target_max_concurrent_dials,omitempty"` SimnetEgressBlockLoopback bool `json:"simnet_egress_block_loopback,omitempty"` SimnetEgressBlockPrivate bool `json:"simnet_egress_block_private,omitempty"` SimnetEgressBlockLinkLocal bool `json:"simnet_egress_block_link_local,omitempty"` SimnetEgressBlockMetadata bool `json:"simnet_egress_block_metadata,omitempty"` SimnetSendWindow int `json:"simnet_send_window,omitempty"` SimnetRecvWindow int `json:"simnet_recv_window,omitempty"` SimnetMaxConcurrentStreams int `json:"simnet_max_concurrent_streams,omitempty"` SimnetInitialWindowSize int `json:"simnet_initial_window_size,omitempty"` SimnetMaxFrameSize int `json:"simnet_max_frame_size,omitempty"` SimnetClientMaxConcurrentStreams int `json:"simnet_client_max_concurrent_streams,omitempty"` SimnetClientMaxStreamsPerSession int `json:"simnet_client_max_streams_per_session,omitempty"` SimnetClientSessionIdleTimeoutSecs int `json:"simnet_client_session_idle_timeout_secs,omitempty"` SimnetClientMaxUDPSessions int `json:"simnet_client_max_udp_sessions,omitempty"` } const ( defaultSimnetInboundMaxStreamsPerSession int = 128 defaultSimnetInboundMaxUDPStreamsPerSession int = 64 defaultSimnetInboundMaxHandlerTasksPerSession int = 128 defaultSimnetStreamEventChannelCapacity int = 256 defaultSimnetStreamDataChannelCapacity int = 128 defaultSimnetTargetDialTimeoutMs int = 12_000 defaultSimnetTargetMaxConcurrentDials int = 256 defaultSimnetSessionWindow int = 4 * 1024 * 1024 defaultSimnetMaxConcurrentStreams int = 100 defaultSimnetInitialWindowSize int = 65_535 defaultSimnetMaxFrameSize int = 16_384 defaultSimnetClientMaxConcurrentStreams int = 32 defaultSimnetClientMaxStreamsPerSession int = 512 defaultSimnetClientSessionIdleTimeoutSecs int = 90 defaultSimnetClientMaxUDPSessions int = 64 ) // NormalizeSimnet applies simnet defaults and clears mutually-exclusive fields. // Ported from the Pro reference (NPanel-backend protocol.go NormalizeSimnet) so // the runtime config matches what OmnXT expects. func (m *Protocol) NormalizeSimnet() { if m == nil || m.Type != "simnet" { return } if m.Port == 0 { m.Port = 443 } if strings.TrimSpace(m.SimnetPath) == "" { m.SimnetPath = "/simnet/session" } // Carrier: only h2 is supported in v1; coerce empty/grpc to h2 (matches the // Pro reference normalizeSimnetProtocol). if c := strings.TrimSpace(m.SimnetCarrier); c == "" || c == "grpc" { m.SimnetCarrier = "h2" } m.applySimnetResourceDefaults() if !m.SimnetFallbackEnabled || strings.TrimSpace(m.SimnetFallbackTargetHost) == "" { m.SimnetFallbackEnabled = false m.SimnetFallbackTargetScheme = "" m.SimnetFallbackTargetHost = "" m.SimnetFallbackTargetPort = 0 m.SimnetFallbackHostHeader = "" m.SimnetFallbackTLSSNI = "" } else { m.SimnetFallbackTargetHost = strings.TrimSpace(m.SimnetFallbackTargetHost) m.SimnetFallbackHostHeader = strings.TrimSpace(m.SimnetFallbackHostHeader) m.SimnetFallbackTLSSNI = strings.TrimSpace(m.SimnetFallbackTLSSNI) switch strings.ToLower(strings.TrimSpace(m.SimnetFallbackTargetScheme)) { case "http", "https": m.SimnetFallbackTargetScheme = strings.ToLower(strings.TrimSpace(m.SimnetFallbackTargetScheme)) default: m.SimnetFallbackTargetScheme = "https" } } if !m.SimnetAfEnabled { m.SimnetAfPathMode = "" m.SimnetAfMagicMode = "" m.SimnetAfPathPrefix = "" m.SimnetAfPathSuffix = "" m.SimnetAfResponseJitterMs = 0 m.SimnetAfHandshakePolymorphism = false m.SimnetAfSettingsJitter = false m.SimnetAfFakeHeaderInjection = false return } if m.SimnetAfPathMode == "" { m.SimnetAfPathMode = "api" } if m.SimnetAfMagicMode == "" { m.SimnetAfMagicMode = "derived" } if m.SimnetAfResponseJitterMs == 0 { m.SimnetAfResponseJitterMs = 50 } if !m.SimnetAfHandshakePolymorphism { m.SimnetAfHandshakePolymorphism = true } if !m.SimnetAfSettingsJitter { m.SimnetAfSettingsJitter = true } if !m.SimnetAfFakeHeaderInjection { m.SimnetAfFakeHeaderInjection = true } } func (m *Protocol) applySimnetResourceDefaults() { if m.SimnetInboundMaxStreamsPerSession <= 0 { m.SimnetInboundMaxStreamsPerSession = defaultSimnetInboundMaxStreamsPerSession } if m.SimnetInboundMaxUDPStreamsPerSession <= 0 { m.SimnetInboundMaxUDPStreamsPerSession = defaultSimnetInboundMaxUDPStreamsPerSession } if m.SimnetInboundMaxHandlerTasksPerSession <= 0 { m.SimnetInboundMaxHandlerTasksPerSession = defaultSimnetInboundMaxHandlerTasksPerSession } if m.SimnetStreamEventChannelCapacity <= 0 { m.SimnetStreamEventChannelCapacity = defaultSimnetStreamEventChannelCapacity } if m.SimnetStreamDataChannelCapacity <= 0 { m.SimnetStreamDataChannelCapacity = defaultSimnetStreamDataChannelCapacity } if m.SimnetTargetDialTimeoutMs <= 0 { m.SimnetTargetDialTimeoutMs = defaultSimnetTargetDialTimeoutMs } if m.SimnetTargetMaxConcurrentDials <= 0 { m.SimnetTargetMaxConcurrentDials = defaultSimnetTargetMaxConcurrentDials } if m.SimnetSendWindow <= 0 { m.SimnetSendWindow = defaultSimnetSessionWindow } if m.SimnetRecvWindow <= 0 { m.SimnetRecvWindow = defaultSimnetSessionWindow } if m.SimnetMaxConcurrentStreams <= 0 { m.SimnetMaxConcurrentStreams = defaultSimnetMaxConcurrentStreams } if m.SimnetInitialWindowSize <= 0 { m.SimnetInitialWindowSize = defaultSimnetInitialWindowSize } if m.SimnetMaxFrameSize <= 0 { m.SimnetMaxFrameSize = defaultSimnetMaxFrameSize } if m.SimnetClientMaxConcurrentStreams <= 0 { m.SimnetClientMaxConcurrentStreams = defaultSimnetClientMaxConcurrentStreams } if m.SimnetClientMaxStreamsPerSession <= 0 { m.SimnetClientMaxStreamsPerSession = defaultSimnetClientMaxStreamsPerSession } if m.SimnetClientSessionIdleTimeoutSecs <= 0 { m.SimnetClientSessionIdleTimeoutSecs = defaultSimnetClientSessionIdleTimeoutSecs } if m.SimnetClientMaxUDPSessions <= 0 { m.SimnetClientMaxUDPSessions = defaultSimnetClientMaxUDPSessions } } // Marshal protocol to json func (m *Protocol) Marshal() ([]byte, error) { type Alias Protocol return json.Marshal(&struct { *Alias }{ Alias: (*Alias)(m), }) } // Unmarshal json to protocol func (m *Protocol) Unmarshal(data []byte) error { type Alias Protocol aux := &struct { *Alias }{ Alias: (*Alias)(m), } return json.Unmarshal(data, &aux) } func reorderSortWithServer(tx *gorm.DB) error { var servers []Server if err := tx.Order("sort, id").Find(&servers).Error; err != nil { return err } for i, server := range servers { if server.Sort != i+1 { if err := tx.Exec("UPDATE `servers` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil { return err } } } return nil }