|
|
@@ -23,7 +23,7 @@ import (
|
|
|
)
|
|
|
|
|
|
type tun struct {
|
|
|
- io.ReadWriteCloser
|
|
|
+ f *os.File
|
|
|
Device string
|
|
|
vpnNetworks []netip.Prefix
|
|
|
DefaultMTU int
|
|
|
@@ -31,9 +31,6 @@ type tun struct {
|
|
|
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
|
|
linkAddr *netroute.LinkAddr
|
|
|
l *slog.Logger
|
|
|
-
|
|
|
- // cache out buffer since we need to prepend 4 bytes for tun metadata
|
|
|
- out []byte
|
|
|
}
|
|
|
|
|
|
type ifReq struct {
|
|
|
@@ -124,11 +121,11 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
|
|
|
}
|
|
|
|
|
|
t := &tun{
|
|
|
- ReadWriteCloser: os.NewFile(uintptr(fd), ""),
|
|
|
- Device: name,
|
|
|
- vpnNetworks: vpnNetworks,
|
|
|
- DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
|
|
- l: l,
|
|
|
+ f: os.NewFile(uintptr(fd), ""),
|
|
|
+ Device: name,
|
|
|
+ vpnNetworks: vpnNetworks,
|
|
|
+ DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
|
|
+ l: l,
|
|
|
}
|
|
|
|
|
|
err = t.reload(c, true)
|
|
|
@@ -158,8 +155,8 @@ func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, e
|
|
|
}
|
|
|
|
|
|
func (t *tun) Close() error {
|
|
|
- if t.ReadWriteCloser != nil {
|
|
|
- return t.ReadWriteCloser.Close()
|
|
|
+ if t.f != nil {
|
|
|
+ return t.f.Close()
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
@@ -502,42 +499,103 @@ func delRoute(prefix netip.Prefix, gateway netroute.Addr) error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
|
|
|
+// calls go through libSystem's pinned trampoline. A raw syscall.Syscall(SYS_WRITEV/SYS_READV, ...)
|
|
|
+// on darwin/arm64 emits an SVC #0x80 trap (see $GOROOT/src/syscall/asm_darwin_arm64.s), the path
|
|
|
+// Apple keeps warning they will eventually disallow. We pull the low-level stubs instead of calling
|
|
|
+// unix.Writev/unix.Readv because those take [][]byte and rebuild the []Iovec every call, which
|
|
|
+// heap-allocates the header; linkname'ing the stubs lets us hand them our own stack-allocated
|
|
|
+// iovecs. See golang/go#78049.
|
|
|
+
|
|
|
+//go:linkname tunWritev golang.org/x/sys/unix.writev
|
|
|
+//go:noescape
|
|
|
+func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
|
|
+
|
|
|
+//go:linkname tunReadv golang.org/x/sys/unix.readv
|
|
|
+//go:noescape
|
|
|
+func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
|
|
+
|
|
|
+// Read pulls one IP packet off the utun device, scattering the 4 byte protocol header away from
|
|
|
+// the packet so the payload lands directly in to.
|
|
|
func (t *tun) Read(to []byte) (int, error) {
|
|
|
- buf := make([]byte, len(to)+4)
|
|
|
+ var head [4]byte
|
|
|
|
|
|
- n, err := t.ReadWriteCloser.Read(buf)
|
|
|
+ rc, err := t.f.SyscallConn()
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
|
|
|
- copy(to, buf[4:])
|
|
|
- return n - 4, err
|
|
|
+ var n int
|
|
|
+ var callErr error
|
|
|
+ err = rc.Read(func(fd uintptr) bool {
|
|
|
+ iovecs := []unix.Iovec{
|
|
|
+ {Base: &head[0], Len: 4},
|
|
|
+ {Base: &to[0], Len: uint64(len(to))},
|
|
|
+ }
|
|
|
+ n, callErr = tunReadv(int(fd), iovecs)
|
|
|
+ if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ if callErr != nil {
|
|
|
+ return 0, callErr
|
|
|
+ }
|
|
|
+ if n < 4 {
|
|
|
+ return 0, nil
|
|
|
+ }
|
|
|
+ return n - 4, nil
|
|
|
}
|
|
|
|
|
|
-// Write is only valid for single threaded use
|
|
|
+// Write pushes one IP packet onto the utun 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)
|
|
|
+ // Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
|
|
|
+ rc, err := t.f.SyscallConn()
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+
|
|
|
+ var n int
|
|
|
+ var callErr error
|
|
|
+ err = rc.Write(func(fd uintptr) bool {
|
|
|
+ iovecs := []unix.Iovec{
|
|
|
+ {Base: &head[0], Len: 4},
|
|
|
+ {Base: &from[0], Len: uint64(len(from))},
|
|
|
+ }
|
|
|
+ n, callErr = tunWritev(int(fd), iovecs)
|
|
|
+ // Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
|
|
|
+ // constants into error interfaces on every call.
|
|
|
+ if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ if callErr != nil {
|
|
|
+ return 0, callErr
|
|
|
+ }
|
|
|
|
|
|
- n, err := t.ReadWriteCloser.Write(buf)
|
|
|
- return n - 4, err
|
|
|
+ return n - 4, nil
|
|
|
}
|
|
|
|
|
|
func (t *tun) Networks() []netip.Prefix {
|