fix(xray): block private-range egress in per-peer AmneziaWG IPv6 outbounds

The per-peer IPv6 freedom outbounds the panel injects for AmneziaWG
clients were built with empty settings and their routing rule sits
ahead of the template's private-range block, so a peer's IPv6 traffic
skipped both layers and could reach fc00::/7 hosts on the panel's
network.

Each per-peer outbound now carries the same private-range finalRules
block every other freedom outbound gets.
This commit is contained in:
MHSanaei
2026-11-01 10:03:17 +01:00
parent ecd817658c
commit 082d7ac091
2 changed files with 44 additions and 1 deletions
+12 -1
View File
@@ -962,6 +962,17 @@ func amneziawgV6EgressTag(inboundID int, email string) string {
return fmt.Sprintf("amneziawg-v6-%d-%s", inboundID, email) return fmt.Sprintf("amneziawg-v6-%d-%s", inboundID, email)
} }
// amneziawgV6EgressSettings carries the template's private-range egress block:
// the per-peer rule is matched ahead of the routing-level geoip:private rule.
func amneziawgV6EgressSettings() map[string]any {
return map[string]any{
"finalRules": []any{
map[string]any{"action": "block", "ip": []any{"geoip:private"}},
map[string]any{"action": "allow"},
},
}
}
// injectAmneziawgV6Egress gives every enabled, non-node-hosted AmneziaWG // injectAmneziawgV6Egress gives every enabled, non-node-hosted AmneziaWG
// peer with an IPv6 AllowedIPs entry its own single-purpose freedom // peer with an IPv6 AllowedIPs entry its own single-purpose freedom
// outbound, bound via sendThrough to that exact address, plus a routing // outbound, bound via sendThrough to that exact address, plus a routing
@@ -1061,7 +1072,7 @@ func injectAmneziawgV6Egress(cfg *xray.Config, inbounds []*model.Inbound) {
"tag": tag, "tag": tag,
"protocol": "freedom", "protocol": "freedom",
"sendThrough": v6, "sendThrough": v6,
"settings": map[string]any{}, "settings": amneziawgV6EgressSettings(),
}) })
newRules = append(newRules, map[string]any{ newRules = append(newRules, map[string]any{
"type": "field", "type": "field",
@@ -1053,3 +1053,35 @@ func TestInjectAmneziawgV6Egress_RulesPrependedBeforeExistingRules(t *testing.T)
t.Fatalf("the pre-existing rule must survive, got %+v", routing.Rules[1]) t.Fatalf("the pre-existing rule must survive, got %+v", routing.Rules[1])
} }
} }
// The per-peer rule precedes the template's geoip:private routing rule, so the
// outbound itself must block private-range egress, as "direct" does.
func TestInjectAmneziawgV6Egress_OutboundBlocksPrivateEgress(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
})
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
var outbounds []struct {
Tag string `json:"tag"`
Settings struct {
FinalRules []any `json:"finalRules"`
} `json:"settings"`
}
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
t.Fatal(err)
}
want := `[{"action":"block","ip":["geoip:private"]},{"action":"allow"}]`
for _, o := range outbounds {
if o.Tag != amneziawgV6EgressTag(1, "a@x") {
continue
}
got, _ := json.Marshal(o.Settings.FinalRules)
if string(got) != want {
t.Fatalf("per-peer outbound finalRules = %s, want %s", got, want)
}
return
}
t.Fatalf("no outbound tagged %q in %s", amneziawgV6EgressTag(1, "a@x"), cfg.OutboundConfigs)
}