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:
@@ -510,21 +510,52 @@ func ParseTuic(line string, index int) model.ProxyNode {
|
||||
func ParseWireGuard(line string, index int) model.ProxyNode {
|
||||
normalized := strings.Replace(line, "wg://", "wireguard://", 1)
|
||||
u := parseURL(normalized)
|
||||
// WireGuard keys are base64-encoded and may contain '+' characters.
|
||||
// Go's url.Query() treats '+' as space (form-encoding behavior), which corrupts base64 keys.
|
||||
// Use rawQueryUnescapeForKeys to preserve '+' in key values.
|
||||
return StripUndefined(map[string]any{
|
||||
"name": fragmentName(u, fmt.Sprintf("wireguard-%d", index+1)),
|
||||
"type": "wireguard",
|
||||
"server": u.Hostname(),
|
||||
"port": portFromURL(u, 51820),
|
||||
"ip": firstNonEmpty(paramGet(u, "ip"), paramGet(u, "address")),
|
||||
"ipv6": paramGet(u, "ipv6"),
|
||||
"private-key": firstNonEmpty(userInfo(u), paramGet(u, "private-key"), paramGet(u, "privatekey")),
|
||||
"public-key": firstNonEmpty(paramGet(u, "public-key"), paramGet(u, "publickey"), paramGet(u, "peer-public-key")),
|
||||
"pre-shared-key": firstNonEmpty(paramGet(u, "pre-shared-key"), paramGet(u, "presharedkey"), paramGet(u, "psk")),
|
||||
"reserved": paramGet(u, "reserved"),
|
||||
"udp": true,
|
||||
"name": fragmentName(u, fmt.Sprintf("wireguard-%d", index+1)),
|
||||
"type": "wireguard",
|
||||
"server": u.Hostname(),
|
||||
"port": portFromURL(u, 51820),
|
||||
"ip": firstNonEmpty(rawParamGet(u, "ip"), rawParamGet(u, "address")),
|
||||
"ipv6": rawParamGet(u, "ipv6"),
|
||||
"private-key": firstNonEmpty(userInfo(u), rawParamGet(u, "private-key"), rawParamGet(u, "privatekey")),
|
||||
"public-key": firstNonEmpty(rawParamGet(u, "public-key"), rawParamGet(u, "publickey"), rawParamGet(u, "peer-public-key")),
|
||||
"pre-shared-key": firstNonEmpty(rawParamGet(u, "pre-shared-key"), rawParamGet(u, "presharedkey"), rawParamGet(u, "psk")),
|
||||
"reserved": rawParamGet(u, "reserved"),
|
||||
"udp": true,
|
||||
})
|
||||
}
|
||||
|
||||
// rawParamGet retrieves a query parameter value while preserving '+' characters.
|
||||
// This is necessary for base64-encoded values (e.g., WireGuard keys) where
|
||||
// Go's standard url.Query() would incorrectly convert '+' to spaces.
|
||||
func rawParamGet(u *url.URL, key string) string {
|
||||
raw := u.RawQuery
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
for _, pair := range strings.Split(raw, "&") {
|
||||
kv := strings.SplitN(pair, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
k, _ := url.QueryUnescape(kv[0])
|
||||
if k != key {
|
||||
continue
|
||||
}
|
||||
// Unescape manually, but preserve '+' as literal '+'
|
||||
val := strings.ReplaceAll(kv[1], "+", "%2B")
|
||||
v, err := url.QueryUnescape(val)
|
||||
if err != nil {
|
||||
return kv[1]
|
||||
}
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BoolParam returns true if value is "1" or "true".
|
||||
func BoolParam(value string) bool {
|
||||
return value == "1" || value == "true"
|
||||
|
||||
Reference in New Issue
Block a user