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) were only reclaimed by LRU eviction while processing all host ICMP traffic in the meantime. 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.
109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
package ping_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-tun/internal/gtcpip/header"
|
|
"github.com/sagernet/sing-tun/ping"
|
|
"github.com/sagernet/sing/common/buf"
|
|
"github.com/sagernet/sing/common/logger"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIsClosed(t *testing.T) {
|
|
t.Parallel()
|
|
destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, netip.MustParseAddr("1.1.1.1"), nil, 30*time.Second)
|
|
require.NoError(t, err)
|
|
defer destination.Close()
|
|
time.Sleep(1 * time.Second)
|
|
require.False(t, destination.IsClosed())
|
|
destination.Close()
|
|
require.True(t, destination.IsClosed())
|
|
}
|
|
|
|
type channelWriter struct {
|
|
packets chan []byte
|
|
}
|
|
|
|
func (w *channelWriter) WritePacket(packet []byte) error {
|
|
select {
|
|
case w.packets <- slices.Clone(packet):
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type discardWriter struct{}
|
|
|
|
func (w discardWriter) WritePacket(packet []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func buildEchoRequest(source, destination netip.Addr, identifier, sequence uint16) *buf.Buffer {
|
|
const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize
|
|
packet := buf.NewSize(totalLen)
|
|
ipHdr := header.IPv4(packet.Extend(totalLen))
|
|
ipHdr.Encode(&header.IPv4Fields{
|
|
TotalLength: totalLen,
|
|
TTL: 64,
|
|
Protocol: uint8(header.ICMPv4ProtocolNumber),
|
|
SrcAddr: source,
|
|
DstAddr: destination,
|
|
})
|
|
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
|
|
icmpHdr := header.ICMPv4(ipHdr.Payload())
|
|
icmpHdr.SetType(header.ICMPv4Echo)
|
|
icmpHdr.SetIdent(identifier)
|
|
icmpHdr.SetSequence(sequence)
|
|
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0))
|
|
return packet
|
|
}
|
|
|
|
// darwin unprivileged ICMP sockets and Linux raw sockets receive every
|
|
// loopback ICMP packet regardless of the flow it belongs to, so this test
|
|
// cannot run parallel to TestPing.
|
|
func TestDestinationIdleExpiry(t *testing.T) {
|
|
loopback := netip.MustParseAddr("127.0.0.1")
|
|
writer := &channelWriter{packets: make(chan []byte, 16)}
|
|
destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, writer, time.Second)
|
|
if errors.Is(err, os.ErrPermission) {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
defer destination.Close()
|
|
|
|
err = destination.WritePacket(buildEchoRequest(loopback, loopback, 0x1111, 1))
|
|
require.NoError(t, err)
|
|
select {
|
|
case packet := <-writer.packets:
|
|
replyIPHdr := header.IPv4(packet)
|
|
replyICMPHdr := header.ICMPv4(replyIPHdr.Payload())
|
|
require.Equal(t, header.ICMPv4EchoReply, replyICMPHdr.Type())
|
|
require.Equal(t, uint16(0x1111), replyICMPHdr.Ident())
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("no echo reply received")
|
|
}
|
|
|
|
noise, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, discardWriter{}, 30*time.Second)
|
|
require.NoError(t, err)
|
|
defer noise.Close()
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
var sequence uint16
|
|
for time.Now().Before(deadline) {
|
|
sequence++
|
|
_ = noise.WritePacket(buildEchoRequest(loopback, loopback, 0x2222, sequence))
|
|
if destination.IsClosed() {
|
|
return
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
}
|
|
t.Fatal("flow not closed after idle timeout despite unrelated ICMP traffic")
|
|
}
|