package service import ( "crypto/md5" "encoding/base64" "encoding/json" "fmt" "math" "strconv" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "gorm.io/gorm" ) var serverTypeAliases = map[string]string{ "v2ray": "vmess", "v2node": "", "hysteria2": "hysteria", } var validServerTypes = map[string]struct{}{ "anytls": {}, "http": {}, "hysteria": {}, "mieru": {}, "naive": {}, "shadowsocks": {}, "socks": {}, "trojan": {}, "tuic": {}, "vless": {}, "vmess": {}, } var shadowsocks2022CipherConfigs = map[string]struct { serverKeySize int userKeySize int }{ "2022-blake3-aes-128-gcm": { serverKeySize: 16, userKeySize: 16, }, "2022-blake3-aes-256-gcm": { serverKeySize: 32, userKeySize: 32, }, "2022-blake3-chacha20-poly1305": { serverKeySize: 32, userKeySize: 32, }, } type NodeUser struct { ID int `json:"id"` UUID string `json:"uuid"` SpeedLimit *int `json:"speed_limit"` DeviceLimit *int `json:"device_limit"` } type NodeBaseConfig struct { PushInterval int `json:"push_interval"` PullInterval int `json:"pull_interval"` } type NodeServerConfig struct { Name string `json:"-"` Protocol string `json:"protocol"` RawHost string `json:"-"` Port int `json:"-"` Ports string `json:"-"` ListenIP string `json:"listen_ip"` ServerPort int `json:"server_port"` Network any `json:"network"` NetworkSettings any `json:"networkSettings"` Cipher any `json:"cipher,omitempty"` Plugin any `json:"plugin,omitempty"` PluginOpts any `json:"plugin_opts,omitempty"` ServerKey any `json:"server_key,omitempty"` Host any `json:"host,omitempty"` ServerName any `json:"server_name,omitempty"` Tls any `json:"tls,omitempty"` TlsSettings any `json:"tls_settings,omitempty"` Flow any `json:"flow,omitempty"` Multiplex any `json:"multiplex,omitempty"` UTLS any `json:"utls,omitempty"` UpMbps any `json:"up_mbps,omitempty"` DownMbps any `json:"down_mbps,omitempty"` Version any `json:"version,omitempty"` Obfs any `json:"obfs,omitempty"` ObfsPassword any `json:"obfs-password,omitempty"` CongestionControl any `json:"congestion_control,omitempty"` AuthTimeout any `json:"auth_timeout,omitempty"` ZeroRTTHandshake any `json:"zero_rtt_handshake,omitempty"` Heartbeat any `json:"heartbeat,omitempty"` PaddingScheme any `json:"padding_scheme,omitempty"` Transport any `json:"transport,omitempty"` TrafficPattern any `json:"traffic_pattern,omitempty"` Decryption any `json:"decryption,omitempty"` Routes []model.ServerRoute `json:"routes,omitempty"` CustomOutbounds any `json:"custom_outbounds,omitempty"` CustomRoutes any `json:"custom_routes,omitempty"` CertConfig any `json:"cert_config,omitempty"` BaseConfig NodeBaseConfig `json:"base_config"` } func NormalizeServerType(serverType string) string { serverType = strings.ToLower(strings.TrimSpace(serverType)) if serverType == "" { return "" } if alias, ok := serverTypeAliases[serverType]; ok { return alias } return serverType } func IsValidServerType(serverType string) bool { normalized := NormalizeServerType(serverType) if normalized == "" { return true } _, ok := validServerTypes[normalized] return ok } func FindServer(nodeID, nodeType string) (*model.Server, error) { query := database.DB.Model(&model.Server{}) if normalized := NormalizeServerType(nodeType); normalized != "" { query = query.Where("type = ?", normalized) } var server model.Server if err := query. Where("code = ? OR id = ?", nodeID, nodeID). Order(gorm.Expr("CASE WHEN code = ? THEN 0 ELSE 1 END", nodeID)). First(&server).Error; err != nil { return nil, err } server.Type = NormalizeServerType(server.Type) return &server, nil } func AvailableUsersForNode(node *model.Server) ([]NodeUser, error) { groupIDs := parseIntSlice(node.GroupIDs) if len(groupIDs) == 0 { return []NodeUser{}, nil } var users []NodeUser err := database.DB.Model(&model.User{}). Select("id", "uuid", "speed_limit", "device_limit"). Where("group_id IN ?", groupIDs). Where("u + d < transfer_enable"). Where("(expired_at >= ? OR expired_at IS NULL)", time.Now().Unix()). Where("banned = ?", 0). Scan(&users).Error if err != nil { return []NodeUser{}, err } return users, nil } func AvailableServersForUser(user *model.User) ([]model.Server, error) { var servers []model.Server if err := database.DB.Where("`show` = ?", 1).Order("sort ASC").Find(&servers).Error; err != nil { return nil, err } filtered := make([]model.Server, 0, len(servers)) for _, server := range servers { groupIDs := parseIntSlice(server.GroupIDs) if user.GroupID != nil && len(groupIDs) > 0 && !containsInt(groupIDs, *user.GroupID) { continue } if server.TransferEnable != nil && *server.TransferEnable > 0 && server.U+server.D >= *server.TransferEnable { continue } filtered = append(filtered, server) } return filtered, nil } func GenerateServerPassword(node *model.Server, user *model.User) string { if node == nil || user == nil { return "" } if NormalizeServerType(node.Type) != "shadowsocks" { return user.UUID } settings := parseObject(node.ProtocolSettings) cipher := getMapString(settings, "cipher") config, ok := shadowsocks2022CipherConfigs[cipher] if !ok { return user.UUID } createdAt := resolveServerCreatedAt(node) serverKey := serverKey(createdAt, config.serverKeySize) if serverKey == "" { return user.UUID } return serverKey + ":" + uuidPrefixBase64(user.UUID, config.userKeySize) } func CurrentRate(server *model.Server) float64 { if !server.RateTimeEnable { return float64(server.Rate) } ranges := parseObjectSlice(server.RateTimeRanges) now := time.Now().Format("15:04") for _, item := range ranges { start, _ := item["start"].(string) end, _ := item["end"].(string) if start != "" && end != "" && now >= start && now <= end { if rate, ok := toFloat64(item["rate"]); ok { return rate } } } return float64(server.Rate) } func BuildNodeConfig(node *model.Server) NodeServerConfig { settings := parseObject(node.ProtocolSettings) clientPort, portRange := resolveClientPort(node.Port, node.ServerPort) response := NodeServerConfig{ Name: node.Name, Protocol: node.Type, RawHost: node.Host, Port: clientPort, Ports: portRange, ListenIP: "0.0.0.0", ServerPort: node.ServerPort, Network: getMapAny(settings, "network"), NetworkSettings: getMapAny(settings, "network_settings"), BaseConfig: NodeBaseConfig{ PushInterval: MustGetInt("server_push_interval", 60), PullInterval: MustGetInt("server_pull_interval", 60), }, } switch node.Type { case "shadowsocks": response.Cipher = getMapString(settings, "cipher") response.Plugin = getMapAny(settings, "plugin") response.PluginOpts = getMapAny(settings, "plugin_opts") cipher := getMapString(settings, "cipher") response.ServerKey = "" switch cipher { case "2022-blake3-aes-128-gcm": response.ServerKey = serverKey(node.CreatedAt, 16) case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305": response.ServerKey = serverKey(node.CreatedAt, 32) } case "vmess": response.Tls = getMapInt(settings, "tls") response.Multiplex = getMapAny(settings, "multiplex") response.UTLS = getMapAny(settings, "utls") case "trojan": response.Host = node.Host response.ServerName = getMapString(settings, "server_name") response.Multiplex = getMapAny(settings, "multiplex") response.UTLS = getMapAny(settings, "utls") response.Tls = getMapInt(settings, "tls") if getMapInt(settings, "tls") == 2 { response.TlsSettings = getMapAny(settings, "reality_settings") } else { response.TlsSettings = getMapAny(settings, "tls_settings") } case "vless": response.Tls = getMapInt(settings, "tls") response.Flow = getMapString(settings, "flow") response.Multiplex = getMapAny(settings, "multiplex") response.UTLS = getMapAny(settings, "utls") response.Decryption = "none" if encryption, ok := settings["encryption"].(map[string]any); ok { if enabled, ok := encryption["enabled"].(bool); ok && enabled { if dec := stringify(encryption["decryption"]); dec != "" { response.Decryption = dec } } } if getMapInt(settings, "tls") == 2 { response.TlsSettings = getMapAny(settings, "reality_settings") } else { response.TlsSettings = getMapAny(settings, "tls_settings") } case "hysteria": tls, _ := settings["tls"].(map[string]any) obfs, _ := settings["obfs"].(map[string]any) bandwidth, _ := settings["bandwidth"].(map[string]any) version := getMapInt(settings, "version") response.Version = version response.Host = node.Host response.ServerName = stringify(tls["server_name"]) response.UpMbps = mapAnyInt(bandwidth, "up") response.DownMbps = mapAnyInt(bandwidth, "down") response.Obfs = nil response.ObfsPassword = nil if version == 1 { response.Obfs = stringify(obfs["password"]) } else if version == 2 { if open, ok := obfs["open"].(bool); ok && open { response.Obfs = stringify(obfs["type"]) response.ObfsPassword = stringify(obfs["password"]) } } case "tuic": tls, _ := settings["tls"].(map[string]any) response.Version = getMapInt(settings, "version") response.ServerName = stringify(tls["server_name"]) response.CongestionControl = getMapString(settings, "congestion_control") response.TlsSettings = getMapAny(settings, "tls_settings") response.AuthTimeout = "3s" response.ZeroRTTHandshake = false response.Heartbeat = "3s" case "anytls": tls, _ := settings["tls"].(map[string]any) response.ServerName = stringify(tls["server_name"]) response.PaddingScheme = getMapAny(settings, "padding_scheme") case "naive", "http": response.Tls = getMapInt(settings, "tls") response.TlsSettings = getMapAny(settings, "tls_settings") case "mieru": response.Transport = getMapString(settings, "transport") response.TrafficPattern = getMapString(settings, "traffic_pattern") } response.Routes = nil if routeIDs := parseIntSlice(node.RouteIDs); len(routeIDs) > 0 { var routes []model.ServerRoute if err := database.DB.Select("id", "`match`", "action", "action_value").Where("id IN ?", routeIDs).Find(&routes).Error; err == nil { response.Routes = routes } } response.CustomOutbounds = parseGenericJSON(node.CustomOutbounds) response.CustomRoutes = parseGenericJSON(node.CustomRoutes) response.CertConfig = parseGenericJSON(node.CertConfig) return response } func BuildNodeConfigPayload(node *model.Server) map[string]any { settings := parseObject(node.ProtocolSettings) baseConfig := map[string]any{ "protocol": node.Type, "node_type": node.Type, "nodeType": node.Type, "listen_ip": "0.0.0.0", "server_port": node.ServerPort, "network": getMapAny(settings, "network"), "networkSettings": getMapAny(settings, "network_settings"), "network_settings": getMapAny(settings, "network_settings"), } response := cloneNodePayload(baseConfig) switch node.Type { case "shadowsocks": response["cipher"] = getMapAny(settings, "cipher") response["plugin"] = getMapAny(settings, "plugin") response["plugin_opts"] = getMapAny(settings, "plugin_opts") switch getMapString(settings, "cipher") { case "2022-blake3-aes-128-gcm": response["server_key"] = serverKey(node.CreatedAt, 16) case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305": response["server_key"] = serverKey(node.CreatedAt, 32) default: response["server_key"] = nil } case "vmess": response["tls"] = getMapInt(settings, "tls") response["tlsSettings"] = getMapAny(settings, "tls_settings") response["multiplex"] = getMapAny(settings, "multiplex") case "trojan": response["host"] = node.Host response["server_name"] = getMapAny(settings, "server_name") response["multiplex"] = getMapAny(settings, "multiplex") response["tls"] = getMapInt(settings, "tls") if getMapInt(settings, "tls") == 2 { response["tls_settings"] = getMapAny(settings, "reality_settings") response["tlsSettings"] = getMapAny(settings, "reality_settings") } else { response["tls_settings"] = nil response["tlsSettings"] = nil } case "vless": response["tls"] = getMapInt(settings, "tls") response["flow"] = getMapString(settings, "flow") response["multiplex"] = getMapAny(settings, "multiplex") response["decryption"] = nil if encryption, ok := settings["encryption"].(map[string]any); ok { if enabled, ok := encryption["enabled"].(bool); ok && enabled { if dec := stringify(encryption["decryption"]); dec != "" { response["decryption"] = dec } } } if getMapInt(settings, "tls") == 2 { response["tls_settings"] = getMapAny(settings, "reality_settings") response["tlsSettings"] = getMapAny(settings, "reality_settings") } else { response["tls_settings"] = getMapAny(settings, "tls_settings") response["tlsSettings"] = getMapAny(settings, "tls_settings") } case "hysteria": tls, _ := settings["tls"].(map[string]any) obfs, _ := settings["obfs"].(map[string]any) bandwidth, _ := settings["bandwidth"].(map[string]any) version := getMapInt(settings, "version") response["version"] = version response["host"] = node.Host response["server_name"] = getMapAny(tls, "server_name") response["up_mbps"] = mapAnyInt(bandwidth, "up") response["down_mbps"] = mapAnyInt(bandwidth, "down") switch version { case 1: response["obfs"] = getMapAny(obfs, "password") case 2: if open, ok := obfs["open"].(bool); ok && open { response["obfs"] = getMapAny(obfs, "type") } else { response["obfs"] = nil } response["obfs-password"] = getMapAny(obfs, "password") } case "tuic": tls, _ := settings["tls"].(map[string]any) response["version"] = getMapInt(settings, "version") response["server_name"] = getMapAny(tls, "server_name") response["congestion_control"] = getMapAny(settings, "congestion_control") response["tls_settings"] = getMapAny(settings, "tls_settings") response["auth_timeout"] = "3s" response["zero_rtt_handshake"] = false response["heartbeat"] = "3s" case "anytls": tls, _ := settings["tls"].(map[string]any) response["server_name"] = getMapAny(tls, "server_name") response["padding_scheme"] = getMapAny(settings, "padding_scheme") case "socks": // Base config already matches the original XBoard payload for socks. case "naive": response["tls"] = getMapInt(settings, "tls") response["tls_settings"] = getMapAny(settings, "tls_settings") response["tlsSettings"] = getMapAny(settings, "tls_settings") case "http": response["tls"] = getMapInt(settings, "tls") response["tls_settings"] = getMapAny(settings, "tls_settings") response["tlsSettings"] = getMapAny(settings, "tls_settings") case "mieru": transport := getMapString(settings, "transport") if strings.TrimSpace(transport) == "" { transport = "TCP" } response["transport"] = transport response["traffic_pattern"] = getMapAny(settings, "traffic_pattern") } if routeIDs := parseIntSlice(node.RouteIDs); len(routeIDs) > 0 { var routes []model.ServerRoute if err := database.DB.Select("id", "`match`", "action", "action_value").Where("id IN ?", routeIDs).Find(&routes).Error; err == nil { response["routes"] = routes } } if customOutbounds := parseGenericJSON(node.CustomOutbounds); hasNodePayloadValue(customOutbounds) { response["custom_outbounds"] = customOutbounds } if customRoutes := parseGenericJSON(node.CustomRoutes); hasNodePayloadValue(customRoutes) { response["custom_routes"] = customRoutes } if certConfig := parseObject(node.CertConfig); len(certConfig) > 0 && getMapString(certConfig, "cert_mode") != "none" { response["cert_config"] = certConfig } return response } func BuildNodeBaseConfigPayload() map[string]any { return map[string]any{ "push_interval": MustGetInt("server_push_interval", 60), "pull_interval": MustGetInt("server_pull_interval", 60), } } func cloneNodePayload(values map[string]any) map[string]any { result := make(map[string]any, len(values)) for key, value := range values { result[key] = value } return result } func hasNodePayloadValue(value any) bool { switch typed := value.(type) { case nil: return false case string: return strings.TrimSpace(typed) != "" case []any: return len(typed) > 0 case []map[string]any: return len(typed) > 0 case map[string]any: return len(typed) > 0 default: return true } } func ApplyTrafficDelta(userID int, node *model.Server, upload, download int64) { rate := CurrentRate(node) scaledUpload := int64(math.Round(float64(upload) * rate)) scaledDownload := int64(math.Round(float64(download) * rate)) database.DB.Model(&model.User{}). Where("id = ?", userID). Updates(map[string]any{ "u": gorm.Expr("u + ?", scaledUpload), "d": gorm.Expr("d + ?", scaledDownload), "t": time.Now().Unix(), }) database.DB.Model(&model.Server{}). Where("id = ?", node.ID). Updates(map[string]any{ "u": gorm.Expr("u + ?", scaledUpload), "d": gorm.Expr("d + ?", scaledDownload), }) } func serverKey(createdAt *time.Time, size int) string { if createdAt == nil { return "" } // Match XBoard's Helper::getServerKey(created_at, size): // base64_encode(substr(md5($timestamp), 0, size)) // In XBoard, created_at is cast to a Unix timestamp before md5(). sum := md5.Sum([]byte(strconv.FormatInt(createdAt.Unix(), 10))) hex := fmt.Sprintf("%x", sum) if size > len(hex) { size = len(hex) } return base64.StdEncoding.EncodeToString([]byte(hex[:size])) } func resolveServerCreatedAt(node *model.Server) *time.Time { if node == nil { return nil } if node.ParentID == nil || *node.ParentID <= 0 { return node.CreatedAt } var parent struct { CreatedAt *time.Time `gorm:"column:created_at"` } if err := database.DB.Model(&model.Server{}). Select("created_at"). Where("id = ?", *node.ParentID). First(&parent).Error; err == nil && parent.CreatedAt != nil { return parent.CreatedAt } return node.CreatedAt } func uuidPrefixBase64(uuid string, size int) string { if size <= 0 { return "" } if size > len(uuid) { size = len(uuid) } return base64.StdEncoding.EncodeToString([]byte(uuid[:size])) } func resolveClientPort(rawPort string, fallback int) (int, string) { rawPort = strings.TrimSpace(rawPort) if rawPort == "" { return fallback, "" } if strings.Contains(rawPort, "-") { parts := strings.SplitN(rawPort, "-", 2) start, errStart := strconv.Atoi(strings.TrimSpace(parts[0])) end, errEnd := strconv.Atoi(strings.TrimSpace(parts[1])) if errStart == nil && errEnd == nil && start > 0 && end >= start { if start == end { return start, rawPort } return start + int(time.Now().UnixNano()%int64(end-start+1)), rawPort } } if port, err := strconv.Atoi(rawPort); err == nil && port > 0 { return port, "" } return fallback, "" } func parseIntSlice(raw *string) []int { if raw == nil || strings.TrimSpace(*raw) == "" { return nil } var decoded []any if err := json.Unmarshal([]byte(*raw), &decoded); err == nil { result := make([]int, 0, len(decoded)) for _, item := range decoded { if value, ok := toInt(item); ok { result = append(result, value) } } return result } parts := strings.Split(*raw, ",") result := make([]int, 0, len(parts)) for _, part := range parts { if value, err := strconv.Atoi(strings.TrimSpace(part)); err == nil { result = append(result, value) } } return result } func parseObject(raw *string) map[string]any { if raw == nil || strings.TrimSpace(*raw) == "" { return map[string]any{} } var decoded map[string]any if err := json.Unmarshal([]byte(*raw), &decoded); err != nil { return map[string]any{} } return decoded } func parseObjectSlice(raw *string) []map[string]any { if raw == nil || strings.TrimSpace(*raw) == "" { return nil } var decoded []map[string]any if err := json.Unmarshal([]byte(*raw), &decoded); err != nil { return nil } return decoded } func parseGenericJSON(raw *string) any { if raw == nil || strings.TrimSpace(*raw) == "" { return nil } var decoded any if err := json.Unmarshal([]byte(*raw), &decoded); err != nil { return nil } return decoded } func getMapString(values map[string]any, key string) string { return stringify(values[key]) } func getMapInt(values map[string]any, key string) int { if value, ok := toInt(values[key]); ok { return value } return 0 } func getMapAny(values map[string]any, key string) any { if value, ok := values[key]; ok { return value } return nil } func mapAnyInt(values map[string]any, key string) int { if value, ok := toInt(values[key]); ok { return value } return 0 } func stringify(value any) string { switch typed := value.(type) { case string: return typed case fmt.Stringer: return typed.String() case float64: return strconv.FormatInt(int64(typed), 10) case int: return strconv.Itoa(typed) case int64: return strconv.FormatInt(typed, 10) default: return "" } } func pruneNilMap(values map[string]any) map[string]any { result := make(map[string]any, len(values)) for key, value := range values { if value == nil { continue } if text, ok := value.(string); ok && text == "" { continue } result[key] = value } return result } func toInt(value any) (int, bool) { switch typed := value.(type) { case int: return typed, true case int64: return int(typed), true case float64: return int(typed), true case string: parsed, err := strconv.Atoi(strings.TrimSpace(typed)) return parsed, err == nil default: return 0, false } } func toFloat64(value any) (float64, bool) { switch typed := value.(type) { case float64: return typed, true case int: return float64(typed), true case int64: return float64(typed), true case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64) return parsed, err == nil default: return 0, false } } func containsInt(values []int, target int) bool { for _, value := range values { if value == target { return true } } return false }