Skip to content

Commit 13b3f3e

Browse files
committed
WIP 10 vpn: add gateway support
1 parent ad31bc2 commit 13b3f3e

6 files changed

Lines changed: 457 additions & 24 deletions

File tree

.github/workflows/test.yml

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ jobs:
3333
run: go test -count=1 -v ./...
3434
- name: Test with -race
3535
run: go test -race -count=1 -v ./...
36+
- name: VPN gateway host-network integration test (Linux, root)
37+
# Hidden behind the `vpn_hostnet` build tag and excluded from `go test ./...`.
38+
# Exercises the real netlink/iptables plumbing (SetupNAT/SetupGatewayRoutes
39+
# + teardown/stale-recovery) against the runner's own network, so it needs
40+
# root and only runs on Linux. Compile as the normal user, then run the
41+
# binary under sudo so root never pollutes the Go build cache.
42+
# TODO: if a hard failure ever leaves the runner's egress black-holed
43+
# (default route via a dead awl0) and breaks log upload, add a separate
44+
# `if: always()` cleanup step
45+
if: matrix.os == 'ubuntu-latest'
46+
run: |
47+
go test -c -tags vpn_hostnet -o gw-hostnet.test ./vpn/routes/
48+
sudo ./gw-hostnet.test -test.run '^TestGatewayHostNet' -test.v
3649
- name: Build cmd/awl
3750
run: go build github.com/anywherelan/awl/cmd/awl
3851
- name: Upload cmd/awl build
@@ -103,9 +116,73 @@ jobs:
103116
# TODO: remove this temporal hack for linux
104117
ping awl-tester.awl -w 20 -c 10 || true
105118
119+
# ---- VPN gateway server (exit-node) mode: runtime enable/disable round-trips OS state ----
120+
# awl runs as root here, so enabling the server installs real NAT
121+
# (iptables MASQUERADE + AWL-FORWARD chain) in this netns. We assert the
122+
# commands succeed AND that they actually mutate then fully restore the
123+
# netfilter state. `iptables -S` (no packet/byte counters, unlike
124+
# iptables-save) makes the before/after comparison stable.
125+
nat_state() { sudo iptables -S; sudo iptables -t nat -S; }
126+
NAT_BEFORE=$(nat_state)
127+
./awl cli gateway server enable
128+
test "$(nat_state)" != "$NAT_BEFORE" # enable actually changed netfilter
129+
sudo iptables -S | grep -q AWL-FORWARD # exit-node chain installed
130+
./awl cli gateway server disable
131+
test "$(nat_state)" = "$NAT_BEFORE" # disable restored netfilter exactly
132+
133+
# ---- VPN gateway full-tunnel e2e: client = this runner, exit node = awl-tester ----
134+
# Requires the awl-tester peer to have VPN gateway server enabled AND to
135+
# permit this CI peer as an exit node (WeAllowUsingAsExitNode); both are
136+
# advertised to us via the status protocol. Wait until it shows up as a
137+
# connected, available gateway.
138+
for i in $(seq 1 15); do
139+
if ./awl cli gateway list | grep -E 'awl-tester.*\[connected\]'; then break; fi
140+
sleep 2
141+
done
142+
./awl cli gateway list | grep -E 'awl-tester.*\[connected\]' # fail if never became available
143+
144+
# Probe the egress IP via https://ifconfig.me. Relies on DNS still
145+
# resolving under full-tunnel — on Linux awl does not take over
146+
# /etc/resolv.conf (known DNS-leak limitation), so resolution keeps
147+
# working through the runner's normal resolver. If a real CI run shows
148+
# DNS breaking under the tunnel, pin the IP: ECHO_IP=$(getent ahostsv4
149+
# ifconfig.me ...) and add curl `--resolve ifconfig.me:443:$ECHO_IP`.
150+
curl4() { curl -4 -s --max-time 20 --retry 3 --retry-delay 2 https://ifconfig.me/; }
151+
152+
IP_DIRECT=$(curl4); echo "egress IP (direct): $IP_DIRECT"
153+
154+
./awl cli gateway client use --name awl-tester
155+
./awl cli gateway status
156+
RULE_ON=$(ip rule show); ROUTE_ON=$(ip route show)
157+
# Tolerate a failed curl here so we always reach `client stop` below
158+
# (a broken tunnel must not leave the runner black-holed); the
159+
# `test -n "$IP_GW"` assertion after teardown turns it into a clean failure.
160+
IP_GW=$(curl4 || true); echo "egress IP (via gateway): $IP_GW"
161+
162+
./awl cli gateway client stop
163+
RULE_OFF=$(ip rule show); ROUTE_OFF=$(ip route show)
164+
IP_REVERTED=$(curl4); echo "egress IP (reverted): $IP_REVERTED"
165+
166+
# Assertions run with the gateway already disabled, so a failure here
167+
# can't leave the runner's egress black-holed.
168+
echo "$RULE_ON" | grep -q fwmark # client policy route installed
169+
echo "$ROUTE_ON" | grep -q 'default dev awl0' # full-tunnel default via TUN
170+
test -n "$IP_GW" # traffic really flowed through the exit node
171+
test "$IP_GW" != "$IP_DIRECT" # egress changed => full-tunnel works
172+
test "$IP_REVERTED" = "$IP_DIRECT" # runtime disable reverted egress
173+
! echo "$RULE_OFF" | grep -q fwmark # policy route removed on disable
174+
! echo "$ROUTE_OFF" | grep -q 'default dev awl0' # TUN default removed on disable
175+
176+
# Re-enable so the SIGINT shutdown path (teardownGatewayAtShutdown) is exercised.
177+
./awl cli gateway client use --name awl-tester
178+
106179
sleep 1
107180
sudo kill -SIGINT $awl_pid
108-
sleep 1
181+
sleep 2
182+
# awl is gone: its TUN and the default route via it vanished with the
183+
# process, but the fwmark ip rule is interface-independent — its absence
184+
# proves the shutdown teardown ran. (NAT/ip_forward are server-side only.)
185+
! ip rule show | grep -q fwmark
109186
- name: Run librespeed-cli, awl, librespeed-cli through awl
110187
if: matrix.os == 'macos-latest'
111188
run: |

application_gateway_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ func TestGatewayBidirectional(t *testing.T) {
161161
// flipping the flag in the exit-node config without going through the
162162
// API, so the change does not propagate back to the client yet.
163163
//
164-
// 2. Runtime API revocation propagating to the client (case #5 from
165-
// GATEWAY_TESTING.md). The exit node calls UpdatePeerSettings with
164+
// 2. Runtime API revocation propagating to the client.
165+
// The exit node calls UpdatePeerSettings with
166166
// AllowUsingAsExitNode=false; ExchangeNewStatusInfo runs in the
167167
// background, the client's KnownPeer.CanUseAsVPNGateway() flips to
168168
// false, and any new EnableGateway() call is rejected. The in-memory
@@ -191,7 +191,7 @@ func TestGatewayPermissionDenied(t *testing.T) {
191191
"exit node should NOT forward packets when WeAllowUsingAsExitNode is false")
192192
})
193193

194-
// Case #5 from GATEWAY_TESTING.md: revoke through the proper API.
194+
// Case #5: revoke through the proper API.
195195
// UpdatePeerSettings triggers ExchangeNewStatusInfo in a goroutine,
196196
// propagating the new AllowUsingAsExitNode value to the client; future
197197
// EnableGateway calls are rejected. The in-memory gateway pointer on the
@@ -384,7 +384,7 @@ func TestGatewayWithThreePeers(t *testing.T) {
384384

385385
// Test 1: Normal VPN to regular peer works.
386386
//
387-
// Also covers case #8 from GATEWAY_TESTING.md: when gateway is enabled,
387+
// Also covers case #8: when gateway is enabled,
388388
// traffic to another awl peer must take the per-peer awl path, not the
389389
// gateway path — the exit node must NOT see the packet.
390390
t.Run("NormalVPNToRegularPeer", func(t *testing.T) {

cmd/awl-tray/tray.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func initTray() {
108108
proxyMenu = systray.AddMenuItem("Proxy", "")
109109
proxyRouting = newRoutingMenu(proxyMenu, routingMenuConfig{
110110
noneLabel: "None (no proxy)",
111-
emptyLabel: `No proxies available — set "Allow as exit node" on a peer`,
111+
emptyLabel: `No proxies available — set "Allow as exit node" on a remote device`,
112112
listPeers: listProxyPeers,
113113
currentSelection: currentProxySelection,
114114
selectPeer: func(peerID string) error {
@@ -130,7 +130,7 @@ func initTray() {
130130
gatewayMenu = systray.AddMenuItem("VPN Gateway", "")
131131
cfg := routingMenuConfig{
132132
noneLabel: "None (disabled)",
133-
emptyLabel: `No VPN gateways available — set "Allow as exit node" on a peer`,
133+
emptyLabel: `No VPN gateways available — set "Allow as exit node" on a remote device`,
134134
listPeers: listGatewayPeers,
135135
currentSelection: currentGatewaySelection,
136136
selectPeer: func(peerIDStr string) error {

socks5/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ type Server struct {
3131
// client (whose 0.0.0.0/0-via-TUN default route would otherwise capture these
3232
// sockets). Marking unconditionally is safe: when gateway mode is off there is
3333
// no matching fwmark ip-rule, so the kernel ignores the mark — mirroring how
34-
// libp2p sockets are marked at all times (see application.go / GATEWAY_FEATURE
35-
// §3.1). A nil dialControl yields a plain context-aware dialer.
34+
// libp2p sockets are marked at all times (see application.go).
35+
// A nil dialControl yields a plain context-aware dialer.
3636
func NewServer(dialControl func(network, address string, c syscall.RawConn) error) *Server {
3737
rule := NewUpdatableRule(NewRuleDenyLocalhost())
3838
dialer := &net.Dialer{Control: dialControl}

0 commit comments

Comments
 (0)