Skip to content

Commit e5c0fda

Browse files
authored
Darwin and openbsd in line with the other bsds for tun support (#1703)
1 parent 7bd0bc2 commit e5c0fda

2 files changed

Lines changed: 172 additions & 55 deletions

File tree

overlay/tun_darwin.go

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,14 @@ import (
2323
)
2424

2525
type tun struct {
26-
io.ReadWriteCloser
26+
f *os.File
2727
Device string
2828
vpnNetworks []netip.Prefix
2929
DefaultMTU int
3030
Routes atomic.Pointer[[]Route]
3131
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
3232
linkAddr *netroute.LinkAddr
3333
l *slog.Logger
34-
35-
// cache out buffer since we need to prepend 4 bytes for tun metadata
36-
out []byte
3734
}
3835

3936
type ifReq struct {
@@ -124,11 +121,11 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
124121
}
125122

126123
t := &tun{
127-
ReadWriteCloser: os.NewFile(uintptr(fd), ""),
128-
Device: name,
129-
vpnNetworks: vpnNetworks,
130-
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
131-
l: l,
124+
f: os.NewFile(uintptr(fd), ""),
125+
Device: name,
126+
vpnNetworks: vpnNetworks,
127+
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
128+
l: l,
132129
}
133130

134131
err = t.reload(c, true)
@@ -158,8 +155,8 @@ func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, e
158155
}
159156

160157
func (t *tun) Close() error {
161-
if t.ReadWriteCloser != nil {
162-
return t.ReadWriteCloser.Close()
158+
if t.f != nil {
159+
return t.f.Close()
163160
}
164161
return nil
165162
}
@@ -502,42 +499,103 @@ func delRoute(prefix netip.Prefix, gateway netroute.Addr) error {
502499
return nil
503500
}
504501

502+
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
503+
// calls go through libSystem's pinned trampoline. A raw syscall.Syscall(SYS_WRITEV/SYS_READV, ...)
504+
// on darwin/arm64 emits an SVC #0x80 trap (see $GOROOT/src/syscall/asm_darwin_arm64.s), the path
505+
// Apple keeps warning they will eventually disallow. We pull the low-level stubs instead of calling
506+
// unix.Writev/unix.Readv because those take [][]byte and rebuild the []Iovec every call, which
507+
// heap-allocates the header; linkname'ing the stubs lets us hand them our own stack-allocated
508+
// iovecs. See golang/go#78049.
509+
510+
//go:linkname tunWritev golang.org/x/sys/unix.writev
511+
//go:noescape
512+
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
513+
514+
//go:linkname tunReadv golang.org/x/sys/unix.readv
515+
//go:noescape
516+
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
517+
518+
// Read pulls one IP packet off the utun device, scattering the 4 byte protocol header away from
519+
// the packet so the payload lands directly in to.
505520
func (t *tun) Read(to []byte) (int, error) {
506-
buf := make([]byte, len(to)+4)
521+
var head [4]byte
507522

508-
n, err := t.ReadWriteCloser.Read(buf)
523+
rc, err := t.f.SyscallConn()
524+
if err != nil {
525+
return 0, err
526+
}
509527

510-
copy(to, buf[4:])
511-
return n - 4, err
528+
var n int
529+
var callErr error
530+
err = rc.Read(func(fd uintptr) bool {
531+
iovecs := []unix.Iovec{
532+
{Base: &head[0], Len: 4},
533+
{Base: &to[0], Len: uint64(len(to))},
534+
}
535+
n, callErr = tunReadv(int(fd), iovecs)
536+
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
537+
return false
538+
}
539+
return true
540+
})
541+
if err != nil {
542+
return 0, err
543+
}
544+
if callErr != nil {
545+
return 0, callErr
546+
}
547+
if n < 4 {
548+
return 0, nil
549+
}
550+
return n - 4, nil
512551
}
513552

514-
// Write is only valid for single threaded use
553+
// Write pushes one IP packet onto the utun device.
515554
func (t *tun) Write(from []byte) (int, error) {
516-
buf := t.out
517-
if cap(buf) < len(from)+4 {
518-
buf = make([]byte, len(from)+4)
519-
t.out = buf
520-
}
521-
buf = buf[:len(from)+4]
522-
523555
if len(from) == 0 {
524556
return 0, syscall.EIO
525557
}
526558

527-
// Determine the IP Family for the NULL L2 Header
528559
ipVer := from[0] >> 4
529-
if ipVer == 4 {
530-
buf[3] = syscall.AF_INET
531-
} else if ipVer == 6 {
532-
buf[3] = syscall.AF_INET6
533-
} else {
560+
var head [4]byte
561+
switch ipVer {
562+
case 4:
563+
head[3] = syscall.AF_INET
564+
case 6:
565+
head[3] = syscall.AF_INET6
566+
default:
534567
return 0, fmt.Errorf("unable to determine IP version from packet")
535568
}
536569

537-
copy(buf[4:], from)
570+
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
571+
rc, err := t.f.SyscallConn()
572+
if err != nil {
573+
return 0, err
574+
}
575+
576+
var n int
577+
var callErr error
578+
err = rc.Write(func(fd uintptr) bool {
579+
iovecs := []unix.Iovec{
580+
{Base: &head[0], Len: 4},
581+
{Base: &from[0], Len: uint64(len(from))},
582+
}
583+
n, callErr = tunWritev(int(fd), iovecs)
584+
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
585+
// constants into error interfaces on every call.
586+
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
587+
return false
588+
}
589+
return true
590+
})
591+
if err != nil {
592+
return 0, err
593+
}
594+
if callErr != nil {
595+
return 0, callErr
596+
}
538597

539-
n, err := t.ReadWriteCloser.Write(buf)
540-
return n - 4, err
598+
return n - 4, nil
541599
}
542600

543601
func (t *tun) Networks() []netip.Prefix {

overlay/tun_openbsd.go

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ type tun struct {
5757
l *slog.Logger
5858
f *os.File
5959
fd int
60-
// cache out buffer since we need to prepend 4 bytes for tun metadata
61-
out []byte
6260
}
6361

6462
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
@@ -124,42 +122,103 @@ func (t *tun) Close() error {
124122
return nil
125123
}
126124

125+
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
126+
// calls go through libc's pinned trampoline. OpenBSD's pinsyscall protection rejects a raw
127+
// syscall.Syscall(SYS_WRITEV/SYS_READV, ...) because it doesn't originate from a libc-pinned
128+
// address, so we can't use the syscall.Syscall pattern that freebsd / netbsd use. We pull the
129+
// low-level stubs instead of calling unix.Writev/unix.Readv because those take [][]byte and rebuild
130+
// the []Iovec every call, which heap-allocates the header; linkname'ing the stubs lets us hand them
131+
// our own stack-allocated iovecs. See golang/go#78049.
132+
133+
//go:linkname tunWritev golang.org/x/sys/unix.writev
134+
//go:noescape
135+
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
136+
137+
//go:linkname tunReadv golang.org/x/sys/unix.readv
138+
//go:noescape
139+
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
140+
141+
// Read pulls one IP packet off the tun device, scattering the 4 byte protocol header away from the
142+
// packet so the payload lands directly in to.
127143
func (t *tun) Read(to []byte) (int, error) {
128-
buf := make([]byte, len(to)+4)
144+
var head [4]byte
129145

130-
n, err := t.f.Read(buf)
146+
rc, err := t.f.SyscallConn()
147+
if err != nil {
148+
return 0, err
149+
}
131150

132-
copy(to, buf[4:])
133-
return n - 4, err
151+
var n int
152+
var callErr error
153+
err = rc.Read(func(fd uintptr) bool {
154+
iovecs := []unix.Iovec{
155+
{Base: &head[0], Len: 4},
156+
{Base: &to[0], Len: uint64(len(to))},
157+
}
158+
n, callErr = tunReadv(int(fd), iovecs)
159+
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
160+
return false
161+
}
162+
return true
163+
})
164+
if err != nil {
165+
return 0, err
166+
}
167+
if callErr != nil {
168+
return 0, callErr
169+
}
170+
if n < 4 {
171+
return 0, nil
172+
}
173+
return n - 4, nil
134174
}
135175

136-
// Write is only valid for single threaded use
176+
// Write pushes one IP packet onto the tun device.
137177
func (t *tun) Write(from []byte) (int, error) {
138-
buf := t.out
139-
if cap(buf) < len(from)+4 {
140-
buf = make([]byte, len(from)+4)
141-
t.out = buf
142-
}
143-
buf = buf[:len(from)+4]
144-
145178
if len(from) == 0 {
146179
return 0, syscall.EIO
147180
}
148181

149-
// Determine the IP Family for the NULL L2 Header
150182
ipVer := from[0] >> 4
151-
if ipVer == 4 {
152-
buf[3] = syscall.AF_INET
153-
} else if ipVer == 6 {
154-
buf[3] = syscall.AF_INET6
155-
} else {
183+
var head [4]byte
184+
switch ipVer {
185+
case 4:
186+
head[3] = syscall.AF_INET
187+
case 6:
188+
head[3] = syscall.AF_INET6
189+
default:
156190
return 0, fmt.Errorf("unable to determine IP version from packet")
157191
}
158192

159-
copy(buf[4:], from)
193+
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
194+
rc, err := t.f.SyscallConn()
195+
if err != nil {
196+
return 0, err
197+
}
198+
199+
var n int
200+
var callErr error
201+
err = rc.Write(func(fd uintptr) bool {
202+
iovecs := []unix.Iovec{
203+
{Base: &head[0], Len: 4},
204+
{Base: &from[0], Len: uint64(len(from))},
205+
}
206+
n, callErr = tunWritev(int(fd), iovecs)
207+
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
208+
// constants into error interfaces on every call.
209+
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
210+
return false
211+
}
212+
return true
213+
})
214+
if err != nil {
215+
return 0, err
216+
}
217+
if callErr != nil {
218+
return 0, callErr
219+
}
160220

161-
n, err := t.f.Write(buf)
162-
return n - 4, err
221+
return n - 4, nil
163222
}
164223

165224
func (t *tun) addIp(cidr netip.Prefix) error {

0 commit comments

Comments
 (0)