fix: compile structured custom_routes with match+action into sing-box format
The panel sends structured route rules via custom_routes (not custom_route_rules). Previously these were passed raw to sing-box, causing unmarshal errors when the action field is an object. Now detect and compile them through the same path as custom_route_rules.
This commit is contained in:
@@ -155,8 +155,15 @@ func buildRoutes(panelRoutes []model.RouteRule, customRules []model.CustomRouteR
|
||||
rules = append(rules, compileCustomRouteRule(rule)...)
|
||||
}
|
||||
|
||||
// Raw custom routes remain the escape hatch, but no longer outrank structured rules.
|
||||
// Raw custom routes: detect structured format (match+action) and compile them,
|
||||
// otherwise pass through as raw sing-box rules.
|
||||
for _, cr := range custom {
|
||||
if _, hasMatch := cr["match"]; hasMatch {
|
||||
if parsed, ok := tryParseAsCustomRouteRule(cr); ok {
|
||||
rules = append(rules, compileCustomRouteRule(parsed)...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
rules = append(rules, M(cr))
|
||||
}
|
||||
|
||||
@@ -309,6 +316,81 @@ func compileCustomRouteRule(rule model.CustomRouteRule) []M {
|
||||
return compiled
|
||||
}
|
||||
|
||||
|
||||
// tryParseAsCustomRouteRule attempts to convert a raw map (from custom_routes)
|
||||
// into a structured CustomRouteRule. Returns false if the map doesn't match
|
||||
// the structured format.
|
||||
func tryParseAsCustomRouteRule(cr map[string]any) (model.CustomRouteRule, bool) {
|
||||
var rule model.CustomRouteRule
|
||||
|
||||
// Parse name
|
||||
if name, ok := cr["name"].(string); ok {
|
||||
rule.Name = name
|
||||
}
|
||||
if disabled, ok := cr["disabled"].(bool); ok {
|
||||
rule.Disabled = disabled
|
||||
}
|
||||
|
||||
// Parse match
|
||||
matchRaw, ok := cr["match"].(map[string]any)
|
||||
if !ok {
|
||||
return rule, false
|
||||
}
|
||||
rule.Match = parseRouteMatch(matchRaw)
|
||||
|
||||
// Parse action
|
||||
switch action := cr["action"].(type) {
|
||||
case string:
|
||||
rule.Action = model.RouteAction{Type: action}
|
||||
case map[string]any:
|
||||
if t, ok := action["type"].(string); ok {
|
||||
rule.Action.Type = t
|
||||
}
|
||||
if target, ok := action["target"].(string); ok {
|
||||
rule.Action.Target = target
|
||||
}
|
||||
default:
|
||||
return rule, false
|
||||
}
|
||||
|
||||
return rule, true
|
||||
}
|
||||
|
||||
func parseRouteMatch(m map[string]any) model.RouteMatch {
|
||||
var match model.RouteMatch
|
||||
if v, ok := m["domains"].([]any); ok {
|
||||
match.Domains = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["domain_suffixes"].([]any); ok {
|
||||
match.DomainSuffixes = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["ip_cidrs"].([]any); ok {
|
||||
match.IPCIDRs = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["ports"].([]any); ok {
|
||||
match.Ports = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["networks"].([]any); ok {
|
||||
match.Networks = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["source_cidrs"].([]any); ok {
|
||||
match.SourceCIDRs = toStringSlice(v)
|
||||
}
|
||||
if v, ok := m["source_ports"].([]any); ok {
|
||||
match.SourcePorts = toStringSlice(v)
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func toStringSlice(arr []any) []string {
|
||||
result := make([]string, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if s, ok := v.(string); ok {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
func singboxOutboundForAction(action model.RouteAction) string {
|
||||
switch action.Type {
|
||||
case "direct":
|
||||
|
||||
Reference in New Issue
Block a user