From 4b10271ce08ca5457e4f4aa9242c2a5ad7b9b8f2 Mon Sep 17 00:00:00 2001 From: xboard Date: Thu, 26 Mar 2026 11:35:15 +0800 Subject: [PATCH] fix: add nil checks for TLS config in sing-box inbound builders --- internal/kernel/singbox/config.go | 65 ++++++++++++++++++-------- internal/kernel/singbox/config_test.go | 34 +++++++++----- internal/kernel/xray/config.go | 4 +- internal/kernel/xray/xray.go | 2 +- 4 files changed, 72 insertions(+), 33 deletions(-) diff --git a/internal/kernel/singbox/config.go b/internal/kernel/singbox/config.go index cd62b86..61d68ef 100644 --- a/internal/kernel/singbox/config.go +++ b/internal/kernel/singbox/config.go @@ -451,7 +451,9 @@ func buildVMess(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyF applyMultiplex(base, nc) if nc.TLS == 1 { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } } return base @@ -478,7 +480,9 @@ func buildVLESS(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyF applyMultiplex(base, nc) if nc.TLS == 1 { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } } else if nc.TLS == 2 { base["tls"] = buildRealityConfig(nc) } @@ -504,16 +508,22 @@ func buildTrojan(base M, nc *panel.NodeConfig, users []panel.User, certFile, key applyMultiplex(base, nc) if nc.TLS == 1 { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } } else if nc.TLS == 2 { base["tls"] = buildRealityConfig(nc) } - // Trojan requires TLS or Reality to be enabled. - // If the panel didn't explicitly set TLS=1 or TLS=2, but we have certs, - // we should enable a default TLS config to ensure the inbound can start. + // If still no TLS/Reality but cert paths exist, enable TLS (panel may omit tls=1). if _, ok := base["tls"]; !ok { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } + } + + if base["tls"] == nil { + nlog.Core().Warn("trojan inbound has no TLS (no certificate paths and no Reality); sing-box may refuse to start until cert_mode provides cert/key or Reality is configured") } return base @@ -558,6 +568,10 @@ func buildHysteria(base M, nc *panel.NodeConfig, users []panel.User, certFile, k } tls := buildTLSConfig(nc, certFile, keyFile) + if tls == nil { + nlog.Core().Warn("hysteria requires TLS certificate files on disk; configure cert_mode (self, file, http, dns, or content). Sing-box will not start this inbound without tls.") + return base + } // Hysteria/Hysteria2 uses QUIC and requires ALPN; default to h3 if not set. if _, ok := tls["alpn"]; !ok { tls["alpn"] = []string{"h3"} @@ -584,6 +598,10 @@ func buildTUIC(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyFi } tls := buildTLSConfig(nc, certFile, keyFile) + if tls == nil { + nlog.Core().Warn("tuic requires TLS certificate files on disk; configure cert_mode (self, file, http, dns, or content). Sing-box will not start this inbound without tls.") + return base + } // TUIC requires ALPN for QUIC negotiation; default to h3 if not set by panel. if _, ok := tls["alpn"]; !ok { tls["alpn"] = []string{"h3"} @@ -608,7 +626,11 @@ func buildAnyTLS(base M, nc *panel.NodeConfig, users []panel.User, certFile, key base["padding_scheme"] = string(nc.PaddingScheme) } - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } else { + nlog.Core().Warn("anytls requires TLS certificate files on disk; configure cert_mode (self, file, http, dns, or content). Sing-box will not start this inbound without tls.") + } return base } @@ -625,7 +647,9 @@ func buildNaive(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyF base["users"] = userList if nc.TLS == 1 { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } } return base } @@ -658,7 +682,9 @@ func buildHTTP(base M, nc *panel.NodeConfig, users []panel.User, certFile, keyFi applyProxyProtocol(base, nc) if nc.TLS == 1 { - base["tls"] = buildTLSConfig(nc, certFile, keyFile) + if tls := buildTLSConfig(nc, certFile, keyFile); tls != nil { + base["tls"] = tls + } } return base } @@ -715,7 +741,15 @@ func applyTransport(base M, nc *panel.NodeConfig) { base["transport"] = transport } +// buildTLSConfig returns sing-box inbound TLS options when certificate and key paths exist. +// If either path is missing, it returns nil: the inbound should run without a TLS layer +// (e.g. TLS terminated at nginx/CDN while the panel still shows tls=1). +// Sing-box does not accept a magic "self-signed" path — it tries to open that name as a file. func buildTLSConfig(nc *panel.NodeConfig, certFile, keyFile string) M { + if certFile == "" || keyFile == "" { + return nil + } + tls := M{"enabled": true} serverName := nc.ServerName @@ -735,15 +769,8 @@ 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" - } + tls["certificate_path"] = certFile + tls["key_path"] = keyFile return tls } diff --git a/internal/kernel/singbox/config_test.go b/internal/kernel/singbox/config_test.go index 99d5356..24255db 100644 --- a/internal/kernel/singbox/config_test.go +++ b/internal/kernel/singbox/config_test.go @@ -77,6 +77,21 @@ func TestBuildInbound_VMess_WithTLS(t *testing.T) { assertMapValue(t, tls, "key_path", "/path/key.pem") } +// Panel tls=1 with cert_mode none (no paths): inbound must be plain — TLS terminated at nginx/CDN. +func TestBuildInbound_VMess_TLS_PanelOn_NoCertFiles(t *testing.T) { + nc := &panel.NodeConfig{ + Protocol: "vmess", + ServerPort: 8888, + Network: "ws", + TLS: 1, + ServerName: "example.com", + } + inbound := buildInbound(nc, testUsers, "", "") + if _, exists := inbound["tls"]; exists { + t.Fatal("expected no tls block when cert/key paths are empty (nginx offload)") + } +} + func TestBuildInbound_VMess_NoTLS(t *testing.T) { nc := &panel.NodeConfig{ Protocol: "vmess", @@ -245,13 +260,10 @@ func TestBuildInbound_Trojan_NoTLS(t *testing.T) { TLS: 0, } inbound := buildInbound(nc, testUsers, "", "") - // Trojan requires TLS; buildTrojan should force-enable self-signed TLS - // even when the panel sends tls=0. - tls, exists := inbound["tls"].(M) - if !exists { - t.Fatal("trojan with tls=0 should still get TLS (force-enabled)") + // Without certificate paths there is no TLS layer; sing-box trojan needs certs or Reality. + if _, exists := inbound["tls"]; exists { + t.Fatal("expected no tls when cert paths are empty and tls=0") } - assertMapValue(t, tls, "enabled", true) } func TestBuildInbound_Trojan_WithTLS(t *testing.T) { @@ -643,14 +655,14 @@ func TestBuildTLSConfig_WithCert(t *testing.T) { func TestBuildTLSConfig_NoCert(t *testing.T) { nc := &panel.NodeConfig{ServerName: "example.com"} tls := buildTLSConfig(nc, "", "") - assertMapValue(t, tls, "enabled", true) - // Now we fallback to self-signed certificates when both cert and key are empty - assertMapValue(t, tls, "certificate_path", "self-signed") + if tls != nil { + t.Fatalf("expected nil tls config when cert paths are empty, got %+v", tls) + } } func TestBuildTLSConfig_FallbackToHost(t *testing.T) { nc := &panel.NodeConfig{Host: "fallback.com"} - tls := buildTLSConfig(nc, "", "") + tls := buildTLSConfig(nc, "/c.pem", "/k.pem") assertMapValue(t, tls, "server_name", "fallback.com") } @@ -661,7 +673,7 @@ func TestBuildTLSConfig_TLSSettingsOverride(t *testing.T) { "server_name": "override.com", }, } - tls := buildTLSConfig(nc, "", "") + tls := buildTLSConfig(nc, "/c.pem", "/k.pem") assertMapValue(t, tls, "server_name", "override.com") } diff --git a/internal/kernel/xray/config.go b/internal/kernel/xray/config.go index 0f695e8..0c0ad04 100644 --- a/internal/kernel/xray/config.go +++ b/internal/kernel/xray/config.go @@ -45,8 +45,8 @@ func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.U cfg := M{ "log": M{ "loglevel": xrayLogLevel(kcfg.LogLevel), - "error": "stdout", - "access": "stdout", + "error": "", + "access": "", }, "stats": M{}, "policy": M{ diff --git a/internal/kernel/xray/xray.go b/internal/kernel/xray/xray.go index 4972407..defefa7 100644 --- a/internal/kernel/xray/xray.go +++ b/internal/kernel/xray/xray.go @@ -613,7 +613,7 @@ func (x *Xray) ensureGeoData(nc *panel.NodeConfig) { // marshalConfig builds the xray JSON config and returns the raw bytes. func marshalConfig(cfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) ([]byte, error) { cfgMap := buildConfig(cfg, nc, users, certFile, keyFile) - data, err := json.Marshal(cfgMap) + data, err := json.MarshalIndent(cfgMap, "", " ") if err != nil { return nil, fmt.Errorf("marshal config: %w", err) }