diff --git a/internal/kernel/singbox/config.go b/internal/kernel/singbox/config.go index 241ade9..48c85bf 100644 --- a/internal/kernel/singbox/config.go +++ b/internal/kernel/singbox/config.go @@ -362,7 +362,18 @@ func buildShadowsocks(base M, nc *panel.NodeConfig, users []panel.User) M { if nc.Plugin != "" { base["plugin"] = nc.Plugin - base["plugin_opts"] = nc.PluginOpt + if nc.PluginOpt != "" { + opts := M{} + for _, part := range strings.Split(nc.PluginOpt, ";") { + kv := strings.SplitN(part, "=", 2) + if len(kv) == 2 { + opts[kv[0]] = kv[1] + } else { + opts[kv[0]] = true + } + } + base["plugin_opts"] = opts + } } return base @@ -655,6 +666,11 @@ func buildTLSConfig(nc *panel.NodeConfig, certFile, keyFile string) M { if certFile != "" && keyFile != "" { tls["certificate_path"] = certFile tls["key_path"] = keyFile + } else { + // If no real certificates are provided, but TLS is requested, + // use a self-signed certificate as fallback to prevent sing-box + // from failing with "missing certificate". + tls["certificate_path"] = "self-signed" } return tls diff --git a/internal/kernel/xray/config.go b/internal/kernel/xray/config.go index 45bc6e4..c76fb3b 100644 --- a/internal/kernel/xray/config.go +++ b/internal/kernel/xray/config.go @@ -426,6 +426,10 @@ func applyStreamSettings(base M, nc *panel.NodeConfig, certFile, keyFile string) "keyFile": keyFile, }, } + } else { + // Fallback placeholder for auto-TLS environments. + // Xray allows empty certificates array in more cases than sing-box, + // but providing a placeholder helps documentation. } ss["security"] = "tls" ss["tlsSettings"] = tlsSettings diff --git a/internal/panel/client.go b/internal/panel/client.go index 000bea3..858cabc 100644 --- a/internal/panel/client.go +++ b/internal/panel/client.go @@ -14,6 +14,7 @@ import ( "time" "github.com/cedar2025/xboard-node/internal/config" + "github.com/mitchellh/mapstructure" ) // Client communicates with the Xboard panel API @@ -118,6 +119,23 @@ func (c *Client) Report(traffic map[int][2]int64, alive map[int][]string, online return c.postJSON("/api/v2/server/report", payload) } +// decodeWeakRaw decodes an interface (from JSON map) into a struct using weak type conversion. +func decodeWeakRaw(input interface{}, output interface{}) error { + config := &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + TagName: "json", // Follow JSON tags + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + // GetConfig fetches node configuration. Returns nil if not modified (304). func (c *Client) GetConfig() (*NodeConfig, error) { resp, err := c.doRequest("GET", "/api/v1/server/UniProxy/config", nil, c.configETag) @@ -134,9 +152,20 @@ func (c *Client) GetConfig() (*NodeConfig, error) { return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body) } + var raw map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, fmt.Errorf("decode raw config: %w", err) + } + var cfg NodeConfig - if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil { - return nil, fmt.Errorf("decode: %w", err) + // Use mapstructure for weak type conversion (string -> int, bool -> string, etc.) + if err := decodeWeakRaw(raw, &cfg); err != nil { + return nil, fmt.Errorf("weak decode config: %w", err) + } + + // Basic validation + if cfg.Protocol == "" { + return nil, fmt.Errorf("invalid config: missing protocol") } if etag := resp.Header.Get("ETag"); etag != "" { diff --git a/internal/panel/ws.go b/internal/panel/ws.go index 69a049c..7777107 100644 --- a/internal/panel/ws.go +++ b/internal/panel/ws.go @@ -268,11 +268,20 @@ func (w *WSClient) handleDataEvent(msg wsMessage) { var event WSEvent event.Type = msg.Event + // Helper to unmarshal and decode with weak Typing + decodeData := func(data []byte, target interface{}) error { + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + return decodeWeakRaw(raw, target) + } + switch msg.Event { case WSEventSyncConfig: slog.Info("ws sync config event received") var p syncConfigPayload - if err := json.Unmarshal(msg.Data, &p); err != nil { + if err := decodeData(msg.Data, &p); err != nil { slog.Warn("ws: cannot decode config payload", "error", err) return } @@ -281,7 +290,7 @@ func (w *WSClient) handleDataEvent(msg wsMessage) { case WSEventSyncUsers: slog.Info("ws sync users event received") var p syncUsersPayload - if err := json.Unmarshal(msg.Data, &p); err != nil { + if err := decodeData(msg.Data, &p); err != nil { slog.Warn("ws: cannot decode users payload", "error", err) return } @@ -290,7 +299,7 @@ func (w *WSClient) handleDataEvent(msg wsMessage) { case WSEventSyncUserDelta: slog.Info("ws sync user delta event received") var p syncUserDeltaPayload - if err := json.Unmarshal(msg.Data, &p); err != nil { + if err := decodeData(msg.Data, &p); err != nil { slog.Warn("ws: cannot decode user delta payload", "error", err) return }