mirror of
https://github.com/slackhq/nebula.git
synced 2026-09-30 01:36:37 +02:00
smoke-extra / freebsd-amd64 (push) Failing after 15s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 15s
smoke-extra / netbsd-amd64 (push) Failing after 15s
smoke-extra / openbsd-amd64 (push) Failing after 16s
smoke-extra / linux-386 (push) Failing after 16s
smoke / Run multi node smoke test (push) Failing after 1m34s
Build and test / Static checks (push) Successful in 46s
Build and test / Test linux (push) Failing after 1m11s
Build and test / Test linux-pkcs11 (push) Failing after 1m51s
Build and test / Test linux-boringcrypto (push) Failing after 2m35s
Build and test / Test linux-fips140 (push) Failing after 2m41s
Build and test / Cross-build linux-arm (push) Successful in 2m57s
Build and test / Cross-build linux-mips (push) Successful in 4m0s
Build and test / Cross-build linux-other (push) Successful in 3m2s
Build and test / Cross-build windows (push) Successful in 1m2s
Build and test / Cross-build freebsd (push) Successful in 1m30s
Build and test / Cross-build netbsd (push) Successful in 1m29s
Build and test / Cross-build openbsd (push) Successful in 1m29s
Build and test / Cross-build mobile (push) Successful in 3m7s
smoke-extra / Run windows smoke test (push) Canceled after 0s
smoke / Run self traffic smoke test on macOS (push) Canceled after 0s
Build and test / Test macos (push) Canceled after 0s
Build and test / Test windows (push) Canceled after 0s
Build and test / CI status (push) Canceled after 0s
* save about 10MB of RAM by not importing gopacket except for tests * big ol find-replace
266 lines
8.9 KiB
Go
266 lines
8.9 KiB
Go
package nebula
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"github.com/gaissmai/bart"
|
|
"github.com/slackhq/nebula/firewall"
|
|
"github.com/slackhq/nebula/iputil"
|
|
"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", iputil.IPProtocolTCP, 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", iputil.IPProtocolUDP, 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", iputil.IPProtocolICMP, 2, func() []byte { return echoRequest(8) }}
|
|
icmpv6Echo = l4Proto{"icmpv6", iputil.IPProtocolICMPv6, 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 == iputil.IPProtocolTCP || p.nextHdr == iputil.IPProtocolUDP {
|
|
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 == iputil.IPProtocolTCP || p.nextHdr == iputil.IPProtocolUDP {
|
|
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:]))
|
|
})
|
|
}
|
|
}
|
|
}
|