Skip to content

Commit 58334c7

Browse files
committed
WIP 2
1 parent 076aaa7 commit 58334c7

7 files changed

Lines changed: 87 additions & 4 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ jobs:
5959
run: |
6060
Get-NetNat | Format-List
6161
go test -c -tags vpn_hostnet -o gw-hostnet.test.exe ./vpn/routes/
62-
./gw-hostnet.test.exe -test.run '^TestGatewayHostNet' -test.v
62+
# Flags are quoted: pwsh splits an unquoted `-test.run` at the dot
63+
# into `-test` + `.run`, which the test binary rejects.
64+
./gw-hostnet.test.exe '-test.run' '^TestGatewayHostNet' '-test.v'
6365
- name: Build cmd/awl
6466
run: go build github.com/anywherelan/awl/cmd/awl
6567
- name: Upload cmd/awl build

p2p/p2p.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,17 @@ func (p *P2p) InitHost(hostConfig HostConfig) (host.Host, error) {
200200
return p2pHost, nil
201201
}
202202

203+
// buildTransportOpts wires the socket-marking controlFunc into every place
204+
// libp2p lets us: TCP dials (WithDialerForAddr) and UDP listen sockets
205+
// (quicreuse.OverrideListenUDP — QUIC dials share the listen socket).
206+
//
207+
// TODO(gateway): the TCP *listener* is NOT covered — go-libp2p's TCP
208+
// transport has no listen hook (Listen goes straight to manet.Listen), so
209+
// sockets accepted from it stay unmarked on every platform. With gateway
210+
// client mode on, their replies are routed into the TUN and inbound direct
211+
// TCP degrades (no leak; QUIC/dials/hole-punch/relay still work). Fixing it
212+
// needs an upstream listen hook + marking the listener (accepted sockets
213+
// inherit SO_MARK on Linux / listener options on Windows) — separate task
203214
func (p *P2p) buildTransportOpts(controlFunc func(network, address string, c syscall.RawConn) error) []libp2p.Option {
204215
if controlFunc == nil {
205216
return []libp2p.Option{

service/vpn_gateway.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ func (g *VPNGateway) applyClient() error {
390390
return nil
391391
}
392392

393+
// Temporary exception rather than a restructure: this branching moves into
394+
// the netstate manager in the next MR (plans/windows-vpn-gateway.md §8.2)
395+
// and flattens there.
396+
//nolint:nestif
393397
if g.disableOSSetup {
394398
g.clientRouteState = &routes.RouteState{}
395399
} else {

vpn/routes/nat_windows.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ func SetupNAT(awlSubnet, tunIfName string) (*NATState, error) {
8888
// offline we still bring the server up (parity with Linux, where
8989
// ServerEnabled is applied at startup regardless of connectivity) and
9090
// only warn — transit starts working when SetupNAT next runs with a
91-
// live uplink. Roaming note: if the uplink changes mid-session, the new
92-
// uplink keeps its own forwarding value; toggle the gateway server off
93-
// and on to re-apply. Documented in GATEWAY_FEATURE.md.
91+
// live uplink.
92+
//
93+
// TODO(netstate): if the uplink changes mid-session (or appears after an
94+
// offline start), the new uplink keeps its own forwarding value and
95+
// transit needs a gateway server off/on toggle.
96+
// Fix is a route-change subscriber that re-syncs
97+
// forwarding on the new uplink; lands with the netstate refactoring
9498
forwardingTargets := []winipcfg.LUID{tunLUID}
9599
uplinkRoute, ok, err := uplink.BestDefault(windows.AF_INET, tunLUID)
96100
if err != nil {

vpn/routes/routes_linux.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ func SetupGatewayRoutes(tunIfName string, fwmark uint32) (*RouteState, error) {
121121
return nil, fmt.Errorf("get default routes: %w", err)
122122
}
123123
if len(origDefaults) == 0 {
124+
// TODO(gateway-offline-start): soften to a warning and proceed with an
125+
// empty exemption table — the route monitor below already re-syncs it
126+
// when a default appears (DHCP), so an offline boot with a persisted
127+
// ClientEnabled would self-heal instead of failing Init. Needs a
128+
// reconcile-from-empty verification + doc updates
124129
return nil, fmt.Errorf("no IPv4 default route present, cannot configure VPN gateway")
125130
}
126131

vpn/routes/vpn_hostnet_windows_integration_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
// (extracted wintun.dll from the embeds package), because /1 on-link routes
2929
// need a point-to-point interface and their crash semantics — dying with the
3030
// adapter — are exactly what we assert.
31+
//
32+
// Deliberately NOT covered here (vs the Linux suite):
33+
// - reaction to route changes (Linux R4/R5): on Windows that machinery is
34+
// the sockmark watcher (UNICAST_IF re-bind), not a routes-package
35+
// monitor — its integration test lands with the netstate refactoring,
36+
// see TODO(netstate) in vpn/sockmark/sockmark_windows.go
37+
// - client-route stale recovery / leftover collisions (Linux R2/R3):
38+
// impossible by design — the routes die with the adapter LUID
39+
// (TestGatewayHostNetClientRoutesDieWithAdapter proves exactly that),
40+
// and a fresh adapter is a fresh LUID.
3141
package routes
3242

3343
import (
@@ -100,6 +110,14 @@ func forwardingEnabled(t *testing.T, luid winipcfg.LUID) bool {
100110
return ipIface.ForwardingEnabled
101111
}
102112

113+
func setForwarding(t *testing.T, luid winipcfg.LUID, enabled bool) {
114+
t.Helper()
115+
ipIface, err := luid.IPInterface(windows.AF_INET)
116+
require.NoError(t, err)
117+
ipIface.ForwardingEnabled = enabled
118+
require.NoError(t, ipIface.Set())
119+
}
120+
103121
// wfpRuleInstalled reports whether our BLOCK rule is currently visible in the
104122
// filtering engine, via a fresh read-only session (rules created by a dynamic
105123
// session are visible engine-wide for its lifetime).
@@ -151,6 +169,34 @@ func TestGatewayHostNetNATLifecycle(t *testing.T) {
151169
require.False(t, wfpRuleInstalled(t), "teardown must remove the WFP rule (dynamic session closed)")
152170
}
153171

172+
// ---- Server: teardown must not disable forwarding it did not enable ----
173+
174+
// TestGatewayHostNetNATPreservesExistingForwarding is the Windows counterpart
175+
// of the Linux "pre-existing ip_forward=1 stays on" test: interfaces that
176+
// already forward (other VPNs, containers, ICS — or a previous awl run that
177+
// was killed, leaving the flag set) must be left alone by TeardownNAT.
178+
// Unlike the Linux test (which skips unless the host happens to have
179+
// ip_forward pre-enabled), the per-interface flag lets us set up the
180+
// precondition deterministically.
181+
func TestGatewayHostNetNATPreservesExistingForwarding(t *testing.T) {
182+
verifyNoLeaks(t)
183+
requireAdmin(t)
184+
nicLUID, nicGUID := pickServerTestNIC(t)
185+
186+
if !forwardingEnabled(t, nicLUID) {
187+
setForwarding(t, nicLUID, true)
188+
t.Cleanup(func() { setForwarding(t, nicLUID, false) })
189+
}
190+
191+
state, err := SetupNAT(testAwlSubnet, nicGUID)
192+
require.NoError(t, err)
193+
require.True(t, forwardingEnabled(t, nicLUID))
194+
195+
require.NoError(t, TeardownNAT(state))
196+
require.True(t, forwardingEnabled(t, nicLUID),
197+
"forwarding was already on before setup; teardown must NOT disable it")
198+
}
199+
154200
// ---- Server: stale state recovery ----
155201

156202
func TestGatewayHostNetNATStaleRecovery(t *testing.T) {

vpn/sockmark/sockmark_windows.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ func (m *WindowsMarker) Start(ctx context.Context) error {
120120
// condition is self-healing (the watcher re-binds sockets when connectivity
121121
// appears), so a failure here means "try again once online", not "restart".
122122
// IPv6 is not required: the tunnel is IPv4-only and gateway mode fences IPv6.
123+
//
124+
// TODO(gateway-offline-start): soften this gate to a warning so gateway
125+
// client mode can be enabled/booted offline and self-heal when connectivity appears
123126
func (m *WindowsMarker) Ready() error {
124127
if m.index4.Load() == 0 {
125128
return errors.New("no active network connection (no IPv4 default route)")
@@ -209,6 +212,14 @@ func (m *WindowsMarker) kick() {
209212
// watch is the marker's background goroutine: debounced re-detection on
210213
// network change notifications plus the periodic registry sweep. cleanup
211214
// unregisters the OS callbacks when ctx dies.
215+
//
216+
// TODO(netstate): the callback → debounce → redetect → setsockopt chain has
217+
// no integration coverage (unit tests cover only the registry logic; the
218+
// Linux counterpart R4/R5 hostnet tests cover the routes monitor, which has
219+
// no Windows analogue — the re-bind IS the Windows reaction to route
220+
// changes). Add a hostnet-style test — Start → socket via ControlFunc →
221+
// mutate default routes → assert getsockopt(IP_UNICAST_IF) — after the
222+
// monitor moves into the netstate manager
212223
func (m *WindowsMarker) watch(ctx context.Context, cleanup func()) {
213224
defer cleanup()
214225

0 commit comments

Comments
 (0)