From 23a39c59bed2e845baeddb7947e36d64585e0b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Jul 2026 21:28:10 +0800 Subject: [PATCH] ping: Fix stale flows kept alive by unrelated ICMP traffic Unconnected raw ICMP sockets receive every ICMP packet arriving at the host, so any ICMP traffic refreshed the read deadline of every flow and stale flows (with their raw sockets and goroutines) accumulated forever, making per-ping cost grow linearly with uptime. Expire flows based on their own activity only, and on Linux attach a classic BPF ident filter to each raw socket so other flows' packets are dropped in the kernel instead of waking every flow. --- ping/destination.go | 16 ++++++++++++++-- ping/destination_test.go | 2 +- ping/filter_linux.go | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index 1e5d533..e9e6529 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -10,6 +10,7 @@ import ( "time" "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -30,6 +31,7 @@ type Destination struct { destination netip.Addr writer PacketWriter timeout time.Duration + lastActive common.TypedValue[time.Time] requestAccess sync.Mutex requests map[pingRequest]time.Time } @@ -74,6 +76,7 @@ func ConnectDestination( timeout: timeout, requests: make(map[pingRequest]time.Time), } + d.lastActive.Store(time.Now()) go d.loopRead() return d, nil } @@ -81,14 +84,21 @@ func ConnectDestination( func (d *Destination) loopRead() { defer d.Close() for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) + deadline := d.lastActive.Load().Add(d.timeout) + if !time.Now().Before(deadline) { + return + } + err := d.conn.SetReadDeadline(deadline) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } + buffer := buf.NewSize(maxICMPPacketSize) err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() + if E.IsTimeout(err) { + continue + } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } @@ -159,6 +169,7 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } + d.lastActive.Store(time.Now()) err = d.writer.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) @@ -168,6 +179,7 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { + d.lastActive.Store(time.Now()) if !d.destination.Is6() { ipHdr := header.IPv4(packet.Bytes()) if !ipHdr.IsValid(packet.Len()) { diff --git a/ping/destination_test.go b/ping/destination_test.go index db71139..a09f146 100644 --- a/ping/destination_test.go +++ b/ping/destination_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" diff --git a/ping/filter_linux.go b/ping/filter_linux.go index 13f32a3..2a1d66d 100644 --- a/ping/filter_linux.go +++ b/ping/filter_linux.go @@ -4,7 +4,7 @@ import ( "sync" "syscall" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control"