tailscale/net/tstun/wrap_test.go
Anton Tolchanov b4f46c31bb
Some checks failed
CI / windows (push) Has been cancelled
CI / vm (push) Has been cancelled
checklocks / checklocks (push) Successful in 1m0s
Dockerfile build / deploy (push) Successful in 3m53s
CI / race-root-integration (1/4) (push) Failing after 2m36s
CodeQL / Analyze (go) (push) Failing after 9m13s
CI / race-root-integration (2/4) (push) Successful in 2m33s
CI / race-root-integration (3/4) (push) Successful in 3m46s
CI / race-root-integration (4/4) (push) Failing after 3m43s
CI / test (-race, amd64, 1/3) (push) Failing after 18m39s
CI / test (-coverprofile=/tmp/coverage.out, amd64) (push) Failing after 19m19s
CI / test (-race, amd64, 2/3) (push) Failing after 17m8s
CI / test (-race, amd64, 3/3) (push) Failing after 17m23s
CI / privileged (push) Failing after 30s
CI / race-build (push) Successful in 10m14s
CI / test (386) (push) Failing after 15m46s
CI / cross (386, linux) (push) Successful in 16m47s
CI / cross (amd64, darwin) (push) Successful in 16m50s
CI / cross (amd64, freebsd) (push) Successful in 16m45s
CI / cross (amd64, openbsd) (push) Successful in 16m44s
CI / cross (amd64, windows) (push) Successful in 16m18s
CI / cross (arm, 5, linux) (push) Successful in 16m34s
CI / cross (arm, 7, linux) (push) Successful in 16m27s
CI / cross (arm64, darwin) (push) Successful in 17m30s
CI / cross (arm64, linux) (push) Successful in 16m25s
CI / cross (arm64, windows) (push) Successful in 15m45s
CI / ios (push) Successful in 1m36s
CI / cross (loong64, linux) (push) Successful in 16m30s
CI / crossmin (amd64, plan9) (push) Successful in 10m38s
CI / android (push) Successful in 1m26s
CI / crossmin (ppc64, aix) (push) Successful in 10m43s
CI / tailscale_go (push) Successful in 45s
CI / fuzz (push) Has been skipped
CI / notify_slack (push) Has been cancelled
CI / check_mergeability (push) Has been cancelled
CI / depaware (push) Successful in 1m1s
CI / go_generate (push) Successful in 2m11s
CI / go_mod_tidy (push) Successful in 59s
CI / licenses (push) Successful in 9s
CI / staticcheck (386, windows) (push) Failing after 1m15s
CI / staticcheck (amd64, darwin) (push) Failing after 1m19s
CI / staticcheck (amd64, linux) (push) Failing after 1m19s
CI / staticcheck (amd64, windows) (push) Failing after 1m13s
CI / wasm (push) Successful in 27m59s
wgengine/magicsock: export packet drop metric for outbound errors
This required sharing the dropped packet metric between two packages
(tstun and magicsock), so I've moved its definition to util/usermetric.

Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-31 08:33:24 +00:00

962 lines
24 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tstun
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"expvar"
"fmt"
"net/netip"
"reflect"
"strconv"
"strings"
"testing"
"time"
"unicode"
"unsafe"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tailscale/wireguard-go/tun/tuntest"
"go4.org/mem"
"go4.org/netipx"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/disco"
"tailscale.com/net/connstats"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/tstest"
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netlogtype"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/must"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg"
)
func udp4(src, dst string, sport, dport uint16) []byte {
sip, err := netip.ParseAddr(src)
if err != nil {
panic(err)
}
dip, err := netip.ParseAddr(dst)
if err != nil {
panic(err)
}
header := &packet.UDP4Header{
IP4Header: packet.IP4Header{
Src: sip,
Dst: dip,
IPID: 0,
},
SrcPort: sport,
DstPort: dport,
}
return packet.Generate(header, []byte("udp_payload"))
}
func tcp4syn(src, dst string, sport, dport uint16) []byte {
sip, err := netip.ParseAddr(src)
if err != nil {
panic(err)
}
dip, err := netip.ParseAddr(dst)
if err != nil {
panic(err)
}
ipHeader := packet.IP4Header{
IPProto: ipproto.TCP,
Src: sip,
Dst: dip,
IPID: 0,
}
tcpHeader := make([]byte, 20)
binary.BigEndian.PutUint16(tcpHeader[0:], sport)
binary.BigEndian.PutUint16(tcpHeader[2:], dport)
tcpHeader[13] |= 2 // SYN
both := packet.Generate(ipHeader, tcpHeader)
// 20 byte IP4 + 20 byte TCP
binary.BigEndian.PutUint16(both[2:4], 40)
return both
}
func nets(nets ...string) (ret []netip.Prefix) {
for _, s := range nets {
if i := strings.IndexByte(s, '/'); i == -1 {
ip, err := netip.ParseAddr(s)
if err != nil {
panic(err)
}
bits := uint8(32)
if ip.Is6() {
bits = 128
}
ret = append(ret, netip.PrefixFrom(ip, int(bits)))
} else {
pfx, err := netip.ParsePrefix(s)
if err != nil {
panic(err)
}
ret = append(ret, pfx)
}
}
return ret
}
func ports(s string) filter.PortRange {
if s == "*" {
return filter.PortRange{First: 0, Last: 65535}
}
var fs, ls string
i := strings.IndexByte(s, '-')
if i == -1 {
fs = s
ls = fs
} else {
fs = s[:i]
ls = s[i+1:]
}
first, err := strconv.ParseInt(fs, 10, 16)
if err != nil {
panic(fmt.Sprintf("invalid NetPortRange %q", s))
}
last, err := strconv.ParseInt(ls, 10, 16)
if err != nil {
panic(fmt.Sprintf("invalid NetPortRange %q", s))
}
return filter.PortRange{First: uint16(first), Last: uint16(last)}
}
func netports(netPorts ...string) (ret []filter.NetPortRange) {
for _, s := range netPorts {
i := strings.LastIndexByte(s, ':')
if i == -1 {
panic(fmt.Sprintf("invalid NetPortRange %q", s))
}
npr := filter.NetPortRange{
Net: nets(s[:i])[0],
Ports: ports(s[i+1:]),
}
ret = append(ret, npr)
}
return ret
}
func setfilter(logf logger.Logf, tun *Wrapper) {
protos := views.SliceOf([]ipproto.Proto{
ipproto.TCP,
ipproto.UDP,
})
matches := []filter.Match{
{IPProto: protos, Srcs: nets("5.6.7.8"), Dsts: netports("1.2.3.4:89-90")},
{IPProto: protos, Srcs: nets("1.2.3.4"), Dsts: netports("5.6.7.8:98")},
}
var sb netipx.IPSetBuilder
sb.AddPrefix(netip.MustParsePrefix("1.2.0.0/16"))
ipSet, _ := sb.IPSet()
tun.SetFilter(filter.New(matches, nil, ipSet, ipSet, nil, logf))
}
func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) {
chtun := tuntest.NewChannelTUN()
reg := new(usermetric.Registry)
tun := Wrap(logf, chtun.TUN(), reg)
if secure {
setfilter(logf, tun)
} else {
tun.disableFilter = true
}
tun.Start()
return chtun, tun
}
func newFakeTUN(logf logger.Logf, secure bool) (*fakeTUN, *Wrapper) {
ftun := NewFake()
reg := new(usermetric.Registry)
tun := Wrap(logf, ftun, reg)
if secure {
setfilter(logf, tun)
} else {
tun.disableFilter = true
}
return ftun.(*fakeTUN), tun
}
func TestReadAndInject(t *testing.T) {
chtun, tun := newChannelTUN(t.Logf, false)
defer tun.Close()
const size = 2 // all payloads have this size
written := []string{"w0", "w1"}
injected := []string{"i0", "i1"}
go func() {
for _, packet := range written {
payload := []byte(packet)
chtun.Outbound <- payload
}
}()
for _, packet := range injected {
go func(packet string) {
payload := []byte(packet)
err := tun.InjectOutbound(payload)
if err != nil {
t.Errorf("%s: error: %v", packet, err)
}
}(packet)
}
var buf [MaxPacketSize]byte
var seen = make(map[string]bool)
sizes := make([]int, 1)
// We expect the same packets back, in no particular order.
for i := range len(written) + len(injected) {
packet := buf[:]
buffs := [][]byte{packet}
numPackets, err := tun.Read(buffs, sizes, 0)
if err != nil {
t.Errorf("read %d: error: %v", i, err)
}
if numPackets != 1 {
t.Fatalf("read %d packets, expected %d", numPackets, 1)
}
packet = packet[:sizes[0]]
packetLen := len(packet)
if packetLen != size {
t.Errorf("read %d: got size %d; want %d", i, packetLen, size)
}
got := string(packet)
t.Logf("read %d: got %s", i, got)
seen[got] = true
}
for _, packet := range written {
if !seen[packet] {
t.Errorf("%s not received", packet)
}
}
for _, packet := range injected {
if !seen[packet] {
t.Errorf("%s not received", packet)
}
}
}
func TestWriteAndInject(t *testing.T) {
chtun, tun := newChannelTUN(t.Logf, false)
defer tun.Close()
written := []string{"w0", "w1"}
injected := []string{"i0", "i1"}
go func() {
for _, packet := range written {
payload := []byte(packet)
_, err := tun.Write([][]byte{payload}, 0)
if err != nil {
t.Errorf("%s: error: %v", packet, err)
}
}
}()
for _, packet := range injected {
go func(packet string) {
payload := []byte(packet)
err := tun.InjectInboundCopy(payload)
if err != nil {
t.Errorf("%s: error: %v", packet, err)
}
}(packet)
}
seen := make(map[string]bool)
// We expect the same packets back, in no particular order.
for i := range len(written) + len(injected) {
packet := <-chtun.Inbound
got := string(packet)
t.Logf("read %d: got %s", i, got)
seen[got] = true
}
for _, packet := range written {
if !seen[packet] {
t.Errorf("%s not received", packet)
}
}
for _, packet := range injected {
if !seen[packet] {
t.Errorf("%s not received", packet)
}
}
}
// mustHexDecode is like hex.DecodeString, but panics on error
// and ignores whitespace in s.
func mustHexDecode(s string) []byte {
return must.Get(hex.DecodeString(strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1
}
return r
}, s)))
}
func TestFilter(t *testing.T) {
chtun, tun := newChannelTUN(t.Logf, true)
defer tun.Close()
// Reset the metrics before test. These are global
// so the different tests might have affected them.
tun.metrics.inboundDroppedPacketsTotal.ResetAllForTest()
tun.metrics.outboundDroppedPacketsTotal.ResetAllForTest()
type direction int
const (
in direction = iota
out
)
tests := []struct {
name string
dir direction
drop bool
data []byte
}{
{"short_in", in, true, []byte("\x45xxx")},
{"short_out", out, true, []byte("\x45xxx")},
{"ip97_out", out, false, mustHexDecode("4500 0019 d186 4000 4061 751d 644a 4603 6449 e549 6865 6c6c 6f")},
{"bad_port_in", in, true, udp4("5.6.7.8", "1.2.3.4", 22, 22)},
{"bad_port_out", out, false, udp4("1.2.3.4", "5.6.7.8", 22, 22)},
{"bad_ip_in", in, true, udp4("8.1.1.1", "1.2.3.4", 89, 89)},
{"bad_ip_out", out, false, udp4("1.2.3.4", "8.1.1.1", 98, 98)},
{"good_packet_in", in, false, udp4("5.6.7.8", "1.2.3.4", 89, 89)},
{"good_packet_out", out, false, udp4("1.2.3.4", "5.6.7.8", 98, 98)},
}
// A reader on the other end of the tun.
go func() {
var recvbuf []byte
for {
select {
case <-tun.closed:
return
case recvbuf = <-chtun.Inbound:
// continue
}
for _, tt := range tests {
if tt.drop && bytes.Equal(recvbuf, tt.data) {
t.Errorf("did not drop %s", tt.name)
}
}
}
}()
var buf [MaxPacketSize]byte
stats := connstats.NewStatistics(0, 0, nil)
defer stats.Shutdown(context.Background())
tun.SetStatistics(stats)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var n int
var err error
var filtered bool
sizes := make([]int, 1)
tunStats, _ := stats.TestExtract()
if len(tunStats) > 0 {
t.Errorf("connstats.Statistics.Extract = %v, want {}", stats)
}
if tt.dir == in {
// Use the side effect of updating the last
// activity atomic to determine whether the
// data was actually filtered.
// If it stays zero, nothing made it through
// to the wrapped TUN.
tun.lastActivityAtomic.StoreAtomic(0)
_, err = tun.Write([][]byte{tt.data}, 0)
filtered = tun.lastActivityAtomic.LoadAtomic() == 0
} else {
chtun.Outbound <- tt.data
n, err = tun.Read([][]byte{buf[:]}, sizes, 0)
// In the read direction, errors are fatal, so we return n = 0 instead.
filtered = (n == 0)
}
if err != nil {
t.Errorf("got err %v; want nil", err)
}
if filtered {
if !tt.drop {
t.Errorf("got drop; want accept")
}
} else {
if tt.drop {
t.Errorf("got accept; want drop")
}
}
got, _ := stats.TestExtract()
want := map[netlogtype.Connection]netlogtype.Counts{}
var wasUDP bool
if !tt.drop {
var p packet.Parsed
p.Decode(tt.data)
wasUDP = p.IPProto == ipproto.UDP
switch tt.dir {
case in:
conn := netlogtype.Connection{Proto: ipproto.UDP, Src: p.Dst, Dst: p.Src}
want[conn] = netlogtype.Counts{RxPackets: 1, RxBytes: uint64(len(tt.data))}
case out:
conn := netlogtype.Connection{Proto: ipproto.UDP, Src: p.Src, Dst: p.Dst}
want[conn] = netlogtype.Counts{TxPackets: 1, TxBytes: uint64(len(tt.data))}
}
}
if wasUDP {
if diff := cmp.Diff(got, want, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("stats.TestExtract (-got +want):\n%s", diff)
}
}
})
}
var metricInboundDroppedPacketsACL, metricInboundDroppedPacketsErr, metricOutboundDroppedPacketsACL int64
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonACL}).(*expvar.Int); ok {
metricInboundDroppedPacketsACL = m.Value()
}
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonError}).(*expvar.Int); ok {
metricInboundDroppedPacketsErr = m.Value()
}
if m, ok := tun.metrics.outboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonACL}).(*expvar.Int); ok {
metricOutboundDroppedPacketsACL = m.Value()
}
assertMetricPackets(t, "inACL", 3, metricInboundDroppedPacketsACL)
assertMetricPackets(t, "inError", 0, metricInboundDroppedPacketsErr)
assertMetricPackets(t, "outACL", 1, metricOutboundDroppedPacketsACL)
}
func assertMetricPackets(t *testing.T, metricName string, want, got int64) {
t.Helper()
if want != got {
t.Errorf("%s got unexpected value, got %d, want %d", metricName, got, want)
}
}
func TestAllocs(t *testing.T) {
ftun, tun := newFakeTUN(t.Logf, false)
defer tun.Close()
buf := [][]byte{{0x00}}
err := tstest.MinAllocsPerRun(t, 0, func() {
_, err := ftun.Write(buf, 0)
if err != nil {
t.Errorf("write: error: %v", err)
return
}
})
if err != nil {
t.Error(err)
}
}
func TestClose(t *testing.T) {
ftun, tun := newFakeTUN(t.Logf, false)
data := [][]byte{udp4("1.2.3.4", "5.6.7.8", 98, 98)}
_, err := ftun.Write(data, 0)
if err != nil {
t.Error(err)
}
tun.Close()
_, err = ftun.Write(data, 0)
if err == nil {
t.Error("Expected error from ftun.Write() after Close()")
}
}
func BenchmarkWrite(b *testing.B) {
b.ReportAllocs()
ftun, tun := newFakeTUN(b.Logf, true)
defer tun.Close()
packet := [][]byte{udp4("5.6.7.8", "1.2.3.4", 89, 89)}
for range b.N {
_, err := ftun.Write(packet, 0)
if err != nil {
b.Errorf("err = %v; want nil", err)
}
}
}
func TestAtomic64Alignment(t *testing.T) {
off := unsafe.Offsetof(Wrapper{}.lastActivityAtomic)
if off%8 != 0 {
t.Errorf("offset %v not 8-byte aligned", off)
}
c := new(Wrapper)
c.lastActivityAtomic.StoreAtomic(mono.Now())
}
func TestPeerAPIBypass(t *testing.T) {
reg := new(usermetric.Registry)
wrapperWithPeerAPI := &Wrapper{
PeerAPIPort: func(ip netip.Addr) (port uint16, ok bool) {
if ip == netip.MustParseAddr("100.64.1.2") {
return 60000, true
}
return
},
metrics: registerMetrics(reg),
}
tests := []struct {
name string
w *Wrapper
filter *filter.Filter
pkt []byte
want filter.Response
}{
{
name: "reject_nil_filter",
w: &Wrapper{
PeerAPIPort: func(netip.Addr) (port uint16, ok bool) {
return 60000, true
},
metrics: registerMetrics(reg),
},
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "reject_with_filter",
w: &Wrapper{
metrics: registerMetrics(reg),
},
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "peerapi_bypass_filter",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Accept,
},
{
name: "peerapi_dont_bypass_filter_wrong_port",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60001),
want: filter.Drop,
},
{
name: "peerapi_dont_bypass_filter_wrong_dst_ip",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.3", 1234, 60000),
want: filter.Drop,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := new(packet.Parsed)
p.Decode(tt.pkt)
tt.w.SetFilter(tt.filter)
tt.w.disableTSMPRejected = true
tt.w.logf = t.Logf
if got, _ := tt.w.filterPacketInboundFromWireGuard(p, nil, nil, nil); got != tt.want {
t.Errorf("got = %v; want %v", got, tt.want)
}
})
}
}
// Issue 1526: drop disco frames from ourselves.
func TestFilterDiscoLoop(t *testing.T) {
var memLog tstest.MemLogger
discoPub := key.DiscoPublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 31: 0}))
tw := &Wrapper{logf: memLog.Logf, limitedLogf: memLog.Logf}
tw.SetDiscoKey(discoPub)
uh := packet.UDP4Header{
IP4Header: packet.IP4Header{
IPProto: ipproto.UDP,
Src: netaddr.IPv4(1, 2, 3, 4),
Dst: netaddr.IPv4(5, 6, 7, 8),
},
SrcPort: 9,
DstPort: 10,
}
discobs := discoPub.Raw32()
discoPayload := fmt.Sprintf("%s%s%s", disco.Magic, discobs[:], [disco.NonceLen]byte{})
pkt := make([]byte, uh.Len()+len(discoPayload))
uh.Marshal(pkt)
copy(pkt[uh.Len():], discoPayload)
p := new(packet.Parsed)
p.Decode(pkt)
got, _ := tw.filterPacketInboundFromWireGuard(p, nil, nil, nil)
if got != filter.DropSilently {
t.Errorf("got %v; want DropSilently", got)
}
if got, want := memLog.String(), "[unexpected] received self disco in packet over tstun; dropping\n"; got != want {
t.Errorf("log output mismatch\n got: %q\nwant: %q\n", got, want)
}
memLog.Reset()
pp := new(packet.Parsed)
pp.Decode(pkt)
got, _ = tw.filterPacketOutboundToWireGuard(pp, nil, nil)
if got != filter.DropSilently {
t.Errorf("got %v; want DropSilently", got)
}
if got, want := memLog.String(), "[unexpected] received self disco out packet over tstun; dropping\n"; got != want {
t.Errorf("log output mismatch\n got: %q\nwant: %q\n", got, want)
}
}
// TODO(andrew-d): refactor this test to no longer use addrFam, after #11945
// removed it in peerConfigFromWGConfig
func TestPeerCfg_NAT(t *testing.T) {
node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) wgcfg.Peer {
p := wgcfg.Peer{
PublicKey: key.NewNode().Public(),
AllowedIPs: []netip.Prefix{
netip.PrefixFrom(ip, ip.BitLen()),
},
}
if masqIP.Is4() {
p.V4MasqAddr = ptr.To(masqIP)
} else {
p.V6MasqAddr = ptr.To(masqIP)
}
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
return p
}
test := func(addrFam ipproto.Version) {
var (
noIP netip.Addr
selfNativeIP = netip.MustParseAddr("100.64.0.1")
selfEIP1 = netip.MustParseAddr("100.64.1.1")
selfEIP2 = netip.MustParseAddr("100.64.1.2")
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
peer1IP = netip.MustParseAddr("100.64.0.2")
peer2IP = netip.MustParseAddr("100.64.0.3")
subnet = netip.MustParsePrefix("192.168.0.0/24")
subnetIP = netip.MustParseAddr("192.168.0.1")
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
publicIP = netip.MustParseAddr("8.8.8.8")
)
if addrFam == ipproto.Version6 {
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
peer1IP = netip.MustParseAddr("fd7a:115c:a1e0::b")
peer2IP = netip.MustParseAddr("fd7a:115c:a1e0::c")
subnet = netip.MustParsePrefix("2001:db8::/32")
subnetIP = netip.MustParseAddr("2001:db8::FFFF")
exitRoute = netip.MustParsePrefix("::/0")
publicIP = netip.MustParseAddr("2001:4860:4860::8888")
}
type dnatTest struct {
src netip.Addr
dst netip.Addr
want netip.Addr // new destination after DNAT
}
tests := []struct {
name string
wcfg *wgcfg.Config
snatMap map[netip.Addr]netip.Addr // dst -> src
dnat []dnatTest
}{
{
name: "no-cfg",
wcfg: nil,
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfEIP1},
{peer2IP, selfEIP2, selfEIP2},
},
},
{
name: "single-peer-requires-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP2),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP2,
subnetIP: selfNativeIP,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfEIP1},
{peer2IP, selfEIP2, selfNativeIP}, // NATed
{peer2IP, subnetIP, subnetIP},
},
},
{
name: "multiple-peers-require-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfNativeIP,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfNativeIP},
{peer2IP, selfEIP2, selfNativeIP},
{peer2IP, subnetIP, subnetIP},
},
},
{
name: "multiple-peers-require-nat-with-subnet",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, subnet),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfEIP2,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfNativeIP},
{peer2IP, selfEIP2, selfNativeIP},
{peer2IP, subnetIP, subnetIP},
},
},
{
name: "multiple-peers-require-nat-with-default-route",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, exitRoute),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
publicIP: selfEIP2,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfNativeIP},
{peer2IP, selfEIP2, selfNativeIP},
{peer2IP, subnetIP, subnetIP},
},
},
{
name: "no-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, noIP),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer1IP, selfEIP1, selfEIP1},
{peer2IP, selfEIP2, selfEIP2},
{peer2IP, subnetIP, subnetIP},
},
},
{
name: "exit-node-require-nat-peer-doesnt",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP2, exitRoute),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP2,
publicIP: selfEIP2,
},
dnat: []dnatTest{
{selfNativeIP, selfNativeIP, selfNativeIP},
{peer2IP, selfEIP2, selfNativeIP},
{peer2IP, subnetIP, subnetIP},
},
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) {
pcfg := peerConfigTableFromWGConfig(tc.wcfg)
for peer, want := range tc.snatMap {
if got := pcfg.selectSrcIP(selfNativeIP, peer); got != want {
t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
}
}
for i, dt := range tc.dnat {
if got := pcfg.mapDstIP(dt.src, dt.dst); got != dt.want {
t.Errorf("dnat[%d]: mapDstIP[%v, %v]: got %v; want %v", i, dt.src, dt.dst, got, dt.want)
}
}
if t.Failed() {
t.Logf("%v", pcfg)
}
})
}
}
test(ipproto.Version4)
test(ipproto.Version6)
}
// TestCaptureHook verifies that the Wrapper.captureHook callback is called
// with the correct parameters when various packet operations are performed.
func TestCaptureHook(t *testing.T) {
type captureRecord struct {
path capture.Path
now time.Time
pkt []byte
meta packet.CaptureMeta
}
var captured []captureRecord
hook := func(path capture.Path, now time.Time, pkt []byte, meta packet.CaptureMeta) {
captured = append(captured, captureRecord{
path: path,
now: now,
pkt: pkt,
meta: meta,
})
}
now := time.Unix(1682085856, 0)
_, w := newFakeTUN(t.Logf, true)
w.timeNow = func() time.Time {
return now
}
w.InstallCaptureHook(hook)
defer w.Close()
// Loop reading and discarding packets; this ensures that we don't have
// packets stuck in vectorOutbound
go func() {
var (
buf [MaxPacketSize]byte
sizes = make([]int, 1)
)
for {
_, err := w.Read([][]byte{buf[:]}, sizes, 0)
if err != nil {
return
}
}
}()
// Do operations that should result in a packet being captured.
w.Write([][]byte{
[]byte("Write1"),
[]byte("Write2"),
}, 0)
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData([]byte("InjectInboundPacketBuffer")),
})
buffs := make([][]byte, 1)
buffs[0] = make([]byte, PacketStartOffset+packetBuf.Size())
sizes := make([]int, 1)
w.InjectInboundPacketBuffer(packetBuf, buffs, sizes)
packetBuf = stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData([]byte("InjectOutboundPacketBuffer")),
})
w.InjectOutboundPacketBuffer(packetBuf)
// TODO: test Read
// TODO: determine if we want InjectOutbound to log
// Assert that the right packets are captured.
want := []captureRecord{
{
path: capture.FromPeer,
pkt: []byte("Write1"),
},
{
path: capture.FromPeer,
pkt: []byte("Write2"),
},
{
path: capture.SynthesizedToLocal,
pkt: []byte("InjectInboundPacketBuffer"),
},
{
path: capture.SynthesizedToPeer,
pkt: []byte("InjectOutboundPacketBuffer"),
},
}
for i := range len(want) {
want[i].now = now
}
if !reflect.DeepEqual(captured, want) {
t.Errorf("mismatch between captured and expected packets\ngot: %+v\nwant: %+v",
captured, want)
}
}