mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-06-18 02:16:35 +00:00

A new ICMPResponder interface is introduced to provide different implementations of how the ICMP flows should return to the QUIC connection muxer. Improves usages of netip.AddrPort to leverage the embedded zone field for IPv6 addresses. Closes TUN-8640
414 lines
11 KiB
Go
414 lines
11 KiB
Go
package ingress
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fortytw2/leaktest"
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/rs/zerolog"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/net/icmp"
|
|
"golang.org/x/net/ipv4"
|
|
"golang.org/x/net/ipv6"
|
|
|
|
"github.com/cloudflare/cloudflared/packet"
|
|
quicpogs "github.com/cloudflare/cloudflared/quic"
|
|
"github.com/cloudflare/cloudflared/tracing"
|
|
)
|
|
|
|
var (
|
|
noopLogger = zerolog.Nop()
|
|
localhostIP = netip.MustParseAddr("127.0.0.1")
|
|
localhostIPv6 = netip.MustParseAddr("::1")
|
|
testFunnelIdleTimeout = time.Millisecond * 10
|
|
)
|
|
|
|
// TestICMPProxyEcho makes sure we can send ICMP echo via the Request method and receives response via the
|
|
// ListenResponse method
|
|
//
|
|
// Note: if this test fails on your device under Linux, then most likely you need to make sure that your user
|
|
// is allowed in ping_group_range. See the following gist for how to do that:
|
|
// https://github.com/ValentinBELYN/icmplib/blob/main/docs/6-use-icmplib-without-privileges.md
|
|
func TestICMPRouterEcho(t *testing.T) {
|
|
testICMPRouterEcho(t, true)
|
|
testICMPRouterEcho(t, false)
|
|
}
|
|
|
|
func testICMPRouterEcho(t *testing.T, sendIPv4 bool) {
|
|
defer leaktest.Check(t)()
|
|
|
|
const (
|
|
echoID = 36571
|
|
endSeq = 20
|
|
)
|
|
|
|
router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout)
|
|
require.NoError(t, err)
|
|
|
|
proxyDone := make(chan struct{})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
router.Serve(ctx)
|
|
close(proxyDone)
|
|
}()
|
|
|
|
muxer := newMockMuxer(1)
|
|
responder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
|
|
protocol := layers.IPProtocolICMPv6
|
|
if sendIPv4 {
|
|
protocol = layers.IPProtocolICMPv4
|
|
}
|
|
localIPs := getLocalIPs(t, sendIPv4)
|
|
ips := make([]*packet.IP, len(localIPs))
|
|
for i, localIP := range localIPs {
|
|
ips[i] = &packet.IP{
|
|
Src: localIP,
|
|
Dst: localIP,
|
|
Protocol: protocol,
|
|
TTL: packet.DefaultTTL,
|
|
}
|
|
}
|
|
|
|
var icmpType icmp.Type = ipv6.ICMPTypeEchoRequest
|
|
if sendIPv4 {
|
|
icmpType = ipv4.ICMPTypeEcho
|
|
}
|
|
for seq := 0; seq < endSeq; seq++ {
|
|
for i, ip := range ips {
|
|
pk := packet.ICMP{
|
|
IP: ip,
|
|
Message: &icmp.Message{
|
|
Type: icmpType,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: echoID + i,
|
|
Seq: seq,
|
|
Data: []byte(fmt.Sprintf("icmp echo seq %d", seq)),
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, router.Request(ctx, &pk, responder))
|
|
validateEchoFlow(t, <-muxer.cfdToEdge, &pk)
|
|
}
|
|
}
|
|
|
|
// Make sure funnel cleanup kicks in
|
|
time.Sleep(testFunnelIdleTimeout * 2)
|
|
cancel()
|
|
<-proxyDone
|
|
}
|
|
|
|
func TestTraceICMPRouterEcho(t *testing.T) {
|
|
defer leaktest.Check(t)()
|
|
|
|
tracingCtx := "ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1"
|
|
|
|
router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout)
|
|
require.NoError(t, err)
|
|
|
|
proxyDone := make(chan struct{})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
router.Serve(ctx)
|
|
close(proxyDone)
|
|
}()
|
|
|
|
// Buffer 3 packets, request span, reply span and reply
|
|
muxer := newMockMuxer(3)
|
|
tracingIdentity, err := tracing.NewIdentity(tracingCtx)
|
|
require.NoError(t, err)
|
|
serializedIdentity, err := tracingIdentity.MarshalBinary()
|
|
require.NoError(t, err)
|
|
|
|
responder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
responder.AddTraceContext(tracing.NewTracedContext(ctx, tracingIdentity.String(), &noopLogger), serializedIdentity)
|
|
|
|
echo := &icmp.Echo{
|
|
ID: 12910,
|
|
Seq: 182,
|
|
Data: []byte(t.Name()),
|
|
}
|
|
pk := packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: localhostIP,
|
|
Dst: localhostIP,
|
|
Protocol: layers.IPProtocolICMPv4,
|
|
TTL: packet.DefaultTTL,
|
|
},
|
|
Message: &icmp.Message{
|
|
Type: ipv4.ICMPTypeEcho,
|
|
Code: 0,
|
|
Body: echo,
|
|
},
|
|
}
|
|
|
|
require.NoError(t, router.Request(ctx, &pk, responder))
|
|
firstPK := <-muxer.cfdToEdge
|
|
var requestSpan *quicpogs.TracingSpanPacket
|
|
// The order of receiving reply or request span is not deterministic
|
|
switch firstPK.Type() {
|
|
case quicpogs.DatagramTypeIP:
|
|
// reply packet
|
|
validateEchoFlow(t, firstPK, &pk)
|
|
case quicpogs.DatagramTypeTracingSpan:
|
|
// Request span
|
|
requestSpan = firstPK.(*quicpogs.TracingSpanPacket)
|
|
require.NotEmpty(t, requestSpan.Spans)
|
|
require.True(t, bytes.Equal(serializedIdentity, requestSpan.TracingIdentity))
|
|
default:
|
|
panic(fmt.Sprintf("received unexpected packet type %d", firstPK.Type()))
|
|
}
|
|
|
|
secondPK := <-muxer.cfdToEdge
|
|
if requestSpan != nil {
|
|
// If first packet is request span, second packet should be the reply
|
|
validateEchoFlow(t, secondPK, &pk)
|
|
} else {
|
|
requestSpan = secondPK.(*quicpogs.TracingSpanPacket)
|
|
require.NotEmpty(t, requestSpan.Spans)
|
|
require.True(t, bytes.Equal(serializedIdentity, requestSpan.TracingIdentity))
|
|
}
|
|
|
|
// Reply span
|
|
thirdPacket := <-muxer.cfdToEdge
|
|
replySpan, ok := thirdPacket.(*quicpogs.TracingSpanPacket)
|
|
require.True(t, ok)
|
|
require.NotEmpty(t, replySpan.Spans)
|
|
require.True(t, bytes.Equal(serializedIdentity, replySpan.TracingIdentity))
|
|
require.False(t, bytes.Equal(requestSpan.Spans, replySpan.Spans))
|
|
|
|
echo.Seq++
|
|
pk.Body = echo
|
|
// Only first request for a flow is traced. The edge will not send tracing context for the second request
|
|
newResponder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
require.NoError(t, router.Request(ctx, &pk, newResponder))
|
|
validateEchoFlow(t, <-muxer.cfdToEdge, &pk)
|
|
|
|
select {
|
|
case receivedPacket := <-muxer.cfdToEdge:
|
|
panic(fmt.Sprintf("Receive unexpected packet %+v", receivedPacket))
|
|
default:
|
|
}
|
|
|
|
time.Sleep(testFunnelIdleTimeout * 2)
|
|
cancel()
|
|
<-proxyDone
|
|
}
|
|
|
|
// TestConcurrentRequests makes sure icmpRouter can send concurrent requests to the same destination with different
|
|
// echo ID. This simulates concurrent ping to the same destination.
|
|
func TestConcurrentRequestsToSameDst(t *testing.T) {
|
|
defer leaktest.Check(t)()
|
|
|
|
const (
|
|
concurrentPings = 5
|
|
endSeq = 5
|
|
)
|
|
|
|
router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout)
|
|
require.NoError(t, err)
|
|
|
|
proxyDone := make(chan struct{})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
router.Serve(ctx)
|
|
close(proxyDone)
|
|
}()
|
|
|
|
var wg sync.WaitGroup
|
|
// icmpv4 and icmpv6 each has concurrentPings
|
|
wg.Add(concurrentPings * 2)
|
|
for i := 0; i < concurrentPings; i++ {
|
|
echoID := 38451 + i
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
muxer := newMockMuxer(1)
|
|
responder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
for seq := 0; seq < endSeq; seq++ {
|
|
pk := &packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: localhostIP,
|
|
Dst: localhostIP,
|
|
Protocol: layers.IPProtocolICMPv4,
|
|
TTL: packet.DefaultTTL,
|
|
},
|
|
Message: &icmp.Message{
|
|
Type: ipv4.ICMPTypeEcho,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: echoID,
|
|
Seq: seq,
|
|
Data: []byte(fmt.Sprintf("icmpv4 echo id %d, seq %d", echoID, seq)),
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, router.Request(ctx, pk, responder))
|
|
validateEchoFlow(t, <-muxer.cfdToEdge, pk)
|
|
}
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
muxer := newMockMuxer(1)
|
|
responder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
for seq := 0; seq < endSeq; seq++ {
|
|
pk := &packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: localhostIPv6,
|
|
Dst: localhostIPv6,
|
|
Protocol: layers.IPProtocolICMPv6,
|
|
TTL: packet.DefaultTTL,
|
|
},
|
|
Message: &icmp.Message{
|
|
Type: ipv6.ICMPTypeEchoRequest,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: echoID,
|
|
Seq: seq,
|
|
Data: []byte(fmt.Sprintf("icmpv6 echo id %d, seq %d", echoID, seq)),
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, router.Request(ctx, pk, responder))
|
|
validateEchoFlow(t, <-muxer.cfdToEdge, pk)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
time.Sleep(testFunnelIdleTimeout * 2)
|
|
cancel()
|
|
<-proxyDone
|
|
}
|
|
|
|
// TestICMPProxyRejectNotEcho makes sure it rejects messages other than echo
|
|
func TestICMPRouterRejectNotEcho(t *testing.T) {
|
|
defer leaktest.Check(t)()
|
|
|
|
msgs := []icmp.Message{
|
|
{
|
|
Type: ipv4.ICMPTypeDestinationUnreachable,
|
|
Code: 1,
|
|
Body: &icmp.DstUnreach{
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
{
|
|
Type: ipv4.ICMPTypeTimeExceeded,
|
|
Code: 1,
|
|
Body: &icmp.TimeExceeded{
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
{
|
|
Type: ipv4.ICMPType(2),
|
|
Code: 0,
|
|
Body: &icmp.PacketTooBig{
|
|
MTU: 1280,
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
}
|
|
testICMPRouterRejectNotEcho(t, localhostIP, msgs)
|
|
msgsV6 := []icmp.Message{
|
|
{
|
|
Type: ipv6.ICMPTypeDestinationUnreachable,
|
|
Code: 3,
|
|
Body: &icmp.DstUnreach{
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
{
|
|
Type: ipv6.ICMPTypeTimeExceeded,
|
|
Code: 0,
|
|
Body: &icmp.TimeExceeded{
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
{
|
|
Type: ipv6.ICMPTypePacketTooBig,
|
|
Code: 0,
|
|
Body: &icmp.PacketTooBig{
|
|
MTU: 1280,
|
|
Data: []byte("original packet"),
|
|
},
|
|
},
|
|
}
|
|
testICMPRouterRejectNotEcho(t, localhostIPv6, msgsV6)
|
|
}
|
|
|
|
func testICMPRouterRejectNotEcho(t *testing.T, srcDstIP netip.Addr, msgs []icmp.Message) {
|
|
router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout)
|
|
require.NoError(t, err)
|
|
|
|
muxer := newMockMuxer(1)
|
|
responder := newPacketResponder(muxer, 0, packet.NewEncoder())
|
|
protocol := layers.IPProtocolICMPv4
|
|
if srcDstIP.Is6() {
|
|
protocol = layers.IPProtocolICMPv6
|
|
}
|
|
for _, m := range msgs {
|
|
pk := packet.ICMP{
|
|
IP: &packet.IP{
|
|
Src: srcDstIP,
|
|
Dst: srcDstIP,
|
|
Protocol: protocol,
|
|
TTL: packet.DefaultTTL,
|
|
},
|
|
Message: &m,
|
|
}
|
|
require.Error(t, router.Request(context.Background(), &pk, responder))
|
|
}
|
|
}
|
|
|
|
func validateEchoFlow(t *testing.T, pk quicpogs.Packet, echoReq *packet.ICMP) {
|
|
decoder := packet.NewICMPDecoder()
|
|
decoded, err := decoder.Decode(packet.RawPacket{Data: pk.Payload()})
|
|
require.NoError(t, err, pk)
|
|
require.Equal(t, decoded.Src, echoReq.Dst)
|
|
require.Equal(t, decoded.Dst, echoReq.Src)
|
|
require.Equal(t, echoReq.Protocol, decoded.Protocol)
|
|
|
|
if echoReq.Type == ipv4.ICMPTypeEcho {
|
|
require.Equal(t, ipv4.ICMPTypeEchoReply, decoded.Type)
|
|
} else {
|
|
require.Equal(t, ipv6.ICMPTypeEchoReply, decoded.Type)
|
|
}
|
|
require.Equal(t, 0, decoded.Code)
|
|
require.NotZero(t, decoded.Checksum)
|
|
|
|
require.Equal(t, echoReq.Body, decoded.Body)
|
|
}
|
|
|
|
func getLocalIPs(t *testing.T, ipv4 bool) []netip.Addr {
|
|
interfaces, err := net.Interfaces()
|
|
require.NoError(t, err)
|
|
localIPs := []netip.Addr{}
|
|
for _, i := range interfaces {
|
|
// Skip TUN devices, and Docker Networks
|
|
if strings.Contains(i.Name, "tun") || strings.Contains(i.Name, "docker") || strings.HasPrefix(i.Name, "br-") {
|
|
continue
|
|
}
|
|
addrs, err := i.Addrs()
|
|
require.NoError(t, err)
|
|
for _, addr := range addrs {
|
|
if ipnet, ok := addr.(*net.IPNet); ok && (ipnet.IP.IsPrivate() || ipnet.IP.IsLoopback()) {
|
|
// TODO DEVTOOLS-12514: We only run the IPv6 against the loopback interface due to issues on the CI runners.
|
|
if (ipv4 && ipnet.IP.To4() != nil) || (!ipv4 && ipnet.IP.To4() == nil && ipnet.IP.IsLoopback()) {
|
|
localIPs = append(localIPs, netip.MustParseAddr(ipnet.IP.String()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return localIPs
|
|
}
|