Files
sing-tun/icmp_unprivileged.go
2025-08-22 14:21:21 +08:00

155 lines
4.0 KiB
Go

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()
}