diff --git a/README.md b/README.md index 6a8a9c08..2d0c7ad5 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,4 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -``` \ No newline at end of file +``` diff --git a/go.mod b/go.mod index fb9cd6e2..a5452dec 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/btree v1.1.3 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gvisor v0.0.0-20260807021258-5683e078dbc4 + github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb github.com/metacubex/sing v0.5.7 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a diff --git a/go.sum b/go.sum index e97607b3..ea0785f0 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQux github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/gvisor v0.0.0-20260807021258-5683e078dbc4 h1:NqW3qka+vHQWb59Z5ZcXPWEGuBV8qhWEW/QHxN80qOM= github.com/metacubex/gvisor v0.0.0-20260807021258-5683e078dbc4/go.mod h1:mBJW3UXUusd8ZYO5M6mig0uScIey9iIubgdIiQAk2ug= +github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d h1:hc1OeKdo7YIcmTrzwlJLjNZ4EZDKRHi/Ntv7GdYs2YI= +github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d/go.mod h1:+bbwALZI0pbi2auSG5A3ptdpV2DZ2eLObziXo+P7oj0= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb h1:wk6mHYPURSUvWcUv72gNP79oiylFsscBSDPJ6ieV6Iw= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb/go.mod h1:73ZrCfhdkW4F2E2GAlta3km/S2RHhFNogCMtWZV2anQ= github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= diff --git a/stack.go b/stack.go index d8723967..af39376d 100644 --- a/stack.go +++ b/stack.go @@ -52,6 +52,8 @@ func NewStack( } case "gvisor": return NewGVisor(options) + case "mipstack": + return NewMIPStack(options) case "mixed": if options.IncludeAllNetworks { return nil, ErrIncludeAllNetworks diff --git a/stack_mipstack.go b/stack_mipstack.go new file mode 100644 index 00000000..b289532c --- /dev/null +++ b/stack_mipstack.go @@ -0,0 +1,301 @@ +package tun + +import ( + "context" + "errors" + "net" + "net/netip" + "sync" + "time" + + "github.com/metacubex/mipstack" + "github.com/metacubex/sing/common/buf" + E "github.com/metacubex/sing/common/exceptions" + "github.com/metacubex/sing/common/logger" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +// MIPStack adapts the MIPS userspace IP stack to a TUN and the sing handlers. +// The caller owns the TUN and must close it to release a blocked TUN read. +type MIPStack struct { + ctx context.Context + cancel context.CancelFunc + tun Tun + stack *mipstack.Stack + handler Handler + logger logger.Logger + mapping *DirectRouteMapping + mu sync.Mutex + started bool + closed bool + icmpQueue chan func() + writeMu sync.Mutex + batchTun LinuxTUN + frontHeadroom int + batchSize int + loopback map[netip.Addr]struct{} + interfaceAddresses map[netip.Addr]struct{} + broadcastAddresses map[netip.Addr]struct{} + recvMsgX bool +} + +func NewMIPStack(options StackOptions) (Stack, error) { + if options.Tun == nil || options.Handler == nil { + return nil, E.New("mipstack: TUN and handler are required") + } + var batchTun LinuxTUN + frontHeadroom, batchSize := 0, 1 + if _, ok := options.Tun.(DarwinTUN); ok { + frontHeadroom = 4 + } + if _, ok := options.Tun.(WinTun); ok { + frontHeadroom = 0 + } + if tun, ok := options.Tun.(LinuxTUN); ok { + if tun.BatchSize() < 1 || tun.FrontHeadroom() < 0 { + return nil, E.New("mipstack: invalid TUN batch size or headroom") + } + // Native Linux BatchRead expects a virtio header; a plain TUN must + // continue to use Read even though it implements LinuxTUN. + if tun.BatchSize() > 1 || tun.FrontHeadroom() > 0 { + batchTun, frontHeadroom, batchSize = tun, tun.FrontHeadroom(), tun.BatchSize() + } + } + addresses := append([]netip.Prefix(nil), options.TunOptions.Inet4Address...) + addresses = append(addresses, options.TunOptions.Inet6Address...) + config := mipstack.Config{ + MTU: options.TunOptions.MTU, + Promiscuous: true, + TCP: mipstack.TCPSocketDefaults{ + KeepAlive: true, + KeepAliveConfig: mipstack.KeepAliveConfig{Idle: 15 * time.Second, Interval: 15 * time.Second}, + }, + } + // Interface addresses belong to the host, not the stack. Addressless + // promiscuous mode sends replies to host-originated traffic through the TUN. + interfaceAddresses := make(map[netip.Addr]struct{}, len(addresses)) + broadcastAddresses := make(map[netip.Addr]struct{}) + have6 := len(options.TunOptions.Inet6Address) > 0 + for _, prefix := range addresses { + if !prefix.IsValid() { + return nil, E.New("mipstack: invalid interface prefix") + } + address := prefix.Addr().Unmap() + interfaceAddresses[address] = struct{}{} + if address.Is4() { + if prefix.Addr().Is4() && prefix.Bits() <= 30 { + broadcastAddresses[BroadcastAddr([]netip.Prefix{prefix})] = struct{}{} + } + } else { + have6 = true + } + } + if !have6 && config.MTU > 0 && config.MTU < 1280 { + // Default routes include both families; IPv6 requires an MTU of 1280. + config.Routes = []mipstack.Route{{Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}} + } + ipStack, err := mipstack.New(config) + if err != nil { + return nil, err + } + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithCancel(ctx) + s := &MIPStack{ctx: ctx, cancel: cancel, tun: options.Tun, stack: ipStack, + handler: options.Handler, logger: options.Logger, mapping: NewDirectRouteMapping(options.ICMPTimeout), + batchTun: batchTun, frontHeadroom: frontHeadroom, batchSize: batchSize, + loopback: make(map[netip.Addr]struct{}), + interfaceAddresses: interfaceAddresses, broadcastAddresses: broadcastAddresses, recvMsgX: options.TunOptions.EXP_RecvMsgX, icmpQueue: make(chan func(), 64)} + for _, address := range options.TunOptions.Inet4LoopbackAddress { + s.loopback[address.Unmap()] = struct{}{} + } + for _, address := range options.TunOptions.Inet6LoopbackAddress { + s.loopback[address] = struct{}{} + } + if _, err = mipstack.NewTCPForwarder(ipStack, mipstack.TCPForwarderOptions{MaxInFlight: 1024}, s.forwardTCP); err == nil { + _, err = mipstack.NewUDPForwarder(ipStack, mipstack.UDPForwarderOptions{}, s.forwardUDP) + } + if err == nil { + _, err = mipstack.NewICMPForwarder(ipStack, mipstack.ICMPForwarderOptions{}, s.forwardICMP) + } + if err == nil { + _, err = mipstack.NewIPForwarder(ipStack, mipstack.IPForwarderOptions{}, func(r *mipstack.IPForwarderRequest) { _ = r.Reject() }) + } + if err != nil { + cancel() + ipStack.Close() + return nil, err + } + go s.icmpLoop() + return s, nil +} + +func (s *MIPStack) Start() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.closed { + return net.ErrClosed + } + if err := s.ctx.Err(); err != nil { + return err + } + if s.started { + return nil + } + if err := s.stack.Start(); err != nil { + return err + } + s.started = true + go s.tunLoop() + go s.packetLoop() + go func() { <-s.ctx.Done(); s.Close() }() + return nil +} + +func (s *MIPStack) Close() error { + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return nil + } + s.closed = true + s.cancel() + err := s.stack.Close() + s.mu.Unlock() + return err +} + +func (s *MIPStack) logError(err error, operation string) { + if err != nil && !E.IsClosed(err) && s.ctx.Err() == nil && s.logger != nil { + s.logger.Error(E.Cause(err, "mipstack: ", operation)) + } +} + +func (s *MIPStack) forwardTCP(request *mipstack.TCPForwarderRequest) { + flow := request.Flow() + conn, err := request.Accept(s.ctx) + if err != nil { + return + } + // Accept must finish within the forwarder callback; the connection may then + // be handed to the application independently of the request lifetime. + go func() { + if err := s.handler.NewConnection(s.ctx, conn, M.Metadata{ + Source: M.SocksaddrFromNetIP(flow.Source), Destination: M.SocksaddrFromNetIP(flow.Destination), + }); err != nil { + conn.SetLinger(0) + conn.Close() + } + }() +} + +func (s *MIPStack) forwardUDP(request *mipstack.UDPForwarderRequest) { + flow := request.Flow() + // Requests and their payloads are borrowed only until the callback returns. + payload := buf.As(request.Payload()).ToOwned() + responder, err := request.DetachForReplies() + if err != nil { + payload.Release() + return + } + s.handler.NewPacket(s.ctx, flow.Source, payload, M.Metadata{ + Source: M.SocksaddrFromNetIP(flow.Source), Destination: M.SocksaddrFromNetIP(flow.Destination), + }, func(N.PacketConn) N.PacketWriter { return &mipUDPBackWriter{responder} }) +} + +type mipUDPBackWriter struct { + responder *mipstack.UDPForwarderResponder +} + +func (w *mipUDPBackWriter) WritePacket(packet *buf.Buffer, destination M.Socksaddr) error { + defer packet.Release() + if !destination.IsIP() { + return E.New("mipstack: invalid UDP reply address") + } + _, err := w.responder.ReplyFrom(packet.Bytes(), destination.AddrPort()) + return err +} + +func (s *MIPStack) forwardICMP(request *mipstack.ICMPForwarderRequest) { + message := request.Message() + if !message.IsEchoRequest() { + request.Drop() + return + } + if _, local := s.interfaceAddresses[message.Destination]; local { + // Interface addresses are no longer owned by MIPS, so preserve their + // local echo behavior here without consulting the routing handler. + s.logError(request.ReplyEcho(), "reply interface ICMP") + return + } + // The route may retain its back writer after this callback has returned. + responder, err := request.Detach() + if err != nil { + return + } + select { + case <-s.ctx.Done(): + case s.icmpQueue <- func() { s.processICMP(responder) }: + default: + // Drop on overload instead of blocking the stack's input path. + } +} + +// One bounded queue keeps slow application handlers off the input path. +// The worker owns cache cleanup, including routes created after cancellation. +func (s *MIPStack) icmpLoop() { + defer s.mapping.status.Clear() + for { + select { + case <-s.ctx.Done(): + return + case task := <-s.icmpQueue: + if s.ctx.Err() != nil { + return + } + task() + } + } +} + +func (s *MIPStack) processICMP(responder *mipstack.ICMPForwarderResponder) { + message := responder.Message() + if s.ctx.Err() != nil { + return + } + action, err := s.mapping.Lookup(DirectRouteSession{Source: message.Source, Destination: message.Destination}, func(timeout time.Duration) (DirectRouteDestination, error) { + destination, err := s.handler.PrepareConnection(N.NetworkICMP, + M.SocksaddrFrom(message.Source, 0), M.SocksaddrFrom(message.Destination, 0), + &mipICMPBackWriter{responder}, timeout) + if err != nil && destination != nil { + _ = destination.Close() + destination = nil + } + return destination, err + }) + if s.ctx.Err() != nil { + return + } + switch { + case errors.Is(err, ErrReset): + s.logError(responder.Reject(), "reject ICMP") + case errors.Is(err, ErrDrop): + return + case action != nil: + owned := buf.As(responder.IPPacket()).ToOwned() + s.logError(action.WritePacket(owned), "forward ICMP") + default: + s.logError(responder.ReplyEcho(), "reply ICMP") + } +} + +type mipICMPBackWriter struct { + responder *mipstack.ICMPForwarderResponder +} + +func (w *mipICMPBackWriter) WritePacket(packet []byte) error { + return w.responder.ReplyIPPacket(packet) +} diff --git a/stack_mipstack_darwin_test.go b/stack_mipstack_darwin_test.go new file mode 100644 index 00000000..d2463383 --- /dev/null +++ b/stack_mipstack_darwin_test.go @@ -0,0 +1,133 @@ +//go:build darwin + +package tun + +import ( + "bytes" + "context" + "encoding/binary" + "net/netip" + "os" + "testing" + "time" + + "github.com/metacubex/sing/common/buf" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "golang.org/x/sys/unix" +) + +// Use NativeTun's actual os.File I/O with a nonblocking packet descriptor, +// including utun's four-byte framing, without changing the host's routes. +func TestMIPStackDarwinInterfaceSourceFD(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0) + if err != nil { + t.Fatal(err) + } + for _, fd := range fds { + if err = unix.SetNonblock(fd, true); err != nil { + unix.Close(fds[0]) + unix.Close(fds[1]) + t.Fatal(err) + } + } + device := os.NewFile(uintptr(fds[0]), "mipstack-utun-test") + peer := os.NewFile(uintptr(fds[1]), "mipstack-host-test") + defer device.Close() + defer peer.Close() + if err = peer.SetDeadline(time.Now().Add(3 * time.Second)); err != nil { + t.Fatal(err) + } + source, destination := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10") + if ipv6 { + source, destination = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10") + } + h := &mipTestHandler{ + udp: func(_ context.Context, _ netip.AddrPort, p *buf.Buffer, metadata M.Metadata, init func(N.PacketConn) N.PacketWriter) { + defer p.Release() + if metadata.Source.Addr != source { + t.Error("host source address changed") + } + if err := init(nil).WritePacket(buf.As(p.Bytes()).ToOwned(), metadata.Destination); err != nil { + t.Error(err) + } + }, + icmp: func(string, M.Socksaddr, M.Socksaddr, DirectRouteContext, time.Duration) (DirectRouteDestination, error) { + t.Error("interface echo should not enter route handler") + return nil, ErrDrop + }, + } + options := mipTestOptions(&NativeTun{tunFd: fds[0], tunFile: device}, h) + options.TunOptions.FileDescriptor = fds[0] + options.TunOptions.Inet4Address, options.TunOptions.Inet6Address = nil, nil + prefix := netip.PrefixFrom(source, source.BitLen()) + if ipv6 { + options.TunOptions.Inet6Address = []netip.Prefix{prefix} + } else { + options.TunOptions.Inet4Address = []netip.Prefix{prefix} + } + stack, err := NewMIPStack(options) + if err != nil { + t.Fatal(err) + } + defer stack.Close() + if err = stack.Start(); err != nil { + t.Fatal(err) + } + readPacket := func() []byte { + p := make([]byte, 65539) + n, err := peer.Read(p) + if err != nil { + t.Fatal(err) + } + family := uint32(unix.AF_INET) + if ipv6 { + family = unix.AF_INET6 + } + if n < 4 || binary.BigEndian.Uint32(p[:4]) != family { + t.Fatal("invalid utun framing") + } + return p[4:n] + } + udp := []byte{0x30, 0x39, 0, 53, 0, 12, 0, 0, 't', 'e', 's', 't'} + if _, err = peer.Write(mipTestFrame(mipTestPacket(source, destination, 17, udp))); err != nil { + t.Fatal(err) + } + p := readPacket() + src, dst, ok := mipPacketAddresses(p) + if !ok || src != destination || dst != source { + t.Fatalf("wrong UDP reply addresses: %v -> %v", src, dst) + } + offset := 20 + if ipv6 { + offset = 40 + } + if !bytes.Equal(p[offset+8:], []byte("test")) { + t.Fatal("wrong UDP payload") + } + protocol, requestType, replyType := byte(1), byte(8), byte(0) + if ipv6 { + protocol, requestType, replyType = 58, 128, 129 + } + // The /32 or /128 interface must still answer echo without being + // classified as a directed broadcast or as MIPS local delivery. + echo := []byte{requestType, 0, 0, 0, 0, 1, 0, 2} + if _, err = peer.Write(mipTestFrame(mipTestPacket(source, source, protocol, echo))); err != nil { + t.Fatal(err) + } + p = readPacket() + if p[offset] != replyType { + t.Fatalf("interface echo type: %d", p[offset]) + } + if n := stack.(*MIPStack).stack.Stats().LoopbackPackets; n != 0 { + t.Fatalf("host replies leaked into internal loopback: %d", n) + } + }) + } +} + +// Model utun framing by capability, not by the host running the generic tests. +func (t *mipTestTun) BatchRead() ([]*buf.Buffer, error) { panic("unexpected batch read") } +func (t *mipTestTun) BatchWrite([]*buf.Buffer) error { panic("unexpected batch write") } diff --git a/stack_mipstack_io_test.go b/stack_mipstack_io_test.go new file mode 100644 index 00000000..93fc85cb --- /dev/null +++ b/stack_mipstack_io_test.go @@ -0,0 +1,419 @@ +package tun + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "net" + "net/netip" + "sync/atomic" + "testing" + "time" + + "github.com/metacubex/sing/common/buf" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +// Raw packet devices must remain unframed even when tests run on Darwin. +type mipRawDevice struct { + device *mipTestTun + failRead, failWrite atomic.Bool + writes chan struct{} +} + +func newMIPRawDevice() *mipRawDevice { + return &mipRawDevice{device: newMIPTestTun(), writes: make(chan struct{}, 16)} +} + +func (d *mipRawDevice) Read(p []byte) (int, error) { + if d.failRead.Swap(false) { + return 0, errors.New("temporary read failure") + } + select { + case packet := <-d.device.in: + return copy(p, packet), nil + case <-d.device.done: + return 0, net.ErrClosed + } +} +func (d *mipRawDevice) Write(p []byte) (int, error) { + d.writes <- struct{}{} + if d.failWrite.Swap(false) { + return 0, errors.New("temporary write failure") + } + select { + case d.device.out <- append([]byte(nil), p...): + return len(p), nil + case <-d.device.done: + return 0, net.ErrClosed + } +} +func (d *mipRawDevice) Close() error { return d.device.Close() } + +type mipWindowsDevice struct { + *mipRawDevice + ringFull atomic.Bool + released atomic.Int32 +} + +func (d *mipWindowsDevice) Read([]byte) (int, error) { + return 0, errors.New("Windows must use ReadPacket") +} +func (d *mipWindowsDevice) ReadPacket() ([]byte, func(), error) { + select { + case p := <-d.device.in: + return p, func() { + d.released.Add(1) + for i := range p { + p[i] = 0xa5 + } + }, nil + case <-d.device.done: + return nil, nil, net.ErrClosed + } +} +func (d *mipWindowsDevice) Write(p []byte) (int, error) { + if d.ringFull.Swap(false) { + d.writes <- struct{}{} + return 0, nil + } + return d.mipRawDevice.Write(p) +} + +type mipDarwinDevice struct { + *mipRawDevice + batchReads atomic.Int32 +} + +func (d *mipDarwinDevice) Read([]byte) (int, error) { + return 0, errors.New("Darwin must use BatchRead") +} +func (d *mipDarwinDevice) BatchRead() ([]*buf.Buffer, error) { + select { + case p := <-d.device.in: + d.batchReads.Add(1) + return []*buf.Buffer{buf.As(p).ToOwned()}, nil + case <-d.device.done: + return nil, net.ErrClosed + } +} +func (d *mipDarwinDevice) BatchWrite([]*buf.Buffer) error { + return errors.New("shared Darwin batch descriptors must not be used") +} +func (d *mipDarwinDevice) Write(p []byte) (int, error) { + if len(p) < 5 { + return 0, errors.New("missing utun header") + } + family := uint32(2) + if p[4]>>4 == 6 { + family = 30 + } + if binary.BigEndian.Uint32(p[:4]) != family { + return 0, errors.New("wrong utun family") + } + n, err := d.mipRawDevice.Write(p[4:]) + if err != nil { + return n, err + } + return n + 4, nil +} + +func mipEchoHandler(t *testing.T) *mipTestHandler { + return &mipTestHandler{udp: func(_ context.Context, _ netip.AddrPort, p *buf.Buffer, m M.Metadata, init func(N.PacketConn) N.PacketWriter) { + defer p.Release() + if err := init(nil).WritePacket(buf.As(p.Bytes()).ToOwned(), m.Destination); err != nil { + t.Error(err) + } + }} +} + +func TestMIPStackPlatformRecovery(t *testing.T) { + for _, kind := range []string{"raw", "windows", "darwin"} { + t.Run(kind, func(t *testing.T) { + raw := newMIPRawDevice() + var device Tun = raw + var win *mipWindowsDevice + var darwin *mipDarwinDevice + if kind == "windows" { + win = &mipWindowsDevice{mipRawDevice: raw} + win.ringFull.Store(true) + device = win + } else { + raw.failWrite.Store(true) + } + if kind == "darwin" { + darwin = &mipDarwinDevice{mipRawDevice: raw} + device = darwin + } + if kind == "raw" { + raw.failRead.Store(true) + } + o := mipTestOptions(device, mipEchoHandler(t)) + o.TunOptions.EXP_RecvMsgX = true + s, err := NewStack("mipstack", o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { device.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + src, dst := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10") + packet := mipTestPacket(src, dst, 17, []byte{0x30, 0x39, 0, 53, 0, 12, 0, 0, 't', 'e', 's', 't'}) + raw.device.in <- append([]byte(nil), packet...) + mipReceive(t, raw.writes) + raw.device.in <- append([]byte(nil), packet...) + reply := mipReceive(t, raw.device.out) + if !bytes.Equal(reply[28:], []byte("test")) { + t.Fatal("invalid recovered reply") + } + if win != nil && win.released.Load() != 2 { + t.Fatal("Windows receive buffers not released") + } + if darwin != nil && darwin.batchReads.Load() != 2 { + t.Fatal("Darwin batch path not used") + } + device.Close() + select { + case <-s.(*MIPStack).ctx.Done(): + case <-time.After(time.Second): + t.Fatal("device close did not stop stack") + } + }) + } +} + +func TestMIPStackExternalAddressConfiguration(t *testing.T) { + for _, configuration := range []string{"none", "ipv4", "ipv6"} { + for _, ipv6 := range []bool{false, true} { + t.Run(configuration+map[bool]string{false: "/IPv4", true: "/IPv6"}[ipv6], func(t *testing.T) { + d := newMIPRawDevice() + o := mipTestOptions(d, mipEchoHandler(t)) + if configuration == "none" { + o.TunOptions.MTU = 0 + } + if configuration != "ipv4" { + o.TunOptions.Inet4Address = nil + } + if configuration != "ipv6" { + o.TunOptions.Inet6Address = nil + } + s, err := NewStack("mipstack", o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { d.Close(); s.Close() }) + if addresses := s.(*MIPStack).stack.LocalAddresses(); len(addresses) != 0 { + t.Fatalf("promiscuous stack owns local addresses: %v", addresses) + } + if err = s.Start(); err != nil { + t.Fatal(err) + } + src, dst := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10") + offset := 20 + if ipv6 { + src, dst = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10") + offset = 40 + } + d.device.in <- mipTestPacket(src, dst, 17, []byte{0x30, 0x39, 0, 53, 0, 12, 0, 0, 't', 'e', 's', 't'}) + p := mipReceive(t, d.device.out) + if string(p[offset+8:]) != "test" { + t.Fatal("family not available") + } + if s.(*MIPStack).stack.Stats().LoopbackPackets != 0 { + t.Fatal("host traffic entered stack loopback") + } + }) + } + } +} + +func TestMIPStackLoopbackChecksumValidation(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + src, dst := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("10.0.0.1") + if ipv6 { + src, dst = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("fd00::9") + } + s := &MIPStack{loopback: map[netip.Addr]struct{}{dst: {}}} + p := mipTestTCPPacket(src, dst, []byte("payload")) + p[len(p)-1] ^= 1 + before := append([]byte(nil), p...) + if s.reflectLoopback(p) || !bytes.Equal(p, before) { + t.Fatal("corrupt TCP packet reflected or modified") + } + } +} + +func TestMIPStackMTUAndUnknownProtocol(t *testing.T) { + d := newMIPRawDevice() + o := mipTestOptions(d, &mipTestHandler{}) + o.TunOptions.Inet4Address = nil + o.TunOptions.Inet6Address = nil + o.TunOptions.MTU = 576 + stack, err := NewMIPStack(o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { d.Close(); stack.Close() }) + if err = stack.Start(); err != nil { + t.Fatal(err) + } + d.device.in <- mipTestPacket(netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), 253, make([]byte, 8)) + p := mipReceive(t, d.device.out) + if p[20] != 3 || p[21] != 2 { + t.Fatal("unknown protocol did not return protocol unreachable") + } + o.TunOptions.Inet6Address = []netip.Prefix{netip.MustParsePrefix("fd00::1/64")} + if s, err := NewMIPStack(o); err == nil { + s.Close() + t.Fatal("IPv6 below minimum MTU accepted") + } +} + +func TestMIPStackICMPFailedRouteCleanup(t *testing.T) { + route := &mipTestRoute{packets: make(chan *buf.Buffer, 1)} + h := &mipTestHandler{icmp: func(string, M.Socksaddr, M.Socksaddr, DirectRouteContext, time.Duration) (DirectRouteDestination, error) { + return route, ErrDrop + }} + d := newMIPRawDevice() + stack, err := NewMIPStack(mipTestOptions(d, h)) + if err != nil { + t.Fatal(err) + } + defer d.Close() + defer stack.Close() + if err = stack.Start(); err != nil { + t.Fatal(err) + } + packet := mipTestPacket(netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), 1, []byte{8, 0, 0, 0, 0, 1, 0, 2}) + if _, err = stack.(*MIPStack).stack.Write([][]byte{packet}, 0); err != nil { + t.Fatal(err) + } + mipWaitRouteClosed(t, route) +} + +func mipWaitRouteClosed(t *testing.T, route *mipTestRoute) { + t.Helper() + deadline := time.NewTimer(time.Second) + defer deadline.Stop() + tick := time.NewTicker(time.Millisecond) + defer tick.Stop() + for !route.IsClosed() { + select { + case <-deadline.C: + t.Fatal("ICMP route was not released") + case <-tick.C: + } + } +} + +func TestMIPStackICMPBlockedPrepare(t *testing.T) { + entered, release := make(chan struct{}), make(chan struct{}) + defer close(release) + route := &mipTestRoute{packets: make(chan *buf.Buffer, 1)} + d := newMIPRawDevice() + stack, err := NewMIPStack(mipTestOptions(d, &mipTestHandler{icmp: func(string, M.Socksaddr, M.Socksaddr, DirectRouteContext, time.Duration) (DirectRouteDestination, error) { + close(entered) + <-release + return route, nil + }})) + if err != nil { + t.Fatal(err) + } + defer d.Close() + defer stack.Close() + if err = stack.Start(); err != nil { + t.Fatal(err) + } + s := stack.(*MIPStack) + packet := mipTestPacket(netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), 1, []byte{8, 0, 0, 0, 0, 1, 0, 2}) + returned := make(chan error, 1) + go func() { _, err := s.stack.Write([][]byte{packet}, 0); returned <- err }() + select { + case <-entered: + case <-time.After(time.Second): + t.Fatal("PrepareConnection not called") + } + select { + case err := <-returned: + if err != nil { + t.Fatal(err) + } + case <-time.After(time.Second): + t.Fatal("ICMP callback blocked on PrepareConnection") + } + // Saturate the queue while the worker is blocked; input must still return. + for i := 0; i < cap(s.icmpQueue)+1; i++ { + go func() { _, err := s.stack.Write([][]byte{packet}, 0); returned <- err }() + select { + case err := <-returned: + if err != nil { + t.Fatal(err) + } + case <-time.After(time.Second): + t.Fatal("full ICMP queue blocked input") + } + } + closed := make(chan struct{}) + go func() { s.Close(); close(closed) }() + select { + case <-closed: + case <-time.After(time.Second): + t.Fatal("Close blocked on PrepareConnection") + } + // Release via a separate channel value so the deferred close remains safe. + release <- struct{}{} + mipWaitRouteClosed(t, route) + select { + case p := <-route.packets: + p.Release() + t.Fatal("packet forwarded after shutdown") + default: + } +} + +func TestMIPStackICMPAsyncPacketOwnership(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + entered, release := make(chan struct{}), make(chan struct{}) + defer close(release) + d := newMIPRawDevice() + s, err := NewMIPStack(mipTestOptions(d, &mipTestHandler{icmp: func(string, M.Socksaddr, M.Socksaddr, DirectRouteContext, time.Duration) (DirectRouteDestination, error) { + close(entered) + <-release + return nil, nil + }})) + if err != nil { + t.Fatal(err) + } + defer d.Close() + defer s.Close() + if err = s.Start(); err != nil { + t.Fatal(err) + } + src, dst, protocol, kind, offset := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), byte(1), byte(8), 20 + if ipv6 { + src, dst, protocol, kind, offset = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10"), 58, 128, 40 + } + packet := mipTestPacket(src, dst, protocol, []byte{kind, 0, 0, 0, 0, 1, 0, 2, 't', 'e', 's', 't'}) + if _, err = s.(*MIPStack).stack.Write([][]byte{packet}, 0); err != nil { + t.Fatal(err) + } + select { + case <-entered: + case <-time.After(time.Second): + t.Fatal("PrepareConnection not called") + } + for i := range packet { + packet[i] = 0 + } + release <- struct{}{} + reply := mipReceive(t, d.device.out) + if string(reply[offset+8:]) != "test" { + t.Fatal("async reply used borrowed packet storage") + } + }) + } +} diff --git a/stack_mipstack_linux_test.go b/stack_mipstack_linux_test.go new file mode 100644 index 00000000..76db8677 --- /dev/null +++ b/stack_mipstack_linux_test.go @@ -0,0 +1,91 @@ +//go:build linux + +package tun + +import ( + "bytes" + "context" + "net/netip" + "os" + "testing" + "time" + + "github.com/metacubex/sing/common/buf" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "golang.org/x/sys/unix" +) + +// Exercise NativeTun's actual virtio decoder, GSOSplit and GRO writer without +// creating a privileged kernel TUN. Datagram sockets preserve packet boundaries. +func TestMIPStackNativeLinuxOffload(t *testing.T) { + fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0) + if err != nil { + t.Fatal(err) + } + device := os.NewFile(uintptr(fds[0]), "mipstack-device") + peer := os.NewFile(uintptr(fds[1]), "mipstack-peer") + defer device.Close() + defer peer.Close() + if err = peer.SetDeadline(time.Now().Add(3 * time.Second)); err != nil { + t.Fatal(err) + } + tun := &NativeTun{tunFile: device, vnetHdr: true, txChecksumOffload: true, + writeBuffer: make([]byte, gsoMaxSize+virtioNetHdrLen), tcpGROTable: newTCPGROTable(), udpGROTable: newUDPGROTable()} + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("198.51.100.10") + h := &mipTestHandler{udp: func(_ context.Context, _ netip.AddrPort, p *buf.Buffer, m M.Metadata, init func(N.PacketConn) N.PacketWriter) { + defer p.Release() + if err := init(nil).WritePacket(buf.As(p.Bytes()).ToOwned(), m.Destination); err != nil { + t.Error(err) + } + }} + options := mipTestOptions(tun, h) + options.TunOptions.GSO = true + options.TunOptions._TXChecksumOffload = true + stack, err := NewMIPStack(options) + if err != nil { + t.Fatal(err) + } + defer stack.Close() + if err = stack.Start(); err != nil { + t.Fatal(err) + } + udp := append([]byte{0x30, 0x39, 0, 53, 0, 18, 0, 0}, []byte("abcdefghij")...) + packet := mipTestPacket(src, dst, 17, udp) + frame := make([]byte, virtioNetHdrLen+len(packet)) + hdr := virtioNetHdr{flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, gsoType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, hdrLen: 28, gsoSize: 4, csumStart: 20, csumOffset: 6} + if err = hdr.encode(frame); err != nil { + t.Fatal(err) + } + copy(frame[virtioNetHdrLen:], packet) + if _, err = peer.Write(frame); err != nil { + t.Fatal(err) + } + frames := make([]byte, gsoMaxSize+virtioNetHdrLen) + packets := make([][]byte, idealBatchSize) + sizes := make([]int, len(packets)) + for i := range packets { + packets[i] = make([]byte, gsoMaxSize) + } + var got []byte + for len(got) < 10 { + n, err := peer.Read(frames) + if err != nil { + t.Fatal(err) + } + count, err := handleVirtioRead(frames[:n], packets, sizes, 0) + if err != nil { + t.Fatal(err) + } + for i := 0; i < count; i++ { + p := packets[i][:sizes[i]] + if mipChecksum(append(mipTestPseudo(dst, src, 17, len(p)-20), p[20:]...)) != 0 { + t.Fatal("invalid native offload UDP checksum") + } + got = append(got, p[28:]...) + } + } + if !bytes.Equal(got, []byte("abcdefghij")) { + t.Fatalf("native offload round trip: %q", got) + } +} diff --git a/stack_mipstack_packet.go b/stack_mipstack_packet.go new file mode 100644 index 00000000..c3f847cc --- /dev/null +++ b/stack_mipstack_packet.go @@ -0,0 +1,273 @@ +package tun + +import ( + "encoding/binary" + "errors" + "io" + "net/netip" + + "github.com/metacubex/mipstack" + E "github.com/metacubex/sing/common/exceptions" +) + +func (s *MIPStack) tunLoop() { + defer s.Close() + readCapacity := gsoMaxSize + if _, darwin := s.tun.(DarwinTUN); darwin && s.recvMsgX { + // Darwin batches can contain hundreds of packets. Reserve one MTU + // per packet instead of a full GSO buffer (which utun does not need). + readCapacity, _ = s.stack.MTU() + } + buffers := make([][]byte, s.batchSize) + sizes := make([]int, len(buffers)) + for i := range buffers { + // GSOSplit also accepts unsegmented packets larger than the MTU. + buffers[i] = make([]byte, readCapacity+s.frontHeadroom) + } + inbound := make([][]byte, 0, len(buffers)) + reflected := make([][]byte, 0, len(buffers)) + for s.ctx.Err() == nil { + var n int + var err error + if win, ok := s.tun.(WinTun); ok { + packet, release, readErr := win.ReadPacket() + if len(packet) > 0 { + n = 1 + sizes[0] = copy(buffers[0][s.frontHeadroom:], packet) + } + if release != nil { + release() + } + err = readErr + } else if darwin, ok := s.tun.(DarwinTUN); ok && s.recvMsgX { + packets, readErr := darwin.BatchRead() + for len(buffers) < len(packets) { + buffers = append(buffers, make([]byte, readCapacity+s.frontHeadroom)) + sizes = append(sizes, 0) + } + for i, packet := range packets { + if len(buffers[i]) < packet.Len()+s.frontHeadroom { + buffers[i] = make([]byte, packet.Len()+s.frontHeadroom) + } + sizes[i] = copy(buffers[i][s.frontHeadroom:], packet.Bytes()) + packet.Release() + } + n, err = len(packets), readErr + } else if s.batchTun != nil { + // The TUN consumes virtio metadata and returns complete IP packets, + // including checksums completed by GSOSplit when NEEDS_CSUM is set. + n, err = s.batchTun.BatchRead(buffers, s.frontHeadroom, sizes) + } else { + var size int + size, err = s.tun.Read(buffers[0]) + if size > s.frontHeadroom { + n, sizes[0] = 1, size-s.frontHeadroom + } + } + if s.ctx.Err() != nil { + return + } + inbound, reflected = inbound[:0], reflected[:0] + for i := 0; i < n; i++ { + if sizes[i] <= 0 { + continue + } + packet := buffers[i][s.frontHeadroom : s.frontHeadroom+sizes[i]] + source, destination, ok := mipPacketAddresses(packet) + if !ok || source.IsLoopback() { + continue + } + _, broadcast := s.broadcastAddresses[destination] + // Match the existing gVisor link filter and keep non-unicast + // packets outside MIPS's private loopback address space. + if broadcast || !destination.IsGlobalUnicast() || s.reflectLoopback(packet) { + reflected = append(reflected, buffers[i][:s.frontHeadroom+sizes[i]]) + } else { + inbound = append(inbound, packet) + } + } + // Write reflected packets before the next read reuses their storage. + if len(reflected) > 0 { + if writeErr := s.writePackets(reflected); writeErr != nil { + if s.ioFailed(writeErr, "reflect TCP loopback") { + return + } + } + } + if len(inbound) > 0 { + if _, writeErr := s.stack.Write(inbound, 0); writeErr != nil { + if s.ioFailed(writeErr, "input packet") { + return + } + } + } + if errors.Is(err, ErrTooManySegments) { + // The successfully split prefix has already been consumed. One + // oversized batch must not shut down the stack for later traffic. + s.logError(err, "split TUN packet") + continue + } + if err != nil { + if s.ioFailed(err, "read TUN") { + return + } + if _, win := s.tun.(WinTun); win { + return + } + } + } +} + +func mipPacketAddresses(packet []byte) (netip.Addr, netip.Addr, bool) { + if len(packet) > 0 { + switch packet[0] >> 4 { + case 4: + if len(packet) >= 20 { + source, _ := netip.AddrFromSlice(packet[12:16]) + destination, _ := netip.AddrFromSlice(packet[16:20]) + return source, destination, true + } + case 6: + if len(packet) >= 40 { + source, _ := netip.AddrFromSlice(packet[8:24]) + destination, _ := netip.AddrFromSlice(packet[24:40]) + return source, destination, true + } + } + } + return netip.Addr{}, netip.Addr{}, false +} + +func (s *MIPStack) packetLoop() { + defer s.Close() + mtu, _ := s.stack.MTU() + capacity := mtu + if s.batchTun != nil { + // BatchWrite may coalesce packets in place. Leave room for GRO rather + // than limiting each backing buffer to one MTU-sized segment. + capacity = gsoMaxSize + } + batchSize := s.stack.BatchSize() + if s.batchTun != nil && s.batchSize < batchSize { + batchSize = s.batchSize + } + packets := make([][]byte, batchSize) + sizes := make([]int, len(packets)) + for i := range packets { + packets[i] = make([]byte, capacity+s.frontHeadroom) + } + for { + for i := range packets { + packets[i] = packets[i][:cap(packets[i])] + } + n, err := s.stack.Read(packets, sizes, s.frontHeadroom) + for i := 0; i < n; i++ { + packets[i] = packets[i][:s.frontHeadroom+sizes[i]] + } + if n > 0 { + if writeErr := s.writePackets(packets[:n]); writeErr != nil { + if s.ioFailed(writeErr, "write TUN") { + return + } + } + } + if err != nil { + s.logError(err, "output packet") + return + } + } +} + +// Serialize stack output and loopback reflection, including the mutable GRO +// state of batch devices. Every input packet has a complete checksum even when +// TXChecksumOffload is enabled; BatchWrite owns any conversion to partial sums. +func (s *MIPStack) writePackets(packets [][]byte) error { + s.writeMu.Lock() + defer s.writeMu.Unlock() + if err := s.ctx.Err(); err != nil { + return err + } + if s.batchTun != nil { + // Native BatchWrite reports bytes (possibly after coalescing), not a + // packet count. Do not compare its return value with len(packets). + _, err := s.batchTun.BatchWrite(packets, s.frontHeadroom) + return err + } + var result error + for _, packet := range packets { + if s.frontHeadroom == 4 { + family := uint32(2) // Darwin AF_INET / AF_INET6, network byte order. + if packet[s.frontHeadroom]>>4 == 6 { + family = 30 + } + binary.BigEndian.PutUint32(packet, family) + } + n, err := s.tun.Write(packet) + if err != nil { + if E.IsClosed(err) { + return err + } + result = errors.Join(result, err) + continue + } + if n == 0 { + if _, win := s.tun.(WinTun); win { + continue + } + } + if n != len(packet) { + result = errors.Join(result, io.ErrShortWrite) + } + } + return result +} + +// reflectLoopback preserves the existing stack behavior: swap IP addresses +// for TCP sent to a configured loopback address, keeping ports and payload. +// Address swapping preserves the one's-complement sum of both the IPv4 +// header and TCP pseudo-header, so it also works on individual IP fragments +// without reassembling them or rewriting a partial transport header. +func (s *MIPStack) reflectLoopback(packet []byte) bool { + if len(s.loopback) == 0 || len(packet) == 0 { + return false + } + source, destination, ok := mipPacketAddresses(packet) + if !ok { + return false + } + if _, match := s.loopback[destination]; !match || !source.IsGlobalUnicast() || !destination.IsGlobalUnicast() { + return false + } + parsed, err := mipstack.ParseIPPacket(packet) + if err != nil || !mipValidLoopback(parsed) { + return false + } + sourceOffset, addressSize := 12, 4 + if destination.Is6() { + sourceOffset, addressSize = 8, 16 + } + copy(packet[sourceOffset:sourceOffset+addressSize], destination.AsSlice()) + copy(packet[sourceOffset+addressSize:sourceOffset+2*addressSize], source.AsSlice()) + return true +} + +// The complete transport checksum is unavailable on non-atomic fragments. +func mipValidLoopback(parsed mipstack.IPPacket) bool { + if fragment, ok := parsed.Fragment(); ok && !fragment.IsAtomic() { + if fragment.Offset != 0 { + return fragment.Protocol == mipstack.ProtocolTCP + } + parsed.Protocol, parsed.Payload = fragment.Protocol, fragment.Payload + parsed.MoreFragments, parsed.FragmentOffset = false, 0 + protocol, _, err := parsed.UpperLayer() + return err == nil && protocol == mipstack.ProtocolTCP + } + _, err := parsed.TCPSegment() + return err == nil +} + +// Packet loss or a transient device failure is not a stack shutdown. +func (s *MIPStack) ioFailed(err error, operation string) bool { + s.logError(err, operation) + return s.ctx.Err() != nil || E.IsClosed(err) || errors.Is(err, io.EOF) +} diff --git a/stack_mipstack_packet_test.go b/stack_mipstack_packet_test.go new file mode 100644 index 00000000..6ca8d6aa --- /dev/null +++ b/stack_mipstack_packet_test.go @@ -0,0 +1,390 @@ +package tun + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "io" + "net" + "net/netip" + "sync/atomic" + "testing" + + "github.com/metacubex/sing/common/buf" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +type mipOffloadInput struct { + packet []byte + options GSOOptions +} + +// Model the LinuxTUN boundary on every host: GSOSplit is the same platform- +// independent implementation called by NativeTun after decoding virtio headers. +type mipBatchTestTun struct { + *mipTestTun + input chan mipOffloadInput + headroom int + batch int + reads atomic.Int32 + writes atomic.Int32 +} + +func newMIPBatchTestTun(headroom int) *mipBatchTestTun { + return &mipBatchTestTun{mipTestTun: newMIPTestTun(), input: make(chan mipOffloadInput, 8), headroom: headroom, batch: 8} +} + +func (tun *mipBatchTestTun) FrontHeadroom() int { return tun.headroom } +func (tun *mipBatchTestTun) BatchSize() int { return tun.batch } +func (tun *mipBatchTestTun) TXChecksumOffload() bool { return true } +func (tun *mipBatchTestTun) Read([]byte) (int, error) { + return 0, errors.New("offload TUN used plain Read") +} +func (tun *mipBatchTestTun) Write([]byte) (int, error) { + return 0, errors.New("offload TUN used plain Write") +} + +func (tun *mipBatchTestTun) BatchRead(packets [][]byte, offset int, sizes []int) (int, error) { + if offset != tun.headroom || len(packets) != tun.batch { + return 0, errors.New("incorrect batch read layout") + } + select { + case input := <-tun.input: + tun.reads.Add(1) + return GSOSplit(input.packet, input.options, packets, sizes, offset) + case <-tun.done: + return 0, net.ErrClosed + } +} + +func (tun *mipBatchTestTun) BatchWrite(packets [][]byte, offset int) (int, error) { + if offset != tun.headroom || len(packets) > tun.batch { + return 0, errors.New("incorrect batch write layout") + } + tun.writes.Add(1) + total := 0 + for i, p := range packets { + if len(p) <= offset { + return total, io.ErrShortBuffer + } + select { + case tun.out <- append([]byte(nil), p[offset:]...): + case <-tun.done: + return total, net.ErrClosed + } + total += len(p) + // Native GRO mutates both bytes and slice lengths. The next stack + // read must restore capacity instead of using these shortened slices. + for j := range p { + p[j] = 0xa5 + } + packets[i] = p[:offset] + } + return total, nil // Linux returns bytes, not packets. +} + +func mipTestPseudo(src, dst netip.Addr, protocol byte, length int) []byte { + p := append(append([]byte(nil), src.AsSlice()...), dst.AsSlice()...) + if src.Is4() { + return append(p, 0, protocol, byte(length>>8), byte(length)) + } + return append(p, 0, 0, byte(length>>8), byte(length), 0, 0, 0, protocol) +} + +func mipTestTCPPacket(src, dst netip.Addr, payload []byte) []byte { + tcp := make([]byte, 20+len(payload)) + binary.BigEndian.PutUint16(tcp, 12345) + binary.BigEndian.PutUint16(tcp[2:], 443) + binary.BigEndian.PutUint32(tcp[4:], 100) + tcp[12], tcp[13] = 0x50, 0x18 + binary.BigEndian.PutUint16(tcp[14:], 65535) + copy(tcp[20:], payload) + binary.BigEndian.PutUint16(tcp[16:], mipChecksum(append(mipTestPseudo(src, dst, 6, len(tcp)), tcp...))) + // mipTestPacket's transport checksum helper handles UDP/ICMP; replace + // its transport bytes with the independently checksummed TCP segment. + p := mipTestPacket(src, dst, 6, tcp) + copy(p[len(p)-len(tcp):], tcp) + return p +} + +func mipCheckTCP(t *testing.T, p []byte) { + t.Helper() + offset := 20 + src, _ := netip.AddrFromSlice(p[12:16]) + dst, _ := netip.AddrFromSlice(p[16:20]) + if p[0]>>4 == 6 { + offset = 40 + src, _ = netip.AddrFromSlice(p[8:24]) + dst, _ = netip.AddrFromSlice(p[24:40]) + } else if mipChecksum(p[:20]) != 0 { + t.Fatal("invalid IPv4 checksum") + } + if mipChecksum(append(mipTestPseudo(src, dst, 6, len(p)-offset), p[offset:]...)) != 0 { + t.Fatal("invalid TCP checksum") + } +} + +func TestMIPStackBatchUDP(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + for _, headroom := range []int{0, 10} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6]+map[int]string{0: "/batch", 10: "/virtio"}[headroom], func(t *testing.T) { + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("198.51.100.10") + ipLen := 20 + if ipv6 { + src, dst = netip.MustParseAddr("fd00::2"), netip.MustParseAddr("2001:db8::10") + ipLen = 40 + } + tun := newMIPBatchTestTun(headroom) + payloads := make(chan string, 8) + h := &mipTestHandler{udp: func(_ context.Context, _ netip.AddrPort, p *buf.Buffer, m M.Metadata, init func(N.PacketConn) N.PacketWriter) { + defer p.Release() + payloads <- string(p.Bytes()) + if err := init(nil).WritePacket(buf.As(p.Bytes()).ToOwned(), m.Destination); err != nil { + t.Error(err) + } + }} + o := mipTestOptions(tun, h) + o.TunOptions.GSO = true + o.TunOptions._TXChecksumOffload = true + s, err := NewMIPStack(o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { tun.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + udp := append([]byte{0x30, 0x39, 0, 53, 0, 18, 0, 0}, []byte("abcdefghij")...) + tun.input <- mipOffloadInput{packet: mipTestPacket(src, dst, 17, udp), options: GSOOptions{GSOType: GSOUDPL4, HdrLen: uint16(ipLen + 8), CsumStart: uint16(ipLen), CsumOffset: 6, GSOSize: 4, NeedsCsum: true}} + for _, want := range []string{"abcd", "efgh", "ij"} { + if got := mipReceive(t, payloads); got != want { + t.Fatalf("split payload %q, want %q", got, want) + } + p := mipReceive(t, tun.out) + if string(p[ipLen+8:]) != want { + t.Fatalf("bad batch reply: %x", p) + } + if mipChecksum(append(mipTestPseudo(dst, src, 17, len(p)-ipLen), p[ipLen:]...)) != 0 { + t.Fatal("invalid UDP reply checksum") + } + } + // NEEDS_CSUM without segmentation must be completed before mipstack + // sees it. Send a larger second datagram to exercise buffer restoration. + udp = append([]byte{0x30, 0x39, 0, 53, 0, 40, 0, 0}, bytes.Repeat([]byte{'z'}, 32)...) + p := mipTestPacket(src, dst, 17, udp) + binary.BigEndian.PutUint16(p[ipLen+6:], ^mipChecksum(mipTestPseudo(src, dst, 17, len(udp)))) + tun.input <- mipOffloadInput{packet: p, options: GSOOptions{GSOType: GSONone, CsumStart: uint16(ipLen), CsumOffset: 6, NeedsCsum: true}} + if got := mipReceive(t, payloads); got != string(udp[8:]) { + t.Fatalf("partial checksum packet: %q", got) + } + if p = mipReceive(t, tun.out); !bytes.Equal(p[ipLen+8:], udp[8:]) { + t.Fatal("reused output buffer truncated UDP") + } + if tun.reads.Load() != 2 || tun.writes.Load() == 0 { + t.Fatal("batch path was not exercised") + } + }) + } + } +} + +func TestMIPStackGSOLoopback(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("10.0.0.1") + ipLen, kind := 20, GSOTCPv4 + if ipv6 { + src, dst = netip.MustParseAddr("fd00::2"), netip.MustParseAddr("fd00::9") + ipLen, kind = 40, GSOTCPv6 + } + tun := newMIPBatchTestTun(10) + o := mipTestOptions(tun, &mipTestHandler{tcp: func(_ context.Context, c net.Conn, _ M.Metadata) error { + c.Close() + t.Error("loopback reached TCP handler") + return nil + }}) + o.TunOptions.GSO = true + o.TunOptions._TXChecksumOffload = true + if ipv6 { + o.TunOptions.Inet6LoopbackAddress = []netip.Addr{dst} + } else { + o.TunOptions.Inet4LoopbackAddress = []netip.Addr{dst} + } + s, err := NewMIPStack(o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { tun.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + tun.input <- mipOffloadInput{packet: mipTestTCPPacket(src, dst, []byte("abcdefghij")), options: GSOOptions{GSOType: kind, HdrLen: uint16(ipLen + 20), CsumStart: uint16(ipLen), CsumOffset: 16, GSOSize: 4, NeedsCsum: true}} + for i, want := range []string{"abcd", "efgh", "ij"} { + p := mipReceive(t, tun.out) + mipCheckTCP(t, p) + expected := mipTestTCPPacket(dst, src, []byte(want)) + srcOffset, addrLen := 12, 4 + if ipv6 { + srcOffset, addrLen = 8, 16 + } + if !bytes.Equal(p[srcOffset:srcOffset+2*addrLen], expected[srcOffset:srcOffset+2*addrLen]) { + t.Fatal("loopback IP addresses were not swapped") + } + if binary.BigEndian.Uint16(p[ipLen:]) != 12345 || binary.BigEndian.Uint16(p[ipLen+2:]) != 443 { + t.Fatal("loopback changed TCP ports") + } + if string(p[ipLen+20:]) != want || binary.BigEndian.Uint32(p[ipLen+4:]) != 100+uint32(i*4) { + t.Fatal("incorrect TCP segmentation") + } + if i < 2 && p[ipLen+13]&8 != 0 { + t.Fatal("PSH retained on intermediate segment") + } + } + }) + } +} + +func TestMIPStackLoopbackPackets(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("10.0.0.1") + if ipv6 { + src, dst = netip.MustParseAddr("fd00::2"), netip.MustParseAddr("fd00::9") + } + s := &MIPStack{loopback: map[netip.Addr]struct{}{dst: {}}} + packet := mipTestTCPPacket(src, dst, []byte("data")) + original := append([]byte(nil), packet...) + if !s.reflectLoopback(packet) { + t.Fatal("TCP loopback not intercepted") + } + mipCheckTCP(t, packet) + offset := 20 + if ipv6 { + offset = 40 + } + if !bytes.Equal(packet[offset:], original[offset:]) { + t.Fatal("loopback changed TCP bytes") + } + if ipv6 { + // Hop-by-Hop options followed by a non-initial TCP fragment. + packet = append(append(append([]byte(nil), original[:40]...), []byte{44, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 8, 0, 0, 0, 1}...), []byte("fragment")...) + packet[6] = 0 + binary.BigEndian.PutUint16(packet[4:], uint16(len(packet)-40)) + } else { + packet = append(append([]byte(nil), original[:20]...), []byte("fragment")...) + binary.BigEndian.PutUint16(packet[2:], uint16(len(packet))) + binary.BigEndian.PutUint16(packet[6:], 1) + packet[10], packet[11] = 0, 0 + binary.BigEndian.PutUint16(packet[10:], mipChecksum(packet[:20])) + } + if !s.reflectLoopback(packet) { + t.Fatal("non-initial TCP fragment not reflected") + } + for i := 0; i < len(original); i++ { + p := append([]byte(nil), original[:i]...) + if s.reflectLoopback(p) { + t.Fatalf("truncated packet length %d was reflected", i) + } + } + udp := mipTestPacket(src, dst, 17, []byte{0, 1, 0, 2, 0, 8, 0, 0}) + if s.reflectLoopback(udp) { + t.Fatal("UDP treated as TCP loopback") + } + }) + } +} + +// A plain NativeTun implements LinuxTUN too, but its BatchRead is only usable +// after virtio offload has been enabled. TX checksum offload alone is not a +// reason to select that path. +type mipPlainLinuxTestTun struct{ *mipTestTun } + +func (*mipPlainLinuxTestTun) FrontHeadroom() int { return 0 } +func (*mipPlainLinuxTestTun) BatchSize() int { return 1 } +func (*mipPlainLinuxTestTun) TXChecksumOffload() bool { return true } +func (*mipPlainLinuxTestTun) BatchRead([][]byte, int, []int) (int, error) { + return 0, errors.New("plain TUN used BatchRead") +} +func (*mipPlainLinuxTestTun) BatchWrite([][]byte, int) (int, error) { + return 0, errors.New("plain TUN used BatchWrite") +} + +func TestMIPStackPlainLoopback(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("10.0.0.1") + if ipv6 { + src, dst = netip.MustParseAddr("fd00::2"), netip.MustParseAddr("fd00::9") + } + tun := &mipPlainLinuxTestTun{newMIPTestTun()} + o := mipTestOptions(tun, &mipTestHandler{}) + o.TunOptions._TXChecksumOffload = true + if ipv6 { + o.TunOptions.Inet6LoopbackAddress = []netip.Addr{dst} + } else { + o.TunOptions.Inet4LoopbackAddress = []netip.Addr{dst} + } + s, err := NewMIPStack(o) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { tun.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + tun.in <- mipTestTCPPacket(src, dst, []byte("loopback")) + p := mipReceive(t, tun.out) + if !bytes.Equal(p, mipTestTCPPacket(dst, src, []byte("loopback"))) { + t.Fatal("incorrect plain loopback packet") + } + }) + } +} + +func TestMIPStackSegmentOverflow(t *testing.T) { + tun := newMIPBatchTestTun(10) + tun.batch = 2 + src, dst := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("198.51.100.10") + payloads := make(chan string, 4) + h := &mipTestHandler{udp: func(_ context.Context, _ netip.AddrPort, p *buf.Buffer, _ M.Metadata, _ func(N.PacketConn) N.PacketWriter) { + defer p.Release() + payloads <- string(p.Bytes()) + }} + s, err := NewMIPStack(mipTestOptions(tun, h)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { tun.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + udp := append([]byte{0x30, 0x39, 0, 53, 0, 18, 0, 0}, []byte("abcdefghij")...) + tun.input <- mipOffloadInput{packet: mipTestPacket(src, dst, 17, udp), options: GSOOptions{GSOType: GSOUDPL4, HdrLen: 28, CsumStart: 20, CsumOffset: 6, GSOSize: 4, NeedsCsum: true}} + if got := mipReceive(t, payloads); got != "abcd" { + t.Fatalf("split prefix: %q", got) + } + udp = append([]byte{0x30, 0x39, 0, 53, 0, 12, 0, 0}, []byte("next")...) + tun.input <- mipOffloadInput{packet: mipTestPacket(src, dst, 17, udp)} + if got := mipReceive(t, payloads); got != "next" { + t.Fatalf("traffic after segment overflow: %q", got) + } +} + +func (t *mipPlainLinuxTestTun) Read(p []byte) (int, error) { + select { + case packet := <-t.in: + return copy(p, packet), nil + case <-t.done: + return 0, net.ErrClosed + } +} +func (t *mipPlainLinuxTestTun) Write(p []byte) (int, error) { + select { + case t.out <- append([]byte(nil), p...): + return len(p), nil + case <-t.done: + return 0, net.ErrClosed + } +} diff --git a/stack_mipstack_test.go b/stack_mipstack_test.go new file mode 100644 index 00000000..b78e3ed9 --- /dev/null +++ b/stack_mipstack_test.go @@ -0,0 +1,597 @@ +package tun + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "io" + "net" + "net/netip" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/metacubex/mipstack" + "github.com/metacubex/sing/common/buf" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +type mipTestTun struct { + in, out chan []byte + done chan struct{} + once sync.Once +} + +func newMIPTestTun() *mipTestTun { + return &mipTestTun{in: make(chan []byte, 64), out: make(chan []byte, 64), done: make(chan struct{})} +} + +func mipTestFrame(packet []byte) []byte { + frame := make([]byte, PacketOffset+len(packet)) + copy(frame[PacketOffset:], packet) + if PacketOffset > 0 { + family := uint32(2) + if packet[0]>>4 == 6 { + family = 30 + } + binary.BigEndian.PutUint32(frame, family) + } + return frame +} + +func (tun *mipTestTun) Read(p []byte) (int, error) { + select { + case packet := <-tun.in: + return copy(p, mipTestFrame(packet)), nil + case <-tun.done: + return 0, net.ErrClosed + } +} + +func (tun *mipTestTun) Write(p []byte) (int, error) { + if len(p) <= PacketOffset { + return 0, io.ErrShortBuffer + } + if PacketOffset > 0 { + family := uint32(2) + if p[PacketOffset]>>4 == 6 { + family = 30 + } + if binary.BigEndian.Uint32(p[:PacketOffset]) != family { + return 0, errors.New("invalid utun header") + } + } + select { + case tun.out <- append([]byte(nil), p[PacketOffset:]...): + return len(p), nil + case <-tun.done: + return 0, net.ErrClosed + } +} + +func (tun *mipTestTun) Close() error { tun.once.Do(func() { close(tun.done) }); return nil } + +type mipTestHandler struct { + Handler + tcp func(context.Context, net.Conn, M.Metadata) error + udp func(context.Context, netip.AddrPort, *buf.Buffer, M.Metadata, func(N.PacketConn) N.PacketWriter) + icmp func(string, M.Socksaddr, M.Socksaddr, DirectRouteContext, time.Duration) (DirectRouteDestination, error) +} + +func (h *mipTestHandler) NewConnection(ctx context.Context, c net.Conn, m M.Metadata) error { + return h.tcp(ctx, c, m) +} +func (h *mipTestHandler) NewPacket(ctx context.Context, key netip.AddrPort, p *buf.Buffer, m M.Metadata, init func(N.PacketConn) N.PacketWriter) { + h.udp(ctx, key, p, m, init) +} +func (h *mipTestHandler) PrepareConnection(n string, src, dst M.Socksaddr, w DirectRouteContext, timeout time.Duration) (DirectRouteDestination, error) { + return h.icmp(n, src, dst, w, timeout) +} + +func mipTestOptions(tun Tun, h Handler) StackOptions { + return StackOptions{Context: context.Background(), Tun: tun, Handler: h, ICMPTimeout: time.Minute, + TunOptions: Options{MTU: 1500, Inet4Address: []netip.Prefix{netip.MustParsePrefix("172.19.0.1/30")}, Inet6Address: []netip.Prefix{netip.MustParsePrefix("fd00::1/64")}}} +} + +func startMIPTestStack(t *testing.T, h Handler) (*MIPStack, *mipTestTun) { + t.Helper() + tun := newMIPTestTun() + stack, err := NewStack("mipstack", mipTestOptions(tun, h)) + if err != nil { + t.Fatal(err) + } + s := stack.(*MIPStack) + t.Cleanup(func() { tun.Close(); s.Close() }) + if err = s.Start(); err != nil { + t.Fatal(err) + } + return s, tun +} + +func mipReceive[T any](t *testing.T, ch <-chan T) T { + t.Helper() + select { + case value := <-ch: + return value + case <-time.After(3 * time.Second): + t.Fatal("timed out waiting for packet or handler") + var zero T + return zero + } +} + +func mipChecksum(p []byte) uint16 { + var sum uint32 + for len(p) >= 2 { + sum += uint32(binary.BigEndian.Uint16(p)) + p = p[2:] + } + if len(p) != 0 { + sum += uint32(p[0]) << 8 + } + for sum>>16 != 0 { + sum = (sum & 65535) + sum>>16 + } + return ^uint16(sum) +} + +// Build independent wire packets so adapter tests also exercise validation +// and checksums rather than calling the forwarders directly. +func mipTestPacket(src, dst netip.Addr, protocol byte, payload []byte) []byte { + payload = append([]byte(nil), payload...) + checksumOffset := 2 + if protocol == 17 { + checksumOffset = 6 + } + if protocol == 17 || protocol == 58 { + pseudo := append(append([]byte(nil), src.AsSlice()...), dst.AsSlice()...) + if src.Is4() { + pseudo = append(pseudo, 0, protocol, byte(len(payload)>>8), byte(len(payload))) + } else { + pseudo = append(pseudo, 0, 0, byte(len(payload)>>8), byte(len(payload)), 0, 0, 0, protocol) + } + sum := mipChecksum(append(pseudo, payload...)) + if sum == 0 { + sum = 65535 + } + binary.BigEndian.PutUint16(payload[checksumOffset:], sum) + } else { + binary.BigEndian.PutUint16(payload[checksumOffset:], mipChecksum(payload)) + } + if src.Is4() { + p := make([]byte, 20+len(payload)) + p[0] = 0x45 + p[8] = 64 + p[9] = protocol + binary.BigEndian.PutUint16(p[2:], uint16(len(p))) + copy(p[12:16], src.AsSlice()) + copy(p[16:20], dst.AsSlice()) + binary.BigEndian.PutUint16(p[10:], mipChecksum(p[:20])) + copy(p[20:], payload) + return p + } + p := make([]byte, 40+len(payload)) + p[0] = 0x60 + p[6] = protocol + p[7] = 64 + binary.BigEndian.PutUint16(p[4:], uint16(len(payload))) + copy(p[8:24], src.AsSlice()) + copy(p[24:40], dst.AsSlice()) + copy(p[40:], payload) + return p +} + +func TestMIPStackUDP(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + src, dst, reply := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), netip.MustParseAddr("203.0.113.20") + if ipv6 { + src, dst, reply = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10"), netip.MustParseAddr("2001:db8::20") + } + type received struct { + packet *buf.Buffer + metadata M.Metadata + writer N.PacketWriter + key netip.AddrPort + } + packets := make(chan received, 2) + s, tun := startMIPTestStack(t, &mipTestHandler{udp: func(_ context.Context, key netip.AddrPort, p *buf.Buffer, m M.Metadata, init func(N.PacketConn) N.PacketWriter) { + packets <- received{p, m, init(nil), key} + }}) + udp := []byte{0x30, 0x39, 0, 53, 0, 12, 0, 0, 't', 'e', 's', 't'} + tun.in <- mipTestPacket(src, dst, 17, udp) + r := mipReceive(t, packets) + defer r.packet.Release() + if r.key != netip.AddrPortFrom(src, 12345) || r.metadata.Source.AddrPort() != r.key || r.metadata.Destination.AddrPort() != netip.AddrPortFrom(dst, 53) { + t.Fatalf("wrong metadata: %+v", r) + } + // A second input reuses the TUN read buffer; the retained payload and + // asynchronous writer from the first callback must remain valid. + udp[8] = 'n' + tun.in <- mipTestPacket(src, dst, 17, udp) + second := mipReceive(t, packets) + second.packet.Release() + if string(r.packet.Bytes()) != "test" { + t.Fatal("borrowed UDP payload escaped callback") + } + if err := r.writer.WritePacket(buf.As([]byte("reply")).ToOwned(), M.SocksaddrFrom(reply, 5353)); err != nil { + t.Fatal(err) + } + wire := mipReceive(t, tun.out) + header := 20 + if ipv6 { + header = 40 + } + expected := mipTestPacket(reply, src, 17, []byte{0x14, 0xe9, 0x30, 0x39, 0, 13, 0, 0, 'r', 'e', 'p', 'l', 'y'}) + if !bytes.Equal(wire[header:], expected[header:]) { + t.Fatalf("incorrect UDP reply: %x", wire) + } + if ipv6 { + if !bytes.Equal(wire[8:40], expected[8:40]) { + t.Fatal("incorrect IPv6 addresses") + } + } else if !bytes.Equal(wire[12:20], expected[12:20]) { + t.Fatal("incorrect IPv4 addresses") + } + s.Close() + if err := r.writer.WritePacket(buf.As([]byte("late")).ToOwned(), M.SocksaddrFrom(reply, 5353)); !errors.Is(err, net.ErrClosed) { + t.Fatalf("write after close: %v", err) + } + }) + } +} + +func TestMIPStackTCP(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + for _, batch := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6]+map[bool]string{false: "/plain", true: "/GSO"}[batch], func(t *testing.T) { + src, dst, network := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), "tcp4" + if ipv6 { + src, dst, network = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10"), "tcp6" + } + metadata := make(chan M.Metadata, 1) + h := &mipTestHandler{tcp: func(_ context.Context, c net.Conn, m M.Metadata) error { + defer c.Close() + metadata <- m + _, err := io.Copy(c, c) + return err + }} + var tun *mipTestTun + var batchTun *mipBatchTestTun + if batch { + batchTun = newMIPBatchTestTun(10) + tun = batchTun.mipTestTun + options := mipTestOptions(batchTun, h) + options.TunOptions.GSO = true + options.TunOptions._TXChecksumOffload = true + stack, err := NewMIPStack(options) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { tun.Close(); stack.Close() }) + if err = stack.Start(); err != nil { + t.Fatal(err) + } + } else { + _, tun = startMIPTestStack(t, h) + } + client, err := mipstack.New(mipstack.Config{LocalAddresses: []netip.Prefix{netip.PrefixFrom(src, src.BitLen())}, MTU: 1500}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { client.Close() }) + if err = client.Start(); err != nil { + t.Fatal(err) + } + go func() { + p := make([]byte, 65535) + sizes := []int{0} + for { + n, err := client.Read([][]byte{p}, sizes, 0) + if err != nil { + return + } + if n > 0 { + if batchTun != nil { + packet := append([]byte(nil), p[:sizes[0]]...) + ipLen, kind := 20, GSOTCPv4 + if ipv6 { + ipLen, kind = 40, GSOTCPv6 + } + tcpLen := int(packet[ipLen+12]>>4) * 4 + options := GSOOptions{CsumStart: uint16(ipLen), CsumOffset: 16, NeedsCsum: true} + binary.BigEndian.PutUint16(packet[ipLen+16:], ^mipChecksum(mipTestPseudo(src, dst, 6, len(packet)-ipLen))) + if len(packet) > ipLen+tcpLen { + options.GSOType, options.HdrLen, options.GSOSize = kind, uint16(ipLen+tcpLen), 2 + } + select { + case batchTun.input <- mipOffloadInput{packet, options}: + case <-tun.done: + return + } + continue + } + select { + case tun.in <- append([]byte(nil), p[:sizes[0]]...): + case <-tun.done: + return + } + } + } + }() + go func() { + for { + select { + case p := <-tun.out: + if _, err := client.Write([][]byte{p}, 0); err != nil { + return + } + case <-tun.done: + return + } + } + }() + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + conn, err := client.DialTCP(ctx, network, netip.AddrPortFrom(src, 12345), netip.AddrPortFrom(dst, 443)) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(3 * time.Second)) + if _, err = conn.Write([]byte("hello")); err != nil { + t.Fatal(err) + } + p := make([]byte, 5) + if _, err = io.ReadFull(conn, p); err != nil { + t.Fatal(err) + } + if string(p) != "hello" { + t.Fatal("incorrect TCP data") + } + m := mipReceive(t, metadata) + if m.Source.AddrPort() != netip.AddrPortFrom(src, 12345) || m.Destination.AddrPort() != netip.AddrPortFrom(dst, 443) { + t.Fatalf("incorrect metadata: %+v", m) + } + }) + } + } +} + +func TestMIPStackICMP(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + for _, policy := range []string{"echo", "drop", "reset"} { + t.Run(policy, func(t *testing.T) { + src, dst, protocol, echoType := netip.MustParseAddr("172.19.0.1"), netip.MustParseAddr("198.51.100.10"), byte(1), byte(8) + if ipv6 { + src, dst, protocol, echoType = netip.MustParseAddr("fd00::1"), netip.MustParseAddr("2001:db8::10"), 58, 128 + } + called := make(chan struct{}, 1) + _, tun := startMIPTestStack(t, &mipTestHandler{icmp: func(network string, source, destination M.Socksaddr, _ DirectRouteContext, _ time.Duration) (DirectRouteDestination, error) { + if network != N.NetworkICMP || source.Addr != src || destination.Addr != dst { + t.Error("incorrect ICMP metadata") + } + called <- struct{}{} + if policy == "drop" { + return nil, ErrDrop + } + if policy == "reset" { + return nil, ErrReset + } + return nil, nil + }}) + tun.in <- mipTestPacket(src, dst, protocol, []byte{echoType, 0, 0, 0, 0, 1, 0, 2, 'p', 'i', 'n', 'g'}) + mipReceive(t, called) + if policy == "drop" { + select { + case <-tun.out: + t.Fatal("drop produced a response") + case <-time.After(50 * time.Millisecond): + } + return + } + wire := mipReceive(t, tun.out) + offset := 20 + if ipv6 { + offset = 40 + } + want := byte(0) + if ipv6 { + want = 129 + } + if policy == "reset" { + want = 3 + if ipv6 { + want = 1 + } + } + if policy == "reset" { + code := byte(13) + if ipv6 { + code = 1 + } + if wire[offset+1] != code { + t.Fatalf("incorrect ICMP rejection code: %d", wire[offset+1]) + } + } + if wire[offset] != want { + t.Fatalf("unexpected ICMP type: %d", wire[offset]) + } + if policy == "echo" && !bytes.Equal(wire[offset+4:], []byte{0, 1, 0, 2, 'p', 'i', 'n', 'g'}) { + t.Fatal("echo did not preserve identifier, sequence and payload") + } + }) + } + }) + } +} + +func TestMIPStackLifecycleAndOptions(t *testing.T) { + tun := newMIPTestTun() + defer tun.Close() + base := mipTestOptions(tun, &mipTestHandler{}) + for name, modify := range map[string]func(*StackOptions){ + "invalid prefix": func(o *StackOptions) { o.TunOptions.Inet4Address = []netip.Prefix{{}} }, + } { + t.Run(name, func(t *testing.T) { + o := base + modify(&o) + s, err := NewMIPStack(o) + if err == nil { + s.Close() + t.Fatal("unsupported configuration accepted") + } + }) + } + base.IncludeAllNetworks = true + s, err := NewMIPStack(base) + if err != nil { + t.Fatal(err) + } + if err = s.Close(); err != nil { + t.Fatal(err) + } + if err = s.Close(); err != nil { + t.Fatal(err) + } + if err = s.Start(); !errors.Is(err, net.ErrClosed) { + t.Fatalf("start after close: %v", err) + } + s, err = NewMIPStack(base) + if err != nil { + t.Fatal(err) + } + defer s.Close() + if err = s.Start(); err != nil { + t.Fatal(err) + } + if err = s.Start(); err != nil { + t.Fatal(err) + } + select { + case <-tun.done: + t.Fatal("stack closed caller-owned TUN") + default: + } +} + +type mipTestRoute struct { + packets chan *buf.Buffer + closed atomic.Bool +} + +func (r *mipTestRoute) WritePacket(p *buf.Buffer) error { + r.packets <- p + return nil +} + +func (r *mipTestRoute) Close() error { + r.closed.Store(true) + return nil +} + +func (r *mipTestRoute) IsClosed() bool { return r.closed.Load() } + +func TestMIPStackICMPDirectRoute(t *testing.T) { + for _, ipv6 := range []bool{false, true} { + t.Run(map[bool]string{false: "IPv4", true: "IPv6"}[ipv6], func(t *testing.T) { + src, dst, protocol, echoType := netip.MustParseAddr("172.19.0.2"), netip.MustParseAddr("198.51.100.10"), byte(1), byte(8) + if ipv6 { + src, dst, protocol, echoType = netip.MustParseAddr("fd00::2"), netip.MustParseAddr("2001:db8::10"), 58, 128 + } + route := &mipTestRoute{packets: make(chan *buf.Buffer, 2)} + writers := make(chan DirectRouteContext, 2) + var calls atomic.Int32 + s, tun := startMIPTestStack(t, &mipTestHandler{icmp: func(_ string, _, _ M.Socksaddr, writer DirectRouteContext, timeout time.Duration) (DirectRouteDestination, error) { + if timeout != time.Minute { + t.Error("ICMP timeout was not passed to route") + } + calls.Add(1) + writers <- writer + return route, nil + }}) + request := mipTestPacket(src, dst, protocol, []byte{echoType, 0, 0, 0, 0, 1, 0, 2, 'a'}) + tun.in <- request + writer := mipReceive(t, writers) + if r := writer.(*mipICMPBackWriter).responder; !bytes.Equal(r.IPPacket(), request) || len(r.Message().Payload) == 0 { + t.Fatal("detached ICMP writer did not retain its packet snapshot") + } + packet := mipReceive(t, route.packets) + defer packet.Release() + // Reuse the route and overwrite the input buffer, retaining the first + // packet and back writer beyond the original forwarder callback. + tun.in <- mipTestPacket(src, dst, protocol, []byte{echoType, 0, 0, 0, 0, 1, 0, 3, 'b'}) + second := mipReceive(t, route.packets) + second.Release() + if calls.Load() != 1 { + t.Fatal("ICMP route was not cached") + } + if !bytes.Equal(packet.Bytes(), request) { + t.Fatal("forwarded ICMP packet was not independently owned") + } + replyType := byte(0) + if ipv6 { + replyType = 129 + } + reply := mipTestPacket(dst, src, protocol, []byte{replyType, 0, 0, 0, 0, 1, 0, 2, 'a'}) + if err := writer.WritePacket(reply); err != nil { + t.Fatal(err) + } + wire := mipReceive(t, tun.out) + if !ipv6 { + // MIPS assigns an IPv4 identification value on output. Check + // its checksum, then normalize those two fields for comparison. + if mipChecksum(wire[:20]) != 0 { + t.Fatal("invalid IPv4 header checksum") + } + copy(reply[4:6], wire[4:6]) + reply[10], reply[11] = 0, 0 + binary.BigEndian.PutUint16(reply[10:], mipChecksum(reply[:20])) + } + if !bytes.Equal(wire, reply) { + t.Fatalf("incorrect direct ICMP reply: %x", wire) + } + s.Close() + mipWaitRouteClosed(t, route) + if err := writer.WritePacket(reply); !errors.Is(err, net.ErrClosed) { + t.Fatalf("late ICMP write: %v", err) + } + }) + } +} + +func TestMIPStackContextCancellation(t *testing.T) { + tun := newMIPTestTun() + defer tun.Close() + options := mipTestOptions(tun, &mipTestHandler{}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + options.Context = ctx + stack, err := NewMIPStack(options) + if err != nil { + t.Fatal(err) + } + defer stack.Close() + if err = stack.Start(); err != nil { + t.Fatal(err) + } + result := make(chan error, 1) + go func() { + _, err := stack.(*MIPStack).stack.Read([][]byte{make([]byte, 1500)}, []int{0}, 0) + result <- err + }() + cancel() + if err := mipReceive(t, result); !E.IsClosed(err) { + t.Fatalf("packet read after cancellation: %v", err) + } + select { + case <-tun.done: + t.Fatal("context cancellation closed caller-owned TUN") + default: + } +}