diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 99d9bfe2..4937d53c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -41,3 +41,24 @@ jobs: run: make fips140-all GOALS=smoke-docker timeout-minutes: 10 + + smoke-self: + name: Run self traffic smoke test on macOS + runs-on: macos-latest + steps: + + - uses: actions/checkout@v7 + + - uses: actions/setup-go@v7 + with: + go-version: '1.26' + check-latest: true + + - name: build + run: make bin + + - name: run smoke-self + working-directory: ./.github/workflows/smoke + run: ./smoke-self.sh + + timeout-minutes: 10 diff --git a/.github/workflows/smoke/smoke-self.sh b/.github/workflows/smoke/smoke-self.sh new file mode 100755 index 00000000..14430ed8 --- /dev/null +++ b/.github/workflows/smoke/smoke-self.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +# A host must be able to reach its own overlay address. Where the kernel sends +# that traffic through the tun rather than over loopback, nebula sees it and +# hands it straight back (immediatelyForwardToSelf), and whether the kernel +# accepts what comes back is only answerable against a real kernel. Runs one +# nebula on this machine as root and aims every probe at its own address. + +set -e -x + +set -o pipefail + +V4=192.0.2.1 +V6=2001:db8::1 + +case "$(uname -s)" in + Darwin) TUN_DEV=utun ;; + *) TUN_DEV=tun0 ;; +esac + +ROOT="$(cd ../../.. && pwd)" + +rm -rf build/self +mkdir -p build/self +cd build/self + +cleanup() { + echo + echo " *** cleanup" + echo + + set +e + if [ -n "$NEBULA_PID" ] + then + sudo kill "$NEBULA_PID" + fi + { kill $(jobs -p); wait; } 2>/dev/null + sed 's/^/ [self] /' nebula.log +} + +trap cleanup EXIT + +# perl is on every platform this runs on; timeout(1) is not. +alarm() { + perl -e 'alarm shift; exec @ARGV' "$@" +} + +RESULTS="" +FAILED="" +probe() { + local name="$1" + shift + if "$@" + then + RESULTS="$RESULTS $name=ok" + else + RESULTS="$RESULTS $name=FAIL" + FAILED="$FAILED $name" + fi +} + +# Send one datagram, then wait for the listener to have written it out. +udp_probe() { + echo self | alarm 5 nc -u -w1 "$1" 3000 || true + set +x + for _ in $(seq 1 20) + do + if grep -q self "$2" + then + set -x + return 0 + fi + sleep 0.25 + done + set -x + return 1 +} + +"$ROOT/nebula-cert" ca -name "Smoke Test" +"$ROOT/nebula-cert" sign -name self -networks "$V4/24,$V6/64" + +HOST=self AM_LIGHTHOUSE=true TUN_DEV="$TUN_DEV" ../../genconfig.sh >self.yml + +"$ROOT/nebula" -config self.yml -test + +sudo -v +sudo "$ROOT/nebula" -config self.yml >nebula.log 2>&1 & +NEBULA_PID=$! + +for _ in $(seq 1 40) +do + ifconfig | grep "inet6 $V6 " >/dev/null && break + sleep 0.25 +done +ifconfig | grep "inet $V4 " +ifconfig | grep "inet6 $V6 " + +nc -l "$V4" 2000 >/dev/null & +nc -l "$V6" 2000 >/dev/null & +nc -u -l "$V4" 3000 >udp4.txt & +nc -u -l "$V6" 3000 >udp6.txt & +sleep 1 + +set +x +echo +echo " *** Testing self traffic from $V4" +echo +set -x +probe icmp4 alarm 5 ping -c1 "$V4" +probe tcp4 alarm 5 nc -z "$V4" 2000 +probe udp4 udp_probe "$V4" udp4.txt + +set +x +echo +echo " *** Testing self traffic from $V6" +echo +set -x +probe icmp6 alarm 5 ping6 -c1 "$V6" +probe tcp6 alarm 5 nc -z "$V6" 2000 +probe udp6 udp_probe "$V6" udp6.txt + +set +x +echo +echo " *** self traffic:$RESULTS" +echo +if [ -n "$FAILED" ] +then + echo "self traffic failed:$FAILED" >&2 + exit 1 +fi diff --git a/Makefile b/Makefile index 722d9883..4161a09b 100644 --- a/Makefile +++ b/Makefile @@ -338,10 +338,13 @@ smoke-relay-docker: bin-docker smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1 smoke-docker-ipv6: smoke-docker +smoke-self: bin + cd .github/workflows/smoke/ && ./smoke-self.sh + smoke-vagrant/%: bin-docker build/%/nebula cd .github/workflows/smoke/ && ./build.sh $* cd .github/workflows/smoke/ && ./smoke-vagrant.sh $* .FORCE: -.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin bin-windows bin-windows-arm64 bin-darwin bin-freebsd bin-freebsd-arm64 bin-boringcrypto bin-fips140 bin-pkcs11 bin-docker boringcrypto build-test-mobile debug docker e2e e2ev e2evv e2evvv e2evvvv e2e-bench fips140 fips140-all $(ALL_GOFIPS140:%=fips140-%) install proto release release-linux release-freebsd release-openbsd release-netbsd release-boringcrypto release-fips140 service smoke-docker smoke-relay-docker smoke-docker-ipv6 test test-pkcs11 test-cov-html vet smoke-vagrant/% +.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin bin-windows bin-windows-arm64 bin-darwin bin-freebsd bin-freebsd-arm64 bin-boringcrypto bin-fips140 bin-pkcs11 bin-docker boringcrypto build-test-mobile debug docker e2e e2ev e2evv e2evvv e2evvvv e2e-bench fips140 fips140-all $(ALL_GOFIPS140:%=fips140-%) install proto release release-linux release-freebsd release-openbsd release-netbsd release-boringcrypto release-fips140 service smoke-docker smoke-relay-docker smoke-docker-ipv6 smoke-self test test-pkcs11 test-cov-html vet smoke-vagrant/% .DEFAULT_GOAL := bin diff --git a/inside.go b/inside.go index 875003e0..8eedbc49 100644 --- a/inside.go +++ b/inside.go @@ -58,6 +58,9 @@ func (f *Interface) consumeInsidePacket(pkt tio.Packet, fwPacket *firewall.Parse // kernel as one giant blob; segment first so the loopback // path sees one IP datagram per Write. err := tio.SegmentSuperpacket(pkt, func(seg []byte) error { + // The kernel may have left the transport checksum for hardware + // offload to finish; nothing between here and the tun will. + iputil.SetTransportChecksum(seg) _, werr := f.queues[q].Write(seg) return werr }) diff --git a/inside_test.go b/inside_test.go new file mode 100644 index 00000000..02d64c36 --- /dev/null +++ b/inside_test.go @@ -0,0 +1,264 @@ +package nebula + +import ( + "encoding/binary" + "io" + "net/netip" + "testing" + + "github.com/gaissmai/bart" + "github.com/slackhq/nebula/firewall" + "github.com/slackhq/nebula/overlay/tio" + "github.com/slackhq/nebula/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + ipv4HeaderLen = 20 + ipv6HeaderLen = 40 +) + +// capturingTun is a tio.Queue that records what is written to it. A queue that +// discards writes is indistinguishable from a packet that was never forwarded. +type capturingTun struct { + writes [][]byte +} + +func (c *capturingTun) Read() ([]tio.Packet, error) { return nil, io.EOF } +func (c *capturingTun) Close() error { return nil } + +func (c *capturingTun) Write(b []byte) (int, error) { + c.writes = append(c.writes, append([]byte(nil), b...)) + return len(b), nil +} + +func newSelfForwardInterface(myAddrs ...netip.Addr) (*Interface, *capturingTun) { + vpnAddrs := &bart.Lite{} + for _, a := range myAddrs { + vpnAddrs.Insert(netip.PrefixFrom(a, a.BitLen())) + } + + tun := &capturingTun{} + return &Interface{ + l: test.NewLogger(), + myVpnAddrsTable: vpnAddrs, + myBroadcastAddrsTable: &bart.Lite{}, + queues: []tio.Queue{tun}, + }, tun +} + +func consumeInside(f *Interface, packet []byte) { + f.consumeInsidePacket(tio.Packet{Bytes: packet}, &firewall.ParsedPacket{}, make([]byte, 12), nil, make([]byte, mtu), 0, nil) +} + +// l4Proto describes one upper-layer header for these tests: its IP next-header +// value, where its checksum field sits within the header, and how to build a +// minimal instance of it. +type l4Proto struct { + name string + nextHdr uint8 + cksumAt int + build func() []byte +} + +var ( + tcpSyn = l4Proto{"tcp", firewall.ProtoTCP, 16, func() []byte { + h := make([]byte, 20) + binary.BigEndian.PutUint16(h[0:2], 49152) + binary.BigEndian.PutUint16(h[2:4], 443) + binary.BigEndian.PutUint32(h[4:8], 0x11223344) // sequence + h[12] = 5 << 4 // data offset, no options + h[13] = 0x02 // SYN + binary.BigEndian.PutUint16(h[14:16], 65535) // window + return h + }} + + udpDatagram = l4Proto{"udp", firewall.ProtoUDP, 6, func() []byte { + h := make([]byte, 8+4) + binary.BigEndian.PutUint16(h[0:2], 49152) + binary.BigEndian.PutUint16(h[2:4], 53) + binary.BigEndian.PutUint16(h[4:6], uint16(len(h))) + copy(h[8:], "ping") + return h + }} + + icmpEcho = l4Proto{"icmp", firewall.ProtoICMP, 2, func() []byte { return echoRequest(8) }} + icmpv6Echo = l4Proto{"icmpv6", firewall.ProtoICMPv6, 2, func() []byte { return echoRequest(128) }} +) + +// echoRequest builds an echo request body. The type differs between ICMP and +// ICMPv6, the rest of the header does not. +func echoRequest(typ uint8) []byte { + h := make([]byte, 8) + h[0] = typ + binary.BigEndian.PutUint16(h[4:6], 0xbeef) // identifier + binary.BigEndian.PutUint16(h[6:8], 1) // sequence + return h +} + +func buildIPv6(src, dst netip.Addr, p l4Proto) []byte { + l4 := p.build() + pkt := make([]byte, ipv6HeaderLen+len(l4)) + pkt[0] = 0x60 + binary.BigEndian.PutUint16(pkt[4:6], uint16(len(l4))) + pkt[6] = p.nextHdr + pkt[7] = 64 + copy(pkt[8:24], src.AsSlice()) + copy(pkt[24:40], dst.AsSlice()) + copy(pkt[ipv6HeaderLen:], l4) + if l4 := pkt[ipv6HeaderLen:]; p.nextHdr == firewall.ProtoTCP || p.nextHdr == firewall.ProtoUDP { + sum := ipv6PseudoheaderSum(src, dst, uint32(p.nextHdr), uint32(len(l4))) + binary.BigEndian.PutUint16(l4[p.cksumAt:], ^fold(sumBytes(l4, sum))) + } + return pkt +} + +func buildIPv4(src, dst netip.Addr, p l4Proto) []byte { + l4 := p.build() + pkt := make([]byte, ipv4HeaderLen+len(l4)) + pkt[0] = 0x45 + binary.BigEndian.PutUint16(pkt[2:4], uint16(len(pkt))) + pkt[8] = 64 + pkt[9] = p.nextHdr + copy(pkt[12:16], src.AsSlice()) + copy(pkt[16:20], dst.AsSlice()) + copy(pkt[ipv4HeaderLen:], l4) + if l4 := pkt[ipv4HeaderLen:]; p.nextHdr == firewall.ProtoTCP || p.nextHdr == firewall.ProtoUDP { + sum := sumBytes(pkt[12:20], uint32(p.nextHdr)+uint32(len(l4))) + binary.BigEndian.PutUint16(l4[p.cksumAt:], ^fold(sumBytes(l4, sum))) + } + return pkt +} + +// ipv6PseudoheaderSum is the RFC 2460 section 8.1 pseudo-header sum: source, +// destination, a 32 bit upper-layer packet length and a 32 bit zero-padded next +// header. Kept local to the test so these assertions do not check nebula's +// checksum code against itself. +func ipv6PseudoheaderSum(src, dst netip.Addr, nextHeader, length uint32) uint32 { + var csum uint32 + s, d := src.AsSlice(), dst.AsSlice() + for i := 0; i < 16; i += 2 { + csum += uint32(s[i])<<8 | uint32(s[i+1]) + csum += uint32(d[i])<<8 | uint32(d[i+1]) + } + return csum + length + nextHeader +} + +func sumBytes(b []byte, csum uint32) uint32 { + for i := 0; i+1 < len(b); i += 2 { + csum += uint32(b[i])<<8 | uint32(b[i+1]) + } + if len(b)%2 == 1 { + csum += uint32(b[len(b)-1]) << 8 + } + return csum +} + +func fold(csum uint32) uint16 { + for csum > 0xffff { + csum = (csum >> 16) + (csum & 0xffff) + } + return uint16(csum) +} + +// l4ChecksumValid6 verifies an IPv6 upper-layer checksum the way a receiver +// does: the pseudo-header plus the whole upper-layer segment, checksum field +// included, folds to 0xffff. The next header field is the upper-layer protocol +// only while there are no extension headers, which is all this file builds. +func l4ChecksumValid6(pkt []byte) bool { + src, _ := netip.AddrFromSlice(pkt[8:24]) + dst, _ := netip.AddrFromSlice(pkt[24:40]) + l4 := pkt[ipv6HeaderLen:] + return fold(sumBytes(l4, ipv6PseudoheaderSum(src, dst, uint32(pkt[6]), uint32(len(l4))))) == 0xffff +} + +// l4ChecksumValid4 is the IPv4 counterpart: the RFC 793/768 pseudo-header is +// source, destination, a zero byte, the protocol and the upper-layer length. +func l4ChecksumValid4(pkt []byte) bool { + ihl := int(pkt[0]&0x0f) << 2 + l4 := pkt[ihl:] + return fold(sumBytes(l4, sumBytes(pkt[12:20], uint32(pkt[9])+uint32(len(l4))))) == 0xffff +} + +// TestConsumeInsidePacketSelfTraffic covers the self-addressed branch of +// consumeInsidePacket, taken where immediatelyForwardToSelf is set (see +// inside_bsd.go): the packet goes straight back to the tun, ahead of the +// firewall and the handshake. +func TestConsumeInsidePacketSelfTraffic(t *testing.T) { + v4 := netip.MustParseAddr("100.100.1.42") + v6 := netip.MustParseAddr("fd00::42") + + tests := []struct { + name string + addr netip.Addr + pkt []byte + }{ + {"ipv4/tcp", v4, buildIPv4(v4, v4, tcpSyn)}, + {"ipv4/udp", v4, buildIPv4(v4, v4, udpDatagram)}, + {"ipv4/icmp", v4, buildIPv4(v4, v4, icmpEcho)}, + {"ipv6/tcp", v6, buildIPv6(v6, v6, tcpSyn)}, + {"ipv6/udp", v6, buildIPv6(v6, v6, udpDatagram)}, + {"ipv6/icmpv6", v6, buildIPv6(v6, v6, icmpv6Echo)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, tun := newSelfForwardInterface(tt.addr) + // consumeInsidePacket writes through the slice it is handed, so a + // packet that arrived with a valid checksum must come back out of + // bytes taken before the call, unchanged. + want := append([]byte(nil), tt.pkt...) + consumeInside(f, tt.pkt) + + if immediatelyForwardToSelf { + require.Len(t, tun.writes, 1) + assert.Equal(t, want, tun.writes[0]) + } else { + assert.Empty(t, tun.writes, "self traffic reaches the tun over loopback here and must be dropped") + } + }) + } +} + +// TestConsumeInsidePacketSelfTrafficChecksum shows that the self-forward +// returns the bytes it was handed, so a packet that arrived with a wrong +// upper-layer checksum is written back with that same wrong checksum and the +// kernel drops it on re-entry. +// +// This is how a macOS host loses TCP and UDP to its own IPv6 overlay address: +// the kernel writes only the pseudo-header sum into the checksum field and +// defers completion to hardware offload, state that does not survive the +// crossing into userspace. Which kernels do this, for which protocols and IP +// versions, is a property of the kernel and belongs to a test against a live +// one; here the checksum is simply wrong, and the forward must make it right. +func TestConsumeInsidePacketSelfTrafficChecksum(t *testing.T) { + if !immediatelyForwardToSelf { + t.Skip("self traffic never reaches the tun on this platform") + } + versions := []struct { + name string + addr netip.Addr + build func(src, dst netip.Addr, p l4Proto) []byte + l4At int + valid func(pkt []byte) bool + }{ + {"v4", netip.MustParseAddr("100.100.1.42"), buildIPv4, ipv4HeaderLen, l4ChecksumValid4}, + {"v6", netip.MustParseAddr("fd00::42"), buildIPv6, ipv6HeaderLen, l4ChecksumValid6}, + } + for _, v := range versions { + for _, p := range []l4Proto{tcpSyn, udpDatagram} { + t.Run(v.name+"/"+p.name, func(t *testing.T) { + pkt := v.build(v.addr, v.addr, p) + binary.BigEndian.PutUint16(pkt[v.l4At+p.cksumAt:], 0x1234) + require.False(t, v.valid(pkt), "the packet under test must start with a wrong checksum") + f, tun := newSelfForwardInterface(v.addr) + consumeInside(f, pkt) + require.Len(t, tun.writes, 1) + assert.True(t, v.valid(tun.writes[0]), + "a forwarded %s packet must carry a valid checksum, got 0x%04x", + p.name, binary.BigEndian.Uint16(tun.writes[0][v.l4At+p.cksumAt:])) + }) + } + } +} diff --git a/iputil/checksum.go b/iputil/checksum.go new file mode 100644 index 00000000..c35697a3 --- /dev/null +++ b/iputil/checksum.go @@ -0,0 +1,147 @@ +package iputil + +import ( + "encoding/binary" + + "github.com/google/gopacket/layers" + "github.com/slackhq/nebula/overlay/checksum" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const udpHeaderLen = 8 + +// SetTransportChecksum recomputes the TCP or UDP checksum of an IPv4 or IPv6 +// packet in place. +// +// A kernel that offloads checksums to the NIC hands a packet to a tun with the +// transport checksum unfinished: only the pseudo-header sum is in the field and +// the rest is left for hardware that a tun does not have. A packet written +// straight back to that tun is dropped on re-entry unless the checksum is +// completed first. ICMP is left alone; it arrived complete on the kernels this +// was measured against. +// +// So is any packet whose transport header cannot be located: fragments, unknown +// extension headers and truncated packets. An IPv6 fragment header is declined +// even when it carries the whole datagram (RFC 6946 atomic fragment), because +// the walk reports only that a fragment header was present. +func SetTransportChecksum(packet []byte) { + if len(packet) < 1 { + return + } + switch int(packet[0] >> 4) { + case ipv4.Version: + setTransportChecksum4(packet) + case ipv6.Version: + setTransportChecksum6(packet) + } +} + +func setTransportChecksum4(packet []byte) { + if len(packet) < ipv4.HeaderLen { + return + } + ihl := int(packet[0]&0x0f) << 2 + end := int(binary.BigEndian.Uint16(packet[2:4])) + if ihl < ipv4.HeaderLen || end < ihl || end > len(packet) { + return + } + // The checksum covers the whole datagram, which a fragment (MF set or a + // non-zero offset) does not carry. + if binary.BigEndian.Uint16(packet[6:8])&0x3fff != 0 { + return + } + + transport, ok := transportExtent(packet[ihl:end], packet[9]) + if !ok { + return + } + csum := ipv4PseudoheaderChecksum(packet[12:16], packet[16:20], uint32(packet[9]), uint32(len(transport))) + writeTransportChecksum(transport, layers.IPProtocol(packet[9]), csum) +} + +func setTransportChecksum6(packet []byte) { + if len(packet) < ipv6.HeaderLen { + return + } + end := ipv6.HeaderLen + int(binary.BigEndian.Uint16(packet[4:6])) + if end > len(packet) { + return + } + + // The checksum covers the whole datagram, which a fragment does not carry. + // An unknown extension header hides where the transport header starts. A + // chain longer than the walk's budget ends it early, at an offset that was + // never checked against the packet. + proto, offset, _, anyFragment, err := IPv6FindUpperProtocol(packet[:end]) + if err != nil || anyFragment || offset >= end { + return + } + + transport, ok := transportExtent(packet[offset:end], proto) + if !ok { + return + } + csum := ipv6PseudoheaderChecksum(packet[8:24], packet[24:40], uint32(proto), uint32(len(transport))) + writeTransportChecksum(transport, layers.IPProtocol(proto), csum) +} + +// transportExtent narrows a segment to the length its own header declares. UDP +// carries a Length field, and RFC 768 and RFC 8200 section 8.1 both make that +// field, not the IP payload extent, the length the pseudo-header counts and the +// checksum covers; a datagram padded out to a link's minimum frame is the usual +// way the two differ. TCP has no such field, so its segment runs to the end of +// the IP payload. A Length that overruns the bytes IP delivered describes a +// datagram that is not there. +func transportExtent(transport []byte, proto uint8) ([]byte, bool) { + if layers.IPProtocol(proto) != layers.IPProtocolUDP { + return transport, true + } + if len(transport) < udpHeaderLen { + return nil, false + } + ulen := int(binary.BigEndian.Uint16(transport[4:6])) + if ulen < udpHeaderLen || ulen > len(transport) { + return nil, false + } + return transport[:ulen], true +} + +// writeTransportChecksum stores the checksum of transport, taken over the +// pseudo-header sum csum, in the header's checksum field. A UDP checksum that +// computes to zero goes on the wire as 0xffff: zero means no checksum was +// computed (RFC 768), and over IPv6 the checksum is mandatory (RFC 8200 +// section 8.1). +func writeTransportChecksum(transport []byte, proto layers.IPProtocol, csum uint32) { + var at, minLen int + switch proto { + case layers.IPProtocolTCP: + at, minLen = 16, 20 + case layers.IPProtocolUDP: + at, minLen = 6, udpHeaderLen + default: + return + } + if len(transport) < minLen { + return + } + + transport[at], transport[at+1] = 0, 0 + sum := ^checksum.Checksum(transport, fold(csum)) + if sum == 0 && proto == layers.IPProtocolUDP { + sum = 0xffff + } + binary.BigEndian.PutUint16(transport[at:], sum) +} + +// fold reduces a pseudo-header sum to the 16 bit seed Checksum takes. Carrying +// the high half back into the low half is what keeps the reduction lossless, so +// the seed sums exactly as the wider value would; 0xffff is its fixed point. +// Every term of that sum comes from a 16 bit field, so it stays far below the +// width at which the accumulator would wrap. +func fold(csum uint32) uint16 { + for csum > 0xffff { + csum = (csum >> 16) + (csum & 0xffff) + } + return uint16(csum) +} diff --git a/iputil/checksum_test.go b/iputil/checksum_test.go new file mode 100644 index 00000000..5c451ab5 --- /dev/null +++ b/iputil/checksum_test.go @@ -0,0 +1,242 @@ +package iputil + +import ( + "encoding/binary" + "net" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/ipv6" +) + +// serialize builds a packet with gopacket, whose checksums are computed +// independently of this package. +func serialize(t *testing.T, ls ...gopacket.SerializableLayer) []byte { + buf := gopacket.NewSerializeBuffer() + require.NoError(t, gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}, ls...)) + return append([]byte(nil), buf.Bytes()...) +} + +// withExtensionHeader inserts an 8 byte IPv6 extension header of the given +// type between the IPv6 header and its payload. The transport checksum does not +// change: the pseudo-header counts only upper-layer bytes. +func withExtensionHeader(pkt []byte, typ layers.IPProtocol, hdr [8]byte) []byte { + hdr[0] = pkt[6] + out := make([]byte, 0, len(pkt)+8) + out = append(out, pkt[:40]...) + out = append(out, hdr[:]...) + out = append(out, pkt[40:]...) + out[6] = byte(typ) + binary.BigEndian.PutUint16(out[4:6], binary.BigEndian.Uint16(pkt[4:6])+8) + return out +} + +// truncate copies the first n bytes into a buffer of exactly that capacity, so +// a read past the length panics instead of quietly succeeding. +func truncate(pkt []byte, n int) []byte { + out := make([]byte, n) + copy(out, pkt) + return out +} + +// extChain builds an IPv6 packet fronted by n Destination Options headers. Each +// points at another one, so the walk spends its whole budget without reaching a +// transport header. lastExtLen inflates the final header's declared length, +// which is how the walk ends up past the end of the packet. +func extChain(n int, lastExtLen byte) []byte { + pkt := make([]byte, ipv6.HeaderLen) + pkt[0], pkt[6], pkt[7] = 0x60, 60, 64 + for i := range n { + h := make([]byte, 8) + h[0] = 60 + if i == n-1 { + h[1] = lastExtLen + } + pkt = append(pkt, h...) + } + pkt = append(pkt, make([]byte, 20)...) + binary.BigEndian.PutUint16(pkt[4:6], uint16(len(pkt)-ipv6.HeaderLen)) + return pkt +} + +func TestSetTransportChecksum(t *testing.T) { + // Source and destination differ so that a pseudo-header built from the wrong + // one, or from the two swapped, does not land on the same checksum anyway. + v4 := func(proto layers.IPProtocol) *layers.IPv4 { + return &layers.IPv4{Version: 4, TTL: 64, Id: 0x1234, Protocol: proto, SrcIP: net.IPv4(192, 0, 2, 1).To4(), DstIP: net.IPv4(198, 51, 100, 2).To4()} + } + v6 := func(proto layers.IPProtocol) *layers.IPv6 { + return &layers.IPv6{Version: 6, HopLimit: 64, NextHeader: proto, SrcIP: net.ParseIP("2001:db8::1"), DstIP: net.ParseIP("2001:db8:1::2")} + } + tcp := func(ip gopacket.NetworkLayer) *layers.TCP { + l := &layers.TCP{SrcPort: 49152, DstPort: 443, SYN: true, Window: 65535} + require.NoError(t, l.SetNetworkLayerForChecksum(ip)) + return l + } + udp := func(ip gopacket.NetworkLayer) *layers.UDP { + l := &layers.UDP{SrcPort: 49152, DstPort: 53} + require.NoError(t, l.SetNetworkLayerForChecksum(ip)) + return l + } + payload := gopacket.Payload("self") + nop := layers.IPv4Option{OptionType: 1, OptionLength: 1} + + ip4tcp := v4(layers.IPProtocolTCP) + ip4opts := v4(layers.IPProtocolTCP) + ip4opts.Options = []layers.IPv4Option{nop, nop, nop, nop} + ip4udp := v4(layers.IPProtocolUDP) + ip6tcp := v6(layers.IPProtocolTCP) + ip6udp := v6(layers.IPProtocolUDP) + hopByHop := [8]byte{0, 0, 1, 4} // next header, length 0, PadN of 4 + + // Bytes past the length the IP header declares are not part of the + // datagram and must not be summed. + trailing4 := append(serialize(t, ip4tcp, tcp(ip4tcp), payload), []byte("trailing")...) + trailing6 := append(serialize(t, ip6tcp, tcp(ip6tcp), payload), []byte("trailing")...) + + // A datagram padded out past the length UDP declares: the pseudo-header + // counts the UDP Length field, so the checksum is the unpadded one. + padded4 := append(serialize(t, ip4udp, udp(ip4udp), payload), []byte("pad!")...) + binary.BigEndian.PutUint16(padded4[2:4], uint16(len(padded4))) + padded6 := append(serialize(t, ip6udp, udp(ip6udp), payload), []byte("pad!")...) + binary.BigEndian.PutUint16(padded6[4:6], uint16(len(padded6)-ipv6.HeaderLen)) + + // Corrupting the checksum and asking for it back must yield gopacket's + // packet, byte for byte. + recomputed := []struct { + name string + pkt []byte + cksum int + }{ + {"v4 tcp", serialize(t, ip4tcp, tcp(ip4tcp), payload), 20 + 16}, + {"v4 tcp with ip options", serialize(t, ip4opts, tcp(ip4opts), payload), 24 + 16}, + {"v4 udp", serialize(t, ip4udp, udp(ip4udp), payload), 20 + 6}, + {"v4 tcp header only", serialize(t, ip4tcp, tcp(ip4tcp)), 20 + 16}, + {"v4 udp header only", serialize(t, ip4udp, udp(ip4udp)), 20 + 6}, + {"v6 tcp", serialize(t, ip6tcp, tcp(ip6tcp), payload), 40 + 16}, + {"v6 udp", serialize(t, ip6udp, udp(ip6udp), payload), 40 + 6}, + {"v6 udp header only", serialize(t, ip6udp, udp(ip6udp)), 40 + 6}, + {"v6 tcp behind hop-by-hop", withExtensionHeader(serialize(t, ip6tcp, tcp(ip6tcp), payload), layers.IPProtocolIPv6HopByHop, hopByHop), 48 + 16}, + {"v4 tcp with bytes past the total length", trailing4, 20 + 16}, + {"v6 tcp with bytes past the payload length", trailing6, 40 + 16}, + {"v4 udp padded past its declared length", padded4, 20 + 6}, + {"v6 udp padded past its declared length", padded6, 40 + 6}, + } + for _, tt := range recomputed { + t.Run(tt.name, func(t *testing.T) { + got := append([]byte(nil), tt.pkt...) + binary.BigEndian.PutUint16(got[tt.cksum:], 0x1234) + require.NotEqual(t, tt.pkt, got) + SetTransportChecksum(got) + assert.Equal(t, tt.pkt, got) + }) + } + + ip4frag := v4(layers.IPProtocolTCP) + ip4frag.Flags = layers.IPv4MoreFragments + ip4later := v4(layers.IPProtocolTCP) + ip4later.FragOffset = 1 + ip4icmp := v4(layers.IPProtocolICMPv4) + + badIHL := serialize(t, ip4tcp, tcp(ip4tcp), payload) + badIHL[0] = 0x44 // header length 16, shorter than an ipv4 header + shortTotalLen := serialize(t, ip4tcp, tcp(ip4tcp), payload) + binary.BigEndian.PutUint16(shortTotalLen[2:4], 10) // shorter than the header it introduces + cutTCP := serialize(t, ip4tcp, tcp(ip4tcp), payload) + binary.BigEndian.PutUint16(cutTCP[2:4], 20+19) // one byte short of a tcp header + cutTCP = truncate(cutTCP, 20+19) + cutUDP := serialize(t, ip4udp, udp(ip4udp), payload) + binary.BigEndian.PutUint16(cutUDP[2:4], 20+7) // one byte short of a udp header + cutUDP = truncate(cutUDP, 20+7) + // Two bytes short, so a transport header survives whole and the minimum + // length check cannot stand in for the bounds check. + cutV6 := truncate(serialize(t, ip6tcp, tcp(ip6tcp), payload), 62) + fragment := [8]byte{0, 0, 0, 1, 0, 0, 0, 1} // next header, reserved, offset 0 with M set, id + overrun4 := serialize(t, ip4udp, udp(ip4udp), payload) + binary.BigEndian.PutUint16(overrun4[24:26], uint16(len(overrun4)-20+1)) // one byte past what ip delivered + overrun6 := serialize(t, ip6udp, udp(ip6udp), payload) + binary.BigEndian.PutUint16(overrun6[44:46], uint16(len(overrun6)-ipv6.HeaderLen+1)) + shortUDPLen := serialize(t, ip4udp, udp(ip4udp), payload) + binary.BigEndian.PutUint16(shortUDPLen[24:26], 7) // shorter than the header it counts + + // Where the checksum cannot be completed the packet is left as it came. + untouched := []struct { + name string + pkt []byte + cksum int + }{ + {"v4 first fragment", serialize(t, ip4frag, tcp(ip4frag), payload), 20 + 16}, + {"v4 later fragment", serialize(t, ip4later, tcp(ip4later), payload), 20 + 16}, + {"v4 icmp", serialize(t, ip4icmp, &layers.ICMPv4{TypeCode: layers.CreateICMPv4TypeCode(8, 0), Id: 1, Seq: 1}, payload), 20 + 2}, + {"v4 header length below the minimum", badIHL, 20 + 16}, + {"v4 total length below the header length", shortTotalLen, 20 + 16}, + {"v4 truncated below its total length", truncate(serialize(t, ip4tcp, tcp(ip4tcp), payload), 30), -1}, + {"v4 tcp header cut short", cutTCP, 20 + 16}, + {"v4 udp header cut short", cutUDP, -1}, + {"v6 fragment", withExtensionHeader(serialize(t, ip6tcp, tcp(ip6tcp), payload), layers.IPProtocolIPv6Fragment, fragment), 48 + 16}, + {"v6 truncated below its payload length", truncate(serialize(t, ip6tcp, tcp(ip6tcp), payload), 50), -1}, + {"v6 truncated with a whole transport header still present", cutV6, 40 + 16}, + {"v6 extension header chain longer than the walk", extChain(9, 0), 112 + 16}, + {"v6 extension header chain running past the packet", extChain(8, 255), 104 + 16}, + {"v4 udp length past the end of the datagram", overrun4, 20 + 6}, + {"v6 udp length past the end of the datagram", overrun6, 40 + 6}, + {"v4 udp length below a udp header", shortUDPLen, 20 + 6}, + } + for _, tt := range untouched { + t.Run(tt.name, func(t *testing.T) { + if tt.cksum >= 0 { + binary.BigEndian.PutUint16(tt.pkt[tt.cksum:], 0x1234) + } + want := append([]byte(nil), tt.pkt...) + SetTransportChecksum(tt.pkt) + assert.Equal(t, want, tt.pkt) + }) + } + + t.Run("too short to carry a header", func(t *testing.T) { + for _, pkt := range [][]byte{nil, {}, {0x45}, {0x60}} { + assert.NotPanics(t, func() { SetTransportChecksum(pkt) }) + } + }) + + t.Run("tcp checksum of zero goes out as zero", func(t *testing.T) { + pkt := serialize(t, ip4tcp, tcp(ip4tcp), gopacket.Payload{0, 0}) + c := binary.BigEndian.Uint16(pkt[36:38]) + require.NotZero(t, c) + // Only udp reserves zero to mean "not computed", so tcp keeps it. + binary.BigEndian.PutUint16(pkt[40:42], c) + SetTransportChecksum(pkt) + assert.Zero(t, binary.BigEndian.Uint16(pkt[36:38])) + }) + + t.Run("udp checksum of zero goes out as 0xffff", func(t *testing.T) { + pkt := serialize(t, ip4udp, udp(ip4udp), gopacket.Payload{0, 0}) + c := binary.BigEndian.Uint16(pkt[26:28]) + require.NotZero(t, c) + // The one's complement sum is now 0xffff - c; adding c to the payload + // makes it 0xffff, whose complement is zero. + binary.BigEndian.PutUint16(pkt[28:30], c) + SetTransportChecksum(pkt) + assert.Equal(t, uint16(0xffff), binary.BigEndian.Uint16(pkt[26:28])) + }) +} + +func TestFold(t *testing.T) { + // 0xffff is the fold's fixed point, so a loop bound one notch tight never + // terminates on it. + for _, tt := range []struct { + in uint32 + want uint16 + }{ + {0, 0}, + {0xffff, 0xffff}, + {0x10000, 1}, + {0x1fffe, 0xffff}, + {0xffffffff, 0xffff}, + } { + assert.Equal(t, tt.want, fold(tt.in)) + } +}