mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
feat(listener/tun): adopt a host-provided device fd on Android
An unprivileged Android app cannot open /dev/net/tun, so the device can only come from a VpnService: the Java side creates and configures it (addresses, routes, MTU, DNS via VpnService.Builder) and hands its fd over as the "fd" metadata, either as a fixed descriptor or — for an embedder, which is what the platform's own VPN is — as a provider resolved when each device is built, so a device rebuilt after its session ended never copies a closed or recycled descriptor. The device is created with tun.CreateUnmonitoredTUNFromFD (no netlink listener, no setMTU — the builder already set it), which makes the Linux variant, with its netlink and resolvectl calls, inapplicable: it is now linux && !android. The interface lookup in the accept loop was fatal on any error; it is informational, and on Android the device may not be listed in this process.
This commit is contained in:
@@ -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,
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user