Skip to content

Commit 3f6a676

Browse files
committed
WIP 12 vpn: add gateway support
1 parent 1f1aaff commit 3f6a676

3 files changed

Lines changed: 58 additions & 19 deletions

File tree

awldns/awldns_test.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,30 @@ func NewResolverClient(address string) *net.Resolver {
8080
}
8181

8282
func FindFreePort() int {
83-
l, err := net.Listen("tcp", "127.0.0.1:0")
84-
if err != nil {
85-
panic(fmt.Sprintf("failed to listen on a port: %v", err))
83+
const maxAttempts = 50
84+
var lastErr error
85+
for i := 0; i < maxAttempts; i++ {
86+
l, err := net.Listen("tcp", "127.0.0.1:0")
87+
if err != nil {
88+
lastErr = err
89+
continue
90+
}
91+
port := l.Addr().(*net.TCPAddr).Port
92+
93+
// The DNS resolver listens on both TCP and UDP on this port, so it must be
94+
// free for both. A TCP-free port is not guaranteed to be UDP-free, and on
95+
// Windows the chosen port may fall inside an OS-excluded range (Hyper-V/WSL
96+
// reservations), which fails the UDP bind with WSAEACCES. Release the port
97+
// and try another instead of giving up.
98+
u, err := net.ListenPacket("udp", l.Addr().String())
99+
if err != nil {
100+
_ = l.Close()
101+
lastErr = err
102+
continue
103+
}
104+
_ = u.Close()
105+
_ = l.Close()
106+
return port
86107
}
87-
defer l.Close()
88-
89-
port := l.Addr().(*net.TCPAddr).Port
90-
91-
u, err := net.ListenPacket("udp", l.Addr().String())
92-
if err != nil {
93-
panic(fmt.Sprintf("failed to listen on a udp port: %v", err))
94-
}
95-
defer u.Close()
96-
97-
return port
108+
panic(fmt.Sprintf("failed to find a free tcp+udp port after %d attempts: %v", maxAttempts, lastErr))
98109
}

service/tunnel.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ func (t *Tunnel) RefreshPeersList() {
126126
t.peersLock.Lock()
127127
defer t.peersLock.Unlock()
128128

129+
if t.isClosed.Load() {
130+
return
131+
}
132+
129133
t.conf.RLock()
130134
defer t.conf.RUnlock()
131135
for _, knownPeer := range t.conf.KnownPeers {

vpn/routes/vpn_hostnet_integration_test.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,13 @@ func assertNATApplied(t *testing.T) {
226226
got := lines(cmdOut(t, "iptables", "-S", awlForwardChain))
227227
require.Equal(t, want, got, "AWL-FORWARD chain content/order")
228228

229+
// iptables -S prints matches in its own canonical order (-s before -i,
230+
// -d before -o), regardless of the order the code passes them in
231+
// (outboundJumpArgs/returnJumpArgs use -i/-s and -o/-d), so assert against
232+
// that canonical form.
229233
filter := cmdOut(t, "iptables", "-S", "FORWARD")
230-
require.Contains(t, filter, "-i "+testTunIf+" -s "+testAwlSubnet+" -j "+awlForwardChain, "outbound jump")
231-
require.Contains(t, filter, "-o "+testTunIf+" -d "+testAwlSubnet+" -j "+awlForwardChain, "return jump")
234+
require.Contains(t, filter, "-s "+testAwlSubnet+" -i "+testTunIf+" -j "+awlForwardChain, "outbound jump")
235+
require.Contains(t, filter, "-d "+testAwlSubnet+" -o "+testTunIf+" -j "+awlForwardChain, "return jump")
232236

233237
nat := cmdOut(t, "iptables", "-t", "nat", "-S", "POSTROUTING")
234238
require.Contains(t, nat, "-s "+testAwlSubnet+" ! -o "+testTunIf+" -j MASQUERADE", "MASQUERADE")
@@ -270,7 +274,7 @@ func snapshotNet(t *testing.T) string {
270274
}
271275
section("ip rule", cmdOut(t, "ip", "rule", "show"))
272276
section("route main", cmdOut(t, "ip", "-4", "route", "show"))
273-
section("route awl-table", cmdOut(t, "ip", "-4", "route", "show", "table", strconv.Itoa(tableID)))
277+
section("route awl-table", routeTableDump(t, tableID))
274278
section("iptables filter", cmdOut(t, "iptables", "-S"))
275279
section("iptables nat", cmdOut(t, "iptables", "-t", "nat", "-S"))
276280
return b.String()
@@ -340,8 +344,28 @@ func mustCmd(t *testing.T, name string, args ...string) {
340344

341345
func cmdOut(t *testing.T, name string, args ...string) string {
342346
t.Helper()
343-
out, err := exec.Command(name, args...).Output()
344-
require.NoErrorf(t, err, "%s %s", name, strings.Join(args, " "))
347+
// CombinedOutput (not Output) so a failing command surfaces its stderr
348+
// diagnostic in the test log instead of a bare "exit status N". On success
349+
// these commands print nothing to stderr, so the captured value is unchanged.
350+
out, err := exec.Command(name, args...).CombinedOutput()
351+
require.NoErrorf(t, err, "%s %s: %s", name, strings.Join(args, " "), out)
352+
return string(out)
353+
}
354+
355+
// routeTableDump returns the routes in the given table, tolerating the
356+
// "table does not exist" case. Newer iproute2/kernels (e.g. Ubuntu 24.04) make
357+
// `ip route show table <id>` fail with exit 2 ("FIB table does not exist") when
358+
// the table has never held a route, whereas older versions returned empty with
359+
// exit 0. Both mean the same thing here — an empty table — so normalise to "".
360+
func routeTableDump(t *testing.T, table int) string {
361+
t.Helper()
362+
out, err := exec.Command("ip", "-4", "route", "show", "table", strconv.Itoa(table)).CombinedOutput()
363+
if err != nil {
364+
if strings.Contains(string(out), "does not exist") {
365+
return ""
366+
}
367+
require.NoErrorf(t, err, "ip -4 route show table %d: %s", table, out)
368+
}
345369
return string(out)
346370
}
347371

0 commit comments

Comments
 (0)