feat: sing-box Clash template support + Docker CI + frontend scaffold
- Clash→sing-box template conversion engine (singbox_template.go) - Auto-map 40+ Clash rule-providers to sing-box .srs binary rule-sets - Fix sing-box 1.11+/1.12+ migrations (sniff/block/WireGuard/DNS/rule_set) - Fix WireGuard URI '+' parsing bug (rawParamGet) - Add REJECT block outbound for selector group references - Skip rules referencing rule-sets with no sing-box equivalent - Verified: sing-box run succeeds with ACL4SSR template (16 rule-sets loaded) - Add Dockerfile (multi-stage Go build) - Add GitHub Actions workflow (multi-arch: amd64+arm64, push to ghcr.io) - Add frontend scaffold (Vue3 + Vite + Tailwind)
This commit is contained in:
+77
-44
@@ -10,9 +10,16 @@ import (
|
||||
|
||||
// RenderSingBoxJson renders proxies as a sing-box JSON config.
|
||||
// Per review-resolution #42: full structure with log, inbounds, outbounds, route.
|
||||
// Updated for sing-box 1.11+ migration: legacy inbound fields (sniff) → route rule actions,
|
||||
// legacy special outbound (block) → route rule action (reject), WireGuard outbound → endpoint.
|
||||
func RenderSingBoxJson(proxies []model.ProxyNode) string {
|
||||
var nodeOutbounds []map[string]any
|
||||
var wireGuardEndpoints []map[string]any
|
||||
for _, p := range proxies {
|
||||
if wg := ToSingBoxWireGuardEndpoint(p); wg != nil {
|
||||
wireGuardEndpoints = append(wireGuardEndpoints, wg)
|
||||
continue
|
||||
}
|
||||
if out := ToSingBoxOutbound(p); out != nil {
|
||||
nodeOutbounds = append(nodeOutbounds, out)
|
||||
}
|
||||
@@ -25,29 +32,26 @@ func RenderSingBoxJson(proxies []model.ProxyNode) string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
// return minimal config even with no supported nodes
|
||||
}
|
||||
|
||||
// Build outbounds: PROXY (selector), AUTO (urltest), node outbounds, DIRECT, REJECT
|
||||
// Build outbounds: PROXY (selector), AUTO (urltest), node outbounds, DIRECT
|
||||
// (REJECT is now a route rule action, not a special outbound)
|
||||
proxyOutbounds := append([]string{"AUTO"}, tags...)
|
||||
|
||||
outbounds := []any{
|
||||
map[string]any{
|
||||
"type": "selector",
|
||||
"tag": "PROXY",
|
||||
"outbounds": proxyOutbounds,
|
||||
"default": "AUTO",
|
||||
"interrupt_exist_connections": false,
|
||||
"type": "selector",
|
||||
"tag": "PROXY",
|
||||
"outbounds": proxyOutbounds,
|
||||
"default": "AUTO",
|
||||
"interrupt_exist_connections": false,
|
||||
},
|
||||
map[string]any{
|
||||
"type": "urltest",
|
||||
"tag": "AUTO",
|
||||
"outbounds": tags,
|
||||
"url": model.TestURL,
|
||||
"interval": "5m",
|
||||
"tolerance": 50,
|
||||
"interrupt_exist_connections": false,
|
||||
"type": "urltest",
|
||||
"tag": "AUTO",
|
||||
"outbounds": tags,
|
||||
"url": model.TestURL,
|
||||
"interval": "5m",
|
||||
"tolerance": 50,
|
||||
"interrupt_exist_connections": false,
|
||||
},
|
||||
}
|
||||
for _, out := range nodeOutbounds {
|
||||
@@ -55,7 +59,6 @@ func RenderSingBoxJson(proxies []model.ProxyNode) string {
|
||||
}
|
||||
outbounds = append(outbounds,
|
||||
map[string]any{"type": "direct", "tag": "DIRECT"},
|
||||
map[string]any{"type": "block", "tag": "REJECT"},
|
||||
)
|
||||
|
||||
doc := map[string]any{
|
||||
@@ -66,16 +69,26 @@ func RenderSingBoxJson(proxies []model.ProxyNode) string {
|
||||
"tag": "mixed-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 7890,
|
||||
"sniff": true,
|
||||
},
|
||||
},
|
||||
"outbounds": outbounds,
|
||||
"route": map[string]any{
|
||||
"auto_detect_interface": true,
|
||||
"final": "PROXY",
|
||||
"rules": []any{
|
||||
// Migrated from legacy inbound sniff field (sing-box 1.11+)
|
||||
map[string]any{"action": "sniff"},
|
||||
// Migrated from legacy block outbound (sing-box 1.11+)
|
||||
map[string]any{"action": "reject", "outbound": "REJECT"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add WireGuard endpoints if any (sing-box 1.11+ migration)
|
||||
if len(wireGuardEndpoints) > 0 {
|
||||
doc["endpoints"] = wireGuardEndpoints
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(doc, "", " ")
|
||||
return string(data)
|
||||
}
|
||||
@@ -243,31 +256,6 @@ func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any {
|
||||
"password": proxy["password"],
|
||||
})
|
||||
|
||||
case "wireguard":
|
||||
var localAddress []string
|
||||
if ip := stringSetting(proxy["ip"]); ip != "" {
|
||||
localAddress = append(localAddress, ip)
|
||||
}
|
||||
if ipv6 := stringSetting(proxy["ipv6"]); ipv6 != "" {
|
||||
localAddress = append(localAddress, ipv6)
|
||||
}
|
||||
result := util.StripUndefined(map[string]any{
|
||||
"type": "wireguard",
|
||||
"tag": name,
|
||||
"server": server,
|
||||
"server_port": port,
|
||||
"private_key": proxy["private-key"],
|
||||
"peer_public_key": proxy["public-key"],
|
||||
"pre_shared_key": proxy["pre-shared-key"],
|
||||
})
|
||||
if len(localAddress) > 0 {
|
||||
result["local_address"] = localAddress
|
||||
}
|
||||
if reserved := parseWireGuardReserved(proxy["reserved"]); reserved != nil {
|
||||
result["reserved"] = reserved
|
||||
}
|
||||
return result
|
||||
|
||||
case "vmess":
|
||||
var tls any
|
||||
if getBool(proxy, "tls") {
|
||||
@@ -309,6 +297,51 @@ func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToSingBoxWireGuardEndpoint converts a WireGuard proxy node to a sing-box endpoint.
|
||||
// Per sing-box 1.11+ migration: WireGuard outbound → endpoint format.
|
||||
// Returns nil for non-wireguard proxies.
|
||||
func ToSingBoxWireGuardEndpoint(proxy model.ProxyNode) map[string]any {
|
||||
ptype := getString(proxy, "type")
|
||||
if ptype != "wireguard" {
|
||||
return nil
|
||||
}
|
||||
server := getString(proxy, "server")
|
||||
port := getInt(proxy, "port")
|
||||
name := getString(proxy, "name")
|
||||
|
||||
var addresses []string
|
||||
if ip := stringSetting(proxy["ip"]); ip != "" {
|
||||
addresses = append(addresses, ip)
|
||||
}
|
||||
if ipv6 := stringSetting(proxy["ipv6"]); ipv6 != "" {
|
||||
addresses = append(addresses, ipv6)
|
||||
}
|
||||
|
||||
peer := util.StripUndefined(map[string]any{
|
||||
"address": server,
|
||||
"port": port,
|
||||
"public_key": proxy["public-key"],
|
||||
"allowed_ips": []string{"0.0.0.0/0", "::/0"},
|
||||
})
|
||||
if psk := stringSetting(proxy["pre-shared-key"]); psk != "" {
|
||||
peer["pre_shared_key"] = psk
|
||||
}
|
||||
if reserved := parseWireGuardReserved(proxy["reserved"]); reserved != nil {
|
||||
peer["reserved"] = reserved
|
||||
}
|
||||
|
||||
endpoint := util.StripUndefined(map[string]any{
|
||||
"type": "wireguard",
|
||||
"tag": name,
|
||||
"private_key": proxy["private-key"],
|
||||
"peers": []any{peer},
|
||||
})
|
||||
if len(addresses) > 0 {
|
||||
endpoint["address"] = addresses
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func parseWireGuardReserved(value any) []int {
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
|
||||
Reference in New Issue
Block a user