diff --git a/listener/tun/listener.go b/listener/tun/listener.go index 2912e78..f9ee82a 100644 --- a/listener/tun/listener.go +++ b/listener/tun/listener.go @@ -100,15 +100,16 @@ func (l *tunListener) listenLoop(ready context.CancelCauseFunc) { return err } - itf, err := net.InterfaceByName(name) - if err != nil { - return err + // Informational only: on Android the device belongs to the + // VpnService and the interface may not be listed in this process. + if itf, err := net.InterfaceByName(name); err != nil { + l.log.Warnf("interface %s: %v", name, err) + } else { + addrs, _ := itf.Addrs() + l.log.Infof("name: %s, net: %s, mtu: %d, addrs: %s", + itf.Name, ip, l.md.config.MTU, addrs) } - addrs, _ := itf.Addrs() - l.log.Infof("name: %s, net: %s, mtu: %d, addrs: %s", - itf.Name, ip, l.md.config.MTU, addrs) - ctx = ictx.ContextWithMetadata(ctx, mdx.NewMetadata(map[string]any{ "config": l.md.config, })) diff --git a/listener/tun/metadata.go b/listener/tun/metadata.go index c0c543d..5736be9 100644 --- a/listener/tun/metadata.go +++ b/listener/tun/metadata.go @@ -20,6 +20,13 @@ const ( type metadata struct { config *tun_util.Config guid string + // fd yields the descriptor of a device created outside gost, currently + // only the Android VpnService; nil when gost creates its own device. It is + // a function rather than a number because a descriptor is resolved per + // device: by the time the tun listener rebuilds one (a session ended, the + // VPN was rebuilt), a number handed out at configure time may be closed — + // or worse, recycled by an unrelated file. + fd func() int } func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { @@ -121,5 +128,17 @@ func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { l.md.guid = mdutil.GetString(md, "guid", "tun.guid") + // A device created outside gost (the Android VpnService) arrives either as + // a fixed descriptor or — for an embedder, which is what the platform's own + // VPN is — as a provider that resolves one when each device is built. + if md != nil { + switch v := md.Get("fd").(type) { + case int: + l.md.fd = func() int { return v } + case func() int: + l.md.fd = v + } + } + return } diff --git a/listener/tun/tun.go b/listener/tun/tun.go index 267108b..24afd42 100644 --- a/listener/tun/tun.go +++ b/listener/tun/tun.go @@ -78,6 +78,13 @@ func (l *tunListener) createTunDevice() (dev io.ReadWriteCloser, name string, er return } + return newTunDevice(ifce) +} + +// newTunDevice adapts a tun device to the byte-stream conn this package hands +// out: the device reads and writes packets in batches, the listener speaks +// io.ReadWriteCloser. +func newTunDevice(ifce tun.Device) (dev io.ReadWriteCloser, name string, err error) { batchSize := ifce.BatchSize() rbufs := make([][]byte, batchSize) diff --git a/listener/tun/tun_android.go b/listener/tun/tun_android.go new file mode 100644 index 0000000..e74a38d --- /dev/null +++ b/listener/tun/tun_android.go @@ -0,0 +1,64 @@ +//go:build android + +package tun + +import ( + "errors" + "fmt" + "io" + "net" + "syscall" + + "golang.zx2c4.com/wireguard/tun" +) + +const ( + readOffset = 0 + writeOffset = 0 +) + +// createTun adopts the tun device of an Android VpnService. An unprivileged app +// cannot open /dev/net/tun, so the device is created on the Java side +// (VpnService.Builder, which also sets the addresses, routes, MTU and DNS) and +// its fd arrives as the "fd" metadata. That is why nothing here goes through +// netlink, the interface list or resolvectl. +func (l *tunListener) createTun() (dev io.ReadWriteCloser, name string, ip net.IP, err error) { + if l.md.fd == nil { + err = errors.New("tun: no host device fd") + return + } + + // The fd belongs to the host and outlives this device, so each device reads + // from a copy of it: closing the device closes the copy, and a later + // recreate (an entrypoint restart, a reload) still has a live fd to copy. + // The provider waits for the VPN to come up, so a start that races it ends + // up with a device rather than an error. + src := l.md.fd() + if src < 0 { + err = errors.New("tun: no device fd: is the host's VPN up?") + return + } + fd, err := syscall.Dup(src) + if err != nil { + err = fmt.Errorf("tun: dup device fd %d: %v", src, err) + return + } + + ifce, name, err := tun.CreateUnmonitoredTUNFromFD(fd) + if err != nil { + syscall.Close(fd) + return + } + + dev, _, err = newTunDevice(ifce) + if err != nil { + ifce.Close() + return + } + + if len(l.md.config.Net) > 0 { + ip = l.md.config.Net[0].IP + } + + return +} diff --git a/listener/tun/tun_linux.go b/listener/tun/tun_linux.go index 6302c26..7d3cea6 100644 --- a/listener/tun/tun_linux.go +++ b/listener/tun/tun_linux.go @@ -1,3 +1,5 @@ +//go:build linux && !android + package tun import (