mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-23 06:15:01 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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 (кол-во мусорных пакетов)",
|
||||
|
||||
Reference in New Issue
Block a user