|
33 | 33 | run: go test -count=1 -v ./... |
34 | 34 | - name: Test with -race |
35 | 35 | 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 |
36 | 49 | - name: Build cmd/awl |
37 | 50 | run: go build github.com/anywherelan/awl/cmd/awl |
38 | 51 | - name: Upload cmd/awl build |
@@ -103,9 +116,73 @@ jobs: |
103 | 116 | # TODO: remove this temporal hack for linux |
104 | 117 | ping awl-tester.awl -w 20 -c 10 || true |
105 | 118 |
|
| 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 | +
|
106 | 179 | sleep 1 |
107 | 180 | 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 |
109 | 186 | - name: Run librespeed-cli, awl, librespeed-cli through awl |
110 | 187 | if: matrix.os == 'macos-latest' |
111 | 188 | run: | |
|
0 commit comments