mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-31 18:25:00 +03:00
feat(amneziawg): restore per-client public IPv6 identity (Phase 3.5)
Adds internal/amneziawg.FirstIPv6 and a new internal/amneziawgnet/v6alias.go that aliases each IPv6-enabled peer's own address onto the host NIC (ip -6 addr add), wired into the Manager's Ensure/Remove/Reconcile/StopAll lifecycle. internal/web/service/xray.go's new injectAmneziawgV6Egress gives each such peer a dedicated freedom outbound (sendThrough) plus a routing rule matching its own email, so its outbound connections carry a distinct public source address again -- restoring what the embedded-architecture hard cutover temporarily dropped. Scoped to outbound source identity only (not unsolicited inbound/port-forwarding, which stays the separate Phase 3.6); no frontend changes needed since IPv6Enabled/IPv6ExternalInterface were already in the UI and per-peer opt-in is just an IPv6 AllowedIPs entry, same as today.
This commit is contained in:
@@ -379,6 +379,17 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
// like routing any other protocol.
|
||||
injectAmneziawgnetSocks(xrayConfig, inbounds)
|
||||
|
||||
// Restores each opted-in peer's own distinct public IPv6 source identity
|
||||
// for its outbound connections, dropped by the hard cutover above (see
|
||||
// Phase 3.5 of the migration plan) — a peer that has an IPv6 address in
|
||||
// its AllowedIPs, on an inbound with IPv6Enabled, gets its own freedom
|
||||
// outbound bound to that exact address via sendThrough.
|
||||
// internal/amneziawgnet's own Manager is responsible for actually
|
||||
// aliasing that address onto the host (see v6alias.go) so the kernel
|
||||
// lets Xray bind an egress socket to it at all; this call only builds
|
||||
// the Xray-side outbound/routing-rule half.
|
||||
injectAmneziawgV6Egress(xrayConfig, inbounds)
|
||||
|
||||
// Wire the panel's own HTTP traffic through the configured outbound, after
|
||||
// the subscription merge so subscription outbound tags are valid targets.
|
||||
if egressTag, err := s.settingService.GetPanelOutbound(); err != nil {
|
||||
@@ -753,6 +764,152 @@ func injectAmneziawgnetSocks(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
}
|
||||
}
|
||||
|
||||
// amneziawgV6EgressTag returns the stable, globally-unique freedom outbound
|
||||
// tag for one peer's IPv6 source-identity egress. Stable across config
|
||||
// regenerations (a pure function of two stable identifiers), so
|
||||
// internal/xray/hot_diff.go's tag-keyed outbound/routing diffing recognizes
|
||||
// "unchanged" rather than remove+recreate on every poll. The inbound.Id
|
||||
// prefix is defense in depth, not load-bearing on its own: email is already
|
||||
// enforced globally unique across the whole panel's client table
|
||||
// (model.ClientRecord.Email has a gorm uniqueIndex) — kept anyway since it
|
||||
// costs nothing and makes the tag self-describing, matching
|
||||
// NodeEgressInboundTag's own style.
|
||||
func amneziawgV6EgressTag(inboundID int, email string) string {
|
||||
return fmt.Sprintf("amneziawg-v6-%d-%s", inboundID, email)
|
||||
}
|
||||
|
||||
// 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
|
||||
// rule sending only that peer's own traffic through it — restoring the
|
||||
// per-client public IPv6 identity the hard cutover temporarily dropped
|
||||
// (Phase 3.5 of the migration plan). Scoped to outbound source identity
|
||||
// only: it depends on internal/amneziawgnet's own alias mechanism actually
|
||||
// giving the host that address at the OS level (see v6alias.go) — without
|
||||
// that, sendThrough would simply fail to bind and Xray would fall back to
|
||||
// its default outbound, not error out.
|
||||
//
|
||||
// The routing rule matches both inboundTag and user: SocksInboundSettings
|
||||
// (used by injectAmneziawgnetSocks above) already authenticates each
|
||||
// connection as the peer's own email via stock SOCKS5 auth, and a stock
|
||||
// Xray SOCKS5 inbound sets that connection's stats/routing identity from
|
||||
// the authenticated username — so "user" reliably isolates exactly one
|
||||
// peer's traffic, the same building block Finding 3 of the migration plan
|
||||
// already established for per-client stats.
|
||||
//
|
||||
// Modeled on injectNodeEgresses (the established N-per-slice inbound+rule
|
||||
// precedent, not injectAmneziawgnetSocks itself, which only ever emits a
|
||||
// single inbound and never touches outbounds/routing) and
|
||||
// mergeSubscriptionOutbounds's unmarshal-append-remarshal pattern for
|
||||
// cfg.OutboundConfigs. Synthetic rules are prepended ahead of whatever's
|
||||
// already in the routing rules array, the same pattern injectNodeEgresses/
|
||||
// injectMtprotoEgress already use for their own always-must-win infra
|
||||
// rules — this never touches the admin's own saved Routing-page rule
|
||||
// order.
|
||||
func injectAmneziawgV6Egress(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
// Protocol is checked alongside Tag, not just Tag alone: a tag collision
|
||||
// with some unrelated (non-socks) inbound must not be mistaken for this
|
||||
// instance's own relay having been created.
|
||||
liveInboundTags := make(map[string]struct{}, len(cfg.InboundConfigs))
|
||||
for i := range cfg.InboundConfigs {
|
||||
if cfg.InboundConfigs[i].Protocol == "socks" {
|
||||
liveInboundTags[cfg.InboundConfigs[i].Tag] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var existingOutbounds []any
|
||||
if len(cfg.OutboundConfigs) > 0 {
|
||||
if err := json.Unmarshal(cfg.OutboundConfigs, &existingOutbounds); err != nil {
|
||||
logger.Warning("amneziawg v6 egress: outbounds section is unparsable, skipping injection:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
usedOutboundTags := make(map[string]struct{}, len(existingOutbounds))
|
||||
for _, o := range existingOutbounds {
|
||||
if m, ok := o.(map[string]any); ok {
|
||||
if t, ok := m["tag"].(string); ok {
|
||||
usedOutboundTags[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routing := map[string]any{}
|
||||
if len(cfg.RouterConfig) > 0 {
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
logger.Warning("amneziawg v6 egress: routing section is unparsable, skipping injection:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
rules, _ := routing["rules"].([]any)
|
||||
newRules := make([]any, 0)
|
||||
newOutbounds := make([]any, 0)
|
||||
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Protocol != model.AmneziaWG || !inbound.Enable || inbound.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
if _, live := liveInboundTags[inbound.Tag]; !live {
|
||||
// The relay inbound itself wasn't created this pass (e.g. a tag
|
||||
// collision inside injectAmneziawgnetSocks) -- no SOCKS5 inbound
|
||||
// exists for hot_diff.go's inboundTag match to ever fire against.
|
||||
continue
|
||||
}
|
||||
inst, ok := amneziawg.InstanceFromInbound(inbound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, p := range inst.Peers {
|
||||
if p.Email == "" {
|
||||
continue
|
||||
}
|
||||
v6 := amneziawg.FirstIPv6(p.AllowedIPs)
|
||||
if v6 == "" {
|
||||
continue
|
||||
}
|
||||
tag := amneziawgV6EgressTag(inbound.Id, p.Email)
|
||||
if _, taken := usedOutboundTags[tag]; taken {
|
||||
logger.Warning("amneziawg v6 egress: outbound tag [", tag, "] already exists, skipping peer [", p.Email, "]")
|
||||
continue
|
||||
}
|
||||
usedOutboundTags[tag] = struct{}{}
|
||||
newOutbounds = append(newOutbounds, map[string]any{
|
||||
"tag": tag,
|
||||
"protocol": "freedom",
|
||||
"sendThrough": v6,
|
||||
"settings": map[string]any{},
|
||||
})
|
||||
newRules = append(newRules, map[string]any{
|
||||
"type": "field",
|
||||
"inboundTag": []any{inbound.Tag},
|
||||
"user": []any{p.Email},
|
||||
"outboundTag": tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(newOutbounds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
merged := make([]any, 0, len(existingOutbounds)+len(newOutbounds))
|
||||
merged = append(merged, existingOutbounds...)
|
||||
merged = append(merged, newOutbounds...)
|
||||
combined, err := json.MarshalIndent(merged, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("amneziawg v6 egress: failed to rebuild outbounds section, skipping injection:", err)
|
||||
return
|
||||
}
|
||||
cfg.OutboundConfigs = json_util.RawMessage(combined)
|
||||
|
||||
routing["rules"] = append(newRules, rules...)
|
||||
newRouting, err := json.Marshal(routing)
|
||||
if err != nil {
|
||||
logger.Warning("amneziawg v6 egress: failed to rebuild routing section, skipping injection:", err)
|
||||
return
|
||||
}
|
||||
cfg.RouterConfig = json_util.RawMessage(newRouting)
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -709,3 +709,266 @@ func TestInjectAmneziawgnetSocks_TagCollisionSkipsThatInboundOnly(t *testing.T)
|
||||
t.Fatal("awg-2's relay inbound must still be created despite awg-1's tag collision")
|
||||
}
|
||||
}
|
||||
|
||||
// amneziawgV6Inbound builds an AmneziaWG inbound with IPv6 enabled and a
|
||||
// given external interface -- amneziawgInbound's own ServerSettings never
|
||||
// sets these, so injectAmneziawgV6Egress's tests need their own variant.
|
||||
func amneziawgV6Inbound(id int, tag string, ext6 string, clients []model.Client) *model.Inbound {
|
||||
server := amneziawg.ServerSettings{
|
||||
SubnetIP: "10.8.1.0", SubnetCIDR: 24,
|
||||
IPv6Enabled: true, IPv6ExternalInterface: ext6,
|
||||
}
|
||||
settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
|
||||
return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
|
||||
}
|
||||
|
||||
// injectAmneziawgV6Egress runs after injectAmneziawgnetSocks in the real
|
||||
// GetXrayConfig() pipeline and depends on its relay inbound already
|
||||
// existing (see the "live" tag check) -- every test below calls both, in
|
||||
// that order, to match production.
|
||||
func injectAmneziawgSocksThenV6(cfg *xray.Config, inbounds []*model.Inbound) {
|
||||
injectAmneziawgnetSocks(cfg, inbounds)
|
||||
injectAmneziawgV6Egress(cfg, inbounds)
|
||||
}
|
||||
|
||||
type v6EgressRouting struct {
|
||||
Rules []struct {
|
||||
InboundTag []string `json:"inboundTag"`
|
||||
User []string `json:"user"`
|
||||
OutboundTag string `json:"outboundTag"`
|
||||
Type string `json:"type"`
|
||||
} `json:"rules"`
|
||||
}
|
||||
|
||||
type v6EgressOutbound struct {
|
||||
Tag string `json:"tag"`
|
||||
Protocol string `json:"protocol"`
|
||||
SendThrough string `json:"sendThrough"`
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_CreatesOutboundAndRuleForV6Peer(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
inbound := amneziawgV6Inbound(7, "awg-7", "eth0", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32", "fd86:ea04:1115::2/128"}},
|
||||
})
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
|
||||
|
||||
var outbounds []v6EgressOutbound
|
||||
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantTag := amneziawgV6EgressTag(7, "a@x")
|
||||
var got *v6EgressOutbound
|
||||
for i := range outbounds {
|
||||
if outbounds[i].Tag == wantTag {
|
||||
got = &outbounds[i]
|
||||
}
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("expected an outbound tagged %q, got %+v", wantTag, outbounds)
|
||||
}
|
||||
if got.Protocol != "freedom" || got.SendThrough != "fd86:ea04:1115::2" {
|
||||
t.Fatalf("outbound must be a freedom outbound bound to the peer's own v6 address, got %+v", got)
|
||||
}
|
||||
// Pre-existing outbounds (direct, warp) must survive untouched.
|
||||
if len(outbounds) != 3 {
|
||||
t.Fatalf("expected the 2 pre-existing outbounds plus 1 new one, got %+v", outbounds)
|
||||
}
|
||||
|
||||
var routing v6EgressRouting
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ruleIdx := -1
|
||||
for i := range routing.Rules {
|
||||
if routing.Rules[i].OutboundTag == wantTag {
|
||||
ruleIdx = i
|
||||
}
|
||||
}
|
||||
if ruleIdx == -1 {
|
||||
t.Fatalf("expected a routing rule targeting %q, got %+v", wantTag, routing.Rules)
|
||||
}
|
||||
rule := routing.Rules[ruleIdx]
|
||||
if rule.Type != "field" || len(rule.User) != 1 || rule.User[0] != "a@x" ||
|
||||
len(rule.InboundTag) != 1 || rule.InboundTag[0] != "awg-7" {
|
||||
t.Fatalf("rule must match this peer's email and inbound tag, got %+v", rule)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_SkipsPeerWithoutV6Address(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
before := string(cfg.OutboundConfigs)
|
||||
inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, // v4 only
|
||||
})
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
|
||||
if string(cfg.OutboundConfigs) != before {
|
||||
t.Fatalf("a peer with no v6 AllowedIPs entry must not get an outbound, got %s", cfg.OutboundConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_MultiplePeersEachGetOwnOutboundAndRule(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"}},
|
||||
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
|
||||
})
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
|
||||
|
||||
var outbounds []v6EgressOutbound
|
||||
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tagA, tagB := amneziawgV6EgressTag(1, "a@x"), amneziawgV6EgressTag(1, "b@x")
|
||||
seen := map[string]string{}
|
||||
for _, o := range outbounds {
|
||||
seen[o.Tag] = o.SendThrough
|
||||
}
|
||||
if seen[tagA] != "fd86:ea04:1115::2" || seen[tagB] != "fd86:ea04:1115::3" {
|
||||
t.Fatalf("each peer must get its own outbound bound to its own address, got %+v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_StableTagAcrossRegenerations(t *testing.T) {
|
||||
// Same instance data, two independent injections -- hot_diff.go relies on
|
||||
// the tag being a pure function of (inboundID, email) so it recognizes
|
||||
// "unchanged" rather than remove+recreate on every poll.
|
||||
inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
|
||||
})
|
||||
cfg1 := egressTestConfig()
|
||||
injectAmneziawgSocksThenV6(cfg1, []*model.Inbound{inbound})
|
||||
cfg2 := egressTestConfig()
|
||||
injectAmneziawgSocksThenV6(cfg2, []*model.Inbound{inbound})
|
||||
|
||||
var out1, out2 []v6EgressOutbound
|
||||
json.Unmarshal(cfg1.OutboundConfigs, &out1)
|
||||
json.Unmarshal(cfg2.OutboundConfigs, &out2)
|
||||
if len(out1) != len(out2) || out1[len(out1)-1].Tag != out2[len(out2)-1].Tag {
|
||||
t.Fatalf("tag must be stable across independent regenerations, got %+v vs %+v", out1, out2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_SkipsWrongProtocolOrNodeHostedOrDisabled(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
before := string(cfg.OutboundConfigs)
|
||||
vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
|
||||
nodeID := 5
|
||||
nodeHosted := amneziawgV6Inbound(2, "awg-2", "eth0", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
|
||||
})
|
||||
nodeHosted.NodeID = &nodeID
|
||||
disabled := amneziawgV6Inbound(3, "awg-3", "eth0", []model.Client{
|
||||
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
|
||||
})
|
||||
disabled.Enable = false
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{vless, nodeHosted, disabled})
|
||||
if string(cfg.OutboundConfigs) != before {
|
||||
t.Fatalf("wrong-protocol, node-hosted, and disabled inbounds must never get a v6 outbound, got %s", cfg.OutboundConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_SkipsWhenRelayInboundNotCreated(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
// A pre-existing inbound already holds this AmneziaWG inbound's tag, so
|
||||
// injectAmneziawgnetSocks (called first, matching production order)
|
||||
// skips creating its relay SOCKS5 inbound entirely.
|
||||
cfg.InboundConfigs = append(cfg.InboundConfigs,
|
||||
xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: "awg-1"})
|
||||
before := string(cfg.OutboundConfigs)
|
||||
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})
|
||||
if string(cfg.OutboundConfigs) != before {
|
||||
t.Fatalf("no v6 outbound should be created when the relay inbound itself never got created, got %s", cfg.OutboundConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_OutboundTagCollisionSkipsThatPeerOnly(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"}},
|
||||
{Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
|
||||
})
|
||||
// Pre-seed a colliding outbound tag for a@x specifically.
|
||||
collidingTag := amneziawgV6EgressTag(1, "a@x")
|
||||
existing, _ := json.Marshal([]any{map[string]any{"tag": collidingTag, "protocol": "freedom"}})
|
||||
cfg.OutboundConfigs = json_util.RawMessage(existing)
|
||||
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
|
||||
|
||||
var outbounds []v6EgressOutbound
|
||||
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tagB := amneziawgV6EgressTag(1, "b@x")
|
||||
foundB := false
|
||||
countA := 0
|
||||
for _, o := range outbounds {
|
||||
if o.Tag == collidingTag {
|
||||
countA++
|
||||
}
|
||||
if o.Tag == tagB {
|
||||
foundB = true
|
||||
}
|
||||
}
|
||||
if countA != 1 {
|
||||
t.Fatalf("a@x's pre-existing outbound must not be duplicated, got %d copies", countA)
|
||||
}
|
||||
if !foundB {
|
||||
t.Fatal("b@x must still get its own outbound despite a@x's tag collision")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_BadOutboundsOrRoutingSkips(t *testing.T) {
|
||||
inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
|
||||
})
|
||||
|
||||
cfg := egressTestConfig()
|
||||
cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
|
||||
injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
|
||||
if string(cfg.OutboundConfigs) != `{not json` {
|
||||
t.Fatalf("unparsable outbounds must be left untouched, got %s", cfg.OutboundConfigs)
|
||||
}
|
||||
|
||||
cfg2 := egressTestConfig()
|
||||
cfg2.RouterConfig = json_util.RawMessage(`{not json`)
|
||||
injectAmneziawgSocksThenV6(cfg2, []*model.Inbound{inbound})
|
||||
if string(cfg2.RouterConfig) != `{not json` {
|
||||
t.Fatalf("unparsable routing must be left untouched, got %s", cfg2.RouterConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_NoQualifyingPeerLeavesConfigUntouched(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
beforeOut, beforeRoute := string(cfg.OutboundConfigs), string(cfg.RouterConfig)
|
||||
inbound := amneziawgV6Inbound(1, "awg-1", "eth0", nil) // no clients at all
|
||||
injectAmneziawgV6Egress(cfg, []*model.Inbound{inbound})
|
||||
if string(cfg.OutboundConfigs) != beforeOut || string(cfg.RouterConfig) != beforeRoute {
|
||||
t.Fatalf("an inbound with no qualifying peer must leave the config byte-identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_RulesPrependedBeforeExistingRules(t *testing.T) {
|
||||
cfg := egressTestConfig() // already has one rule, targeting "api"
|
||||
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 routing v6EgressRouting
|
||||
if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routing.Rules) != 2 {
|
||||
t.Fatalf("expected the new rule plus the pre-existing one, got %+v", routing.Rules)
|
||||
}
|
||||
if routing.Rules[0].OutboundTag != amneziawgV6EgressTag(1, "a@x") {
|
||||
t.Fatalf("the new infra rule must be prepended ahead of the pre-existing rule, got %+v", routing.Rules[0])
|
||||
}
|
||||
if routing.Rules[1].OutboundTag != "api" {
|
||||
t.Fatalf("the pre-existing rule must survive, got %+v", routing.Rules[1])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user