net/packet: generate ICMP destination unreachable packets
Updates tailscale/corp#34257 Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ICMPEchoPayload generates a new random ID/Sequence pair, and returns a uint32
|
// ICMPEchoPayload generates a new random ID/Sequence pair, and returns a uint32
|
||||||
@@ -26,3 +27,64 @@ func ICMPEchoPayload(payload []byte) (idSeq uint32, buf []byte) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// icmpDestUnreachableUnusedLen is the number of unused bytes that both ICMPv4
|
||||||
|
// and ICMPv6 "Destination Unreachable" messages have between header and bits
|
||||||
|
// from the invoking packet.
|
||||||
|
const icmpDestUnreachableUnusedLen = 4
|
||||||
|
const minIPv6MTU = 1280 // RFC 2460, section 5
|
||||||
|
|
||||||
|
// GenerateICMPHostUnreachable builds an ICMPv4 or ICMPv6 "Destination
|
||||||
|
// Unreachable" message according to RFC 792 and RFC 4443, section 3.1.
|
||||||
|
// from and to must both be of the same address family; otherwise
|
||||||
|
// GenerateICMPHostUnreachable returns nil.
|
||||||
|
func GenerateICMPHostUnreachable(from, to netip.Addr, invoking *Parsed) []byte {
|
||||||
|
buf := invoking.Buffer()
|
||||||
|
switch {
|
||||||
|
case from.Is4() && to.Is4():
|
||||||
|
// RFC 792
|
||||||
|
// 0 1 2 3
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | Type | Code | Checksum |
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | unused |
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | Internet Header + 64 bits of Original Data Datagram |
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
ipHeaderLen := len(buf) - len(invoking.Transport())
|
||||||
|
return Generate(ICMP4Header{
|
||||||
|
IP4Header: IP4Header{Src: from, Dst: to},
|
||||||
|
Type: ICMP4Unreachable,
|
||||||
|
Code: ICMP4HostUnreachable,
|
||||||
|
}, icmpDestUnreachablePayload(buf, ipHeaderLen+8))
|
||||||
|
case from.Is6() && to.Is6():
|
||||||
|
// RFC 4443, section 3.1
|
||||||
|
// 0 1 2 3
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | Type | Code | Checksum |
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | Unused |
|
||||||
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
// | As much of invoking packet |
|
||||||
|
// + as possible without the ICMPv6 packet +
|
||||||
|
// | exceeding the minimum IPv6 MTU |
|
||||||
|
maxOrig := minIPv6MTU - (ICMP6Header{}).Len() - icmpDestUnreachableUnusedLen
|
||||||
|
return Generate(ICMP6Header{
|
||||||
|
IP6Header: IP6Header{Src: from, Dst: to},
|
||||||
|
Type: ICMP6Unreachable,
|
||||||
|
Code: ICMP6AddressUnreachable,
|
||||||
|
}, icmpDestUnreachablePayload(buf, maxOrig))
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// icmpDestUnreachablePayload composes the payload for an ICMP "Destination Unreachable" packet.
|
||||||
|
func icmpDestUnreachablePayload(orig []byte, maxOrig int) []byte {
|
||||||
|
n := min(len(orig), maxOrig)
|
||||||
|
payload := make([]byte, icmpDestUnreachableUnusedLen+n)
|
||||||
|
copy(payload[icmpDestUnreachableUnusedLen:], orig[:n])
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ type ICMP4Code uint8
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ICMP4NoCode ICMP4Code = 0
|
ICMP4NoCode ICMP4Code = 0
|
||||||
|
|
||||||
|
// ICMP4HostUnreachable is the code used with ICMP4Unreachable to
|
||||||
|
// indicate that the destination host could not be reached.
|
||||||
|
ICMP4HostUnreachable ICMP4Code = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// ICMP4Header is an IPv4+ICMPv4 header.
|
// ICMP4Header is an IPv4+ICMPv4 header.
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ type ICMP6Code uint8
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ICMP6NoCode ICMP6Code = 0
|
ICMP6NoCode ICMP6Code = 0
|
||||||
|
|
||||||
|
// ICMP6AddressUnreachable is the code used with ICMP6Unreachable to
|
||||||
|
// indicate that the destination address could not be reached. It is
|
||||||
|
// the IPv6 equivalent of ICMP4HostUnreachable.
|
||||||
|
ICMP6AddressUnreachable ICMP6Code = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// ICMP6Header is an IPv4+ICMPv4 header.
|
// ICMP6Header is an IPv4+ICMPv4 header.
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user