Files
tailscale/net/packet/icmp_test.go
T

191 lines
5.5 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"bytes"
"net/netip"
"testing"
"tailscale.com/types/ipproto"
)
func TestGenerateICMPHostUnreachable(t *testing.T) {
const (
clientPort = 1234
serverPort = 80
)
makeInvokingPacket := func(src, dst netip.Addr, payloadLen int) *Parsed {
udpPayload := bytes.Repeat([]byte("x"), payloadLen)
var invoking []byte
if dst.Is6() {
invoking = Generate(UDP6Header{
IP6Header: IP6Header{Src: src, Dst: dst},
SrcPort: clientPort,
DstPort: serverPort,
}, udpPayload)
} else {
invoking = Generate(UDP4Header{
IP4Header: IP4Header{Src: src, Dst: dst},
SrcPort: clientPort,
DstPort: serverPort,
}, udpPayload)
}
var invokingPacket Parsed
invokingPacket.Decode(invoking)
return &invokingPacket
}
maxEmbeddedV4Length := 28 // IP header (20) + 64 bits (8) of original data datagram
// As much of the packet as fits in min IPv6 MTU, which is
// 1280 - 40 (IPv6 header) - 4 (ICMPv6 header) - 4 (unused).
maxEmbeddedV6Length := minIPv6MTU - 40 - 4 - 4
tests := []struct {
name string
invokingPacket *Parsed
wantProto ipproto.Proto
wantType uint8
wantCode uint8
fitsWhole bool
}{
{
name: "ipv4-fits-whole",
invokingPacket: makeInvokingPacket(
netip.MustParseAddr("100.70.0.1"),
netip.MustParseAddr("10.64.0.2"),
0,
),
wantProto: ipproto.ICMPv4,
wantType: uint8(ICMP4Unreachable),
wantCode: uint8(ICMP4HostUnreachable),
fitsWhole: true,
},
{
name: "ipv4-truncated-to-ip-header-plus-8",
invokingPacket: makeInvokingPacket(
netip.MustParseAddr("100.70.0.1"),
netip.MustParseAddr("10.64.0.2"),
100,
),
wantProto: ipproto.ICMPv4,
wantType: uint8(ICMP4Unreachable),
wantCode: uint8(ICMP4HostUnreachable),
},
{
name: "ipv6-fits-whole",
invokingPacket: makeInvokingPacket(
netip.MustParseAddr("fd7a:115c:a1e0::1"),
netip.MustParseAddr("fd7a:115c:a1e0::2"),
5,
),
wantProto: ipproto.ICMPv6,
wantType: uint8(ICMP6Unreachable),
wantCode: uint8(ICMP6AddressUnreachable),
fitsWhole: true,
},
{
name: "ipv6-truncated-to-min-mtu",
invokingPacket: makeInvokingPacket(
netip.MustParseAddr("fd7a:115c:a1e0::1"),
netip.MustParseAddr("fd7a:115c:a1e0::2"),
minIPv6MTU*2,
),
wantProto: ipproto.ICMPv6,
wantType: uint8(ICMP6Unreachable),
wantCode: uint8(ICMP6AddressUnreachable),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iDst := tt.invokingPacket.Dst.Addr()
iSrc := tt.invokingPacket.Src.Addr()
// Error message's src is invoking dst and vice-versa.
raw := GenerateICMPHostUnreachable(iDst, iSrc, tt.invokingPacket)
if raw == nil {
t.Fatal("GenerateICMPHostUnreachable returned nil")
}
var p Parsed
p.Decode(raw)
if !p.IsError() {
t.Fatal("wanted an ICMP error")
}
if got := p.IPProto; got != tt.wantProto {
t.Errorf("proto: got %v, want %v", got, tt.wantProto)
}
if want, got := iDst, p.Src.Addr(); want != got {
t.Errorf("src: got %v, want %v", got, want)
}
if want, got := iSrc, p.Dst.Addr(); want != got {
t.Errorf("dst: got %v, want %v", got, want)
}
var gotType, gotCode uint8
if iDst.Is6() {
h := p.ICMP6Header()
gotType, gotCode = uint8(h.Type), uint8(h.Code)
} else {
h := p.ICMP4Header()
gotType, gotCode = uint8(h.Type), uint8(h.Code)
}
if gotType != tt.wantType || gotCode != tt.wantCode {
t.Errorf("type/code: got %d/%d, want %d/%d", gotType, gotCode, tt.wantType, tt.wantCode)
}
// The ICMP body must be a 4-byte zeroed "unused" field followed by
// the embedded invoking packet.
body := p.Payload()
if len(body) < icmpDestUnreachableUnusedLen {
t.Fatalf("ICMP body too short: %d bytes", len(body))
}
if unused := body[:icmpDestUnreachableUnusedLen]; !bytes.Equal(unused, make([]byte, icmpDestUnreachableUnusedLen)) {
t.Errorf("unused field: got % x, want all zero", unused)
}
embedded := body[icmpDestUnreachableUnusedLen:]
if !bytes.HasPrefix(tt.invokingPacket.b, embedded) {
t.Errorf("embedded packet is not a prefix of the invoking packet:\n embedded=% x\n orig=% x", embedded, tt.invokingPacket.b[:min(len(tt.invokingPacket.b), len(embedded))])
}
if !tt.fitsWhole {
// embedded should be truncated to the max
wantLen := maxEmbeddedV4Length
if iSrc.Is6() {
wantLen = maxEmbeddedV6Length
}
if len(embedded) != wantLen {
t.Errorf("embedded length: got %d, want %d (orig %d)", len(embedded), wantLen, len(tt.invokingPacket.b))
}
} else {
// should decode to the invoking packet
var orig Parsed
orig.Decode(embedded)
if got := orig.IPProto; got != ipproto.UDP {
t.Errorf("embedded proto: got %v, want UDP", got)
}
if want, got := netip.AddrPortFrom(iSrc, clientPort), orig.Src; want != got {
t.Errorf("embedded src: got %v, want %v", got, want)
}
if want, got := netip.AddrPortFrom(iDst, serverPort), orig.Dst; want != got {
t.Errorf("embedded dst: got %v, want %v", got, want)
}
}
})
}
}
func TestGenerateICMPHostUnreachableMixedFamily(t *testing.T) {
v4 := netip.MustParseAddr("100.70.0.1")
v6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
var empty Parsed
if got := GenerateICMPHostUnreachable(v4, v6, &empty); got != nil {
t.Errorf("mixed family: got % x, want nil", got)
}
if got := GenerateICMPHostUnreachable(netip.Addr{}, v4, &empty); got != nil {
t.Errorf("invalid from: got % x, want nil", got)
}
}