From 08795d2e04ecdfbee416df84b6be725219680cdd Mon Sep 17 00:00:00 2001 From: xboard Date: Mon, 16 Mar 2026 01:56:52 +0800 Subject: [PATCH] refactor: enforce panel precedence for outbound tags - Avoid duplicate 'block'/'direct' tag error in sing-box/Xray - Let panel or local config override internal defaults - Ref #1 --- internal/kernel/singbox/config.go | 36 +++++++++---------- internal/kernel/singbox/config_test.go | 49 ++++++++++++++++++++++++++ internal/kernel/xray/config.go | 23 +++++++++--- internal/kernel/xray/config_test.go | 49 ++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 23 deletions(-) diff --git a/internal/kernel/singbox/config.go b/internal/kernel/singbox/config.go index 48c85bf..edc767f 100644 --- a/internal/kernel/singbox/config.go +++ b/internal/kernel/singbox/config.go @@ -17,20 +17,32 @@ import ( type M = map[string]interface{} func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M { - outbounds := []M{ - {"type": "direct", "tag": "direct"}, - {"type": "block", "tag": "block"}, - } + var outbounds []M + tags := make(map[string]bool) // Panel-defined custom outbounds (structured, converted to sing-box native) for _, co := range nc.CustomOutbounds { outbounds = append(outbounds, outboundConfigToSingbox(co)) + tags[strings.ToLower(co.Tag)] = true } - // Static outbounds from local config file (already kernel-native format) + + // Static outbounds from local config file for _, co := range kcfg.CustomOutbound { + tag, _ := co["tag"].(string) + if tag != "" { + tags[strings.ToLower(tag)] = true + } outbounds = append(outbounds, M(co)) } + // Add default outbounds only if not already defined + if !tags["direct"] { + outbounds = append([]M{{"type": "direct", "tag": "direct"}}, outbounds...) + } + if !tags["block"] { + outbounds = append(outbounds, M{"type": "block", "tag": "block"}) + } + cfg := M{ "log": M{ "level": kcfg.LogLevel, @@ -361,19 +373,7 @@ func buildShadowsocks(base M, nc *panel.NodeConfig, users []panel.User) M { base["users"] = userList if nc.Plugin != "" { - base["plugin"] = nc.Plugin - 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 - } + slog.Warn("sing-box shadowsocks inbound does not support plugin, ignoring", "plugin", nc.Plugin) } return base diff --git a/internal/kernel/singbox/config_test.go b/internal/kernel/singbox/config_test.go index 37edbc5..f365bf5 100644 --- a/internal/kernel/singbox/config_test.go +++ b/internal/kernel/singbox/config_test.go @@ -469,6 +469,55 @@ func TestBuildConfig(t *testing.T) { assertMapValue(t, inbounds[0], "type", "shadowsocks") } +func TestBuildConfig_OutboundPriority(t *testing.T) { + kcfg := config.KernelConfig{ + LogLevel: "info", + CustomOutbound: []map[string]any{ + {"tag": "block", "type": "dns"}, // Local static override + }, + } + nc := &panel.NodeConfig{ + Protocol: "shadowsocks", + ServerPort: 111, + Cipher: "aes-128-gcm", + CustomOutbounds: []panel.OutboundConfig{ + {Tag: "direct", Protocol: "socks", Settings: map[string]any{"address": "1.2.3.4"}}, // Panel override + }, + } + + cfg := buildConfig(kcfg, nc, testUsers, "", "") + outbounds := cfg["outbounds"].([]M) + + // We have 2 overrides in input, so we should have exactly 2 outbounds total + // (no auto-generated defaults for these tags) + if len(outbounds) != 2 { + t.Errorf("outbounds: got %d, want 2", len(outbounds)) + } + + foundDirect := false + foundBlock := false + + for _, o := range outbounds { + tag := o["tag"].(string) + if tag == "direct" { + foundDirect = true + if o["type"] != "socks" { + t.Errorf("expected 'direct' outbound to be type 'socks' (panel priority), got %v", o["type"]) + } + } + if tag == "block" { + foundBlock = true + if o["type"] != "dns" { + t.Errorf("expected 'block' outbound to be type 'dns' (static config priority), got %v", o["type"]) + } + } + } + + if !foundDirect || !foundBlock { + t.Errorf("missing outbounds: direct=%v, block=%v", foundDirect, foundBlock) + } +} + func TestBuildConfig_AllProtocols_ValidJSON(t *testing.T) { protocols := []struct { name string diff --git a/internal/kernel/xray/config.go b/internal/kernel/xray/config.go index c76fb3b..e6bcd10 100644 --- a/internal/kernel/xray/config.go +++ b/internal/kernel/xray/config.go @@ -14,20 +14,33 @@ import ( type M = map[string]interface{} func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M { - outbounds := []M{ - {"protocol": "freedom", "tag": "direct"}, - {"protocol": "blackhole", "tag": "block"}, - } + var outbounds []M + tags := make(map[string]bool) // Panel-defined custom outbounds (structured, converted to Xray native) for _, co := range nc.CustomOutbounds { outbounds = append(outbounds, outboundConfigToXray(co)) + tags[strings.ToLower(co.Tag)] = true } - // Static outbounds from local config file (already kernel-native format) + + // Static outbounds from local config file for _, co := range kcfg.CustomOutbound { + tag, _ := co["tag"].(string) + if tag != "" { + tags[strings.ToLower(tag)] = true + } outbounds = append(outbounds, M(co)) } + // Add default outbounds only if not already defined (Issue #1: Panel priority) + if !tags["direct"] { + outbounds = append([]M{{"protocol": "freedom", "tag": "direct"}}, outbounds...) + } + if !tags["block"] { + // block is often added after direct but before others for safety + outbounds = append(outbounds, M{"protocol": "blackhole", "tag": "block"}) + } + cfg := M{ "log": M{ "loglevel": xrayLogLevel(kcfg.LogLevel), diff --git a/internal/kernel/xray/config_test.go b/internal/kernel/xray/config_test.go index e27b966..5dbf5d0 100644 --- a/internal/kernel/xray/config_test.go +++ b/internal/kernel/xray/config_test.go @@ -18,6 +18,55 @@ var testUsers = []panel.User{ {ID: 5, UUID: "4d5965c8-a60c-452a-a943-af83ec0bb0db"}, } +func TestBuildConfig_OutboundPriority(t *testing.T) { + kcfg := config.KernelConfig{ + LogLevel: "info", + CustomOutbound: []map[string]any{ + {"tag": "block", "protocol": "dns"}, // Local static override + }, + } + nc := &panel.NodeConfig{ + Protocol: "shadowsocks", + ServerPort: 111, + Cipher: "aes-128-gcm", + CustomOutbounds: []panel.OutboundConfig{ + {Tag: "direct", Protocol: "socks", Settings: map[string]any{"address": "1.2.3.4"}}, // Panel override + }, + } + + cfg := buildConfig(kcfg, nc, testUsers, "", "") + outbounds := cfg["outbounds"].([]M) + + // Since we overrode both 'direct' and 'block', the result should contain + // exactly these two custom outbounds, without auto-generated defaults. + if len(outbounds) != 2 { + t.Errorf("outbounds: got %d, want 2", len(outbounds)) + } + + foundDirect := false + foundBlock := false + + for _, o := range outbounds { + tag := o["tag"].(string) + if tag == "direct" { + foundDirect = true + if o["protocol"] != "socks" { + t.Errorf("expected 'direct' protocol to be 'socks' (panel priority), got %v", o["protocol"]) + } + } + if tag == "block" { + foundBlock = true + if o["protocol"] != "dns" { + t.Errorf("expected 'block' protocol to be 'dns' (static config priority), got %v", o["protocol"]) + } + } + } + + if !foundDirect || !foundBlock { + t.Errorf("missing outbounds: direct=%v, block=%v", foundDirect, foundBlock) + } +} + func TestBuildConfig_AllProtocols_ValidJSON(t *testing.T) { protocols := []struct { name string