From 6f37433d43101e36f6f37ed68f804e12df17fa6c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 15:14:49 +0100 Subject: [PATCH] fix(amneziawgnet): stop tests binding fixed ports in the ephemeral range The race job failed TestEnsureRebuildsWhenS4ChangesTheDerivedMTU with "bind 127.0.0.1:58720: address already in use". The package hardcoded WireGuard and port-forward ports 58712-58922, all inside Linux's ephemeral range (32768-60999), so any concurrent :0 socket could already hold one; several were also reused across tests (58713 x3, 58715, 58717, 58720). Tests that never dial the server now use ListenPort 0; tests that need the number take an OS-assigned one via freeUDPPort / freeTCPUDPPort (the latter for port forwards, which bind TCP and UDP on the same port). --- internal/amneziawgnet/bench_test.go | 2 +- internal/amneziawgnet/device_test.go | 10 +++--- internal/amneziawgnet/diagnostics_test.go | 2 +- internal/amneziawgnet/egress_domain_test.go | 14 ++------ internal/amneziawgnet/manager_test.go | 12 +++---- internal/amneziawgnet/portfwd_test.go | 14 ++++---- internal/amneziawgnet/relay_e2e_test.go | 37 +++++++++++++++++++-- internal/amneziawgnet/udp_test.go | 2 +- internal/amneziawgnet/v6alias_test.go | 2 +- 9 files changed, 58 insertions(+), 37 deletions(-) diff --git a/internal/amneziawgnet/bench_test.go b/internal/amneziawgnet/bench_test.go index 61bd8e50b..cd8aaf231 100644 --- a/internal/amneziawgnet/bench_test.go +++ b/internal/amneziawgnet/bench_test.go @@ -210,7 +210,7 @@ func (t *benchTunnel) dial(b *testing.B, dest netip.AddrPort) *gonet.TCPConn { func BenchmarkTunnelThroughput(b *testing.B) { const chunkSize = 64 << 10 const serverAddr = "10.203.0.1" - tun := newBenchTunnel(b, 58714, serverAddr, "10.203.0.2") + tun := newBenchTunnel(b, freeUDPPort(b), serverAddr, "10.203.0.2") defer tun.closeFn() b.Run("upload", func(b *testing.B) { diff --git a/internal/amneziawgnet/device_test.go b/internal/amneziawgnet/device_test.go index 821cfc095..06d939dd9 100644 --- a/internal/amneziawgnet/device_test.go +++ b/internal/amneziawgnet/device_test.go @@ -39,7 +39,7 @@ func TestNewDeviceHandshakeForwarderAndIdentity(t *testing.T) { t.Fatalf("generate client keypair: %v", err) } - const listenPort = 58712 // fixed loopback test port, matches the validated Phase 0 spike approach + listenPort := freeUDPPort(t) const wantEmail = "test-peer@example.com" inst := amneziawg.Instance{ @@ -243,7 +243,7 @@ func TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip(t *testing.T) { t.Fatalf("generate header protection key: %v", err) } - const listenPort = 58713 // fixed loopback test port, distinct from the handshake test above + listenPort := freeUDPPort(t) const contentPaddingAddition = "20-40" inst := amneziawg.Instance{ @@ -429,7 +429,7 @@ func TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip(t *testing.T) { t.Fatalf("generate client keypair: %v", err) } - const listenPort = 58721 // fixed loopback test port, distinct from every other test in this package + listenPort := freeUDPPort(t) inst := amneziawg.Instance{ Id: 3, @@ -591,7 +591,7 @@ func TestValidatedObfuscationAlwaysApplies(t *testing.T) { }}, } - for i, tc := range cases { + for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { o := base tc.mut(&o) @@ -599,7 +599,7 @@ func TestValidatedObfuscationAlwaysApplies(t *testing.T) { return // rejected before saving, which is the whole point } inst := amneziawg.Instance{ - Id: 88, InterfaceName: "awgcontract", ListenPort: 58900 + i, + Id: 88, InterfaceName: "awgcontract", ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.198.0.1/24"}, MTU: 1420, Obfuscation: o, diff --git a/internal/amneziawgnet/diagnostics_test.go b/internal/amneziawgnet/diagnostics_test.go index c87b79b77..5344aac04 100644 --- a/internal/amneziawgnet/diagnostics_test.go +++ b/internal/amneziawgnet/diagnostics_test.go @@ -45,7 +45,7 @@ func TestDiagnoseDeviceReportsListenPortAndPeerState(t *testing.T) { t.Fatalf("generate idle-peer keypair: %v", err) } - const listenPort = 58713 // distinct from device_test.go's fixed port + listenPort := freeUDPPort(t) const activeEmail = "active@example.com" const idleEmail = "idle@example.com" diff --git a/internal/amneziawgnet/egress_domain_test.go b/internal/amneziawgnet/egress_domain_test.go index 164cc598d..dea4f0977 100644 --- a/internal/amneziawgnet/egress_domain_test.go +++ b/internal/amneziawgnet/egress_domain_test.go @@ -57,12 +57,7 @@ func newPairedTunnelForTest(t *testing.T) *pairedTunnel { if err != nil { t.Fatal(err) } - pc, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - listenPort := pc.LocalAddr().(*net.UDPAddr).Port - pc.Close() + listenPort := freeUDPPort(t) obf := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20} serverInst := amneziawg.Instance{ @@ -133,12 +128,7 @@ func newPairedTunnelV6ForTest(t *testing.T) *pairedTunnel { if err != nil { t.Fatal(err) } - pc, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - listenPort := pc.LocalAddr().(*net.UDPAddr).Port - pc.Close() + listenPort := freeUDPPort(t) obf := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20} serverInst := amneziawg.Instance{ diff --git a/internal/amneziawgnet/manager_test.go b/internal/amneziawgnet/manager_test.go index 497b4360d..6cc8e8827 100644 --- a/internal/amneziawgnet/manager_test.go +++ b/internal/amneziawgnet/manager_test.go @@ -29,7 +29,7 @@ func TestManagerLifecycle(t *testing.T) { inst := amneziawg.Instance{ Id: 3, InterfaceName: "awgtest3", - ListenPort: 58714, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.203.0.1/24"}, @@ -117,7 +117,7 @@ func TestEnsureRebuildsWhenS4ChangesTheDerivedMTU(t *testing.T) { inst := amneziawg.Instance{ Id: 9 + i, InterfaceName: fmt.Sprintf("awgtest%d", 9+i), - ListenPort: 58719 + i, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.209.0.1/24"}, @@ -214,7 +214,7 @@ func TestEnsureUnchangedInstanceDoesNotResetLivePeers(t *testing.T) { inst := amneziawg.Instance{ Id: 4, InterfaceName: "awgtest4", - ListenPort: 58715, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.204.0.1/24"}, @@ -351,7 +351,7 @@ func TestEnsureDropsFlowsOfReassignedTunnelAddress(t *testing.T) { inst := amneziawg.Instance{ Id: 9, InterfaceName: "awgtest9", - ListenPort: 58720, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.206.0.1/24"}, @@ -465,7 +465,7 @@ func TestEnsureHeaderProtectionKeyChangeReconfiguresInPlace(t *testing.T) { inst := amneziawg.Instance{ Id: 7, InterfaceName: "awgtest7", - ListenPort: 58717, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.207.0.1/24"}, @@ -525,7 +525,7 @@ func TestEnsureRejectsHeaderProtectionKeyWithLowS1S4(t *testing.T) { inst := amneziawg.Instance{ Id: 8, InterfaceName: "awgtest8", - ListenPort: 58718, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.208.0.1/24"}, diff --git a/internal/amneziawgnet/portfwd_test.go b/internal/amneziawgnet/portfwd_test.go index 9a55c7f29..a61777f28 100644 --- a/internal/amneziawgnet/portfwd_test.go +++ b/internal/amneziawgnet/portfwd_test.go @@ -145,7 +145,7 @@ func TestPortForwardSetReconcileOpensAndClosesListeners(t *testing.T) { gs := newTestStack(t, "10.211.0.1") set := NewPortForwardSet(gs, 501) - const port = 58910 + port := freeTCPUDPPort(t) inst := amneziawg.Instance{Peers: []amneziawg.Peer{ peerWithPortsAndIPs("a@x", fmt.Sprintf("%d", port), "10.211.0.2/32"), }} @@ -190,8 +190,7 @@ func TestPortForwardSetReconcileSurvivesPreBoundPort(t *testing.T) { gs := newTestStack(t, "10.211.1.1") set := NewPortForwardSet(gs, 502) - const collidingPort = 58911 - const okPort = 58912 + collidingPort, okPort := freeTCPUDPPort(t), freeTCPUDPPort(t) blocker, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", collidingPort)) if err != nil { t.Fatalf("pre-bind test port: %v", err) @@ -280,9 +279,8 @@ func TestPortForwardRoundTripTCPAndUDP(t *testing.T) { t.Fatalf("generate client keypair: %v", err) } - const listenPort = 58920 // fixed loopback test port, matches this package's existing test convention - const tcpPort = 58921 - const udpPort = 58922 + listenPort := freeUDPPort(t) + tcpPort, udpPort := freeTCPUDPPort(t), freeTCPUDPPort(t) const clientAddr = "10.202.0.2" inst := amneziawg.Instance{ @@ -510,7 +508,7 @@ func waitPortForwardSlots(t *testing.T, slots chan struct{}, want int) { } func TestPortForwardUDPSessionsAreCapped(t *testing.T) { - const port = 58913 + port := freeUDPPort(t) const free, sources = 3, 10 held := leavePortForwardSlots(t, portForwardUDPSlots, free) @@ -551,7 +549,7 @@ func TestPortForwardUDPSessionsAreCapped(t *testing.T) { } func TestPortForwardTCPConnectionsAreCapped(t *testing.T) { - const port = 58914 + port := freePort(t) const free, clients = 3, 10 held := leavePortForwardSlots(t, portForwardTCPSlots, free) diff --git a/internal/amneziawgnet/relay_e2e_test.go b/internal/amneziawgnet/relay_e2e_test.go index b1a00a3d9..2d5a5756c 100644 --- a/internal/amneziawgnet/relay_e2e_test.go +++ b/internal/amneziawgnet/relay_e2e_test.go @@ -77,7 +77,7 @@ func TestSocksRelayAgainstRealXray(t *testing.T) { t.Fatalf("generate client keypair: %v", err) } - const listenPort = 58715 + listenPort := freeUDPPort(t) inst := amneziawg.Instance{ Id: 4, InterfaceName: "awgtest4", @@ -305,7 +305,7 @@ func TestManagerEnsureAutomaticallyWiresRelay(t *testing.T) { } const wantEmail = "manager-e2e-peer@example.com" - const listenPort = 58716 + listenPort := freeUDPPort(t) const inboundID = 5 tcpEcho, tcpEchoAddr := startTCPEcho(t, localIP) @@ -595,6 +595,39 @@ func freePort(t *testing.T) int { return l.Addr().(*net.TCPAddr).Port } +// freeUDPPort asks the OS for a free loopback UDP port: a fixed one sits in the +// Linux ephemeral range, where any concurrent :0 socket can already hold it. +func freeUDPPort(tb testing.TB) int { + tb.Helper() + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + tb.Fatal(err) + } + defer pc.Close() + return pc.LocalAddr().(*net.UDPAddr).Port +} + +// freeTCPUDPPort finds a loopback port free for both protocols, since a port +// forward binds a TCP and a UDP listener on the same number. +func freeTCPUDPPort(t *testing.T) int { + t.Helper() + for range 20 { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + port := l.Addr().(*net.TCPAddr).Port + pc, err := net.ListenPacket("udp", fmt.Sprintf("127.0.0.1:%d", port)) + l.Close() + if err == nil { + pc.Close() + return port + } + } + t.Fatal("no loopback port free for both TCP and UDP") + return 0 +} + func waitForPort(t *testing.T, port int) { t.Helper() deadline := time.Now().Add(15 * time.Second) diff --git a/internal/amneziawgnet/udp_test.go b/internal/amneziawgnet/udp_test.go index c2d79307b..a2fc7006c 100644 --- a/internal/amneziawgnet/udp_test.go +++ b/internal/amneziawgnet/udp_test.go @@ -34,7 +34,7 @@ func TestNewDeviceUDPHandlerAndReply(t *testing.T) { t.Fatalf("generate client keypair: %v", err) } - const listenPort = 58713 // distinct from the TCP test's port + listenPort := freeUDPPort(t) const wantEmail = "udp-test-peer@example.com" const echoPayload = "hello-from-client" diff --git a/internal/amneziawgnet/v6alias_test.go b/internal/amneziawgnet/v6alias_test.go index 7b4dce784..e11e94404 100644 --- a/internal/amneziawgnet/v6alias_test.go +++ b/internal/amneziawgnet/v6alias_test.go @@ -305,7 +305,7 @@ func TestManagerRemoveLeavesHostOwnedAddressInPlace(t *testing.T) { inst := amneziawg.Instance{ Id: 7, InterfaceName: "awgtest7", - ListenPort: 58717, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.207.0.1/24", "fd86:ea04:1115::1/64"},