sing-tun-lx/ping/destination.go
2026-05-19 16:26:55 +08:00

224 lines
6.4 KiB
Go

package ping
import (
"context"
"errors"
"net/netip"
"os"
"runtime"
"sync"
"time"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/internal/gtcpip/header"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ tun.DirectRouteDestination = (*Destination)(nil)
type Destination struct {
conn *Conn
ctx context.Context
logger logger.ContextLogger
destination netip.Addr
routeContext tun.DirectRouteContext
timeout time.Duration
requestAccess sync.Mutex
requests map[pingRequest]time.Time
}
type pingRequest struct {
Source netip.Addr
Destination netip.Addr
Identifier uint16
Sequence uint16
}
func ConnectDestination(
ctx context.Context,
logger logger.ContextLogger,
controlFunc control.Func,
destination netip.Addr,
routeContext tun.DirectRouteContext,
timeout time.Duration,
) (tun.DirectRouteDestination, error) {
var (
conn *Conn
err error
)
switch runtime.GOOS {
case "darwin", "ios", "windows":
conn, err = Connect(ctx, false, controlFunc, destination, timeout)
default:
conn, err = Connect(ctx, true, controlFunc, destination, timeout)
if errors.Is(err, os.ErrPermission) {
conn, err = Connect(ctx, false, controlFunc, destination, timeout)
}
}
if err != nil {
return nil, err
}
d := &Destination{
conn: conn,
ctx: ctx,
logger: logger,
destination: destination,
routeContext: routeContext,
timeout: timeout,
requests: make(map[pingRequest]time.Time),
}
go d.loopRead()
return d, nil
}
func (d *Destination) loopRead() {
defer d.Close()
for {
buffer := buf.NewPacket()
err := d.conn.SetReadDeadline(time.Now().Add(d.timeout))
if err != nil {
d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn"))
}
err = d.conn.ReadIP(buffer)
if err != nil {
buffer.Release()
if !E.IsClosed(err) {
d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply"))
}
return
}
if !d.destination.Is6() {
ipHdr := header.IPv4(buffer.Bytes())
if !ipHdr.IsValid(buffer.Len()) {
d.logger.ErrorContext(d.ctx, E.New("invalid IPv4 header received"))
continue
}
if ipHdr.PayloadLength() < header.ICMPv4MinimumSize {
d.logger.ErrorContext(d.ctx, E.New("invalid ICMPv4 header received"))
continue
}
icmpHdr := header.ICMPv4(ipHdr.Payload())
if d.needFilter() {
if icmpHdr.Type() != header.ICMPv4EchoReply {
continue
}
var requestExists bool
request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}
d.requestAccess.Lock()
_, loaded := d.requests[request]
if loaded {
requestExists = true
delete(d.requests, request)
}
d.requestAccess.Unlock()
if !requestExists {
continue
}
}
d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence())
} else {
ipHdr := header.IPv6(buffer.Bytes())
if !ipHdr.IsValid(buffer.Len()) {
d.logger.ErrorContext(d.ctx, E.New("invalid IPv6 header received"))
continue
}
if ipHdr.PayloadLength() < header.ICMPv6MinimumSize {
d.logger.ErrorContext(d.ctx, E.New("invalid ICMPv6 header received"))
continue
}
icmpHdr := header.ICMPv6(ipHdr.Payload())
if d.needFilter() {
if icmpHdr.Type() != header.ICMPv6EchoReply {
continue
}
var requestExists bool
request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}
d.requestAccess.Lock()
_, loaded := d.requests[request]
if loaded {
requestExists = true
delete(d.requests, request)
}
d.requestAccess.Unlock()
if !requestExists {
continue
}
}
d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence())
}
err = d.routeContext.WritePacket(buffer.Bytes())
if err != nil {
d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply"))
}
buffer.Release()
}
}
func (d *Destination) WritePacket(packet *buf.Buffer) error {
if !d.destination.Is6() {
ipHdr := header.IPv4(packet.Bytes())
if !ipHdr.IsValid(packet.Len()) {
return E.New("invalid IPv4 header")
}
if ipHdr.PayloadLength() < header.ICMPv4MinimumSize {
return E.New("invalid ICMPv4 header")
}
icmpHdr := header.ICMPv4(ipHdr.Payload())
if d.needFilter() {
d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()})
}
d.logger.TraceContext(d.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence())
} else {
ipHdr := header.IPv6(packet.Bytes())
if !ipHdr.IsValid(packet.Len()) {
return E.New("invalid IPv6 header")
}
if ipHdr.PayloadLength() < header.ICMPv6MinimumSize {
return E.New("invalid ICMPv6 header")
}
icmpHdr := header.ICMPv6(ipHdr.Payload())
if d.needFilter() {
d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()})
}
d.logger.TraceContext(d.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence())
}
return d.conn.WriteIP(packet)
}
func (d *Destination) needFilter() bool {
return !d.conn.isLinuxUnprivileged()
}
func (d *Destination) registerRequest(request pingRequest) {
const requestsLimit = 1024
d.requestAccess.Lock()
defer d.requestAccess.Unlock()
now := time.Now()
var (
oldestRequest pingRequest
oldestCreateAt = now
)
for oldRequest, createdAt := range d.requests {
if now.Sub(createdAt) > d.timeout {
delete(d.requests, oldRequest)
} else if createdAt.Before(oldestCreateAt) {
oldestRequest = oldRequest
oldestCreateAt = createdAt
}
}
if len(d.requests) > requestsLimit {
delete(d.requests, oldestRequest)
}
d.requests[request] = now
}
func (d *Destination) Close() error {
return d.conn.Close()
}
func (d *Destination) IsClosed() bool {
return d.conn.IsClosed()
}