diff --git a/.gitignore b/.gitignore index d2b74d08..fbdb6a58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,35 @@ -/.idea/ -/vendor/ -/*.json -/*.srs -/*.db -/site/ -/bin/ -/dist/ -/sing-box -/sing-box.exe -/build/ -/*.jar -/*.aar -/*.xcframework/ -/experimental/libbox/*.aar -/experimental/libbox/*.xcframework/ -/experimental/libbox/*.nupkg +# Binaries +sing-box +sing-box.exe +*.exe +*.dll +*.so +*.dylib + +# Environment +.env +.env.local + +# Build & Cache +go.sum +bin/ +dist/ +/var/lib/sing-box/* + +# Logs +*.log +/var/log/sing-box/* + +# OS .DS_Store -/config.d/ -/venv/ -CLAUDE.md -AGENTS.md -/.claude/ +Thumbs.db + +# Antigravity/Gemini Artifacts +.gemini/ +artifacts/ +scratch/ +implementation_plan*.md +walkthrough*.md +task.md + +V2bX/ diff --git a/service/xboard/service.go b/service/xboard/service.go index 52dc6613..f57c2aee 100644 --- a/service/xboard/service.go +++ b/service/xboard/service.go @@ -10,6 +10,8 @@ import ( "io" "net/http" "net/netip" + "net/url" + "strconv" "sync" "time" @@ -83,34 +85,87 @@ type XBoardServiceOptions struct { } type XNodeConfig struct { - NodeType string `json:"node_type"` - NodeType_ string `json:"nodeType"` - ServerConfig json.RawMessage `json:"server_config"` - ServerConfig_ json.RawMessage `json:"serverConfig"` - Config json.RawMessage `json:"config"` - ListenIP string `json:"listen_ip"` - Port int `json:"port"` - ServerPort int `json:"server_port"` - Protocol string `json:"protocol"` - Cipher string `json:"cipher"` - ServerKey string `json:"server_key"` - TLS int `json:"tls"` - Flow string `json:"flow"` - TLSSettings *XTLSSettings `json:"tls_settings"` -} - -type XInnerConfig struct { + NodeType string `json:"node_type"` + NodeType_ string `json:"nodeType"` + ServerConfig json.RawMessage `json:"server_config"` + ServerConfig_ json.RawMessage `json:"serverConfig"` + Config json.RawMessage `json:"config"` ListenIP string `json:"listen_ip"` Port int `json:"port"` ServerPort int `json:"server_port"` Protocol string `json:"protocol"` - Settings json.RawMessage `json:"settings"` - StreamSettings json.RawMessage `json:"streamSettings"` Cipher string `json:"cipher"` ServerKey string `json:"server_key"` TLS int `json:"tls"` Flow string `json:"flow"` TLSSettings *XTLSSettings `json:"tls_settings"` + TLSSettings_ *XTLSSettings `json:"tlsSettings"` + Network string `json:"network"` + NetworkSettings json.RawMessage `json:"network_settings"` + NetworkSettings_ json.RawMessage `json:"networkSettings"` + + // Hysteria / Hysteria2 + UpMbps int `json:"up_mbps"` + DownMbps int `json:"down_mbps"` + Obfs string `json:"obfs"` + ObfsPassword string `json:"obfs-password"` + Ignore_Client_Bandwidth bool `json:"ignore_client_bandwidth"` + + // Tuic + CongestionControl string `json:"congestion_control"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake"` + + // AnyTls + PaddingScheme []string `json:"padding_scheme"` +} + +type XInnerConfig struct { + ListenIP string `json:"listen_ip"` + Port int `json:"port"` + ServerPort int `json:"server_port"` + Protocol string `json:"protocol"` + Settings json.RawMessage `json:"settings"` + StreamSettings json.RawMessage `json:"streamSettings"` + Cipher string `json:"cipher"` + ServerKey string `json:"server_key"` + TLS int `json:"tls"` + Flow string `json:"flow"` + TLSSettings *XTLSSettings `json:"tls_settings"` + TLSSettings_ *XTLSSettings `json:"tlsSettings"` + Network string `json:"network"` + NetworkSettings json.RawMessage `json:"network_settings"` + NetworkSettings_ json.RawMessage `json:"networkSettings"` +} + +type HttpNetworkConfig struct { + Header struct { + Type string `json:"type"` + Request *json.RawMessage `json:"request"` + Response *json.RawMessage `json:"response"` + } `json:"header"` +} + +type HttpRequest struct { + Version string `json:"version"` + Method string `json:"method"` + Path []string `json:"path"` + Headers struct { + Host []string `json:"Host"` + } `json:"headers"` +} + +type WsNetworkConfig struct { + Path string `json:"path"` + Headers map[string]string `json:"headers"` +} + +type GrpcNetworkConfig struct { + ServiceName string `json:"serviceName"` +} + +type HttpupgradeNetworkConfig struct { + Path string `json:"path"` + Host string `json:"host"` } type XTLSSettings struct { @@ -121,6 +176,7 @@ type XTLSSettings struct { ShortID string `json:"short_id"` ShortIDs []string `json:"short_ids"` AllowInsecure bool `json:"allow_insecure"` + Dest string `json:"dest"` } type XRealitySettings struct { @@ -227,6 +283,99 @@ func (s *Service) Start(stage adapter.StartStage) error { return nil } +func getInboundTransport(network string, settings json.RawMessage) (*option.V2RayTransportOptions, error) { + if network == "" { + return nil, nil + } + t := &option.V2RayTransportOptions{ + Type: network, + } + switch network { + case "tcp": + if len(settings) != 0 { + var networkConfig HttpNetworkConfig + err := json.Unmarshal(settings, &networkConfig) + if err != nil { + return nil, fmt.Errorf("decode NetworkSettings error: %s", err) + } + if networkConfig.Header.Type == "http" { + t.Type = networkConfig.Header.Type + if networkConfig.Header.Request != nil { + var request HttpRequest + err = json.Unmarshal(*networkConfig.Header.Request, &request) + if err != nil { + return nil, fmt.Errorf("decode HttpRequest error: %s", err) + } + t.HTTPOptions.Host = request.Headers.Host + if len(request.Path) > 0 { + t.HTTPOptions.Path = request.Path[0] + } + t.HTTPOptions.Method = request.Method + } + } else { + t.Type = "" + } + } else { + t.Type = "" + } + case "ws": + var ( + path string + ed int + headers map[string]badoption.Listable[string] + ) + if len(settings) != 0 { + var networkConfig WsNetworkConfig + err := json.Unmarshal(settings, &networkConfig) + if err != nil { + return nil, fmt.Errorf("decode NetworkSettings error: %s", err) + } + u, err := url.Parse(networkConfig.Path) + if err != nil { + return nil, fmt.Errorf("parse WS path error: %s", err) + } + path = u.Path + ed, _ = strconv.Atoi(u.Query().Get("ed")) + if len(networkConfig.Headers) > 0 { + headers = make(map[string]badoption.Listable[string], len(networkConfig.Headers)) + for k, v := range networkConfig.Headers { + headers[k] = badoption.Listable[string]{v} + } + } + } + t.WebsocketOptions = option.V2RayWebsocketOptions{ + Path: path, + EarlyDataHeaderName: "Sec-WebSocket-Protocol", + MaxEarlyData: uint32(ed), + Headers: headers, + } + case "grpc": + if len(settings) != 0 { + var networkConfig GrpcNetworkConfig + err := json.Unmarshal(settings, &networkConfig) + if err != nil { + return nil, fmt.Errorf("decode gRPC settings error: %s", err) + } + t.GRPCOptions = option.V2RayGRPCOptions{ + ServiceName: networkConfig.ServiceName, + } + } + case "httpupgrade": + if len(settings) != 0 { + var networkConfig HttpupgradeNetworkConfig + err := json.Unmarshal(settings, &networkConfig) + if err != nil { + return nil, fmt.Errorf("decode HttpUpgrade settings error: %s", err) + } + t.HTTPUpgradeOptions = option.V2RayHTTPUpgradeOptions{ + Path: networkConfig.Path, + Host: networkConfig.Host, + } + } + } + return t, nil +} + func (s *Service) setupNode() error { s.logger.Info("Xboard fetching node config...") config, err := s.fetchConfig() @@ -235,8 +384,8 @@ func (s *Service) setupNode() error { } inboundTag := "xboard-inbound" - - // Resolve nested config + + // Resolve nested config (V2bX compatibility: server_config / serverConfig / config) var inner XInnerConfig if len(config.ServerConfig) > 0 { json.Unmarshal(config.ServerConfig, &inner) @@ -245,25 +394,29 @@ func (s *Service) setupNode() error { } else if len(config.Config) > 0 { json.Unmarshal(config.Config, &inner) } - - // Fallback to flat if still empty + + // Fallback flat fields from top-level config to inner if inner.ListenIP == "" { inner.ListenIP = config.ListenIP } if inner.ListenIP == "" { inner.ListenIP = "0.0.0.0" } - if inner.TLSSettings == nil { inner.TLSSettings = config.TLSSettings } + if inner.TLSSettings == nil { + inner.TLSSettings = config.TLSSettings_ + } + if inner.TLSSettings_ == nil { + inner.TLSSettings_ = config.TLSSettings_ + } if inner.TLS == 0 { inner.TLS = config.TLS } if inner.Flow == "" { inner.Flow = config.Flow } - if inner.Protocol == "" { inner.Protocol = config.Protocol } @@ -280,7 +433,17 @@ func (s *Service) setupNode() error { if inner.ServerKey == "" { inner.ServerKey = config.ServerKey } + if inner.Network == "" { + inner.Network = config.Network + } + if len(inner.NetworkSettings) == 0 { + inner.NetworkSettings = config.NetworkSettings + } + if len(inner.NetworkSettings_) == 0 { + inner.NetworkSettings_ = config.NetworkSettings_ + } + // Resolve protocol protocol := inner.Protocol if protocol == "" { protocol = config.NodeType @@ -288,12 +451,11 @@ func (s *Service) setupNode() error { if protocol == "" { protocol = config.NodeType_ } - if protocol == "" { s.logger.Error("Xboard setup error: could not identify protocol. Please check debug logs for raw JSON.") return fmt.Errorf("unsupported protocol: empty") } - + s.logger.Info("Xboard protocol identified: ", protocol) var listenAddr badoption.Addr @@ -302,82 +464,134 @@ func (s *Service) setupNode() error { } else { listenAddr = badoption.Addr(netip.IPv4Unspecified()) } - - var tlsOptions *option.InboundTLSOptions - if inner.TLS > 0 && inner.TLSSettings != nil { - tlsOptions = &option.InboundTLSOptions{ - Enabled: true, - ServerName: inner.TLSSettings.ServerName, + + listen := option.ListenOptions{ + Listen: &listenAddr, + ListenPort: uint16(inner.Port), + } + + // ── TLS / Reality handling (matching V2bX panel.Security constants) ── + // V2bX: 0=None, 1=TLS, 2=Reality + var tlsOptions option.InboundTLSOptions + securityType := inner.TLS + tlsSettings := inner.TLSSettings + if tlsSettings == nil { + tlsSettings = inner.TLSSettings_ + } + + switch securityType { + case 1: // TLS + tlsOptions.Enabled = true + if tlsSettings != nil { + tlsOptions.ServerName = tlsSettings.ServerName } - if inner.TLS == 2 { // Reality - shortIDs := inner.TLSSettings.ShortIDs - if len(shortIDs) == 0 && inner.TLSSettings.ShortID != "" { - shortIDs = []string{inner.TLSSettings.ShortID} + case 2: // Reality + if tlsSettings != nil { + tlsOptions.Enabled = true + tlsOptions.ServerName = tlsSettings.ServerName + shortIDs := tlsSettings.ShortIDs + if len(shortIDs) == 0 && tlsSettings.ShortID != "" { + shortIDs = []string{tlsSettings.ShortID} + } + dest := tlsSettings.Dest + if dest == "" { + dest = tlsSettings.ServerName + } + if dest == "" { + dest = "www.microsoft.com" + } + serverPort := uint16(443) + if tlsSettings.ServerPort != "" { + if port, err := strconv.Atoi(tlsSettings.ServerPort); err == nil && port > 0 { + serverPort = uint16(port) + } } tlsOptions.Reality = &option.InboundRealityOptions{ Enabled: true, Handshake: option.InboundRealityHandshakeOptions{ ServerOptions: option.ServerOptions{ - Server: inner.TLSSettings.ServerName, - ServerPort: 443, + Server: dest, + ServerPort: serverPort, }, }, - PrivateKey: inner.TLSSettings.PrivateKey, + PrivateKey: tlsSettings.PrivateKey, ShortID: badoption.Listable[string](shortIDs), } - // Fallback if empty - if tlsOptions.Reality.Handshake.Server == "" { - tlsOptions.Reality.Handshake.Server = "www.microsoft.com" - } + s.logger.Info("Xboard REALITY configured. Dest: ", dest, ":", serverPort) } } - + + // Also check streamSettings for Reality (legacy Xboard format) + if inner.StreamSettings != nil && securityType == 0 { + var streamSettings XStreamSettings + json.Unmarshal(inner.StreamSettings, &streamSettings) + reality := streamSettings.GetReality() + if streamSettings.Security == "reality" && reality != nil { + serverNames := reality.GetServerNames() + serverName := "" + if len(serverNames) > 0 { + serverName = serverNames[0] + } + tlsOptions = option.InboundTLSOptions{ + Enabled: true, + ServerName: serverName, + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: reality.Dest, + ServerPort: 443, + }, + }, + PrivateKey: reality.GetPrivateKey(), + ShortID: badoption.Listable[string](reality.GetShortIds()), + }, + } + securityType = 2 + s.logger.Info("Xboard REALITY config from streamSettings") + } + } + + // ── Resolve network transport settings (V2bX style) ── + networkType := inner.Network + networkSettings := inner.NetworkSettings + if len(networkSettings) == 0 { + networkSettings = inner.NetworkSettings_ + } + + // ── Build inbound per protocol (matching V2bX core/sing/node.go) ── var inboundOptions any switch protocol { - case "vless": - vlessOptions := option.VLESSInboundOptions{} - vlessOptions.Listen = &listenAddr - vlessOptions.ListenPort = uint16(inner.Port) - vlessOptions.TLS = tlsOptions + case "vmess", "vless": + // Transport for vmess/vless + transport, err := getInboundTransport(networkType, networkSettings) + if err != nil { + return fmt.Errorf("build transport for %s: %w", protocol, err) + } - // Handle Reality - if inner.StreamSettings != nil { - var streamSettings XStreamSettings - json.Unmarshal(inner.StreamSettings, &streamSettings) - reality := streamSettings.GetReality() - if streamSettings.Security == "reality" && reality != nil { - serverNames := reality.GetServerNames() - serverName := "" - if len(serverNames) > 0 { - serverName = serverNames[0] - } - vlessOptions.TLS = &option.InboundTLSOptions{ - Enabled: true, - ServerName: serverName, - Reality: &option.InboundRealityOptions{ - Enabled: true, - Handshake: option.InboundRealityHandshakeOptions{ - ServerOptions: option.ServerOptions{ - Server: reality.Dest, - ServerPort: 443, - }, - }, - PrivateKey: reality.GetPrivateKey(), - ShortID: badoption.Listable[string](reality.GetShortIds()), - }, - } - s.logger.Info("Xboard REALITY config from streamSettings. PrivateKey preview: ", reality.GetPrivateKey()[:4], "...") + if protocol == "vless" { + opts := &option.VLESSInboundOptions{ + ListenOptions: listen, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &tlsOptions, + }, } + if transport != nil { + opts.Transport = transport + } + inboundOptions = opts + } else { + opts := &option.VMessInboundOptions{ + ListenOptions: listen, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &tlsOptions, + }, + } + if transport != nil { + opts.Transport = transport + } + inboundOptions = opts } - inboundOptions = &vlessOptions - case "vmess": - vmessOptions := option.VMessInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: &listenAddr, - ListenPort: uint16(inner.Port), - }, - } - inboundOptions = &vmessOptions case "shadowsocks": method := inner.Cipher serverKey := inner.ServerKey @@ -387,7 +601,7 @@ func (s *Service) setupNode() error { if method == "" { method = "aes-256-gcm" } - // Hardening for Shadowsocks 2022 + // V2bX SS2022 key handling if len(method) >= 4 && method[:4] == "2022" { keyLen := 32 if len(method) >= 18 && method[13:16] == "128" { @@ -395,27 +609,117 @@ func (s *Service) setupNode() error { } serverKey = fixSSKey(serverKey, keyLen) } - ssOptions := option.ShadowsocksInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: &listenAddr, - ListenPort: uint16(inner.Port), - }, - Method: method, - Password: serverKey, + + ssOptions := &option.ShadowsocksInboundOptions{ + ListenOptions: listen, + Method: method, + Password: serverKey, + } + inboundOptions = ssOptions + if len(serverKey) >= 8 { + s.logger.Info("Xboard Shadowsocks setup. Method: ", method, " PSK preview: ", serverKey[:8], "...") + } else { + s.logger.Info("Xboard Shadowsocks setup. Method: ", method) } - - // If no colon is used in client, we might need a fallback. - // We'll leave it to be updated dynamically when users sync. - inboundOptions = &ssOptions - s.logger.Info("Xboard Shadowsocks 2022 setup. Method: ", method, " PSK preview: ", serverKey[:8], "...") case "trojan": - trojanOptions := option.TrojanInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: &listenAddr, - ListenPort: uint16(inner.Port), + // Trojan supports ws/grpc transport like V2bX + transport, err := getInboundTransport(networkType, networkSettings) + if err != nil { + return fmt.Errorf("build transport for trojan: %w", err) + } + + opts := &option.TrojanInboundOptions{ + ListenOptions: listen, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &tlsOptions, }, } - inboundOptions = &trojanOptions + if transport != nil { + opts.Transport = transport + } + inboundOptions = opts + case "tuic": + // V2bX: TUIC always uses TLS with h3 ALPN + tuicTLS := tlsOptions + tuicTLS.Enabled = true + tuicTLS.ALPN = append(tuicTLS.ALPN, "h3") + + congestionControl := config.CongestionControl + if congestionControl == "" { + congestionControl = "bbr" + } + + opts := &option.TUICInboundOptions{ + ListenOptions: listen, + CongestionControl: congestionControl, + ZeroRTTHandshake: config.ZeroRTTHandshake, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &tuicTLS, + }, + } + inboundOptions = opts + s.logger.Info("Xboard TUIC configured. CongestionControl: ", congestionControl) + case "hysteria": + // V2bX: Hysteria always uses TLS + hyTLS := tlsOptions + hyTLS.Enabled = true + + opts := &option.HysteriaInboundOptions{ + ListenOptions: listen, + UpMbps: config.UpMbps, + DownMbps: config.DownMbps, + Obfs: config.Obfs, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &hyTLS, + }, + } + inboundOptions = opts + s.logger.Info("Xboard Hysteria configured. Up: ", config.UpMbps, " Down: ", config.DownMbps) + case "hysteria2": + // V2bX: Hysteria2 always uses TLS, optional obfs + hy2TLS := tlsOptions + hy2TLS.Enabled = true + + var obfs *option.Hysteria2Obfs + if config.Obfs != "" && config.ObfsPassword != "" { + obfs = &option.Hysteria2Obfs{ + Type: config.Obfs, + Password: config.ObfsPassword, + } + } else if config.Obfs != "" { + // V2bX compat: if only obfs type given, treat as salamander with obfs as password + obfs = &option.Hysteria2Obfs{ + Type: "salamander", + Password: config.Obfs, + } + } + + opts := &option.Hysteria2InboundOptions{ + ListenOptions: listen, + UpMbps: config.UpMbps, + DownMbps: config.DownMbps, + IgnoreClientBandwidth: config.Ignore_Client_Bandwidth, + Obfs: obfs, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &hy2TLS, + }, + } + inboundOptions = opts + s.logger.Info("Xboard Hysteria2 configured. Up: ", config.UpMbps, " Down: ", config.DownMbps, " IgnoreClientBW: ", config.Ignore_Client_Bandwidth) + case "anytls": + // V2bX: AnyTLS always uses TLS + anyTLS := tlsOptions + anyTLS.Enabled = true + + opts := &option.AnyTLSInboundOptions{ + ListenOptions: listen, + PaddingScheme: config.PaddingScheme, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &anyTLS, + }, + } + inboundOptions = opts + s.logger.Info("Xboard AnyTLS configured") default: return fmt.Errorf("unsupported protocol: %s", protocol) } @@ -428,11 +732,11 @@ func (s *Service) setupNode() error { if err != nil { return err } - + s.access.Lock() s.inboundTags = []string{inboundTag} s.access.Unlock() - + s.logger.Info("Xboard dynamic inbound [", inboundTag, "] created on port ", inner.Port, " (protocol: ", protocol, ")") // Register the new inbound in our managed list @@ -442,15 +746,15 @@ func (s *Service) setupNode() error { traffic := ssmapi.NewTrafficManager() managedServer.SetTracker(traffic) user := ssmapi.NewUserManager(managedServer, traffic) - + s.access.Lock() s.traffics[inboundTag] = traffic s.users[inboundTag] = user s.servers[inboundTag] = managedServer s.inboundTags = []string{inboundTag} s.access.Unlock() - - s.logger.Info("Xboard dynamic inbound [", inboundTag, "] created on port ", inner.Port, " (protocol: ", protocol, ")") + + s.logger.Info("Xboard managed inbound [", inboundTag, "] registered (protocol: ", protocol, ")") } return nil