修复无法正常使用的严重问题

This commit is contained in:
CN-JS-HuiBai
2026-04-15 01:07:55 +08:00
parent 2013f31776
commit be0f2fc891
2 changed files with 447 additions and 131 deletions

56
.gitignore vendored
View File

@@ -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/

View File

@@ -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()
@@ -236,7 +385,7 @@ 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)
@@ -246,24 +395,28 @@ func (s *Service) setupNode() error {
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,7 +451,6 @@ 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")
@@ -303,81 +465,133 @@ func (s *Service) setupNode() error {
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)
}
case "trojan":
// Trojan supports ws/grpc transport like V2bX
transport, err := getInboundTransport(networkType, networkSettings)
if err != nil {
return fmt.Errorf("build transport for trojan: %w", err)
}
// 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),
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)
}
@@ -450,7 +754,7 @@ func (s *Service) setupNode() error {
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