mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-13 11:18:52 +00:00

Fragmented datagrams would be processed instead of being dumped right away. In reality, thse datagrams would be dropped anyway later so there should functionally not be any change. Additionally, the feature is off by default. Closes #16203 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
225 lines
6.2 KiB
Go
225 lines
6.2 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package magicsock
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"golang.org/x/net/bpf"
|
|
"golang.org/x/sys/cpu"
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/disco"
|
|
)
|
|
|
|
func TestParseUDPPacket(t *testing.T) {
|
|
src4 := netip.MustParseAddrPort("127.0.0.1:12345")
|
|
dst4 := netip.MustParseAddrPort("127.0.0.2:54321")
|
|
|
|
src6 := netip.MustParseAddrPort("[::1]:12345")
|
|
dst6 := netip.MustParseAddrPort("[::2]:54321")
|
|
|
|
udp4Packet := []byte{
|
|
// IPv4 header
|
|
0x45, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x11, 0x00, 0x00,
|
|
0x7f, 0x00, 0x00, 0x01, // source ip
|
|
0x7f, 0x00, 0x00, 0x02, // dest ip
|
|
|
|
// UDP header
|
|
0x30, 0x39, // src port
|
|
0xd4, 0x31, // dest port
|
|
0x00, 0x12, // length; 8 bytes header + 10 bytes payload = 18 bytes
|
|
0x00, 0x00, // checksum; unused
|
|
|
|
// Payload: disco magic plus 4 bytes
|
|
0x54, 0x53, 0xf0, 0x9f, 0x92, 0xac, 0x00, 0x01, 0x02, 0x03,
|
|
}
|
|
udp6Packet := []byte{
|
|
// IPv6 header
|
|
0x60, 0x00, 0x00, 0x00,
|
|
0x00, 0x12, // payload length
|
|
0x11, // next header: UDP
|
|
0x00, // hop limit; unused
|
|
|
|
// Source IP
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
|
// Dest IP
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
|
|
|
// UDP header
|
|
0x30, 0x39, // src port
|
|
0xd4, 0x31, // dest port
|
|
0x00, 0x12, // length; 8 bytes header + 10 bytes payload = 18 bytes
|
|
0x00, 0x00, // checksum; unused
|
|
|
|
// Payload: disco magic plus 4 bytes
|
|
0x54, 0x53, 0xf0, 0x9f, 0x92, 0xac, 0x00, 0x01, 0x02, 0x03,
|
|
}
|
|
|
|
// Verify that parsing the UDP packet works correctly.
|
|
t.Run("IPv4", func(t *testing.T) {
|
|
src, dst, payload := parseUDPPacket(udp4Packet, false)
|
|
if src != src4 {
|
|
t.Errorf("src = %v; want %v", src, src4)
|
|
}
|
|
if dst != dst4 {
|
|
t.Errorf("dst = %v; want %v", dst, dst4)
|
|
}
|
|
if !bytes.HasPrefix(payload, []byte(disco.Magic)) {
|
|
t.Errorf("payload = %x; must start with %x", payload, disco.Magic)
|
|
}
|
|
})
|
|
t.Run("IPv6", func(t *testing.T) {
|
|
src, dst, payload := parseUDPPacket(udp6Packet, true)
|
|
if src != src6 {
|
|
t.Errorf("src = %v; want %v", src, src6)
|
|
}
|
|
if dst != dst6 {
|
|
t.Errorf("dst = %v; want %v", dst, dst6)
|
|
}
|
|
if !bytes.HasPrefix(payload, []byte(disco.Magic)) {
|
|
t.Errorf("payload = %x; must start with %x", payload, disco.Magic)
|
|
}
|
|
})
|
|
t.Run("Truncated", func(t *testing.T) {
|
|
truncateBy := func(b []byte, n int) []byte {
|
|
if n >= len(b) {
|
|
return nil
|
|
}
|
|
return b[:len(b)-n]
|
|
}
|
|
|
|
src, dst, payload := parseUDPPacket(truncateBy(udp4Packet, 11), false)
|
|
if payload != nil {
|
|
t.Errorf("payload = %x; want nil", payload)
|
|
}
|
|
if src.IsValid() || dst.IsValid() {
|
|
t.Errorf("src = %v, dst = %v; want invalid", src, dst)
|
|
}
|
|
|
|
src, dst, payload = parseUDPPacket(truncateBy(udp6Packet, 11), true)
|
|
if payload != nil {
|
|
t.Errorf("payload = %x; want nil", payload)
|
|
}
|
|
if src.IsValid() || dst.IsValid() {
|
|
t.Errorf("src = %v, dst = %v; want invalid", src, dst)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEthernetProto(t *testing.T) {
|
|
htons := func(x uint16) int {
|
|
// Network byte order is big-endian; write the value as
|
|
// big-endian to a byte slice and read it back in the native
|
|
// endian-ness. This is a no-op on a big-endian platform and a
|
|
// byte swap on a little-endian platform.
|
|
var b [2]byte
|
|
binary.BigEndian.PutUint16(b[:], x)
|
|
return int(binary.NativeEndian.Uint16(b[:]))
|
|
}
|
|
|
|
if v4 := ethernetProtoIPv4(); v4 != htons(unix.ETH_P_IP) {
|
|
t.Errorf("ethernetProtoIPv4 = 0x%04x; want 0x%04x", v4, htons(unix.ETH_P_IP))
|
|
}
|
|
if v6 := ethernetProtoIPv6(); v6 != htons(unix.ETH_P_IPV6) {
|
|
t.Errorf("ethernetProtoIPv6 = 0x%04x; want 0x%04x", v6, htons(unix.ETH_P_IPV6))
|
|
}
|
|
|
|
// As a way to verify that the htons function is working correctly,
|
|
// assert that the ETH_P_IP value returned from our function matches
|
|
// the value defined in the unix package based on whether the host is
|
|
// big-endian (network byte order) or little-endian.
|
|
if cpu.IsBigEndian {
|
|
if v4 := ethernetProtoIPv4(); v4 != unix.ETH_P_IP {
|
|
t.Errorf("ethernetProtoIPv4 = 0x%04x; want 0x%04x", v4, unix.ETH_P_IP)
|
|
}
|
|
} else {
|
|
if v4 := ethernetProtoIPv4(); v4 == unix.ETH_P_IP {
|
|
t.Errorf("ethernetProtoIPv4 = 0x%04x; want 0x%04x", v4, htons(unix.ETH_P_IP))
|
|
} else {
|
|
t.Logf("ethernetProtoIPv4 = 0x%04x, correctly different from 0x%04x", v4, unix.ETH_P_IP)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBpfDiscardV4(t *testing.T) {
|
|
// Good packet as a reference for what should not be rejected
|
|
udp4Packet := []byte{
|
|
// IPv4 header
|
|
0x45, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x11, 0x00, 0x00,
|
|
0x7f, 0x00, 0x00, 0x01, // source ip
|
|
0x7f, 0x00, 0x00, 0x02, // dest ip
|
|
|
|
// UDP header
|
|
0x30, 0x39, // src port
|
|
0xd4, 0x31, // dest port
|
|
0x00, 0x12, // length; 8 bytes header + 10 bytes payload = 18 bytes
|
|
0x00, 0x00, // checksum; unused
|
|
|
|
// Payload: disco magic plus 32 bytes for key and 24 bytes for nonce
|
|
0x54, 0x53, 0xf0, 0x9f, 0x92, 0xac, 0x00, 0x01, 0x02, 0x03,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
}
|
|
|
|
vm, err := bpf.NewVM(magicsockFilterV4)
|
|
if err != nil {
|
|
t.Fatalf("failed creating BPF VM: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
replace map[int]byte
|
|
accept bool
|
|
}{
|
|
{
|
|
name: "base accepted datagram",
|
|
replace: map[int]byte{},
|
|
accept: true,
|
|
},
|
|
{
|
|
name: "more fragments",
|
|
replace: map[int]byte{
|
|
6: 0x20,
|
|
},
|
|
accept: false,
|
|
},
|
|
{
|
|
name: "some fragment",
|
|
replace: map[int]byte{
|
|
7: 0x01,
|
|
},
|
|
accept: false,
|
|
},
|
|
}
|
|
|
|
udp4PacketChanged := make([]byte, len(udp4Packet))
|
|
copy(udp4PacketChanged, udp4Packet)
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
for k, v := range tt.replace {
|
|
udp4PacketChanged[k] = v
|
|
}
|
|
ret, err := vm.Run(udp4PacketChanged)
|
|
if err != nil {
|
|
t.Fatalf("BPF VM error: %v", err)
|
|
}
|
|
|
|
if (ret != 0) != tt.accept {
|
|
t.Errorf("expected accept=%v, got ret=%v", tt.accept, ret)
|
|
}
|
|
})
|
|
}
|
|
}
|