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.
307 lines
9.7 KiB
Go
307 lines
9.7 KiB
Go
package ping_test
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/gvisor/pkg/rand"
|
|
"github.com/sagernet/sing-tun/internal/gtcpip/header"
|
|
"github.com/sagernet/sing-tun/ping"
|
|
"github.com/sagernet/sing/common/buf"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPing(t *testing.T) {
|
|
t.Parallel()
|
|
const addr4 = "127.0.0.1"
|
|
t.Run("ipv4", func(t *testing.T) {
|
|
t.Run("unprivileged", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.SkipNow()
|
|
}
|
|
t.Run("read-icmp", func(t *testing.T) {
|
|
testPingIPv4ReadICMP(t, false, addr4)
|
|
})
|
|
t.Run("read-ip", func(t *testing.T) {
|
|
testPingIPv4ReadIP(t, false, addr4)
|
|
})
|
|
t.Run("write-ip", func(t *testing.T) {
|
|
testPingIPv4WriteIP(t, false, addr4)
|
|
})
|
|
})
|
|
t.Run("privileged", func(t *testing.T) {
|
|
if runtime.GOOS != "windows" && os.Getuid() != 0 {
|
|
t.SkipNow()
|
|
}
|
|
t.Run("read-icmp", func(t *testing.T) {
|
|
testPingIPv4ReadICMP(t, true, addr4)
|
|
})
|
|
t.Run("read-ip", func(t *testing.T) {
|
|
testPingIPv4ReadIP(t, true, addr4)
|
|
})
|
|
t.Run("write-ip", func(t *testing.T) {
|
|
testPingIPv4WriteIP(t, true, addr4)
|
|
})
|
|
})
|
|
})
|
|
// const addr6 = "2606:4700:4700::1001"
|
|
const addr6 = "::1"
|
|
t.Run("ipv6", func(t *testing.T) {
|
|
t.Run("unprivileged", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.SkipNow()
|
|
}
|
|
t.Run("read-icmp", func(t *testing.T) {
|
|
testPingIPv6ReadICMP(t, false, addr6)
|
|
})
|
|
t.Run("read-ip", func(t *testing.T) {
|
|
testPingIPv6ReadIP(t, false, addr6)
|
|
})
|
|
t.Run("write-ip", func(t *testing.T) {
|
|
testPingIPv6WriteIP(t, false, addr6)
|
|
})
|
|
})
|
|
t.Run("privileged", func(t *testing.T) {
|
|
if runtime.GOOS != "windows" && os.Getuid() != 0 {
|
|
t.SkipNow()
|
|
}
|
|
t.Run("read-icmp", func(t *testing.T) {
|
|
testPingIPv6ReadICMP(t, true, addr6)
|
|
})
|
|
t.Run("read-ip", func(t *testing.T) {
|
|
testPingIPv6ReadIP(t, true, addr6)
|
|
})
|
|
t.Run("write-ip", func(t *testing.T) {
|
|
testPingIPv6WriteIP(t, true, addr6)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
request := make(header.ICMPv4, header.ICMPv4MinimumSize)
|
|
request.SetType(header.ICMPv4Echo)
|
|
request.SetIdent(uint16(rand.Uint32()))
|
|
request.SetChecksum(header.ICMPv4Checksum(request, 0))
|
|
|
|
err = conn.WriteICMP(buf.As(request).ToOwned())
|
|
require.NoError(t, err)
|
|
|
|
conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1"))
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
|
|
response := buf.NewPacket()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
if runtime.GOOS == "linux" && privileged {
|
|
response.Reset()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
}
|
|
ipHdr := header.IPv4(response.Bytes())
|
|
require.NotZero(t, ipHdr.TTL())
|
|
icmpHdr := header.ICMPv4(ipHdr.Payload())
|
|
require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type())
|
|
require.Equal(t, request.Ident(), icmpHdr.Ident())
|
|
}
|
|
|
|
func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
request := make(header.ICMPv4, header.ICMPv4MinimumSize)
|
|
request.SetType(header.ICMPv4Echo)
|
|
request.SetIdent(uint16(rand.Uint32()))
|
|
request.SetChecksum(header.ICMPv4Checksum(request, 0))
|
|
|
|
err = conn.WriteICMP(buf.As(request).ToOwned())
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
|
|
response := buf.NewPacket()
|
|
err = conn.ReadICMP(response)
|
|
require.NoError(t, err)
|
|
|
|
if runtime.GOOS == "linux" && privileged {
|
|
response.Reset()
|
|
err = conn.ReadICMP(response)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
icmpHdr := header.ICMPv4(response.Bytes())
|
|
require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type())
|
|
require.Equal(t, request.Ident(), icmpHdr.Ident())
|
|
}
|
|
|
|
func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
request := make(header.ICMPv6, header.ICMPv6MinimumSize)
|
|
request.SetType(header.ICMPv6EchoRequest)
|
|
request.SetIdent(uint16(rand.Uint32()))
|
|
|
|
err = conn.WriteICMP(buf.As(request).ToOwned())
|
|
require.NoError(t, err)
|
|
|
|
conn.SetLocalAddr(netip.MustParseAddr("::1"))
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
|
|
response := buf.NewPacket()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" && privileged {
|
|
response.Reset()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
}
|
|
ipHdr := header.IPv6(response.Bytes())
|
|
require.NotZero(t, ipHdr.HopLimit())
|
|
icmpHdr := header.ICMPv6(ipHdr.Payload())
|
|
require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type())
|
|
require.Equal(t, request.Ident(), icmpHdr.Ident())
|
|
}
|
|
|
|
func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
request := make(header.ICMPv6, header.ICMPv6MinimumSize)
|
|
request.SetType(header.ICMPv6EchoRequest)
|
|
request.SetIdent(uint16(rand.Uint32()))
|
|
|
|
err = conn.WriteICMP(buf.As(request).ToOwned())
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
|
|
response := buf.NewPacket()
|
|
err = conn.ReadICMP(response)
|
|
require.NoError(t, err)
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" && privileged {
|
|
response.Reset()
|
|
err = conn.ReadICMP(response)
|
|
require.NoError(t, err)
|
|
}
|
|
icmpHdr := header.ICMPv6(response.Bytes())
|
|
require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type())
|
|
require.Equal(t, request.Ident(), icmpHdr.Ident())
|
|
}
|
|
|
|
// testPingIPv4WriteIP exercises the WriteIP send path, which is what the real
|
|
// TUN flow uses (Destination.WritePacket -> Conn.WriteIP). Unlike the ReadIP/
|
|
// ReadICMP tests, which send via WriteICMP, this path runs the SetTTL call that
|
|
// regressed in ebb52fb: on privileged Linux / unprivileged macOS the socket is
|
|
// wrapped in a BindPacketConn that does not expose SyscallConn, so
|
|
// ipv4.NewConn(c.conn).SetTTL returned "invalid connection" and the echo
|
|
// request was never sent.
|
|
func testPingIPv4WriteIP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
ident := uint16(rand.Uint32())
|
|
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: netip.MustParseAddr("127.0.0.1"),
|
|
DstAddr: netip.MustParseAddr(addr),
|
|
})
|
|
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
|
|
icmpHdr := header.ICMPv4(ipHdr.Payload())
|
|
icmpHdr.SetType(header.ICMPv4Echo)
|
|
icmpHdr.SetIdent(ident)
|
|
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0))
|
|
|
|
conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1"))
|
|
err = conn.WriteIP(packet)
|
|
require.NoError(t, err, "WriteIP must send the echo request")
|
|
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
response := buf.NewPacket()
|
|
defer response.Release()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
respIP := header.IPv4(response.Bytes())
|
|
require.NotZero(t, respIP.TTL())
|
|
respICMP := header.ICMPv4(respIP.Payload())
|
|
require.Equal(t, header.ICMPv4EchoReply, respICMP.Type())
|
|
require.Equal(t, ident, respICMP.Ident())
|
|
}
|
|
|
|
func testPingIPv6WriteIP(t *testing.T, privileged bool, addr string) {
|
|
conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0)
|
|
if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" {
|
|
t.SkipNow()
|
|
}
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
ident := uint16(rand.Uint32())
|
|
const payloadLen = header.ICMPv6MinimumSize
|
|
packet := buf.NewSize(header.IPv6MinimumSize + payloadLen)
|
|
ipHdr := header.IPv6(packet.Extend(header.IPv6MinimumSize + payloadLen))
|
|
ipHdr.Encode(&header.IPv6Fields{
|
|
PayloadLength: payloadLen,
|
|
TransportProtocol: header.ICMPv6ProtocolNumber,
|
|
HopLimit: 64,
|
|
SrcAddr: netip.MustParseAddr("::1"),
|
|
DstAddr: netip.MustParseAddr(addr),
|
|
})
|
|
icmpHdr := header.ICMPv6(ipHdr.Payload())
|
|
icmpHdr.SetType(header.ICMPv6EchoRequest)
|
|
icmpHdr.SetIdent(ident)
|
|
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
|
|
Header: icmpHdr,
|
|
Src: ipHdr.SourceAddressSlice(),
|
|
Dst: ipHdr.DestinationAddressSlice(),
|
|
}))
|
|
|
|
conn.SetLocalAddr(netip.MustParseAddr("::1"))
|
|
err = conn.WriteIP(packet)
|
|
require.NoError(t, err, "WriteIP must send the echo request")
|
|
|
|
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
|
|
response := buf.NewPacket()
|
|
defer response.Release()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
if runtime.GOOS == "darwin" {
|
|
response.Reset()
|
|
err = conn.ReadIP(response)
|
|
require.NoError(t, err)
|
|
}
|
|
respIP := header.IPv6(response.Bytes())
|
|
require.NotZero(t, respIP.HopLimit())
|
|
respICMP := header.ICMPv6(respIP.Payload())
|
|
require.Equal(t, header.ICMPv6EchoReply, respICMP.Type())
|
|
require.Equal(t, ident, respICMP.Ident())
|
|
}
|