diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index 462a880a0..7712e06c3 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -962,6 +962,17 @@ func amneziawgV6EgressTag(inboundID int, email string) string { 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 // peer with an IPv6 AllowedIPs entry its own single-purpose freedom // 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, "protocol": "freedom", "sendThrough": v6, - "settings": map[string]any{}, + "settings": amneziawgV6EgressSettings(), }) newRules = append(newRules, map[string]any{ "type": "field", diff --git a/internal/web/service/xray_config_inject_test.go b/internal/web/service/xray_config_inject_test.go index 71f4dd760..f5433d101 100644 --- a/internal/web/service/xray_config_inject_test.go +++ b/internal/web/service/xray_config_inject_test.go @@ -1053,3 +1053,35 @@ func TestInjectAmneziawgV6Egress_RulesPrependedBeforeExistingRules(t *testing.T) 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) +}