634b5a7bd0
- model: SimNet protocol fields + NormalizeSimnet; type+port protocol uniqueness - server config: OmnXT runtime config delivery via compatible() simnet case - credentials: derive per-user psk/key_id from subscription (pkg/simnet), no new table - subscription: adapter buildOmnxtSimnetConfigs + base64 buildOmnxtProtocolLinks + OmnXT SimNet application (migration 02161) - UA gating: hide experimental protocols from non first-party clients (download + JSON node-list) - admin: normalize simnet on create/update and on GET responses - tests: 21 simnet unit tests; full suite green
384 lines
17 KiB
Go
384 lines
17 KiB
Go
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
|
||
}
|