Add ping proxy implementation

This commit is contained in:
世界
2025-08-22 14:21:21 +08:00
parent 933bd2b2d5
commit 036d61a0aa
9 changed files with 348 additions and 7 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+29
View File
@@ -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
}
}
+112
View File
@@ -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()
}
+22
View File
@@ -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
}
+154
View File
@@ -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()
}
+22
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+4 -2
View File
@@ -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)
}