Files
3x-ui/internal/web/service/xray_setting_routing_sync.go
T
MHSanaei 8c63f7cc81 fix(xray): keep source- and domains-scoped routing rules when an inbound is deleted
removeInboundTagFromRules drops a routing rule whose inboundTag list becomes
empty only if the rule has no other matcher, but routingMatcherKeys omitted
xray-core's canonical source and domains keys. A rule scoped by source or domains
(common in hand-authored or imported configs) therefore lost its whole body —
including a security-relevant block — when its single listed inbound was deleted,
instead of just having the tag trimmed. Recognize source and domains as live
matchers.
2026-07-15 03:17:33 +02:00

312 lines
7.0 KiB
Go

package service
import (
"encoding/json"
)
var routingMatcherKeys = []string{
"domain", "domains", "ip", "port", "sourcePort", "localPort", "network",
"source", "sourceIP", "localIP", "user", "vlessRoute", "protocol", "attrs", "process",
}
func readInboundTags(raw any) []string {
switch tags := raw.(type) {
case []string:
return append([]string(nil), tags...)
case string:
if tags == "" {
return nil
}
return []string{tags}
case []any:
out := make([]string, 0, len(tags))
for _, item := range tags {
if s, ok := item.(string); ok && s != "" {
out = append(out, s)
}
}
return out
default:
return nil
}
}
func writeInboundTags(rule map[string]any, tags []string) {
if len(tags) == 0 {
delete(rule, "inboundTag")
return
}
rule["inboundTag"] = tags
}
func ruleHasNonInboundMatchers(rule map[string]any) bool {
for _, key := range routingMatcherKeys {
if hasRoutingMatcherValue(rule[key]) {
return true
}
}
return false
}
func hasRoutingMatcherValue(raw any) bool {
switch v := raw.(type) {
case nil:
return false
case string:
return v != ""
case float64, int, int64, bool:
return true
case []string:
return len(v) > 0
case []any:
return len(v) > 0
case map[string]any:
return len(v) > 0
default:
return true
}
}
func replaceInboundTagInRules(rules []map[string]any, oldTag, newTag string) bool {
changed := false
for _, rule := range rules {
if replaceInboundTagInRule(rule, oldTag, newTag) {
changed = true
}
}
return changed
}
func replaceInboundTagInRule(rule map[string]any, oldTag, newTag string) bool {
tags := readInboundTags(rule["inboundTag"])
if len(tags) == 0 {
return false
}
updated := false
for i, tag := range tags {
if tag == oldTag {
tags[i] = newTag
updated = true
}
}
if updated {
writeInboundTags(rule, tags)
}
return updated
}
func removeInboundTagFromRules(rules []map[string]any, deletedTag string) ([]map[string]any, bool) {
if deletedTag == "" {
return rules, false
}
changed := false
out := make([]map[string]any, 0, len(rules))
for _, rule := range rules {
tags := readInboundTags(rule["inboundTag"])
if len(tags) == 0 {
out = append(out, rule)
continue
}
nextTags := make([]string, 0, len(tags))
hadDeleted := false
for _, tag := range tags {
if tag == deletedTag {
hadDeleted = true
continue
}
nextTags = append(nextTags, tag)
}
if !hadDeleted {
out = append(out, rule)
continue
}
changed = true
if len(nextTags) == 0 && !ruleHasNonInboundMatchers(rule) {
continue
}
if len(nextTags) == 0 {
delete(rule, "inboundTag")
} else {
writeInboundTags(rule, nextTags)
}
out = append(out, rule)
}
return out, changed
}
func replaceInboundTagInOutbounds(outbounds []any, oldTag, newTag string) bool {
changed := false
for _, outIface := range outbounds {
out, ok := outIface.(map[string]any)
if !ok {
continue
}
proto, _ := out["protocol"].(string)
if proto != "loopback" {
continue
}
settings, ok := out["settings"].(map[string]any)
if !ok {
continue
}
tag, _ := settings["inboundTag"].(string)
if tag != oldTag {
continue
}
settings["inboundTag"] = newTag
changed = true
}
return changed
}
func removeInboundTagFromOutbounds(outbounds []any, deletedTag string) bool {
changed := false
for _, outIface := range outbounds {
out, ok := outIface.(map[string]any)
if !ok {
continue
}
proto, _ := out["protocol"].(string)
if proto != "loopback" {
continue
}
settings, ok := out["settings"].(map[string]any)
if !ok {
continue
}
tag, _ := settings["inboundTag"].(string)
if tag != deletedTag {
continue
}
delete(settings, "inboundTag")
changed = true
}
return changed
}
func mutateXrayTemplateRouting(raw string, mutate func(cfg map[string]any) bool) (string, bool, error) {
raw = UnwrapXrayTemplateConfig(raw)
var cfg map[string]any
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return raw, false, err
}
if !mutate(cfg) {
return raw, false, nil
}
out, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return raw, false, err
}
return string(out), true, nil
}
func routingRulesFromCfg(cfg map[string]any) []map[string]any {
routing, _ := cfg["routing"].(map[string]any)
if routing == nil {
return nil
}
rawRules, ok := routing["rules"].([]any)
if !ok {
return nil
}
rules := make([]map[string]any, 0, len(rawRules))
for _, item := range rawRules {
rule, ok := item.(map[string]any)
if !ok {
continue
}
rules = append(rules, rule)
}
return rules
}
func setRoutingRulesInCfg(cfg map[string]any, rules []map[string]any) {
routing, _ := cfg["routing"].(map[string]any)
if routing == nil {
routing = map[string]any{}
cfg["routing"] = routing
}
items := make([]any, len(rules))
for i, rule := range rules {
items[i] = rule
}
routing["rules"] = items
}
func outboundsFromCfg(cfg map[string]any) []any {
outbounds, _ := cfg["outbounds"].([]any)
return outbounds
}
// PropagateInboundTagRename rewrites routing rules and loopback outbound
// references when a panel inbound tag changes.
func (s *XraySettingService) PropagateInboundTagRename(oldTag, newTag string) (bool, error) {
if oldTag == "" || newTag == "" || oldTag == newTag {
return false, nil
}
template, err := s.GetXrayConfigTemplate()
if err != nil {
return false, err
}
updated, changed, err := mutateXrayTemplateRouting(template, func(cfg map[string]any) bool {
mutated := false
rules := routingRulesFromCfg(cfg)
if len(rules) > 0 {
if replaceInboundTagInRules(rules, oldTag, newTag) {
setRoutingRulesInCfg(cfg, rules)
mutated = true
}
}
outbounds := outboundsFromCfg(cfg)
if len(outbounds) > 0 && replaceInboundTagInOutbounds(outbounds, oldTag, newTag) {
cfg["outbounds"] = outbounds
mutated = true
}
return mutated
})
if err != nil || !changed {
return false, err
}
if err := s.SaveXraySetting(updated); err != nil {
return false, err
}
return true, nil
}
// RemoveInboundTagReferences drops a deleted inbound tag from routing rules.
// Rules that only matched that inbound are removed; rules with additional
// matchers keep the rule and only lose the inboundTag entry.
func (s *XraySettingService) RemoveInboundTagReferences(deletedTag string) (bool, error) {
if deletedTag == "" {
return false, nil
}
template, err := s.GetXrayConfigTemplate()
if err != nil {
return false, err
}
updated, changed, err := mutateXrayTemplateRouting(template, func(cfg map[string]any) bool {
mutated := false
rules := routingRulesFromCfg(cfg)
if len(rules) > 0 {
nextRules, rulesChanged := removeInboundTagFromRules(rules, deletedTag)
if rulesChanged {
setRoutingRulesInCfg(cfg, nextRules)
mutated = true
}
}
outbounds := outboundsFromCfg(cfg)
if len(outbounds) > 0 && removeInboundTagFromOutbounds(outbounds, deletedTag) {
cfg["outbounds"] = outbounds
mutated = true
}
return mutated
})
if err != nil || !changed {
return false, err
}
if err := s.SaveXraySetting(updated); err != nil {
return false, err
}
return true, nil
}