mirror of
https://github.com/SagerNet/sing-tun.git
synced 2025-09-26 12:41:19 +08:00
525 lines
14 KiB
Go
525 lines
14 KiB
Go
package tun
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"github.com/sagernet/sing-tun/internal/gtcpip/header"
|
|
"github.com/sagernet/sing-tun/internal/rawfile_darwin"
|
|
"github.com/sagernet/sing-tun/internal/stopfd_darwin"
|
|
"github.com/sagernet/sing/common"
|
|
"github.com/sagernet/sing/common/buf"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/shell"
|
|
|
|
"golang.org/x/net/route"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var _ DarwinTUN = (*NativeTun)(nil)
|
|
|
|
const PacketOffset = 4
|
|
|
|
type NativeTun struct {
|
|
tunFd int
|
|
tunFile *os.File
|
|
batchSize int
|
|
iovecs []iovecBuffer
|
|
iovecsOutput []iovecBuffer
|
|
iovecsOutputDefault []unix.Iovec
|
|
msgHdrs []rawfile.MsgHdrX
|
|
msgHdrsOutput []rawfile.MsgHdrX
|
|
buffers []*buf.Buffer
|
|
stopFd stopfd.StopFD
|
|
options Options
|
|
inet4Address [4]byte
|
|
inet6Address [16]byte
|
|
routeSet bool
|
|
sendMsgX bool
|
|
}
|
|
|
|
type iovecBuffer struct {
|
|
mtu int
|
|
buffer *buf.Buffer
|
|
iovecs []unix.Iovec
|
|
}
|
|
|
|
func newIovecBuffer(mtu int) iovecBuffer {
|
|
return iovecBuffer{
|
|
mtu: mtu,
|
|
iovecs: make([]unix.Iovec, 2),
|
|
}
|
|
}
|
|
|
|
func (b *iovecBuffer) nextIovecs() []unix.Iovec {
|
|
if b.iovecs[0].Len == 0 {
|
|
headBuffer := make([]byte, PacketOffset)
|
|
b.iovecs[0].Base = &headBuffer[0]
|
|
b.iovecs[0].SetLen(PacketOffset)
|
|
}
|
|
if b.buffer == nil {
|
|
b.buffer = buf.NewSize(b.mtu)
|
|
b.iovecs[1] = b.buffer.Iovec(b.buffer.Cap())
|
|
}
|
|
return b.iovecs
|
|
}
|
|
|
|
func (b *iovecBuffer) nextIovecsOutput(buffer *buf.Buffer) []unix.Iovec {
|
|
switch header.IPVersion(buffer.Bytes()) {
|
|
case header.IPv4Version:
|
|
b.iovecs[0] = packetHeaderVec4
|
|
case header.IPv6Version:
|
|
b.iovecs[0] = packetHeaderVec6
|
|
}
|
|
b.iovecs[1] = buffer.Iovec(buffer.Len())
|
|
return b.iovecs
|
|
}
|
|
|
|
func (t *NativeTun) Name() (string, error) {
|
|
return unix.GetsockoptString(
|
|
int(t.tunFile.Fd()),
|
|
2, /* #define SYSPROTO_CONTROL 2 */
|
|
2, /* #define UTUN_OPT_IFNAME 2 */
|
|
)
|
|
}
|
|
|
|
func New(options Options) (Tun, error) {
|
|
var tunFd int
|
|
batchSize := ((512 * 1024) / int(options.MTU)) + 1
|
|
if options.FileDescriptor == 0 {
|
|
ifIndex := -1
|
|
_, err := fmt.Sscanf(options.Name, "utun%d", &ifIndex)
|
|
if err != nil {
|
|
return nil, E.New("bad tun name: ", options.Name)
|
|
}
|
|
|
|
tunFd, err = unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = create(tunFd, ifIndex, options.Name, options)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return nil, err
|
|
}
|
|
err = configure(tunFd, options.EXP_MultiPendingPackets, batchSize)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return nil, err
|
|
}
|
|
} else {
|
|
tunFd = options.FileDescriptor
|
|
err := configure(tunFd, options.EXP_MultiPendingPackets, batchSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
nativeTun := &NativeTun{
|
|
tunFd: tunFd,
|
|
tunFile: os.NewFile(uintptr(tunFd), "utun"),
|
|
options: options,
|
|
batchSize: batchSize,
|
|
iovecs: make([]iovecBuffer, batchSize),
|
|
iovecsOutput: make([]iovecBuffer, batchSize),
|
|
msgHdrs: make([]rawfile.MsgHdrX, batchSize),
|
|
msgHdrsOutput: make([]rawfile.MsgHdrX, batchSize),
|
|
stopFd: common.Must1(stopfd.New()),
|
|
sendMsgX: options.EXP_SendMsgX,
|
|
}
|
|
for i := 0; i < batchSize; i++ {
|
|
nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU))
|
|
nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU))
|
|
}
|
|
if len(options.Inet4Address) > 0 {
|
|
nativeTun.inet4Address = options.Inet4Address[0].Addr().As4()
|
|
}
|
|
if len(options.Inet6Address) > 0 {
|
|
nativeTun.inet6Address = options.Inet6Address[0].Addr().As16()
|
|
}
|
|
return nativeTun, nil
|
|
}
|
|
|
|
func (t *NativeTun) Start() error {
|
|
t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name)
|
|
return t.setRoutes()
|
|
}
|
|
|
|
func (t *NativeTun) Close() error {
|
|
defer flushDNSCache()
|
|
return E.Errors(t.unsetRoutes(), t.tunFile.Close())
|
|
}
|
|
|
|
func (t *NativeTun) Read(p []byte) (n int, err error) {
|
|
return t.tunFile.Read(p)
|
|
}
|
|
|
|
func (t *NativeTun) Write(p []byte) (n int, err error) {
|
|
return t.tunFile.Write(p)
|
|
}
|
|
|
|
var (
|
|
packetHeader4 = []byte{0x00, 0x00, 0x00, unix.AF_INET}
|
|
packetHeader6 = []byte{0x00, 0x00, 0x00, unix.AF_INET6}
|
|
packetHeaderVec4 = unix.Iovec{Base: &packetHeader4[0]}
|
|
packetHeaderVec6 = unix.Iovec{Base: &packetHeader6[0]}
|
|
)
|
|
|
|
func init() {
|
|
packetHeaderVec4.SetLen(4)
|
|
packetHeaderVec6.SetLen(4)
|
|
}
|
|
|
|
const utunControlName = "com.apple.net.utun_control"
|
|
|
|
const (
|
|
SIOCAIFADDR_IN6 = 2155899162 // netinet6/in6_var.h
|
|
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
|
|
IN6_IFF_SECURED = 0x0400 // netinet6/in6_var.h
|
|
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
|
)
|
|
|
|
type ifAliasReq struct {
|
|
Name [unix.IFNAMSIZ]byte
|
|
Addr unix.RawSockaddrInet4
|
|
Dstaddr unix.RawSockaddrInet4
|
|
Mask unix.RawSockaddrInet4
|
|
}
|
|
|
|
type ifAliasReq6 struct {
|
|
Name [16]byte
|
|
Addr unix.RawSockaddrInet6
|
|
Dstaddr unix.RawSockaddrInet6
|
|
Mask unix.RawSockaddrInet6
|
|
Flags uint32
|
|
Lifetime addrLifetime6
|
|
}
|
|
|
|
type addrLifetime6 struct {
|
|
Expire float64
|
|
Preferred float64
|
|
Vltime uint32
|
|
Pltime uint32
|
|
}
|
|
|
|
func create(tunFd int, ifIndex int, name string, options Options) error {
|
|
ctlInfo := &unix.CtlInfo{}
|
|
copy(ctlInfo.Name[:], utunControlName)
|
|
err := unix.IoctlCtlInfo(tunFd, ctlInfo)
|
|
if err != nil {
|
|
return os.NewSyscallError("IoctlCtlInfo", err)
|
|
}
|
|
|
|
err = unix.Connect(tunFd, &unix.SockaddrCtl{
|
|
ID: ctlInfo.Id,
|
|
Unit: uint32(ifIndex) + 1,
|
|
})
|
|
if err != nil {
|
|
return os.NewSyscallError("Connect", err)
|
|
}
|
|
|
|
err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(socketFd int) error {
|
|
var ifr unix.IfreqMTU
|
|
copy(ifr.Name[:], name)
|
|
ifr.MTU = int32(options.MTU)
|
|
return unix.IoctlSetIfreqMTU(socketFd, &ifr)
|
|
})
|
|
if err != nil {
|
|
return os.NewSyscallError("IoctlSetIfreqMTU", err)
|
|
}
|
|
if len(options.Inet4Address) > 0 {
|
|
for _, address := range options.Inet4Address {
|
|
ifReq := ifAliasReq{
|
|
Addr: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: address.Addr().As4(),
|
|
},
|
|
Dstaddr: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: address.Addr().As4(),
|
|
},
|
|
Mask: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 32)).String()).As4(),
|
|
},
|
|
}
|
|
copy(ifReq.Name[:], name)
|
|
err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(socketFd int) error {
|
|
if _, _, errno := unix.Syscall(
|
|
syscall.SYS_IOCTL,
|
|
uintptr(socketFd),
|
|
uintptr(unix.SIOCAIFADDR),
|
|
uintptr(unsafe.Pointer(&ifReq)),
|
|
); errno != 0 {
|
|
return os.NewSyscallError("SIOCAIFADDR", errno)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if len(options.Inet6Address) > 0 {
|
|
for _, address := range options.Inet6Address {
|
|
ifReq6 := ifAliasReq6{
|
|
Addr: unix.RawSockaddrInet6{
|
|
Len: unix.SizeofSockaddrInet6,
|
|
Family: unix.AF_INET6,
|
|
Addr: address.Addr().As16(),
|
|
},
|
|
Mask: unix.RawSockaddrInet6{
|
|
Len: unix.SizeofSockaddrInet6,
|
|
Family: unix.AF_INET6,
|
|
Addr: netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 128)).String()).As16(),
|
|
},
|
|
Flags: IN6_IFF_NODAD | IN6_IFF_SECURED,
|
|
Lifetime: addrLifetime6{
|
|
Vltime: ND6_INFINITE_LIFETIME,
|
|
Pltime: ND6_INFINITE_LIFETIME,
|
|
},
|
|
}
|
|
if address.Bits() == 128 {
|
|
ifReq6.Dstaddr = unix.RawSockaddrInet6{
|
|
Len: unix.SizeofSockaddrInet6,
|
|
Family: unix.AF_INET6,
|
|
Addr: address.Addr().Next().As16(),
|
|
}
|
|
}
|
|
copy(ifReq6.Name[:], name)
|
|
err = useSocket(unix.AF_INET6, unix.SOCK_DGRAM, 0, func(socketFd int) error {
|
|
if _, _, errno := unix.Syscall(
|
|
syscall.SYS_IOCTL,
|
|
uintptr(socketFd),
|
|
uintptr(SIOCAIFADDR_IN6),
|
|
uintptr(unsafe.Pointer(&ifReq6)),
|
|
); errno != 0 {
|
|
return os.NewSyscallError("SIOCAIFADDR_IN6", errno)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func configure(tunFd int, multiPendingPackets bool, batchSize int) error {
|
|
err := unix.SetNonblock(tunFd, true)
|
|
if err != nil {
|
|
return os.NewSyscallError("SetNonblock", err)
|
|
}
|
|
if multiPendingPackets {
|
|
const UTUN_OPT_MAX_PENDING_PACKETS = 16
|
|
err = unix.SetsockoptInt(tunFd, 2, UTUN_OPT_MAX_PENDING_PACKETS, batchSize)
|
|
if err != nil {
|
|
return os.NewSyscallError("SetsockoptInt UTUN_OPT_MAX_PENDING_PACKETS", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) {
|
|
for i := 0; i < t.batchSize; i++ {
|
|
iovecs := t.iovecs[i].nextIovecs()
|
|
// Cannot clear only the length field. Older versions of the darwin kernel will check whether other data is empty.
|
|
// https://github.com/Darm64/XNU/blob/xnu-2782.40.9/bsd/kern/uipc_syscalls.c#L2026-L2048
|
|
t.msgHdrs[i] = rawfile.MsgHdrX{}
|
|
t.msgHdrs[i].Msg.Iov = &iovecs[0]
|
|
t.msgHdrs[i].Msg.Iovlen = 2
|
|
}
|
|
n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs)
|
|
if errno != 0 {
|
|
for k := 0; k < n; k++ {
|
|
t.iovecs[k].buffer.Release()
|
|
t.iovecs[k].buffer = nil
|
|
}
|
|
t.buffers = t.buffers[:0]
|
|
return nil, errno
|
|
}
|
|
if n < 1 {
|
|
return nil, nil
|
|
}
|
|
buffers := t.buffers
|
|
for k := 0; k < n; k++ {
|
|
buffer := t.iovecs[k].buffer
|
|
t.iovecs[k].buffer = nil
|
|
buffer.Truncate(int(t.msgHdrs[k].DataLen) - PacketOffset)
|
|
buffers = append(buffers, buffer)
|
|
}
|
|
t.buffers = buffers[:0]
|
|
return buffers, nil
|
|
}
|
|
|
|
func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error {
|
|
if !t.sendMsgX {
|
|
for i, buffer := range buffers {
|
|
t.iovecsOutput[i].nextIovecsOutput(buffer)
|
|
}
|
|
for i := range buffers {
|
|
errno := rawfile.NonBlockingWriteIovec(t.tunFd, t.iovecsOutput[i].iovecs)
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
}
|
|
} else {
|
|
for i, buffer := range buffers {
|
|
iovecs := t.iovecsOutput[i].nextIovecsOutput(buffer)
|
|
t.msgHdrsOutput[i] = rawfile.MsgHdrX{}
|
|
t.msgHdrsOutput[i].Msg.Iov = &iovecs[0]
|
|
t.msgHdrsOutput[i].Msg.Iovlen = 2
|
|
}
|
|
var n int
|
|
for n != len(buffers) {
|
|
sent, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[n:len(buffers)])
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
n += sent
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *NativeTun) TXChecksumOffload() bool {
|
|
return false
|
|
}
|
|
|
|
func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error {
|
|
err := t.unsetRoutes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.options = tunOptions
|
|
return t.setRoutes()
|
|
}
|
|
|
|
func (t *NativeTun) setRoutes() error {
|
|
if t.options.FileDescriptor == 0 {
|
|
routeRanges, err := t.options.BuildAutoRouteRanges(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(routeRanges) > 0 {
|
|
gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr()
|
|
for _, destination := range routeRanges {
|
|
var gateway netip.Addr
|
|
if destination.Addr().Is4() {
|
|
gateway = gateway4
|
|
} else {
|
|
gateway = gateway6
|
|
}
|
|
var interfaceIndex int
|
|
if t.options.InterfaceScope {
|
|
iff, err := t.options.InterfaceFinder.ByName(t.options.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
interfaceIndex = iff.Index
|
|
}
|
|
err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EEXIST) {
|
|
err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway)
|
|
if err != nil {
|
|
return E.Cause(err, "remove existing route: ", destination)
|
|
}
|
|
err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)
|
|
if err != nil {
|
|
return E.Cause(err, "re-add route: ", destination)
|
|
}
|
|
} else {
|
|
return E.Cause(err, "add route: ", destination)
|
|
}
|
|
}
|
|
}
|
|
flushDNSCache()
|
|
t.routeSet = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *NativeTun) unsetRoutes() error {
|
|
if !t.routeSet {
|
|
return nil
|
|
}
|
|
routeRanges, err := t.options.BuildAutoRouteRanges(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr()
|
|
for _, destination := range routeRanges {
|
|
var gateway netip.Addr
|
|
if destination.Addr().Is4() {
|
|
gateway = gateway4
|
|
} else {
|
|
gateway = gateway6
|
|
}
|
|
err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway)
|
|
if err != nil {
|
|
err = E.Errors(err, E.Cause(err, "delete route: ", destination))
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func useSocket(domain, typ, proto int, block func(socketFd int) error) error {
|
|
socketFd, err := unix.Socket(domain, typ, proto)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(socketFd)
|
|
return block(socketFd)
|
|
}
|
|
|
|
func execRoute(rtmType int, interfaceScope bool, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
|
|
routeMessage := route.RouteMessage{
|
|
Type: rtmType,
|
|
Version: unix.RTM_VERSION,
|
|
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
|
|
Seq: 1,
|
|
}
|
|
if rtmType == unix.RTM_ADD {
|
|
routeMessage.Flags |= unix.RTF_UP
|
|
if interfaceScope {
|
|
routeMessage.Flags |= unix.RTF_IFSCOPE
|
|
routeMessage.Index = interfaceIndex
|
|
}
|
|
}
|
|
if gateway.Is4() {
|
|
routeMessage.Addrs = []route.Addr{
|
|
syscall.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
|
|
syscall.RTAX_NETMASK: &route.Inet4Addr{IP: netip.MustParseAddr(net.IP(net.CIDRMask(destination.Bits(), 32)).String()).As4()},
|
|
syscall.RTAX_GATEWAY: &route.Inet4Addr{IP: gateway.As4()},
|
|
}
|
|
} else {
|
|
routeMessage.Addrs = []route.Addr{
|
|
syscall.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
|
|
syscall.RTAX_NETMASK: &route.Inet6Addr{IP: netip.MustParseAddr(net.IP(net.CIDRMask(destination.Bits(), 128)).String()).As16()},
|
|
syscall.RTAX_GATEWAY: &route.Inet6Addr{IP: gateway.As16()},
|
|
}
|
|
}
|
|
request, err := routeMessage.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return useSocket(unix.AF_ROUTE, unix.SOCK_RAW, 0, func(socketFd int) error {
|
|
return common.Error(unix.Write(socketFd, request))
|
|
})
|
|
}
|
|
|
|
func flushDNSCache() {
|
|
go shell.Exec("dscacheutil", "-flushcache").Run()
|
|
}
|