package ingress import ( "context" "fmt" "net" "net/netip" "testing" "github.com/google/gopacket/layers" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "github.com/cloudflare/cloudflared/packet" ) var ( noopLogger = zerolog.Nop() localhostIP = netip.MustParseAddr("127.0.0.1") ) // 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 TestICMPProxyEcho(t *testing.T) { const ( echoID = 36571 endSeq = 100 ) proxy, err := NewICMPProxy(localhostIP, &noopLogger) require.NoError(t, err) proxyDone := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func() { proxy.Serve(ctx) close(proxyDone) }() responder := echoFlowResponder{ decoder: packet.NewICMPDecoder(), respChan: make(chan []byte, 1), } ips := []packet.IP{ { Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, }, } addrs, err := net.InterfaceAddrs() require.NoError(t, err) for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok { ip := ipnet.IP if !ipnet.IP.IsLoopback() && ip.IsPrivate() && ip.To4() != nil { localIP := netip.MustParseAddr(ipnet.IP.String()) ips = append(ips, packet.IP{ Src: localIP, Dst: localIP, Protocol: layers.IPProtocolICMPv4, }) break } } } for seq := 0; seq < endSeq; seq++ { for i, ip := range ips { pk := packet.ICMP{ IP: &ip, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: echoID + i, Seq: seq, Data: []byte(fmt.Sprintf("icmp echo seq %d", seq)), }, }, } require.NoError(t, proxy.Request(&pk, &responder)) responder.validate(t, &pk) } } cancel() <-proxyDone } // TestICMPProxyRejectNotEcho makes sure it rejects messages other than echo func TestICMPProxyRejectNotEcho(t *testing.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"), }, }, } proxy, err := NewICMPProxy(localhostIP, &noopLogger) require.NoError(t, err) responder := echoFlowResponder{ decoder: packet.NewICMPDecoder(), respChan: make(chan []byte), } for _, m := range msgs { pk := packet.ICMP{ IP: &packet.IP{ Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, }, Message: &m, } require.Error(t, proxy.Request(&pk, &responder)) } } type echoFlowResponder struct { decoder *packet.ICMPDecoder respChan chan []byte } func (efr *echoFlowResponder) SendPacket(dst netip.Addr, pk packet.RawPacket) error { copiedPacket := make([]byte, len(pk.Data)) copy(copiedPacket, pk.Data) efr.respChan <- copiedPacket return nil } func (efr *echoFlowResponder) Close() error { close(efr.respChan) return nil } func (efr *echoFlowResponder) validate(t *testing.T, echoReq *packet.ICMP) { pk := <-efr.respChan decoded, err := efr.decoder.Decode(packet.RawPacket{Data: pk}) require.NoError(t, err) require.Equal(t, decoded.Src, echoReq.Dst) require.Equal(t, decoded.Dst, echoReq.Src) require.Equal(t, echoReq.Protocol, decoded.Protocol) require.Equal(t, ipv4.ICMPTypeEchoReply, decoded.Type) require.Equal(t, 0, decoded.Code) require.NotZero(t, decoded.Checksum) require.Equal(t, echoReq.Body, decoded.Body) }