Skip to content

Commit 89ac8d6

Browse files
committed
vpn/netstate: split Manager into per-platform implementations
Replace the cross-platform Manager + marker interface with a per-GOOS Manager type owning the marker state and routes/NAT under one lock. Windows-only helpers (uplink, sock registry, families, winnat) move under the windows constraint; setup/teardown become Manager methods and the fwmark parameter threading is replaced by the awlMark const. Hostnet test suites now exercise the Manager API directly. No behaviour change.
1 parent 42c1db5 commit 89ac8d6

31 files changed

Lines changed: 736 additions & 665 deletions

vpn/iface_windows.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ func newTUN(ifname string, mtu int, localIP net.IP, ipMask net.IPMask) (tun.Devi
7878
}
7979

8080
func (d *Device) InterfaceName() (string, error) {
81-
nativeTun := d.tun.(*tun.NativeTun)
81+
nativeTun, ok := d.tun.(*tun.NativeTun)
82+
if !ok {
83+
// Injected tun device (tests): no wintun LUID to resolve into a GUID,
84+
// report the plain name like the other platforms do.
85+
return d.tun.Name()
86+
}
8287
luid := winipcfg.LUID(nativeTun.LUID())
8388
guid, err := luid.GUID()
8489
if err != nil {

vpn/netstate/families.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

vpn/netstate/families_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build windows
2+
13
package netstate
24

35
import (

vpn/netstate/log.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

vpn/netstate/manager_android.go

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,135 @@
22

33
package netstate
44

5+
import (
6+
"context"
7+
"fmt"
8+
"sync"
9+
"syscall"
10+
)
11+
12+
// ProtectFunc is the type of the callback supplied by the Android host
13+
// application (VpnService.protect via the gomobile/JNI bridge). Returns true
14+
// on success.
15+
type ProtectFunc func(fd int) bool
16+
17+
// Manager on Android delegates everything to the host application: socket
18+
// marking invokes the host-supplied protect callback for each libp2p socket
19+
// so the host's VpnService.protect() can mark it as bypassing the TUN, and
20+
// routes/NAT are owned by the host's VpnService.Builder — Enable*/Disable*
21+
// only track the enabled state so Active reporting stays consistent with the
22+
// other platforms.
23+
//
24+
// The protector is set once at construction (via NewAndroidManager). To
25+
// change it, stop the application and start a new one with a fresh Manager.
26+
// This avoids any runtime synchronisation and matches the gomobile-lib
27+
// lifecycle (one Application instance per StartServer call).
28+
type Manager struct {
29+
protect ProtectFunc
30+
31+
mu sync.Mutex
32+
clientRoutesActive bool
33+
serverNATActive bool
34+
}
35+
36+
// NewManager returns a Manager with a no-op protector (ControlFunc will
37+
// return nil). Production code should construct the Manager via
38+
// NewAndroidManager with a protector wired to VpnService.protect; NewManager
39+
// exists so that the cross-platform default construction path works and
40+
// callers that don't enable gateway mode don't have to special-case Android.
41+
func NewManager() *Manager {
42+
return &Manager{}
43+
}
44+
545
// NewAndroidManager returns a Manager whose socket marker calls the
646
// host-supplied protect callback (VpnService.protect via the gomobile/JNI
7-
// bridge) for each new socket. The routes/NAT sides stay no-ops on Android —
8-
// routing is owned by the host's VpnService.Builder.
47+
// bridge) for each new socket.
948
func NewAndroidManager(protect ProtectFunc) *Manager {
10-
return &Manager{marker: newAndroidMarker(protect)}
49+
return &Manager{protect: protect}
50+
}
51+
52+
// Start is a no-op: socket protection is delegated to the host's
53+
// VpnService.protect, which owns its own network tracking.
54+
func (m *Manager) Start(_ context.Context) error { return nil }
55+
56+
// ControlFunc returns a function compatible with net.Dialer.Control that
57+
// invokes the host-supplied protector for each new socket. Returns nil when
58+
// no protector was supplied (NewManager).
59+
func (m *Manager) ControlFunc() func(network, address string, c syscall.RawConn) error {
60+
if m.protect == nil {
61+
return nil
62+
}
63+
return func(_, _ string, c syscall.RawConn) error {
64+
var sockErr error
65+
err := c.Control(func(fd uintptr) {
66+
// gomobile turns Java-side exceptions into Go panics. If the
67+
// host VpnService has been destroyed between Application.Close
68+
// and a still-running libp2p dial, the JVM ref behind protect
69+
// may be dead; recover so the dial fails cleanly instead of
70+
// crashing the process.
71+
defer func() {
72+
if r := recover(); r != nil {
73+
logger.Warnf("VpnService.protect panicked for fd %d: %v", fd, r)
74+
sockErr = fmt.Errorf("VpnService.protect panic: %v", r)
75+
}
76+
}()
77+
if !m.protect(int(fd)) {
78+
sockErr = fmt.Errorf("VpnService.protect failed for fd %d", fd)
79+
}
80+
})
81+
if err != nil {
82+
return fmt.Errorf("sockmark control: %w", err)
83+
}
84+
return sockErr
85+
}
86+
}
87+
88+
// EnableClientRoutes only records the enabled state: routes are configured
89+
// via VpnService.Builder in the Android app:
90+
// - Gateway mode: builder.addRoute("0.0.0.0", 0) + builder.addRoute("::", 0)
91+
// - Normal mode: builder.addRoute("10.66.0.0", 24) (awl subnet only)
92+
func (m *Manager) EnableClientRoutes(_ string) error {
93+
m.mu.Lock()
94+
defer m.mu.Unlock()
95+
m.clientRoutesActive = true
96+
return nil
97+
}
98+
99+
// DisableClientRoutes only records the disabled state — see EnableClientRoutes.
100+
func (m *Manager) DisableClientRoutes() error {
101+
m.mu.Lock()
102+
defer m.mu.Unlock()
103+
m.clientRoutesActive = false
104+
return nil
105+
}
106+
107+
// ClientRoutesActive reports whether gateway client routes are enabled.
108+
func (m *Manager) ClientRoutesActive() bool {
109+
m.mu.Lock()
110+
defer m.mu.Unlock()
111+
return m.clientRoutesActive
112+
}
113+
114+
// EnableServerNAT only records the enabled state: Android exit node support
115+
// requires root or special system configuration, so no OS state is touched.
116+
func (m *Manager) EnableServerNAT(_, _ string) error {
117+
m.mu.Lock()
118+
defer m.mu.Unlock()
119+
m.serverNATActive = true
120+
return nil
121+
}
122+
123+
// DisableServerNAT only records the disabled state — see EnableServerNAT.
124+
func (m *Manager) DisableServerNAT() error {
125+
m.mu.Lock()
126+
defer m.mu.Unlock()
127+
m.serverNATActive = false
128+
return nil
129+
}
130+
131+
// ServerNATActive reports whether the exit-node NAT is enabled.
132+
func (m *Manager) ServerNATActive() bool {
133+
m.mu.Lock()
134+
defer m.mu.Unlock()
135+
return m.serverNATActive
11136
}
Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build linux && !android
2+
13
package netstate
24

35
import (
@@ -7,74 +9,69 @@ import (
79
"syscall"
810
)
911

10-
// Manager is the single entry point to this package: it owns the socket
11-
// marker (always-on, started once per process) and the runtime OS state of
12-
// VPN gateway mode — the client routes and the server NAT. The struct is the
13-
// same on every platform; the platform-specific behaviour lives in the marker
14-
// and in the setup/teardown functions the methods call. Consumers declare
15-
// their own narrow interfaces over the methods they use (see awl.NetManager,
16-
// service.NetManager, service.SocketMarker).
12+
// awlMark is the numeric value shared by the SO_MARK fwmark applied to marked
13+
// sockets and the policy-routing table ID holding their exemption routes.
14+
// 0x61776C = "awl" in ASCII (lowercase). The two live in different kernel
15+
// namespaces and don't collide; using one value makes awl-owned state
16+
// trivially greppable in `ip rule` / `ip route show table`.
17+
const awlMark = 0x61776C
18+
19+
// Manager owns the Linux OS network state behind AWL's VPN gateway feature:
20+
// the always-on socket marking (SO_MARK, applied via ControlFunc) and the
21+
// runtime state of gateway mode — the client routes and the server NAT.
1722
//
1823
// Enable/Disable methods are idempotent and safe for concurrent use. The
1924
// internal mutex only guards the Manager's own state; the orchestration
2025
// above (service.VPNGateway) still serialises whole enable/disable
2126
// transactions — config, tunnel binding, DNS and the calls here — under its
2227
// own lock.
2328
type Manager struct {
24-
marker marker
25-
2629
mu sync.Mutex
2730
routeState *routeState
2831
natState *natState
2932
}
3033

31-
// NewManager returns the Manager for the current platform. On Android it
32-
// carries a no-op socket protector — use NewAndroidManager to wire
33-
// VpnService.protect.
34+
// NewManager returns the Manager for Linux. Setting SO_MARK requires
35+
// CAP_NET_ADMIN, which AWL already needs for TUN setup, so no extra
36+
// capability is required.
3437
func NewManager() *Manager {
35-
return &Manager{marker: newMarker()}
38+
return &Manager{}
3639
}
3740

38-
// Start performs the marker's initial setup and launches any background
39-
// machinery it needs, living until ctx is cancelled. On Windows this is the
40-
// uplink detection + network-change watcher; elsewhere it is a no-op. Must be
41-
// called before the first libp2p socket is created (see marker.Start for the
42-
// offline-start semantics).
43-
func (m *Manager) Start(ctx context.Context) error {
44-
return m.marker.Start(ctx)
45-
}
41+
// Start is a no-op on Linux: SO_MARK is interpreted by the kernel per packet,
42+
// there is no per-socket state to keep in sync with the network.
43+
func (m *Manager) Start(_ context.Context) error { return nil }
4644

4745
// ControlFunc returns a function compatible with net.Dialer.Control and the
48-
// QUIC ListenUDP override, marking each new socket to bypass the VPN tunnel.
49-
// It returns nil when the platform is not configured (e.g. Android before the
50-
// host app supplies a protector).
46+
// QUIC ListenUDP override, marking each new socket with SO_MARK so its
47+
// traffic bypasses the VPN tunnel via the fwmark ip rule.
5148
func (m *Manager) ControlFunc() func(network, address string, c syscall.RawConn) error {
52-
return m.marker.ControlFunc()
49+
return func(_, _ string, c syscall.RawConn) error {
50+
var sockErr error
51+
err := c.Control(func(fd uintptr) {
52+
sockErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, awlMark)
53+
})
54+
if err != nil {
55+
return fmt.Errorf("sockmark control: %w", err)
56+
}
57+
if sockErr != nil {
58+
return fmt.Errorf("sockmark SO_MARK: %w", sockErr)
59+
}
60+
return nil
61+
}
5362
}
5463

5564
// EnableClientRoutes installs the gateway client routes on the TUN (the
5665
// default-route capture plus the IPv6 fail-closed fence). Idempotent: a
57-
// second call while routes are installed is a no-op. On Windows it refuses
58-
// while no IPv4 uplink is known (marking could not exempt libp2p traffic —
59-
// routing loop); the condition is self-healing, so that is "try again once
60-
// online", not a permanent failure.
66+
// second call while routes are installed is a no-op.
6167
func (m *Manager) EnableClientRoutes(tunIfName string) error {
6268
m.mu.Lock()
6369
defer m.mu.Unlock()
6470

6571
if m.routeState != nil {
6672
return nil
6773
}
68-
// Markers that can be temporarily unable to guarantee loop-free marking
69-
// (Windows: no uplink detected right now) expose Ready. Other platforms
70-
// don't implement the interface and skip the check.
71-
if readier, ok := m.marker.(interface{ Ready() error }); ok {
72-
if err := readier.Ready(); err != nil {
73-
return fmt.Errorf("cannot enable VPN gateway: %w", err)
74-
}
75-
}
76-
77-
state, err := setupGatewayRoutes(tunIfName, m.marker.FWMark())
74+
state, err := m.setupGatewayRoutes(tunIfName)
7875
if err != nil {
7976
return fmt.Errorf("setup gateway routes: %w", err)
8077
}
@@ -95,7 +92,7 @@ func (m *Manager) DisableClientRoutes() error {
9592
}
9693
state := m.routeState
9794
m.routeState = nil
98-
return teardownGatewayRoutes(state)
95+
return m.teardownGatewayRoutes(state)
9996
}
10097

10198
// ClientRoutesActive reports whether gateway client routes are currently
@@ -107,17 +104,16 @@ func (m *Manager) ClientRoutesActive() bool {
107104
}
108105

109106
// EnableServerNAT configures the exit-node data path for the awl subnet
110-
// (Linux: ip_forward + iptables chain + MASQUERADE; Windows: WFP filter +
111-
// per-interface forwarding + WinNAT). Idempotent: a second call while NAT is
112-
// configured is a no-op.
107+
// (ip_forward + iptables chain + MASQUERADE). Idempotent: a second call while
108+
// NAT is configured is a no-op.
113109
func (m *Manager) EnableServerNAT(awlSubnet, tunIfName string) error {
114110
m.mu.Lock()
115111
defer m.mu.Unlock()
116112

117113
if m.natState != nil {
118114
return nil
119115
}
120-
state, err := setupNAT(awlSubnet, tunIfName)
116+
state, err := m.setupNAT(awlSubnet, tunIfName)
121117
if err != nil {
122118
return fmt.Errorf("setup NAT: %w", err)
123119
}
@@ -137,7 +133,7 @@ func (m *Manager) DisableServerNAT() error {
137133
}
138134
state := m.natState
139135
m.natState = nil
140-
return teardownNAT(state)
136+
return m.teardownNAT(state)
141137
}
142138

143139
// ServerNATActive reports whether the exit-node NAT is currently configured.

0 commit comments

Comments
 (0)