Darwin and openbsd in line with the other bsds for tun support

This commit is contained in:
Nate Brown
2026-05-04 11:12:30 -05:00
parent b7e9939e92
commit 3954d9af34
2 changed files with 147 additions and 58 deletions

View File

@@ -57,8 +57,14 @@ type tun struct {
l *slog.Logger
f *os.File
fd int
// cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte
rc syscall.RawConn
// readBuf is the per-tun read scratch reused across calls so we don't allocate per Read.
// OpenBSD's pinsyscall protection forbids raw syscall.Syscall(SYS_READV, ...) and stdlib doesn't keep syscall.readv
// alive for external linkname (only writev gets that treatment via linkname_libc.go)
// so the read path can't use readv and has to do the prefix-skip copy.
// https://github.com/golang/go/issues/78049
readBuf []byte
}
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
@@ -88,13 +94,22 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
l.Warn("Failed to set the tun device as nonblocking", "error", err)
}
mtu := c.GetInt("tun.mtu", DefaultMTU)
f := os.NewFile(uintptr(fd), "")
rc, err := f.SyscallConn()
if err != nil {
return nil, fmt.Errorf("failed to get syscall conn for tun: %w", err)
}
t := &tun{
f: os.NewFile(uintptr(fd), ""),
f: f,
fd: fd,
rc: rc,
Device: deviceName,
vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU),
MTU: mtu,
l: l,
readBuf: make([]byte, mtu+4),
}
err = t.reload(c, true)
@@ -124,42 +139,74 @@ func (t *tun) Close() error {
return nil
}
// tunWritev is linkname'd from the standard library so the writev call goes through libc's pinned syscall trampoline.
// OpenBSD's pinsyscall protection rejects raw syscall.Syscall(SYS_WRITEV, ...) calls because they don't originate from
// the libc-pinned addresses, so we can't use the same syscall.Syscall pattern that freebsd / netbsd use.
// Stdlib's $GOROOT/src/syscall/linkname_libc.go has a bare `//go:linkname writev` directive that both opt-ins external
// linkname and keeps the symbol alive for the linker.
// There is no equivalent for readv, which is why Read still uses a copy-based path.
// https://github.com/golang/go/issues/78049
//go:linkname tunWritev syscall.writev
//go:noescape
func tunWritev(fd int, iovecs []syscall.Iovec) (n uintptr, err error)
// Read pulls one IP packet off the tun device.
func (t *tun) Read(to []byte) (int, error) {
buf := make([]byte, len(to)+4)
if cap(t.readBuf) < len(to)+4 {
t.readBuf = make([]byte, len(to)+4)
}
buf := t.readBuf[:len(to)+4]
n, err := t.f.Read(buf)
copy(to, buf[4:])
return n - 4, err
if err != nil {
return 0, err
}
if n < 4 {
return 0, nil
}
copy(to, buf[4:n])
return n - 4, nil
}
// Write is only valid for single threaded use
// Write pushes one IP packet onto the tun device.
func (t *tun) Write(from []byte) (int, error) {
buf := t.out
if cap(buf) < len(from)+4 {
buf = make([]byte, len(from)+4)
t.out = buf
}
buf = buf[:len(from)+4]
if len(from) == 0 {
return 0, syscall.EIO
}
// Determine the IP Family for the NULL L2 Header
ipVer := from[0] >> 4
if ipVer == 4 {
buf[3] = syscall.AF_INET
} else if ipVer == 6 {
buf[3] = syscall.AF_INET6
} else {
var head [4]byte
switch ipVer {
case 4:
head[3] = syscall.AF_INET
case 6:
head[3] = syscall.AF_INET6
default:
return 0, fmt.Errorf("unable to determine IP version from packet")
}
copy(buf[4:], from)
var n uintptr
var callErr error
err := t.rc.Write(func(fd uintptr) bool {
iovecs := []syscall.Iovec{
{Base: &head[0], Len: 4},
{Base: &from[0], Len: uint64(len(from))},
}
n, callErr = tunWritev(int(fd), iovecs)
if errors.Is(callErr, syscall.EAGAIN) || errors.Is(callErr, syscall.EWOULDBLOCK) || errors.Is(callErr, syscall.EINTR) {
return false
}
return true
})
if err != nil {
return 0, err
}
if callErr != nil {
return 0, callErr
}
n, err := t.f.Write(buf)
return n - 4, err
return int(n) - 4, nil
}
func (t *tun) addIp(cidr netip.Prefix) error {