refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles

Simplifies RouteViaXray after realizing the panel already has everything
needed: the Routing page already lets an admin pick a source inbound tag
and a target outbound (plus, if they want it, a specific source IP) for
any protocol. Bolting a parallel routeThroughXray/routeOutboundTag pair
onto both the client and inbound forms duplicated that mechanism instead
of using it.

Removed entirely: Client/ClientRecord/ServerSettings/Peer's
RouteThroughXray + RouteOutboundTag fields, the effective-routing OR/
fallback logic in InstanceFromInbound, and the Switch+Select UI on both
forms. Nothing configures "route via Xray" as a setting anymore.

In its place, every enabled AmneziaWG inbound now gets its own Xray
TPROXY bridge unconditionally, by default, no toggle:

- internal/amneziawg: every peer's traffic is always TPROXY'd into that
  instance's own bridge (defaultPostUpDown, port derived from the
  inbound's id via EgressPortForInbound so the kernel side and the
  Xray-config side never need to negotiate a runtime value). Since the
  TPROXY rule is now tied to a peer's mere presence rather than an
  opt-in flag, hostRulesFingerprint now covers every peer unconditionally
  (add/remove/re-IP forces a restart, the same way ForwardedPorts always
  did) instead of skipping peers with nothing to opt into.
- internal/web/service/xray.go's injectAmneziawgEgress creates one
  dokodemo-door bridge per qualifying inbound, tagged with that inbound's
  own real tag — the same trick injectMtprotoEgress already uses (reusing
  a real inbound's tag), which is why it's already selectable in the
  panel's Routing page: InboundService.GetInboundTags() is a plain,
  protocol-blind SELECT over every inbound row's tag, no dedicated UI
  plumbing needed. The function never generates a routing rule itself
  anymore — where (if anywhere) that traffic goes is entirely up to
  whatever rules the admin adds through the existing Routing UI.

Frontend: no new UI at all. Tests rewritten to match — one bridge per
inbound with its own tag/port, no rule generation, no opt-in gating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kuzz007
2026-07-25 22:37:08 +03:00
parent 909feefd1d
commit db8253421a
19 changed files with 289 additions and 782 deletions
+44 -93
View File
@@ -645,35 +645,36 @@ const amneziawgEgressDokodemoSettings = `{"allowedNetwork":"tcp,udp","followRedi
// socket.
const amneziawgEgressStreamSettings = `{"sockopt":{"tproxy":"tproxy"}}`
// amneziawgRouteRule is one routed AmneziaWG peer — gathered from every
// enabled AmneziaWG inbound's client list — that injectAmneziawgEgress turns
// into a source-matched routing rule against the shared bridge.
type amneziawgRouteRule struct {
sourceIP string
outboundTag string
}
// injectAmneziawgEgress wires every effectively-routed AmneziaWG peer, across
// every enabled AmneziaWG inbound, into the generated config through one
// loopback dokodemo-door bridge shared by all of them (tag
// amneziawg.EgressTag, port amneziawg.EgressPort) rather than one bridge per
// peer: the TPROXY rule that redirects a peer's traffic there is per-peer
// (see internal/amneziawg's defaultPostUpDown), but distinguishing which peer
// a given connection came from — and picking its own outbound — happens
// here, in Xray's own router, matched against the TPROXY-preserved source
// IP. Reuses amneziawg.InstanceFromInbound for the peer list (rather than
// re-parsing Settings itself) specifically so "is this peer routed, and to
// which outbound" is computed in exactly one place — the same place the
// kernel-side TPROXY rules read it from — and can never quietly diverge
// between the two independent reconcile loops.
// injectAmneziawgEgress gives every enabled AmneziaWG inbound with at least
// one qualifying peer its own loopback dokodemo-door bridge — tagged with
// that inbound's own real tag, so it's already selectable in the panel's
// stock Routing page's inbound-tag picker, exactly the way an mtproto
// inbound's own bridge already is (see injectMtprotoEgress): the picker's
// tag list comes from InboundService.GetInboundTags(), a plain,
// protocol-blind SELECT over every inbound row's tag, so reusing a real
// inbound's own tag needs no dedicated UI plumbing at all.
//
// Mirrors injectMtprotoEgress/injectPanelEgress: an invalid or missing
// outbound target skips that one peer's rule, not the whole bridge; the
// bridge itself is skipped entirely when no peer needs it or its tag is
// already taken by a real inbound. Generated state is hot-appliable and
// never modifies the stored template or restarts the core.
// Every peer's traffic always lands on the bridge — internal/amneziawg's
// defaultPostUpDown TPROXYs it there unconditionally, there is no per-peer
// or per-inbound opt-in flag — but this function never generates a routing
// rule of its own. Whether that traffic goes anywhere beyond Xray's default
// routing is entirely up to whatever rules the admin adds through that same
// stock Routing page (inboundTag + an optional sourceIP to target one
// specific peer + outboundTag, exactly like routing any other protocol).
//
// An inbound is skipped, individually, when its own tag is already taken by
// another config entry — mirroring injectMtprotoEgress/injectPanelEgress's
// own defensive check, even though a real collision shouldn't be possible
// (inbound tags are unique, and the main GenXrayInboundConfig loop already
// excludes mtproto/amneziawg inbounds from ever claiming their own tag
// there). Generated state is hot-appliable and never modifies the stored
// template or restarts the core.
func injectAmneziawgEgress(cfg *xray.Config, inbounds []*model.Inbound) {
var rules []amneziawgRouteRule
existingTags := make(map[string]struct{}, len(cfg.InboundConfigs))
for i := range cfg.InboundConfigs {
existingTags[cfg.InboundConfigs[i].Tag] = struct{}{}
}
for _, inbound := range inbounds {
if inbound.Protocol != model.AmneziaWG || !inbound.Enable || inbound.NodeID != nil {
continue
@@ -682,80 +683,30 @@ func injectAmneziawgEgress(cfg *xray.Config, inbounds []*model.Inbound) {
if !ok {
continue
}
hasQualifyingPeer := false
for _, p := range inst.Peers {
if !p.RouteThroughXray {
continue
if amneziawg.FirstIPv4(p.AllowedIPs) != "" {
hasQualifyingPeer = true
break
}
sourceIP := amneziawg.FirstIPv4(p.AllowedIPs)
if sourceIP == "" {
continue
}
rules = append(rules, amneziawgRouteRule{sourceIP: sourceIP, outboundTag: p.RouteOutboundTag})
}
}
if len(rules) == 0 {
return
}
for i := range cfg.InboundConfigs {
if cfg.InboundConfigs[i].Tag == amneziawg.EgressTag {
logger.Warning("amneziawg egress: inbound tag [", amneziawg.EgressTag, "] already present in generated config, skipping bridge")
return
}
}
routing := map[string]any{}
if len(cfg.RouterConfig) > 0 {
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
logger.Warning("amneziawg egress: routing section is unparsable, skipping injection:", err)
return
}
}
existingRules, _ := routing["rules"].([]any)
newRules := make([]any, 0, len(rules))
for _, r := range rules {
if r.outboundTag == "" {
// No chosen outbound: the peer's traffic still lands on the
// bridge (it's already TPROXY'd there at the kernel level) but
// with no rule of its own it falls through to whatever the rest
// of the router decides, matching injectMtprotoEgress's
// no-outbound-selected behavior.
if !hasQualifyingPeer {
continue
}
if !routingTargetExists(routing, cfg.OutboundConfigs, r.outboundTag) {
logger.Warning("amneziawg egress: target tag [", r.outboundTag, "] not found, skipping rule for [", r.sourceIP, "]")
if _, taken := existingTags[inbound.Tag]; taken {
logger.Warning("amneziawg egress: inbound tag [", inbound.Tag, "] already present in generated config, skipping its bridge")
continue
}
rule := map[string]any{
"type": "field",
"inboundTag": []any{amneziawg.EgressTag},
"source": []any{r.sourceIP + "/32"},
}
if routingTagIsBalancer(routing, r.outboundTag) {
rule["balancerTag"] = r.outboundTag
} else {
rule["outboundTag"] = r.outboundTag
}
newRules = append(newRules, rule)
existingTags[inbound.Tag] = struct{}{}
cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
Listen: json_util.RawMessage(`"127.0.0.1"`),
Port: amneziawg.EgressPortForInbound(inbound.Id),
Protocol: "dokodemo-door",
Settings: json_util.RawMessage(amneziawgEgressDokodemoSettings),
StreamSettings: json_util.RawMessage(amneziawgEgressStreamSettings),
Tag: inbound.Tag,
})
}
if len(newRules) > 0 {
routing["rules"] = append(newRules, existingRules...)
newRouting, err := json.Marshal(routing)
if err != nil {
logger.Warning("amneziawg egress: failed to rebuild routing section, skipping injection:", err)
return
}
cfg.RouterConfig = json_util.RawMessage(newRouting)
}
cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
Listen: json_util.RawMessage(`"127.0.0.1"`),
Port: amneziawg.EgressPort,
Protocol: "dokodemo-door",
Settings: json_util.RawMessage(amneziawgEgressDokodemoSettings),
StreamSettings: json_util.RawMessage(amneziawgEgressStreamSettings),
Tag: amneziawg.EgressTag,
})
}
// mergeSubscriptionOutbounds appends the subscription outbounds to the
+63 -240
View File
@@ -561,49 +561,25 @@ func TestInjectMtprotoEgress_BadRoutingSkips(t *testing.T) {
}
func amneziawgInbound(id int, tag string, clients []model.Client) *model.Inbound {
return amneziawgInboundWithServer(id, tag, amneziawg.ServerSettings{}, clients)
}
// amneziawgInboundWithServer lets a test set inbound-wide fields (namely
// RouteThroughXray/RouteOutboundTag) that InstanceFromInbound now needs —
// injectAmneziawgEgress computes each peer's EFFECTIVE routing decision via
// InstanceFromInbound rather than reading model.Client fields raw, so a
// server block (even a minimal valid one) is required for any of these
// tests to see peers at all.
func amneziawgInboundWithServer(id int, tag string, server amneziawg.ServerSettings, clients []model.Client) *model.Inbound {
if server.SubnetIP == "" {
server.SubnetIP = "10.8.1.0"
}
if server.SubnetCIDR == 0 {
server.SubnetCIDR = 24
}
server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24}
settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
}
type amneziawgRouting struct {
Rules []struct {
InboundTag []string `json:"inboundTag"`
OutboundTag string `json:"outboundTag"`
BalancerTag string `json:"balancerTag"`
Source []string `json:"source"`
Type string `json:"type"`
} `json:"rules"`
}
func TestInjectAmneziawgEgress_WithOutbound(t *testing.T) {
func TestInjectAmneziawgEgress_CreatesBridgeTaggedWithInboundsOwnTag(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
before := string(cfg.RouterConfig)
inbound := amneziawgInbound(7, "awg-7", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("expected the shared bridge to be appended, got %d inbounds", len(cfg.InboundConfigs))
t.Fatalf("expected the bridge to be appended, got %d inbounds", len(cfg.InboundConfigs))
}
ib := cfg.InboundConfigs[1]
if ib.Tag != amneziawg.EgressTag || ib.Protocol != "dokodemo-door" || ib.Port != amneziawg.EgressPort {
t.Fatalf("unexpected bridge inbound: %+v", ib)
if ib.Tag != "awg-7" || ib.Protocol != "dokodemo-door" || ib.Port != amneziawg.EgressPortForInbound(7) {
t.Fatalf("bridge must reuse the inbound's own tag (so it's already selectable in the stock Routing page) and this instance's own derived port, got %+v", ib)
}
if string(ib.Listen) != `"127.0.0.1"` {
t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
@@ -614,94 +590,45 @@ func TestInjectAmneziawgEgress_WithOutbound(t *testing.T) {
if !strings.Contains(string(ib.Settings), `"followRedirect":true`) {
t.Fatalf("bridge must set followRedirect, got %s", ib.Settings)
}
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 2 {
t.Fatalf("expected the egress rule prepended to the existing rule, got %+v", routing.Rules)
}
first := routing.Rules[0]
if first.Type != "field" || first.OutboundTag != "warp" ||
len(first.InboundTag) != 1 || first.InboundTag[0] != amneziawg.EgressTag ||
len(first.Source) != 1 || first.Source[0] != "10.8.1.2/32" {
t.Fatalf("egress rule must bind the shared bridge tag + peer source IP to the outbound, got %+v", first)
}
}
func TestInjectAmneziawgEgress_MultiplePeersDifferentOutbounds(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.8.1.3/32"}, RouteThroughXray: true, RouteOutboundTag: "direct"},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("expected exactly one shared bridge regardless of peer count, got %d inbounds", len(cfg.InboundConfigs))
}
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 3 { // 2 egress rules + the 1 pre-existing rule
t.Fatalf("expected one rule per routed peer, got %+v", routing.Rules)
}
bySource := map[string]string{}
for _, r := range routing.Rules {
if len(r.Source) == 1 {
bySource[r.Source[0]] = r.OutboundTag
}
}
if bySource["10.8.1.2/32"] != "warp" || bySource["10.8.1.3/32"] != "direct" {
t.Fatalf("each peer must route to its own outbound, got %+v", bySource)
}
}
func TestInjectAmneziawgEgress_NoOutboundLeavesRouting(t *testing.T) {
cfg := egressTestConfig()
before := string(cfg.RouterConfig)
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("bridge must still be appended without an outbound, got %+v", cfg.InboundConfigs)
}
// No auto-generated routing rule: it's entirely up to the admin's own
// Routing-page rules, same as any other protocol's inbound tag.
if string(cfg.RouterConfig) != before {
t.Fatalf("no outbound selected means no rule change, got %s", cfg.RouterConfig)
t.Fatalf("injectAmneziawgEgress must never touch the routing section, got %s", cfg.RouterConfig)
}
}
func TestInjectAmneziawgEgress_BalancerTag(t *testing.T) {
func TestInjectAmneziawgEgress_MultipleInboundsEachGetOwnBridge(t *testing.T) {
cfg := egressTestConfig()
cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "lb"},
inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound1, inbound2})
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
if len(cfg.InboundConfigs) != 3 {
t.Fatalf("expected one bridge per inbound (plus the pre-existing one), got %d inbounds: %+v", len(cfg.InboundConfigs), cfg.InboundConfigs)
}
if len(routing.Rules) != 1 || routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
t.Fatalf("a balancer tag must target balancerTag, got %+v", routing.Rules)
byTag := map[string]int{}
for _, ib := range cfg.InboundConfigs[1:] {
byTag[ib.Tag] = ib.Port
}
if byTag["awg-1"] != amneziawg.EgressPortForInbound(1) || byTag["awg-2"] != amneziawg.EgressPortForInbound(2) {
t.Fatalf("each inbound must get its own tag and its own derived port, got %+v", byTag)
}
}
func TestInjectAmneziawgEgress_Disabled(t *testing.T) {
func TestInjectAmneziawgEgress_NoQualifyingPeerSkipsBridge(t *testing.T) {
cases := []struct {
name string
client model.Client
enable bool
}{
{"client disabled", model.Client{Email: "a@x", Enable: false, AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"}, true},
{"RouteThroughXray off", model.Client{Email: "a@x", Enable: true, AllowedIPs: []string{"10.8.1.2/32"}, RouteOutboundTag: "warp"}, true},
{"no AllowedIPs", model.Client{Email: "a@x", Enable: true, RouteThroughXray: true, RouteOutboundTag: "warp"}, true},
{"inbound disabled", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"}, false},
{"client disabled", model.Client{Email: "a@x", Enable: false, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, true},
{"no PublicKey", model.Client{Email: "a@x", Enable: true, AllowedIPs: []string{"10.8.1.2/32"}}, true},
{"no AllowedIPs", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a"}, true},
{"inbound disabled", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -716,148 +643,44 @@ func TestInjectAmneziawgEgress_Disabled(t *testing.T) {
}
}
func TestInjectAmneziawgEgress_TagCollisionSkips(t *testing.T) {
func TestInjectAmneziawgEgress_WrongProtocolOrNodeSkipped(t *testing.T) {
cfg := egressTestConfig()
vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
nodeID := 5
nodeHosted := amneziawgInbound(2, "awg-2", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
})
nodeHosted.NodeID = &nodeID
injectAmneziawgEgress(cfg, []*model.Inbound{vless, nodeHosted})
if len(cfg.InboundConfigs) != 1 {
t.Fatalf("a non-AmneziaWG or node-hosted inbound must never get a bridge, got %+v", cfg.InboundConfigs)
}
}
func TestInjectAmneziawgEgress_TagCollisionSkipsThatInboundOnly(t *testing.T) {
cfg := egressTestConfig()
cfg.InboundConfigs = append(cfg.InboundConfigs,
xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: amneziawg.EgressTag})
before := string(cfg.RouterConfig)
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: "awg-1"})
inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
t.Fatal("a real inbound already owning the shared bridge tag must make injection a no-op")
}
}
func TestInjectAmneziawgEgress_MissingTargetSkipsOnlyThatPeer(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.8.1.3/32"}, RouteThroughXray: true, RouteOutboundTag: "removed-subscription-outbound"},
inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound1, inbound2})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("the bridge must still be created for the peer with a valid target, got %+v", cfg.InboundConfigs)
// Started with 2 (api + the colliding vless entry); only awg-2's bridge
// should have been added, awg-1's skipped since its tag is taken.
if len(cfg.InboundConfigs) != 3 {
t.Fatalf("expected only the non-colliding inbound's bridge to be added, got %+v", cfg.InboundConfigs)
}
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
found := false
for _, ib := range cfg.InboundConfigs {
if ib.Tag == "awg-2" && ib.Protocol == "dokodemo-door" {
found = true
}
}
if len(routing.Rules) != 2 { // only a@x's rule + the 1 pre-existing rule
t.Fatalf("only the peer with a valid target should get a rule, got %+v", routing.Rules)
}
if routing.Rules[0].Source[0] != "10.8.1.2/32" {
t.Fatalf("expected a@x's rule, got %+v", routing.Rules[0])
}
}
func TestInjectAmneziawgEgress_BadOutboundsSkipsRulesKeepsBridge(t *testing.T) {
cfg := egressTestConfig()
cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
before := string(cfg.RouterConfig)
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("unparsable outbounds must still expose the bridge (other peers may not need routing), got %+v", cfg.InboundConfigs)
}
if string(cfg.RouterConfig) != before {
t.Fatalf("unparsable outbounds means no target can resolve, so no rule change, got %s", cfg.RouterConfig)
}
}
func TestInjectAmneziawgEgress_BadRoutingSkipsEverything(t *testing.T) {
cfg := egressTestConfig()
cfg.RouterConfig = json_util.RawMessage(`{not json`)
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "warp"},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 1 {
t.Fatalf("unparsable routing must not expose the bridge either, got %+v", cfg.InboundConfigs)
}
if string(cfg.RouterConfig) != `{not json` {
t.Fatalf("unparsable routing must be left untouched, got %s", cfg.RouterConfig)
}
}
func TestInjectAmneziawgEgress_NoRoutingSection(t *testing.T) {
cfg := egressTestConfig()
cfg.RouterConfig = nil
inbound := amneziawgInbound(1, "awg-1", []model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "direct"},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(cfg.InboundConfigs) != 2 || len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
t.Fatalf("a routing section must be created with the egress rule, got %+v", routing.Rules)
}
}
func TestInjectAmneziawgEgress_ServerLevelRoutesPeerWithNoOwnFlag(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInboundWithServer(1, "awg-1",
amneziawg.ServerSettings{RouteThroughXray: true, RouteOutboundTag: "warp"},
[]model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
if len(cfg.InboundConfigs) != 2 {
t.Fatalf("the inbound-wide default must route a peer with no client-level flag of its own, got %+v", cfg.InboundConfigs)
}
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 2 || routing.Rules[0].OutboundTag != "warp" || routing.Rules[0].Source[0] != "10.8.1.2/32" {
t.Fatalf("expected a rule routing the peer to the inbound's default outbound, got %+v", routing.Rules)
}
}
func TestInjectAmneziawgEgress_ClientTagOverridesServerTag(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInboundWithServer(1, "awg-1",
amneziawg.ServerSettings{RouteThroughXray: true, RouteOutboundTag: "warp"},
[]model.Client{
// Routed by the server-wide default, but picks its own outbound.
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteOutboundTag: "direct"},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 2 || routing.Rules[0].OutboundTag != "direct" {
t.Fatalf("a client's own outbound tag must win over the inbound's default, got %+v", routing.Rules)
}
}
func TestInjectAmneziawgEgress_ServerLevelOffClientCanStillOptIn(t *testing.T) {
cfg := egressTestConfig()
inbound := amneziawgInboundWithServer(1, "awg-1",
amneziawg.ServerSettings{}, // server-wide routing off
[]model.Client{
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}, RouteThroughXray: true, RouteOutboundTag: "direct"},
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.8.1.3/32"}},
})
injectAmneziawgEgress(cfg, []*model.Inbound{inbound})
var routing amneziawgRouting
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
t.Fatal(err)
}
if len(routing.Rules) != 2 || routing.Rules[0].Source[0] != "10.8.1.2/32" {
t.Fatalf("a@x must be routed on its own opt-in even with the inbound default off, got %+v", routing.Rules)
if !found {
t.Fatal("awg-2's bridge must still be created despite awg-1's tag collision")
}
}
-10
View File
@@ -922,11 +922,6 @@
"amneziaWgAllowedIPsHint": "Leave empty to auto-assign; separate entries with commas",
"amneziaWgForwardedPorts": "Forwarded Ports",
"amneziaWgForwardedPortsHint": "Ports/ranges DNAT'd to this client, e.g. 80, 443, 8000-8100. Leave empty for none.",
"amneziaWgRouteThroughXray": "Route via Xray",
"amneziaWgRouteThroughXrayHint": "Send this client's traffic through Xray instead of straight out the server's network interface.",
"amneziaWgRouteOutboundTag": "Outbound",
"amneziaWgRouteOutboundTagHint": "Which Xray outbound (or balancer) this client's traffic exits through. Leave empty to use Xray's default routing.",
"amneziaWgRouteOutboundTagPlaceholder": "Select an outbound",
"amneziaWgConfig": "AmneziaWG config",
"mtprotoSecret": "MTProto secret",
"mtprotoSecretHint": "The client's FakeTLS secret. Regenerate to rotate it.",
@@ -1945,11 +1940,6 @@
"ipv6SubnetHint": "e.g. fd86:ea04:1115::/64. Required when IPv6 is enabled.",
"ipv6ExternalInterface": "IPv6 External Interface",
"ipv6ExternalInterfaceHint": "Host NIC for the NDP proxy entries. Leave empty to reuse External Interface.",
"routeThroughXray": "Route via Xray (all clients)",
"routeThroughXrayHint": "Send every client's traffic through Xray by default instead of straight out the server's network interface. A client can still enable this on its own even when it's off here.",
"routeOutboundTag": "Default Outbound",
"routeOutboundTagHint": "Default Xray outbound (or balancer) for clients routed through Xray. A client's own outbound choice overrides this. Leave empty to use Xray's default routing.",
"routeOutboundTagPlaceholder": "Select an outbound",
"obfuscation": "Obfuscation parameters",
"regenerateObfuscation": "Regenerate",
"jc": "Jc (junk packet count)",
-10
View File
@@ -922,11 +922,6 @@
"amneziaWgAllowedIPsHint": "Оставьте пустым для автоназначения; разделяйте записи запятыми",
"amneziaWgForwardedPorts": "Проброс портов",
"amneziaWgForwardedPortsHint": "Порты/диапазоны, DNAT'ящиеся на этого клиента, например 80, 443, 8000-8100. Оставьте пустым, если не нужно.",
"amneziaWgRouteThroughXray": "Маршрутизировать через Xray",
"amneziaWgRouteThroughXrayHint": "Направлять трафик этого клиента через Xray вместо прямого выхода через сетевой интерфейс сервера.",
"amneziaWgRouteOutboundTag": "Исходящий",
"amneziaWgRouteOutboundTagHint": "Через какой исходящий (outbound) или балансировщик Xray выходит трафик этого клиента. Оставьте пустым, чтобы использовать маршрутизацию Xray по умолчанию.",
"amneziaWgRouteOutboundTagPlaceholder": "Выберите исходящий",
"amneziaWgConfig": "Конфиг AmneziaWG",
"mtprotoSecret": "Секрет MTProto",
"mtprotoSecretHint": "Секрет FakeTLS клиента. Перегенерируйте, чтобы сменить.",
@@ -1828,11 +1823,6 @@
"ipv6SubnetHint": "Например, fd86:ea04:1115::/64. Обязательно при включённом IPv6.",
"ipv6ExternalInterface": "Внешний интерфейс для IPv6",
"ipv6ExternalInterfaceHint": "Сетевой интерфейс хоста для записей NDP-прокси. Оставьте пустым, чтобы использовать «Внешний интерфейс».",
"routeThroughXray": "Маршрутизировать через Xray (все клиенты)",
"routeThroughXrayHint": "Направлять трафик всех клиентов через Xray по умолчанию вместо прямого выхода через сетевой интерфейс сервера. Клиент может включить это индивидуально, даже если здесь выключено.",
"routeOutboundTag": "Исходящий по умолчанию",
"routeOutboundTagHint": "Исходящий (outbound) или балансировщик Xray по умолчанию для клиентов, маршрутизируемых через Xray. Собственный выбор клиента имеет приоритет. Оставьте пустым для маршрутизации Xray по умолчанию.",
"routeOutboundTagPlaceholder": "Выберите исходящий",
"obfuscation": "Параметры обфускации",
"regenerateObfuscation": "Сгенерировать заново",
"jc": "Jc (кол-во мусорных пакетов)",