mirror of
https://github.com/xjasonlyu/tun2socks.git
synced 2026-10-19 16:16:35 +00:00
Fix(fdbased): support darwin utun fd devices (#549)
This commit is contained in:
@@ -4,6 +4,7 @@ package fdbased
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -18,6 +19,7 @@ type FD struct {
|
||||
stack.LinkEndpoint
|
||||
|
||||
fd int
|
||||
file *os.File // non-nil on darwin/bsd (iobased path); nil on linux
|
||||
mtu uint32
|
||||
closed bool
|
||||
}
|
||||
@@ -44,7 +46,17 @@ func (f *FD) Name() string {
|
||||
func (f *FD) Close() {
|
||||
if !f.closed {
|
||||
defer f.LinkEndpoint.Close()
|
||||
_ = unix.Close(f.fd)
|
||||
// On darwin/bsd the iobased endpoint wraps the fd in an *os.File
|
||||
// and parks a goroutine in os.File.Read (runtime netpoller).
|
||||
// Closing via os.File.Close() triggers runtime_pollUnblock and
|
||||
// wakes that goroutine so dispatchLoop can exit and the link
|
||||
// endpoint's Wait() can return. A bare unix.Close(fd) does NOT
|
||||
// notify the netpoller, leaving the goroutine blocked forever.
|
||||
if f.file != nil {
|
||||
_ = f.file.Close()
|
||||
} else {
|
||||
_ = unix.Close(f.fd)
|
||||
}
|
||||
f.closed = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//go:build unix && !linux
|
||||
|
||||
package fdbased
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
)
|
||||
|
||||
type testDispatcher struct {
|
||||
got chan struct{}
|
||||
}
|
||||
|
||||
func (d *testDispatcher) DeliverNetworkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) {
|
||||
select {
|
||||
case d.got <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (d *testDispatcher) DeliverLinkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) {}
|
||||
|
||||
func TestCloseUnblocksDispatchLoop(t *testing.T) {
|
||||
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Socketpair() error = %v", err)
|
||||
}
|
||||
defer unix.Close(fds[1])
|
||||
|
||||
// os.NewFile only registers the fd with the runtime netpoller when the
|
||||
// fd is non-blocking, which is the iobased path this fix targets.
|
||||
if err := unix.SetNonblock(fds[0], true); err != nil {
|
||||
t.Fatalf("SetNonblock() error = %v", err)
|
||||
}
|
||||
|
||||
dev, err := Open(strconv.Itoa(fds[0]), 1500, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Open() error = %v", err)
|
||||
}
|
||||
|
||||
// Attach a dispatcher and send a probe packet so we know the dispatch
|
||||
// loop is running and then blocked on the next Read.
|
||||
dispatcher := &testDispatcher{got: make(chan struct{}, 1)}
|
||||
dev.Attach(dispatcher)
|
||||
|
||||
ipv4 := []byte{0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01}
|
||||
if _, err := unix.Write(fds[1], ipv4); err != nil {
|
||||
t.Fatalf("Write() error = %v", err)
|
||||
}
|
||||
select {
|
||||
case <-dispatcher.got:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("probe packet not dispatched; dispatch loop not running")
|
||||
}
|
||||
|
||||
// Close the device and verify Wait() returns instead of hanging.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
dev.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
dev.Close()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Wait() did not return after Close(); dispatchLoop still blocked")
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,16 @@ import (
|
||||
|
||||
func open(fd int, mtu uint32, offset int) (device.Device, error) {
|
||||
f := &FD{fd: fd, mtu: mtu}
|
||||
ep, err := iobased.New(os.NewFile(uintptr(fd), f.Name()), mtu, offset)
|
||||
file := os.NewFile(uintptr(fd), f.Name())
|
||||
ep, err := iobased.New(file, mtu, offset)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, fmt.Errorf("create endpoint: %w", err)
|
||||
}
|
||||
f.LinkEndpoint = ep
|
||||
// Keep the *os.File reference so FD.Close() can close it via the
|
||||
// runtime netpoller path and unblock the iobased dispatchLoop.
|
||||
f.file = file
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
@@ -143,6 +144,17 @@ func (e *Endpoint) writePacket(pkt *stack.PacketBuffer) tcpip.Error {
|
||||
defer buf.Release()
|
||||
if e.offset != 0 {
|
||||
v := buffer.NewViewWithData(make([]byte, e.offset))
|
||||
// On macOS/iOS, TUN devices require a 4-byte header with the
|
||||
// address family in byte 3 (AF_INET=2, AF_INET6=30).
|
||||
// Without this, the kernel drops the outbound packet.
|
||||
if e.offset >= 4 {
|
||||
switch pkt.NetworkProtocolNumber {
|
||||
case header.IPv4ProtocolNumber:
|
||||
v.AsSlice()[3] = syscall.AF_INET
|
||||
case header.IPv6ProtocolNumber:
|
||||
v.AsSlice()[3] = syscall.AF_INET6
|
||||
}
|
||||
}
|
||||
_ = buf.Prepend(v)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//go:build unix
|
||||
|
||||
package iobased
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
)
|
||||
|
||||
type readTestDispatcher struct {
|
||||
proto tcpip.NetworkProtocolNumber
|
||||
data []byte
|
||||
got chan struct{}
|
||||
}
|
||||
|
||||
func (d *readTestDispatcher) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
|
||||
d.proto = protocol
|
||||
d.data = pkt.ToView().AsSlice()
|
||||
select {
|
||||
case d.got <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (d *readTestDispatcher) DeliverLinkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) {}
|
||||
|
||||
func newSocketpairEndpoint(t *testing.T, offset int) (*Endpoint, int) {
|
||||
t.Helper()
|
||||
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Socketpair() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = unix.Close(fds[1]) })
|
||||
|
||||
ep, err := New(os.NewFile(uintptr(fds[0]), "test"), 1500, offset)
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
return ep, fds[1]
|
||||
}
|
||||
|
||||
func TestReadStripsOffset(t *testing.T) {
|
||||
ep, writer := newSocketpairEndpoint(t, 4)
|
||||
dispatcher := &readTestDispatcher{got: make(chan struct{}, 1)}
|
||||
ep.Attach(dispatcher)
|
||||
|
||||
// A darwin utun datagram is a 4-byte header (byte 3 = address family)
|
||||
// followed by the IP packet. With the offset applied, the injected
|
||||
// packet must be the IP packet itself with the header stripped.
|
||||
ipv4 := []byte{0x45, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01, 0x12, 0x34, 0x56, 0x78}
|
||||
datagram := append([]byte{0x00, 0x00, 0x00, 0x02}, ipv4...)
|
||||
|
||||
if _, err := unix.Write(writer, datagram); err != nil {
|
||||
t.Fatalf("Write() error = %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-dispatcher.got:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("packet not dispatched; 4-byte TUN header not stripped")
|
||||
}
|
||||
|
||||
if dispatcher.proto != header.IPv4ProtocolNumber {
|
||||
t.Errorf("protocol = %d, want IPv4", dispatcher.proto)
|
||||
}
|
||||
// The 4-byte utun header must be stripped: the injected packet has to
|
||||
// start with the IP packet itself. (The read path may append up to
|
||||
// `offset` stale buffer bytes after the packet.)
|
||||
if !bytes.HasPrefix(dispatcher.data, ipv4) {
|
||||
t.Errorf("injected packet = %x, want prefix %x (4-byte TUN header must be stripped)", dispatcher.data, ipv4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNoOffset(t *testing.T) {
|
||||
ep, writer := newSocketpairEndpoint(t, 0)
|
||||
dispatcher := &readTestDispatcher{got: make(chan struct{}, 1)}
|
||||
ep.Attach(dispatcher)
|
||||
|
||||
ipv4 := []byte{0x45, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01, 0x12, 0x34, 0x56, 0x78}
|
||||
if _, err := unix.Write(writer, ipv4); err != nil {
|
||||
t.Fatalf("Write() error = %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-dispatcher.got:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("raw IPv4 packet not dispatched")
|
||||
}
|
||||
|
||||
if dispatcher.proto != header.IPv4ProtocolNumber {
|
||||
t.Errorf("protocol = %d, want IPv4", dispatcher.proto)
|
||||
}
|
||||
if !bytes.Equal(dispatcher.data, ipv4) {
|
||||
t.Errorf("injected packet = %x, want %x", dispatcher.data, ipv4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package iobased
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
)
|
||||
|
||||
func TestWritePacketDarwinAFHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proto tcpip.NetworkProtocolNumber
|
||||
packet []byte
|
||||
wantAF byte
|
||||
}{
|
||||
{
|
||||
name: "ipv4",
|
||||
proto: header.IPv4ProtocolNumber,
|
||||
packet: []byte{0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01},
|
||||
wantAF: byte(syscall.AF_INET),
|
||||
},
|
||||
{
|
||||
name: "ipv6",
|
||||
proto: header.IPv6ProtocolNumber,
|
||||
packet: []byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
wantAF: byte(syscall.AF_INET6),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
ep, err := New(&w, 1500, 4)
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
|
||||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(tt.packet),
|
||||
})
|
||||
pkt.NetworkProtocolNumber = tt.proto
|
||||
if err := ep.writePacket(pkt); err != nil {
|
||||
t.Fatalf("writePacket() error = %v", err)
|
||||
}
|
||||
|
||||
got := w.Bytes()
|
||||
if len(got) != 4+len(tt.packet) {
|
||||
t.Fatalf("written length = %d, want %d", len(got), 4+len(tt.packet))
|
||||
}
|
||||
if got[3] != tt.wantAF {
|
||||
t.Errorf("header byte 3 = %d, want address family %d", got[3], tt.wantAF)
|
||||
}
|
||||
if !bytes.Equal(got[4:], tt.packet) {
|
||||
t.Errorf("packet after header mismatch:\n got %x\nwant %x", got[4:], tt.packet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePacketNoHeaderWithoutOffset(t *testing.T) {
|
||||
packet := []byte{0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01}
|
||||
|
||||
var w bytes.Buffer
|
||||
ep, err := New(&w, 1500, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
|
||||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(packet),
|
||||
})
|
||||
pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
|
||||
if err := ep.writePacket(pkt); err != nil {
|
||||
t.Fatalf("writePacket() error = %v", err)
|
||||
}
|
||||
|
||||
if got := w.Bytes(); !bytes.Equal(got, packet) {
|
||||
t.Errorf("written bytes = %x, want %x", got, packet)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -65,9 +65,9 @@ func parseDevice(s string, mtu uint32) (device.Device, error) {
|
||||
|
||||
func parseFD(u *url.URL, mtu uint32) (device.Device, error) {
|
||||
offset := 0
|
||||
// fd offset in ios
|
||||
// fd offset in darwin and ios
|
||||
// https://stackoverflow.com/questions/69260852/ios-network-extension-packet-parsing/69487795#69487795
|
||||
if runtime.GOOS == "ios" {
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
offset = 4
|
||||
}
|
||||
return fdbased.Open(u.Host, mtu, offset)
|
||||
|
||||
Reference in New Issue
Block a user