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
This commit is contained in:
@@ -17,20 +17,32 @@ import (
|
|||||||
type M = map[string]interface{}
|
type M = map[string]interface{}
|
||||||
|
|
||||||
func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M {
|
func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M {
|
||||||
outbounds := []M{
|
var outbounds []M
|
||||||
{"type": "direct", "tag": "direct"},
|
tags := make(map[string]bool)
|
||||||
{"type": "block", "tag": "block"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panel-defined custom outbounds (structured, converted to sing-box native)
|
// Panel-defined custom outbounds (structured, converted to sing-box native)
|
||||||
for _, co := range nc.CustomOutbounds {
|
for _, co := range nc.CustomOutbounds {
|
||||||
outbounds = append(outbounds, outboundConfigToSingbox(co))
|
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 {
|
for _, co := range kcfg.CustomOutbound {
|
||||||
|
tag, _ := co["tag"].(string)
|
||||||
|
if tag != "" {
|
||||||
|
tags[strings.ToLower(tag)] = true
|
||||||
|
}
|
||||||
outbounds = append(outbounds, M(co))
|
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{
|
cfg := M{
|
||||||
"log": M{
|
"log": M{
|
||||||
"level": kcfg.LogLevel,
|
"level": kcfg.LogLevel,
|
||||||
@@ -361,19 +373,7 @@ func buildShadowsocks(base M, nc *panel.NodeConfig, users []panel.User) M {
|
|||||||
base["users"] = userList
|
base["users"] = userList
|
||||||
|
|
||||||
if nc.Plugin != "" {
|
if nc.Plugin != "" {
|
||||||
base["plugin"] = nc.Plugin
|
slog.Warn("sing-box shadowsocks inbound does not support plugin, ignoring", "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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return base
|
return base
|
||||||
|
|||||||
@@ -469,6 +469,55 @@ func TestBuildConfig(t *testing.T) {
|
|||||||
assertMapValue(t, inbounds[0], "type", "shadowsocks")
|
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) {
|
func TestBuildConfig_AllProtocols_ValidJSON(t *testing.T) {
|
||||||
protocols := []struct {
|
protocols := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -14,20 +14,33 @@ import (
|
|||||||
type M = map[string]interface{}
|
type M = map[string]interface{}
|
||||||
|
|
||||||
func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M {
|
func buildConfig(kcfg config.KernelConfig, nc *panel.NodeConfig, users []panel.User, certFile, keyFile string) M {
|
||||||
outbounds := []M{
|
var outbounds []M
|
||||||
{"protocol": "freedom", "tag": "direct"},
|
tags := make(map[string]bool)
|
||||||
{"protocol": "blackhole", "tag": "block"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panel-defined custom outbounds (structured, converted to Xray native)
|
// Panel-defined custom outbounds (structured, converted to Xray native)
|
||||||
for _, co := range nc.CustomOutbounds {
|
for _, co := range nc.CustomOutbounds {
|
||||||
outbounds = append(outbounds, outboundConfigToXray(co))
|
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 {
|
for _, co := range kcfg.CustomOutbound {
|
||||||
|
tag, _ := co["tag"].(string)
|
||||||
|
if tag != "" {
|
||||||
|
tags[strings.ToLower(tag)] = true
|
||||||
|
}
|
||||||
outbounds = append(outbounds, M(co))
|
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{
|
cfg := M{
|
||||||
"log": M{
|
"log": M{
|
||||||
"loglevel": xrayLogLevel(kcfg.LogLevel),
|
"loglevel": xrayLogLevel(kcfg.LogLevel),
|
||||||
|
|||||||
@@ -18,6 +18,55 @@ var testUsers = []panel.User{
|
|||||||
{ID: 5, UUID: "4d5965c8-a60c-452a-a943-af83ec0bb0db"},
|
{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) {
|
func TestBuildConfig_AllProtocols_ValidJSON(t *testing.T) {
|
||||||
protocols := []struct {
|
protocols := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user