Files
tailscale/feature/conn25/datapath_test.go
Michael Ben-Ami 6f281ccbcd feature/conn25: add on-remove hook for flows in FlowTable
The hook fires when a flow is removed for any reason (LRU capacity eviction,
tuple-collision displacement, or idle-time expiry). The hook is invoked
exactly once per flow, after the flow table mutex is released, so callbacks
may safely acquire other locks.

We rename the IPMapper interface to Conn25Datapath, and add
ClientFlowCreated/ClientFlowRemoved methods so *Conn25 can keep client-side
address assignments alive while traffic is in flight. Those methods are
currently stubbed for future work.

Connector flows do not currently call these methods.

Updates tailscale/corp#38630
Updates tailscale/corp#43180

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-06-12 10:44:42 -04:00

438 lines
14 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package conn25
import (
"bytes"
"errors"
"net/netip"
"testing"
"go4.org/netipx"
"tailscale.com/net/packet"
"tailscale.com/net/tstun"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
"tailscale.com/util/eventbus/eventbustest"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/filter/filtertype"
)
type testConn25 struct {
clientTransitIPForMagicIPFn func(netip.Addr) (netip.Addr, error)
connectorRealIPForTransitIPConnectionFn func(netip.Addr, netip.Addr) (netip.Addr, error)
}
func (tc *testConn25) ClientTransitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error) {
return tc.clientTransitIPForMagicIPFn(magicIP)
}
func (tc *testConn25) ConnectorRealIPForTransitIPConnection(srcIP netip.Addr, transitIP netip.Addr) (netip.Addr, error) {
return tc.connectorRealIPForTransitIPConnectionFn(srcIP, transitIP)
}
func (tc *testConn25) ClientFlowCreated(transitIP netip.Addr) {}
func (tc *testConn25) ClientFlowRemoved(transitIP netip.Addr) {}
func TestHandlePacketFromTunDevice(t *testing.T) {
clientSrcIP := netip.MustParseAddr("100.70.0.1")
magicIP := netip.MustParseAddr("10.64.0.1")
unusedMagicIP := netip.MustParseAddr("10.64.0.2")
transitIP := netip.MustParseAddr("169.254.0.1")
realIP := netip.MustParseAddr("240.64.0.1")
clientPort := uint16(1234)
serverPort := uint16(80)
tests := []struct {
description string
p *packet.Parsed
throwMappingErr bool
expectedSrc netip.AddrPort
expectedDst netip.AddrPort
expectedFilterResponse filter.Response
}{
{
description: "accept-and-nat-new-client-flow-mapped-magic-ip",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(magicIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(transitIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "drop-unmapped-magic-ip",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(unusedMagicIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(unusedMagicIP, serverPort),
expectedFilterResponse: filter.Drop,
},
{
description: "accept-dont-nat-other-mapping-error",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(magicIP, serverPort),
},
throwMappingErr: true,
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(magicIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "accept-dont-nat-uninteresting-client-side",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(realIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(realIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "accept-dont-nat-uninteresting-connector-side",
p: &packet.Parsed{
Src: netip.AddrPortFrom(realIP, serverPort),
Dst: netip.AddrPortFrom(clientSrcIP, clientPort),
},
expectedSrc: netip.AddrPortFrom(realIP, serverPort),
expectedDst: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedFilterResponse: filter.Accept,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
mock := &testConn25{}
mock.clientTransitIPForMagicIPFn = func(mip netip.Addr) (netip.Addr, error) {
if tt.throwMappingErr {
return netip.Addr{}, errors.New("synthetic mapping error")
}
if mip == magicIP {
return transitIP, nil
}
if mip == unusedMagicIP {
return netip.Addr{}, ErrUnmappedMagicIP
}
return netip.Addr{}, nil
}
dph := newDatapathHandler(mock, t.Logf)
tt.p.IPProto = ipproto.UDP
tt.p.IPVersion = 4
tt.p.StuffForTesting(40)
if want, got := tt.expectedFilterResponse, dph.HandlePacketFromTunDevice(tt.p); want != got {
t.Errorf("unexpected filter response: want %v, got %v", want, got)
}
if want, got := tt.expectedSrc, tt.p.Src; want != got {
t.Errorf("unexpected packet src: want %v, got %v", want, got)
}
if want, got := tt.expectedDst, tt.p.Dst; want != got {
t.Errorf("unexpected packet dst: want %v, got %v", want, got)
}
})
}
}
func newFakeTUN(t *testing.T) *tstun.Wrapper {
t.Helper()
// Create a TUN device and wrap it.
fake := tstun.NewFake()
reg := new(usermetric.Registry)
bus := eventbustest.NewBus(t)
tun := tstun.Wrap(t.Logf, fake, reg, bus)
// Create a packet filter. We're not testing the filter, so just make
// one that allows everything through.
protos := views.SliceOf([]ipproto.Proto{
ipproto.TCP,
ipproto.UDP,
})
allIPs := netip.MustParsePrefix("0.0.0.0/0")
matches := []filter.Match{
{
IPProto: protos,
Srcs: []netip.Prefix{allIPs},
Dsts: []filtertype.NetPortRange{{Net: allIPs, Ports: filtertype.AllPorts}},
},
}
var sb netipx.IPSetBuilder
sb.AddPrefix(allIPs)
ipSet, _ := sb.IPSet()
tun.SetFilter(filter.New(matches, nil, ipSet, ipSet, nil, t.Logf))
// Start the TUN device.
tun.Start()
return tun
}
func TestHandlePacketFromWireGuard(t *testing.T) {
clientSrcIP := netip.MustParseAddr("100.70.0.1")
unknownSrcIP := netip.MustParseAddr("100.99.99.99")
transitIP := netip.MustParseAddr("169.254.0.1")
realIP := netip.MustParseAddr("240.64.0.1")
clientPort := uint16(1234)
serverPort := uint16(80)
tests := []struct {
description string
p *packet.Parsed
throwMappingErr bool
expectedSrc netip.AddrPort
expectedDst netip.AddrPort
expectedFilterResponse filter.Response
expectedInjectedPkt []byte
}{
{
description: "accept-and-nat-new-connector-flow-mapped-src-and-transit-ip",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(transitIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(realIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "drop-unmapped-src-and-transit-ip",
p: &packet.Parsed{
Src: netip.AddrPortFrom(unknownSrcIP, clientPort),
Dst: netip.AddrPortFrom(transitIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(unknownSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(transitIP, serverPort),
expectedFilterResponse: filter.Drop,
expectedInjectedPkt: packet.Generate(packet.TailscaleRejectedHeader{
IPSrc: transitIP,
IPDst: unknownSrcIP,
Proto: ipproto.UDP,
Src: netip.AddrPortFrom(unknownSrcIP, clientPort),
Dst: netip.AddrPortFrom(transitIP, serverPort),
Reason: packet.RejectedDueToUnknownAppConnectorTransitIP,
}, nil),
},
{
description: "accept-dont-nat-other-mapping-error",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(transitIP, serverPort),
},
throwMappingErr: true,
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(transitIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "accept-dont-nat-uninteresting-connector-side",
p: &packet.Parsed{
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(realIP, serverPort),
},
expectedSrc: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedDst: netip.AddrPortFrom(realIP, serverPort),
expectedFilterResponse: filter.Accept,
},
{
description: "accept-dont-nat-uninteresting-client-side",
p: &packet.Parsed{
Src: netip.AddrPortFrom(realIP, serverPort),
Dst: netip.AddrPortFrom(clientSrcIP, clientPort),
},
expectedSrc: netip.AddrPortFrom(realIP, serverPort),
expectedDst: netip.AddrPortFrom(clientSrcIP, clientPort),
expectedFilterResponse: filter.Accept,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
mock := &testConn25{}
mock.connectorRealIPForTransitIPConnectionFn = func(src, tip netip.Addr) (netip.Addr, error) {
if tt.throwMappingErr {
return netip.Addr{}, errors.New("synthetic mapping error")
}
if tip == transitIP {
if src == clientSrcIP {
return realIP, nil
} else {
return netip.Addr{}, ErrUnmappedSrcAndTransitIP
}
}
return netip.Addr{}, nil
}
dph := newDatapathHandler(mock, t.Logf)
tun := newFakeTUN(t)
defer tun.Close()
tt.p.IPProto = ipproto.UDP
tt.p.IPVersion = 4
tt.p.StuffForTesting(40)
if want, got := tt.expectedFilterResponse, dph.HandlePacketFromWireGuard(tt.p, tun); want != got {
t.Errorf("unexpected filter response: want %v, got %v", want, got)
}
if want, got := tt.expectedSrc, tt.p.Src; want != got {
t.Errorf("unexpected packet src: want %v, got %v", want, got)
}
if want, got := tt.expectedDst, tt.p.Dst; want != got {
t.Errorf("unexpected packet dst: want %v, got %v", want, got)
}
if tt.expectedInjectedPkt != nil {
var buf [tstun.MaxPacketSize]byte
bufs := [][]byte{buf[:]}
sizes := []int{0}
n, err := tun.Read(bufs, sizes, 0)
if err != nil {
t.Errorf("error reading injected packet: %v", err)
}
if n != 1 {
t.Errorf("expected to read 1 packet, got %d", n)
}
if want, got := tt.expectedInjectedPkt, buf[:sizes[0]]; !bytes.Equal(want, got) {
t.Errorf("unexpected contents of injected packet: want %+x, got %+x", want, got)
}
}
})
}
}
func TestClientFlowCache(t *testing.T) {
getTransitIPCalled := false
clientSrcIP := netip.MustParseAddr("100.70.0.1")
magicIP := netip.MustParseAddr("10.64.0.1")
transitIP := netip.MustParseAddr("169.254.0.1")
clientPort := uint16(1234)
serverPort := uint16(80)
mock := &testConn25{}
mock.clientTransitIPForMagicIPFn = func(mip netip.Addr) (netip.Addr, error) {
if getTransitIPCalled {
t.Errorf("ClientGetTransitIPForMagicIP unexpectedly called more than once")
}
getTransitIPCalled = true
return transitIP, nil
}
dph := newDatapathHandler(mock, t.Logf)
outgoing := packet.Parsed{
IPProto: ipproto.UDP,
IPVersion: 4,
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(magicIP, serverPort),
}
outgoing.StuffForTesting(40)
o1 := outgoing
if dph.HandlePacketFromTunDevice(&o1) != filter.Accept {
t.Errorf("first call to HandlePacketFromTunDevice was not accepted")
}
if want, got := netip.AddrPortFrom(transitIP, serverPort), o1.Dst; want != got {
t.Errorf("unexpected packet dst after first call: want %v, got %v", want, got)
}
// The second call should use the cache.
o2 := outgoing
if dph.HandlePacketFromTunDevice(&o2) != filter.Accept {
t.Errorf("second call to HandlePacketFromTunDevice was not accepted")
}
if want, got := netip.AddrPortFrom(transitIP, serverPort), o2.Dst; want != got {
t.Errorf("unexpected packet dst after second call: want %v, got %v", want, got)
}
// Return traffic should have the Transit IP as the source,
// and be SNATed to the Magic IP.
incoming := &packet.Parsed{
IPProto: ipproto.UDP,
IPVersion: 4,
Src: netip.AddrPortFrom(transitIP, serverPort),
Dst: netip.AddrPortFrom(clientSrcIP, clientPort),
}
incoming.StuffForTesting(40)
tun := newFakeTUN(t)
defer tun.Close()
if dph.HandlePacketFromWireGuard(incoming, tun) != filter.Accept {
t.Errorf("call to HandlePacketFromWireGuard was not accepted")
}
if want, got := netip.AddrPortFrom(magicIP, serverPort), incoming.Src; want != got {
t.Errorf("unexpected packet src after second call: want %v, got %v", want, got)
}
}
func TestConnectorFlowCache(t *testing.T) {
getRealIPCalled := false
clientSrcIP := netip.MustParseAddr("100.70.0.1")
transitIP := netip.MustParseAddr("169.254.0.1")
realIP := netip.MustParseAddr("240.64.0.1")
clientPort := uint16(1234)
serverPort := uint16(80)
mock := &testConn25{}
mock.connectorRealIPForTransitIPConnectionFn = func(src, tip netip.Addr) (netip.Addr, error) {
if getRealIPCalled {
t.Errorf("ConnectorRealIPForTransitIPConnection unexpectedly called more than once")
}
getRealIPCalled = true
return realIP, nil
}
dph := newDatapathHandler(mock, t.Logf)
outgoing := packet.Parsed{
IPProto: ipproto.UDP,
IPVersion: 4,
Src: netip.AddrPortFrom(clientSrcIP, clientPort),
Dst: netip.AddrPortFrom(transitIP, serverPort),
}
outgoing.StuffForTesting(40)
tun := newFakeTUN(t)
defer tun.Close()
o1 := outgoing
if dph.HandlePacketFromWireGuard(&o1, tun) != filter.Accept {
t.Errorf("first call to HandlePacketFromWireGuard was not accepted")
}
if want, got := netip.AddrPortFrom(realIP, serverPort), o1.Dst; want != got {
t.Errorf("unexpected packet dst after first call: want %v, got %v", want, got)
}
// The second call should use the cache.
o2 := outgoing
if dph.HandlePacketFromWireGuard(&o2, tun) != filter.Accept {
t.Errorf("second call to HandlePacketFromWireGuard was not accepted")
}
if want, got := netip.AddrPortFrom(realIP, serverPort), o2.Dst; want != got {
t.Errorf("unexpected packet dst after second call: want %v, got %v", want, got)
}
// Return traffic should have the Real IP as the source,
// and be SNATed to the Transit IP.
incoming := &packet.Parsed{
IPProto: ipproto.UDP,
IPVersion: 4,
Src: netip.AddrPortFrom(realIP, serverPort),
Dst: netip.AddrPortFrom(clientSrcIP, clientPort),
}
incoming.StuffForTesting(40)
if dph.HandlePacketFromTunDevice(incoming) != filter.Accept {
t.Errorf("call to HandlePacketFromTunDevice was not accepted")
}
if want, got := netip.AddrPortFrom(transitIP, serverPort), incoming.Src; want != got {
t.Errorf("unexpected packet src after second call: want %v, got %v", want, got)
}
}