feat(settings): allow a balancer as the panel traffic outbound

The panel egress is injected as a routing rule, so a routing balancer is
a valid target for it (unlike the geodata download, which dials a forced
outbound tag and bypasses the router). Surface routing balancers in the
panel outbound picker as a separate group, and emit balancerTag instead
of outboundTag in the injected egress rule when the configured tag names
a balancer, so the panel's own traffic load-balances across its members.
This commit is contained in:
MHSanaei
2026-06-11 23:32:58 +02:00
parent c47a905ad2
commit 8578b229ce
3 changed files with 116 additions and 9 deletions
+34 -3
View File
@@ -317,9 +317,17 @@ func injectPanelEgress(cfg *xray.Config, outboundTag string) {
}
rules, _ := routing["rules"].([]any)
rule := map[string]any{
"type": "field",
"inboundTag": []any{PanelEgressInboundTag},
"outboundTag": outboundTag,
"type": "field",
"inboundTag": []any{PanelEgressInboundTag},
}
// The configured tag may name a routing balancer instead of a concrete
// outbound. A field rule can target either, so emit the matching key —
// balancerTag load-balances the panel's own traffic across the balancer's
// outbounds, while a plain outbound tag keeps the original behavior.
if routingTagIsBalancer(routing, outboundTag) {
rule["balancerTag"] = outboundTag
} else {
rule["outboundTag"] = outboundTag
}
routing["rules"] = append([]any{rule}, rules...)
newRouting, err := json.Marshal(routing)
@@ -350,6 +358,29 @@ func injectPanelEgress(cfg *xray.Config, outboundTag string) {
})
}
// routingTagIsBalancer reports whether tag names a balancer in the parsed
// routing section. The panel-egress rule targets a balancer via balancerTag and
// a concrete outbound via outboundTag, so the caller picks the key from this.
func routingTagIsBalancer(routing map[string]any, tag string) bool {
if tag == "" {
return false
}
balancers, ok := routing["balancers"].([]any)
if !ok {
return false
}
for _, b := range balancers {
bm, ok := b.(map[string]any)
if !ok {
continue
}
if t, ok := bm["tag"].(string); ok && t == tag {
return true
}
}
return false
}
// mergeSubscriptionOutbounds appends the subscription outbounds to the
// OutboundConfigs array of the xray config. It works on the already-unmarshaled
// template so that manually configured outbounds are never overwritten.
@@ -169,6 +169,55 @@ func TestInjectPanelEgress(t *testing.T) {
}
}
func TestInjectPanelEgress_BalancerTag(t *testing.T) {
cfg := egressTestConfig()
cfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
// A tag that names a balancer must be targeted via balancerTag so the
// router resolves it; an outbound tag coexisting with balancers still uses
// outboundTag.
injectPanelEgress(cfg, "lb")
var routing struct {
Rules []struct {
InboundTag []string `json:"inboundTag"`
OutboundTag string `json:"outboundTag"`
BalancerTag string `json:"balancerTag"`
Type string `json:"type"`
} `json:"rules"`
}
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 1 {
t.Fatalf("expected the egress rule, got %+v", routing.Rules)
}
first := routing.Rules[0]
if first.BalancerTag != "lb" || first.OutboundTag != "" {
t.Fatalf("a balancer tag must target balancerTag, not outboundTag, got %+v", first)
}
if len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
t.Fatalf("egress rule must bind the egress inbound, got %+v", first)
}
// A non-balancer tag alongside balancers keeps the plain outbound path.
cfg2 := egressTestConfig()
cfg2.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
injectPanelEgress(cfg2, "warp")
var routing2 struct {
Rules []struct {
OutboundTag string `json:"outboundTag"`
BalancerTag string `json:"balancerTag"`
} `json:"rules"`
}
if err := json.Unmarshal(cfg2.RouterConfig, &routing2); err != nil {
t.Fatal(err)
}
if routing2.Rules[0].OutboundTag != "warp" || routing2.Rules[0].BalancerTag != "" {
t.Fatalf("a concrete outbound must target outboundTag, got %+v", routing2.Rules[0])
}
}
func TestInjectPanelEgress_PortCollision(t *testing.T) {
cfg := egressTestConfig()
cfg.InboundConfigs = append(cfg.InboundConfigs,