Skip to content

Commit ad31bc2

Browse files
committed
WIP 9 vpn: add gateway support
1 parent e3dbec9 commit ad31bc2

11 files changed

Lines changed: 639 additions & 44 deletions

File tree

README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- [Linux server (`awl`)](#linux-server-awl)
2323
- [Connecting devices](#connecting-devices)
2424
- [Using devices as SOCKS5 proxy](#using-devices-as-socks5-proxy)
25+
- [VPN gateway (full-tunnel exit node)](#vpn-gateway-full-tunnel-exit-node)
2526
- [Configuration](#configuration)
2627
- [Config file location](#config-file-location)
2728
- [Example config](#example-config)
@@ -45,6 +46,7 @@ Some things people use it for:
4546
- SSH / RDP / VNC into your home or work laptop from anywhere, without port forwarding or exposing anything to the internet
4647
- reach selfhosted services (Nextcloud, Home Assistant, Bitwarden, ...) privately
4748
- route traffic through a remote device as a SOCKS5 proxy — useful for bypassing regional blocks
49+
- route *all* your traffic through a remote device at the IP layer — a full-tunnel VPN gateway / exit node
4850
- LAN-style multiplayer gaming across the internet
4951
- keep an old Android phone accessible for apps that only run there (e.g. with [scrcpy](https://github.com/Genymobile/scrcpy))
5052

@@ -65,6 +67,7 @@ Tradeoffs worth knowing about:
6567

6668
- fully peer-to-peer, no coordination server — see [Why Anywherelan](#why-anywherelan) above
6769
- route traffic through a device as a SOCKS5 proxy
70+
- route **all** your traffic through a device — full-tunnel VPN gateway / exit node
6871
- automatic NAT traversal via libp2p; falls back to community relays when a direct path isn't possible
6972
- TLS 1.3 encryption (QUIC or TCP+TLS)
7073
- built-in DNS: reach devices at `work-laptop.awl` instead of typing IPs
@@ -114,7 +117,7 @@ awl's transport security comes from [libp2p](https://docs.libp2p.io/).
114117

115118
awl ships in two desktop flavors:
116119

117-
- **`awl-tray`** — desktop build with a system-tray indicator: status at a glance, start/stop/restart, peer list. Use this for regular desktop usage.
120+
- **`awl-tray`** — desktop build with a system-tray indicator: status at a glance, start/stop/restart, peer list, and quick exit-node selection (SOCKS5 proxy / VPN gateway). Use this for regular desktop usage.
118121
- **`awl`** — headless server build, no GUI. Use this for servers and embedded devices.
119122

120123
Both share the same web UI and the same [CLI](#terminal-based-client).
@@ -242,6 +245,7 @@ In addition to the per-application SOCKS5 proxy, awl can route **all** of your I
242245
- **macOS / Windows / other:** **not supported yet** — awl will refuse to start with VPN gateway enabled. Windows-side code exists but is unfinished; see `vpn/sockmark/sockmark_windows.go` and `vpn/routes/nat_windows.go` for the open work items.
243246
- IPv6 traffic is not tunnelled through the gateway in either direction (it is forwarded as a regular awl peer packet instead).
244247
- Both client and exit-node sides require **CAP_NET_ADMIN** on Linux. AWL needs that already to bring up the TUN, so there is no extra capability to grant.
248+
- The exit node sets up NAT through the host's `iptables` binary, which must be installed (on modern distros it resolves to `iptables-nft`). AWL only sees rules on whatever backend that binary uses; rules created against the other backend by unrelated software are invisible to it.
245249

246250
### Use a friend as your exit node (client side)
247251

@@ -254,9 +258,8 @@ In addition to the per-application SOCKS5 proxy, awl can route **all** of your I
254258
awl cli gateway client use --name="peer-name"
255259
# or: awl cli gateway client use --pid=<peer-id>
256260
```
257-
Calling `client use` again with a different peer atomically switches to the new gateway.
258-
3. **Restart `awl`** for the change to take effect. The OS-level routes and the libp2p socket marking that prevent the gateway from looping back through itself are wired up only at startup.
259-
4. Verify:
261+
This applies immediately — no restart needed. The OS-level routes and the libp2p socket marking that keep the gateway from looping back through itself are installed at runtime. Calling `client use` again with a different peer atomically switches to the new gateway. (On the Android app the change is applied by re-establishing the `VpnService` in place, also without restarting awl.)
262+
3. Verify:
260263
```bash
261264
awl cli gateway status
262265
curl ifconfig.me # should show the exit node's public IP
@@ -265,23 +268,22 @@ In addition to the per-application SOCKS5 proxy, awl can route **all** of your I
265268
To turn the gateway off:
266269

267270
```bash
268-
awl cli gateway client stop
269-
# then restart awl to remove the OS-level routes
271+
awl cli gateway client stop # OS-level routes are removed immediately
270272
```
271273

272-
You can also flip these settings from the web UI / system-tray menu, or by editing `config_awl.json` directly while awl is stopped (`gateway.enabled`, `gateway.exitNodePeerID`).
274+
You can also flip these settings from the web UI or the system-tray menu — both expose the exit-node picker and the "Serve as VPN gateway" toggle — or by editing `config_awl.json` directly while awl is stopped (`gateway.enabled`, `gateway.exitNodePeerID`).
273275

274276
### Serve as an exit node
275277

276278
To allow others to route their internet traffic through this device:
277279

278-
1. Set `gateway.serveAsVPNGateway: true` in `config_awl.json` (it is **off by default** — see "Why off by default" below) and restart awl. Equivalent: `awl cli gateway server enable` (and `awl cli gateway server disable` to turn it back off).
280+
1. Enable it with `awl cli gateway server enable` (and `awl cli gateway server disable` to turn it back off), or set `gateway.serveAsVPNGateway: true` in `config_awl.json` while awl is stopped. It is **off by default** — see "Why off by default" below. You can also use the "Serve as VPN gateway" toggle in the web UI or system tray.
279281
2. For each friend you want to permit, also tick *Allow as exit node* in their per-peer settings (or `awl cli peers allow_exit_node --name=… --allow=true`). The same flag governs SOCKS5; if you want different per-peer policies for SOCKS5 vs VPN gateway, file a request and we'll split it.
280-
3. Restart awl. On startup AWL will:
282+
3. Enabling exit-node mode applies immediately (no restart). AWL will:
281283
- flip `net.ipv4.ip_forward` on,
282284
- install a dedicated `AWL-FORWARD` iptables chain that DROPs traffic to your LAN/CGNAT/link-local subnets and ACCEPTs the rest,
283285
- add a `MASQUERADE` rule for the awl subnet,
284-
- reverse all of the above on a clean shutdown.
286+
- reverse all of the above when you disable it or on a clean shutdown.
285287
4. Connected friends can now pick you in their `gateway list` output once the next status exchange propagates the new state (≤ 5 minutes, or on next reconnect).
286288

287289
### Why off by default

application.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
142142

143143
a.Dns = NewDNSService(a.Conf, a.Eventbus, a.ctx, a.logger)
144144
a.AuthStatus = service.NewAuthStatus(a.P2p, a.Conf, a.Eventbus)
145-
a.SOCKS5, err = service.NewSOCKS5(a.P2p, a.Conf)
145+
a.SOCKS5, err = service.NewSOCKS5(a.P2p, a.Conf, a.SockMarker)
146146
if err != nil {
147147
return fmt.Errorf("failed to init socks5: %v", err)
148148
}

cmd/gomobile-lib/main.go

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
//go:build linux && android
2-
// +build linux,android
32

43
package anywherelan
54

65
import (
76
"context"
87
"fmt"
98
"os"
10-
"strconv"
119

1210
"github.com/libp2p/go-libp2p/p2p/host/eventbus"
11+
"golang.zx2c4.com/wireguard/tun"
1312

1413
"github.com/anywherelan/awl"
1514
"github.com/anywherelan/awl/config"
@@ -20,6 +19,10 @@ import (
2019
var (
2120
globalApp *awl.Application
2221
globalDataDir string
22+
// globalSwapTUN is the swappable TUN wrapper handed to the Application when
23+
// a VPN interface is active. UpdateTunDevice swaps a fresh fd into it after
24+
// the host app re-establishes VpnService with different routes.
25+
globalSwapTUN *vpn.SwappableTUN
2326
)
2427

2528
// All public functions are part of the library
@@ -73,22 +76,60 @@ func StartServer(tunFD int32, protector SocketProtector) (err error) {
7376
}
7477
}()
7578

76-
_ = os.Setenv(vpn.TunFDEnvKey, strconv.Itoa(int(tunFD)))
77-
7879
globalApp = awl.New()
7980
globalApp.SetupLoggerAndConfig()
8081
globalApp.SockMarker = sockmark.NewAndroid(protectorToFunc(protector))
8182

82-
err = globalApp.Init(context.Background(), nil)
83+
// A tunFD of 0 means the host did not establish a VPN interface (VPN
84+
// disabled in config); Init then skips the VPN device entirely. Otherwise
85+
// wrap the fd in a SwappableTUN so the interface can be replaced at runtime
86+
// via UpdateTunDevice without restarting P2P.
87+
var tunDevice tun.Device
88+
if tunFD > 0 {
89+
inner, tunErr := vpn.NewAndroidTUNFromFD(int(tunFD))
90+
if tunErr != nil {
91+
globalApp = nil
92+
return tunErr
93+
}
94+
globalSwapTUN = vpn.NewSwappableTUN(inner)
95+
tunDevice = globalSwapTUN
96+
}
97+
98+
err = globalApp.Init(context.Background(), tunDevice)
8399
if err != nil {
84100
globalApp.Close()
85101
globalApp = nil
102+
if globalSwapTUN != nil {
103+
_ = globalSwapTUN.Close()
104+
globalSwapTUN = nil
105+
}
86106
return err
87107
}
88108

89109
return nil
90110
}
91111

112+
// UpdateTunDevice swaps the live TUN interface for the one backed by tunFD,
113+
// which the host app obtained from a fresh VpnService.establish() (e.g. after
114+
// changing routes to toggle VPN gateway mode). P2P connections are unaffected:
115+
// they run on sockets already protected via VpnService.protect, which bypass
116+
// the VPN regardless of the interface, and the swap replaces only the tun fd
117+
// inside the running Application. The previous fd is owned and closed by Go.
118+
func UpdateTunDevice(tunFD int32) error {
119+
if globalApp == nil {
120+
return fmt.Errorf("server is not running")
121+
}
122+
if globalSwapTUN == nil {
123+
return fmt.Errorf("vpn interface is not active")
124+
}
125+
126+
inner, err := vpn.NewAndroidTUNFromFD(int(tunFD))
127+
if err != nil {
128+
return err
129+
}
130+
return globalSwapTUN.Swap(inner)
131+
}
132+
92133
func protectorToFunc(p SocketProtector) sockmark.ProtectFunc {
93134
if p == nil {
94135
return nil
@@ -100,9 +141,11 @@ func protectorToFunc(p SocketProtector) sockmark.ProtectFunc {
100141

101142
func StopServer() {
102143
if globalApp != nil {
144+
// Application.Close closes the VPN device, which closes globalSwapTUN.
103145
globalApp.Close()
104146
globalApp = nil
105147
}
148+
globalSwapTUN = nil
106149
}
107150

108151
func ImportConfig(data string) error {

service/socks5.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"slices"
99
"strings"
10+
"syscall"
1011
"time"
1112

1213
socks5Proxy "github.com/haxii/socks5"
@@ -18,6 +19,7 @@ import (
1819
"github.com/anywherelan/awl/metrics"
1920
"github.com/anywherelan/awl/protocol"
2021
"github.com/anywherelan/awl/socks5"
22+
"github.com/anywherelan/awl/vpn/sockmark"
2123
)
2224

2325
type SOCKS5 struct {
@@ -29,7 +31,7 @@ type SOCKS5 struct {
2931
server *socks5.Server
3032
}
3133

32-
func NewSOCKS5(p2pService P2p, conf *config.Config) (*SOCKS5, error) {
34+
func NewSOCKS5(p2pService P2p, conf *config.Config, sockMarker sockmark.Marker) (*SOCKS5, error) {
3335
logger := log.Logger("awl/service/socks5")
3436

3537
var client *socks5.Client
@@ -43,7 +45,15 @@ func NewSOCKS5(p2pService P2p, conf *config.Config) (*SOCKS5, error) {
4345
logger.Infof("started socks5 proxy on socks5://%s", conf.SOCKS5.ListenAddress)
4446
}
4547

46-
server := socks5.NewServer()
48+
// Mark the exit-node's outbound sockets so they bypass our own VPN gateway
49+
// default route (if any), mirroring libp2p socket marking. sockMarker is
50+
// always set by Application.Init; ControlFunc may still be nil on platforms
51+
// without socket marking, which NewServer handles.
52+
var dialControl func(network, address string, c syscall.RawConn) error
53+
if sockMarker != nil {
54+
dialControl = sockMarker.ControlFunc()
55+
}
56+
server := socks5.NewServer(dialControl)
4757
socks := &SOCKS5{
4858
logger: logger,
4959
p2p: p2pService,

service/vpn_gateway.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ func (g *VPNGateway) ListAvailableVPNGateways() []entity.AvailableVPNGateway {
147147
// tunnel binding on apply failure.
148148
//
149149
// On android the OS-level apply (routes.SetupGatewayRoutes / sockmark) is a
150-
// no-op — routing is owned by the host's VpnService.Builder, which is only
151-
// rebuilt at daemon (re)start. Runtime calls still flip the in-memory tunnel
152-
// binding and persist config, but a restart is required for the change to
153-
// actually affect OS traffic; the UI is responsible for prompting that
154-
// restart.
150+
// no-op — routing is owned by the host's VpnService.Builder. This call flips
151+
// the in-memory tunnel binding and persists config; the host then re-establishes
152+
// the VpnService with the new routes and hot-swaps the fresh tun fd into the
153+
// running app (see cmd/gomobile-lib UpdateTunDevice and vpn.SwappableTUN), so the
154+
// change takes effect without restarting the daemon.
155155
func (g *VPNGateway) EnableClient(gatewayPeerID peer.ID) error {
156156
if err := VPNGatewayClientSupported(); err != nil {
157157
return err

socks5/server.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net"
88
"sync/atomic"
9+
"syscall"
910

1011
"github.com/haxii/socks5"
1112
"github.com/ipfs/go-log/v2"
@@ -22,15 +23,29 @@ type Server struct {
2223
rule *UpdatableRule
2324
}
2425

25-
func NewServer() *Server {
26+
// NewServer constructs the SOCKS5 exit-node server. dialControl, when non-nil,
27+
// is applied to every outbound connection the server opens to the real
28+
// destination — it is the same socket-marking callback used for libp2p
29+
// sockets (sockmark). This keeps SOCKS5 exit-node traffic going out the host's
30+
// physical NIC, independent of whether this node is itself a VPN gateway
31+
// client (whose 0.0.0.0/0-via-TUN default route would otherwise capture these
32+
// sockets). Marking unconditionally is safe: when gateway mode is off there is
33+
// 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.
36+
func NewServer(dialControl func(network, address string, c syscall.RawConn) error) *Server {
2637
rule := NewUpdatableRule(NewRuleDenyLocalhost())
38+
dialer := &net.Dialer{Control: dialControl}
2739
conf := &socks5.Config{
2840
// fake addr, we don't bind address for server
2941
BindIP: net.IPv4(127, 0, 0, 1),
3042
BindPort: 8000,
3143
Rules: rule,
3244
Logger: NewLogger(),
3345
Resolver: nil,
46+
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
47+
return dialer.DialContext(ctx, network, addr)
48+
},
3449
}
3550
server, err := socks5.New(conf)
3651
if err != nil {

socks5/server_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
func TestProxy(t *testing.T) {
2020
listenAddr := pickFreeAddr(t)
21-
socksServer := NewServer()
21+
socksServer := NewServer(nil)
2222
socksServer.SetRules(NewRulePermitAll())
2323
socksClient, err := NewClient(listenAddr, "", "")
2424
require.NoError(t, err)
@@ -49,7 +49,7 @@ func TestProxy(t *testing.T) {
4949

5050
func TestProxyWithAuth(t *testing.T) {
5151
listenAddr := pickFreeAddr(t)
52-
socksServer := NewServer()
52+
socksServer := NewServer(nil)
5353
socksServer.SetRules(NewRulePermitAll())
5454
socksClient, err := NewClient(listenAddr, "testuser", "testpass")
5555
require.NoError(t, err)

vpn/iface_android.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
//go:build linux && android
2-
// +build linux,android
32

43
package vpn
54

65
import (
76
"fmt"
87
"net"
9-
"os"
10-
"strconv"
118

129
"golang.org/x/sys/unix"
1310
"golang.zx2c4.com/wireguard/tun"
1411
)
1512

16-
// TODO: refactor and remove this hack
17-
const TunFDEnvKey = "AWL_TUN_FD"
18-
19-
func newTUN(ifname string, mtu int, localIP net.IP, ipMask net.IPMask) (tun.Device, error) {
20-
fdStr := os.Getenv(TunFDEnvKey)
21-
tunFD, err := strconv.ParseInt(fdStr, 10, 32)
22-
if err != nil || tunFD == 0 {
23-
return nil, fmt.Errorf("invalid tun FD %s: %v", fdStr, err)
24-
}
25-
26-
tunDevice, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD))
13+
// NewAndroidTUNFromFD wraps a file descriptor handed in by the host app's
14+
// VpnService (from establish().detachFd()) into a tun.Device. The returned
15+
// device takes ownership of fd and closes it on Close.
16+
//
17+
// On Android the awl process never creates the TUN itself — routing is owned by
18+
// VpnService.Builder, and changing routes (e.g. toggling VPN gateway mode)
19+
// requires a fresh establish() and therefore a new fd. The host passes that fd
20+
// down to gomobile, which wraps it here and swaps it into the running
21+
// SwappableTUN without restarting the rest of awl.
22+
func NewAndroidTUNFromFD(fd int) (tun.Device, error) {
23+
tunDevice, _, err := tun.CreateUnmonitoredTUNFromFD(fd)
2724
if err != nil {
28-
unix.Close(int(tunFD))
25+
unix.Close(fd)
2926
return nil, fmt.Errorf("CreateUnmonitoredTUNFromFD: %v", err)
3027
}
3128

3229
return tunDevice, nil
3330
}
3431

32+
// newTUN is the nil-device path of NewDevice. On Android the TUN device must be
33+
// supplied externally via NewAndroidTUNFromFD (the host owns the fd), so being
34+
// asked to create one here is a programming error.
35+
func newTUN(_ string, _ int, _ net.IP, _ net.IPMask) (tun.Device, error) {
36+
return nil, fmt.Errorf("android requires an externally-supplied tun device (use NewAndroidTUNFromFD)")
37+
}
38+
3539
func (d *Device) InterfaceName() (string, error) {
3640
interfaceName, err := d.tun.Name()
3741
if err != nil {

vpn/routes/routes_linux.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,20 @@ func buildFwmarkRule(fwmark uint32) *netlink.Rule {
224224
// SCOPE_LINK because the TUN is a point-to-point device with no gateway —
225225
// this matches what `ip route add default dev awl0` would produce, and using
226226
// the identical shape on RouteDel is required for the kernel to match it.
227+
//
228+
// Dst must be an explicit 0.0.0.0/0 *net.IPNet rather than nil: netlink's
229+
// RouteAdd rejects a route with no Dst.IP, Src and Gw ("either Dst.IP, Src.IP
230+
// or Gw must be set"). A TUN default route has no gateway (point-to-point), so
231+
// the destination is the only field we can populate to satisfy that check.
227232
func buildTunDefaultRoute(tunLinkIndex int) *netlink.Route {
228233
return &netlink.Route{
229234
LinkIndex: tunLinkIndex,
230-
Dst: nil, // 0.0.0.0/0
231-
Scope: netlink.SCOPE_LINK,
232-
Priority: tunRouteMetric,
235+
Dst: &net.IPNet{
236+
IP: net.IPv4zero,
237+
Mask: net.CIDRMask(0, 32),
238+
},
239+
Scope: netlink.SCOPE_LINK,
240+
Priority: tunRouteMetric,
233241
}
234242
}
235243

0 commit comments

Comments
 (0)