TUN inbound: Add macOS support (#5559)

This commit is contained in:
osypai
2026-01-18 11:39:39 +08:00
committed by GitHub
parent cfc78b3ac1
commit 6d6c045a5a
3 changed files with 371 additions and 1 deletions

169
proxy/tun/tun_darwin.go Normal file
View File

@@ -0,0 +1,169 @@
//go:build darwin
package tun
import (
"errors"
"fmt"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const (
utunControlName = "com.apple.net.utun_control"
utunOptIfName = 2
sysprotoControl = 2
)
type DarwinTun struct {
tunFd int
name string
options TunOptions
}
var _ Tun = (*DarwinTun)(nil)
var _ GVisorTun = (*DarwinTun)(nil)
func NewTun(options TunOptions) (Tun, error) {
tunFd, name, err := openUTun(options.Name)
if err != nil {
return nil, err
}
return &DarwinTun{
tunFd: tunFd,
name: name,
options: options,
}, nil
}
func (t *DarwinTun) Start() error {
if t.options.MTU > 0 {
if err := setMTU(t.name, int(t.options.MTU)); err != nil {
return err
}
}
return setState(t.name, true)
}
func (t *DarwinTun) Close() error {
_ = setState(t.name, false)
return unix.Close(t.tunFd)
}
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
return newDarwinEndpoint(t.tunFd, t.options.MTU), nil
}
func openUTun(name string) (int, string, error) {
fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl)
if err != nil {
return -1, "", err
}
ctlInfo := &unix.CtlInfo{}
copy(ctlInfo.Name[:], utunControlName)
if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil {
_ = unix.Close(fd)
return -1, "", err
}
sockaddr := &unix.SockaddrCtl{
ID: ctlInfo.Id,
Unit: parseUTunUnit(name),
}
if err := unix.Connect(fd, sockaddr); err != nil {
_ = unix.Close(fd)
return -1, "", err
}
if err := unix.SetNonblock(fd, true); err != nil {
_ = unix.Close(fd)
return -1, "", err
}
tunName, err := unix.GetsockoptString(fd, sysprotoControl, utunOptIfName)
if err != nil {
_ = unix.Close(fd)
return -1, "", err
}
tunName = strings.TrimRight(tunName, "\x00")
if tunName == "" {
_ = unix.Close(fd)
return -1, "", errors.New("empty utun name")
}
return fd, tunName, nil
}
func parseUTunUnit(name string) uint32 {
var unit uint32
if _, err := fmt.Sscanf(name, "utun%d", &unit); err != nil {
return 0
}
return unit + 1
}
type ifreqMTU struct {
Name [unix.IFNAMSIZ]byte
MTU int32
_ [12]byte
}
type ifreqFlags struct {
Name [unix.IFNAMSIZ]byte
Flags int16
_ [14]byte
}
func setMTU(name string, mtu int) error {
if mtu <= 0 {
return nil
}
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer func() { _ = unix.Close(fd) }()
ifr := ifreqMTU{MTU: int32(mtu)}
copy(ifr.Name[:], name)
return ioctlPtr(fd, unix.SIOCSIFMTU, unsafe.Pointer(&ifr))
}
func setState(name string, up bool) error {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer func() { _ = unix.Close(fd) }()
ifr := ifreqFlags{}
copy(ifr.Name[:], name)
if err := ioctlPtr(fd, unix.SIOCGIFFLAGS, unsafe.Pointer(&ifr)); err != nil {
return err
}
if up {
ifr.Flags |= unix.IFF_UP
} else {
ifr.Flags &^= unix.IFF_UP
}
return ioctlPtr(fd, unix.SIOCSIFFLAGS, unsafe.Pointer(&ifr))
}
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
if errno != 0 {
return errno
}
return nil
}

View File

@@ -0,0 +1,201 @@
//go:build darwin
package tun
import (
"context"
"encoding/binary"
"errors"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const utunHeaderSize = 4
var ErrUnsupportedNetworkProtocol = errors.New("unsupported ip version")
// DarwinEndpoint implements GVisor stack.LinkEndpoint
var _ stack.LinkEndpoint = (*DarwinEndpoint)(nil)
type DarwinEndpoint struct {
tunFd int
mtu uint32
dispatcherCancel context.CancelFunc
}
func newDarwinEndpoint(tunFd int, mtu uint32) *DarwinEndpoint {
return &DarwinEndpoint{
tunFd: tunFd,
mtu: mtu,
}
}
func (e *DarwinEndpoint) MTU() uint32 {
return e.mtu
}
func (e *DarwinEndpoint) SetMTU(_ uint32) {
// not Implemented, as it is not expected GVisor will be asking tun device to be modified
}
func (e *DarwinEndpoint) MaxHeaderLength() uint16 {
return 0
}
func (e *DarwinEndpoint) LinkAddress() tcpip.LinkAddress {
return ""
}
func (e *DarwinEndpoint) SetLinkAddress(_ tcpip.LinkAddress) {
// not Implemented, as it is not expected GVisor will be asking tun device to be modified
}
func (e *DarwinEndpoint) Capabilities() stack.LinkEndpointCapabilities {
return stack.CapabilityRXChecksumOffload
}
func (e *DarwinEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
if e.dispatcherCancel != nil {
e.dispatcherCancel()
e.dispatcherCancel = nil
}
if dispatcher != nil {
ctx, cancel := context.WithCancel(context.Background())
go e.dispatchLoop(ctx, dispatcher)
e.dispatcherCancel = cancel
}
}
func (e *DarwinEndpoint) IsAttached() bool {
return e.dispatcherCancel != nil
}
func (e *DarwinEndpoint) Wait() {
}
func (e *DarwinEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (e *DarwinEndpoint) AddHeader(buffer *stack.PacketBuffer) {
// tun interface doesn't have link layer header, it will be added by the OS
}
func (e *DarwinEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
return true
}
func (e *DarwinEndpoint) Close() {
if e.dispatcherCancel != nil {
e.dispatcherCancel()
e.dispatcherCancel = nil
}
}
func (e *DarwinEndpoint) SetOnCloseAction(_ func()) {
}
func (e *DarwinEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) {
var n int
for _, packetBuffer := range packetBufferList.AsSlice() {
family, err := ipFamilyFromPacket(packetBuffer)
if err != nil {
return n, &tcpip.ErrAborted{}
}
var headerBytes [utunHeaderSize]byte
binary.BigEndian.PutUint32(headerBytes[:], family)
writeSlices := append([][]byte{headerBytes[:]}, packetBuffer.AsSlices()...)
if _, err := unix.Writev(e.tunFd, writeSlices); err != nil {
if errors.Is(err, unix.EAGAIN) {
return n, &tcpip.ErrWouldBlock{}
}
return n, &tcpip.ErrAborted{}
}
n++
}
return n, nil
}
func (e *DarwinEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) {
readSize := int(e.mtu)
if readSize <= 0 {
readSize = 65535
}
readSize += utunHeaderSize
buf := make([]byte, readSize)
for ctx.Err() == nil {
n, err := unix.Read(e.tunFd, buf)
if err != nil {
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
continue
}
e.Attach(nil)
return
}
if n <= utunHeaderSize {
continue
}
networkProtocol, packet, err := parseUTunPacket(buf[:n])
if errors.Is(err, ErrUnsupportedNetworkProtocol) {
continue
}
if err != nil {
e.Attach(nil)
return
}
dispatcher.DeliverNetworkPacket(networkProtocol, packet)
packet.DecRef()
}
}
func parseUTunPacket(packet []byte) (tcpip.NetworkProtocolNumber, *stack.PacketBuffer, error) {
if len(packet) <= utunHeaderSize {
return 0, nil, errors.New("packet too short")
}
family := binary.BigEndian.Uint32(packet[:utunHeaderSize])
var networkProtocol tcpip.NetworkProtocolNumber
switch family {
case uint32(unix.AF_INET):
networkProtocol = header.IPv4ProtocolNumber
case uint32(unix.AF_INET6):
networkProtocol = header.IPv6ProtocolNumber
default:
return 0, nil, ErrUnsupportedNetworkProtocol
}
payload := packet[utunHeaderSize:]
packetBuffer := buffer.MakeWithData(payload)
return networkProtocol, stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: packetBuffer,
IsForwardedPacket: true,
}), nil
}
func ipFamilyFromPacket(packetBuffer *stack.PacketBuffer) (uint32, error) {
for _, slice := range packetBuffer.AsSlices() {
if len(slice) == 0 {
continue
}
switch header.IPVersion(slice) {
case header.IPv4Version:
return uint32(unix.AF_INET), nil
case header.IPv6Version:
return uint32(unix.AF_INET6), nil
default:
return 0, ErrUnsupportedNetworkProtocol
}
}
return 0, errors.New("empty packet")
}

View File

@@ -1,4 +1,4 @@
//go:build !linux && !windows && !android
//go:build !linux && !windows && !android && !darwin
package tun