diff --git a/go.mod b/go.mod index 84d6651..3f7f0a2 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb + github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 github.com/sagernet/sing v0.7.0-beta.1 diff --git a/go.sum b/go.sum index d561248..c964e1c 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38= -github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= diff --git a/icmp.go b/icmp.go new file mode 100644 index 0000000..a1db779 --- /dev/null +++ b/icmp.go @@ -0,0 +1,29 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + "runtime" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "golang.org/x/sys/unix" +) + +func NewICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + } else { + destination, err := NewPrivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + if err != nil { + if E.IsMulti(err, os.ErrPermission, unix.EPERM) { + return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + } + return nil, err + } + return destination, nil + } +} diff --git a/icmp_privileged.go b/icmp_privileged.go new file mode 100644 index 0000000..aef7d18 --- /dev/null +++ b/icmp_privileged.go @@ -0,0 +1,112 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type PrivilegedICMPDestination struct { + ctx context.Context + cancel context.CancelCauseFunc + logger logger.Logger + routeContext DirectRouteContext + isIPv6 bool + localAddr atomic.TypedValue[netip.Addr] + rawConn net.Conn +} + +func NewPrivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + var dialNetwork string + switch network { + case N.NetworkICMPv4: + dialNetwork = "ip4:icmp" + case N.NetworkICMPv6: + dialNetwork = "ip6:icmp" + default: + return nil, E.New("unsupported network: ", network) + } + ctx, cancel := context.WithCancelCause(ctx) + rawConn, err := dialer.DialContext(ctx, dialNetwork, address.String()) + if err != nil { + cancel(err) + return nil, err + } + d := &PrivilegedICMPDestination{ + ctx: ctx, + cancel: cancel, + logger: logger, + routeContext: routeContext, + isIPv6: network == N.NetworkICMPv6, + rawConn: rawConn, + } + go d.loopRead() + return d, nil +} + +func (d *PrivilegedICMPDestination) loopRead() { + for { + buffer := buf.NewPacket() + _, err := buffer.ReadOnceFrom(d.rawConn) + if err != nil { + return + } + if !d.isIPv6 { + ipHdr := header.IPv4(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + } + err = d.routeContext.WritePacket(buffer.Bytes()) + if err != nil { + d.logger.Error(err) + } + } +} + +func (d *PrivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { + if !d.isIPv6 { + ipHdr := header.IPv4(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } else { + ipHdr := header.IPv6(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } + return nil +} + +func (d *PrivilegedICMPDestination) Close() error { + d.cancel(os.ErrClosed) + return d.rawConn.Close() +} diff --git a/icmp_privileged_gvisor.go b/icmp_privileged_gvisor.go new file mode 100644 index 0000000..002207f --- /dev/null +++ b/icmp_privileged_gvisor.go @@ -0,0 +1,22 @@ +//go:build with_gvisor + +package tun + +import ( + "net/netip" + + "github.com/sagernet/gvisor/pkg/tcpip/stack" +) + +func (d *PrivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { + ipHdr := packetBuffer.Network() + if !d.isIPv6 { + d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) + } else { + d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) + } + packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) + _, err := d.rawConn.Write(packetSlice) + return err +} diff --git a/icmp_unprivileged.go b/icmp_unprivileged.go new file mode 100644 index 0000000..d246771 --- /dev/null +++ b/icmp_unprivileged.go @@ -0,0 +1,154 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + "syscall" + "unsafe" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "golang.org/x/sys/unix" +) + +type UnprivilegedICMPDestination struct { + ctx context.Context + cancel context.CancelCauseFunc + logger logger.Logger + routeContext DirectRouteContext + isIPv6 bool + localAddr atomic.TypedValue[netip.Addr] + rawConn net.Conn + ipHdr bool +} + +func NewUnprivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + var ( + isIPv6 bool + fd int + ipHdr bool + err error + ) + var dialNetwork string + switch network { + case N.NetworkICMPv4: + dialNetwork = "ip4:icmp" + case N.NetworkICMPv6: + dialNetwork = "ip6:icmp" + isIPv6 = true + default: + return nil, E.New("unsupported network: ", network) + } + if !isIPv6 { + fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) + } else { + fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) + } + if err != nil { + return nil, err + } + name, nameLen := bufio.ToSockaddr(M.SocksaddrFrom(address, 0).AddrPort()) + err = unixConnect(fd, name, nameLen) + if err != nil { + return nil, err + } + rawConn, err := net.FileConn(os.NewFile(uintptr(fd), "datagram-oriented icmp")) + if err != nil { + syscall.Close(fd) + return nil, err + } + if dialer.Control != nil { + var syscallConn syscall.RawConn + syscallConn, err = rawConn.(syscall.Conn).SyscallConn() + if err != nil { + return nil, err + } + err = dialer.Control(dialNetwork, address.String(), syscallConn) + if err != nil { + return nil, err + } + } + d := &UnprivilegedICMPDestination{ + ctx: ctx, + logger: logger, + routeContext: routeContext, + isIPv6: network == N.NetworkICMPv6, + rawConn: rawConn, + ipHdr: ipHdr, + } + go d.loopRead() + return d, nil +} + +//go:linkname unixConnect golang.org/x/sys/unix.connect +func unixConnect(fd int, addr unsafe.Pointer, addrlen uint32) error + +func (d *UnprivilegedICMPDestination) loopRead() { + for { + buffer := buf.NewPacket() + _, err := buffer.ReadOnceFrom(d.rawConn) + if err != nil { + return + } + if d.ipHdr { + if !d.isIPv6 { + ipHdr := header.IPv4(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + } + err = d.routeContext.WritePacket(buffer.Bytes()) + if err != nil { + d.logger.Error(err) + } + } else { + panic("impl no hdr version for windows and linux") + } + } +} + +func (d *UnprivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { + if !d.isIPv6 { + ipHdr := header.IPv4(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } else { + ipHdr := header.IPv6(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } + return nil +} + +func (d *UnprivilegedICMPDestination) Close() error { + d.cancel(os.ErrClosed) + return d.rawConn.Close() +} diff --git a/icmp_unprivileged_gvisor.go b/icmp_unprivileged_gvisor.go new file mode 100644 index 0000000..daba001 --- /dev/null +++ b/icmp_unprivileged_gvisor.go @@ -0,0 +1,22 @@ +//go:build with_gvisor + +package tun + +import ( + "net/netip" + + "github.com/sagernet/gvisor/pkg/tcpip/stack" +) + +func (d *UnprivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { + ipHdr := packetBuffer.Network() + if !d.isIPv6 { + d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) + } else { + d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) + } + packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) + _, err := d.rawConn.Write(packetSlice) + return err +} diff --git a/route_mapping.go b/route_mapping.go index bd51212..50292b9 100644 --- a/route_mapping.go +++ b/route_mapping.go @@ -36,10 +36,10 @@ func (m *RouteMapping) Lookup(session DirectRouteSession, constructor func() (Di ) action, _, ok := m.status.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { created, err = constructor() - return created, err != nil + return created, err == nil }) if !ok { - return created, err + return nil, err } return action, nil } diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index d93c5e3..a45ba58 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/header/parse" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" @@ -178,7 +179,8 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { Payload: buffer.MakeWithData(p), }) defer packet.DecRef() - err = route.WriteHeaderIncludedPacket(packet) + parse.IPv4(packet) + err = route.WritePacketDirect(packet) if err != nil { return gonet.TranslateNetstackError(err) } @@ -198,7 +200,7 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { Payload: buffer.MakeWithData(p), }) defer packet.DecRef() - err = route.WriteHeaderIncludedPacket(packet) + err = route.WritePacketDirect(packet) if err != nil { return gonet.TranslateNetstackError(err) }