Files
tailscale/feature/conn25/datapath_test.go
Michael Ben-Ami 156e6ae5cd feature/conn25: install all the hooks
Install the previously uninstalled hooks for the filter and tstun
intercepts. Move the DNS manager hook installation into Init() with all
the others. Protect all implementations with a short-circuit if the node
is not configured to use Connectors 2025. The short-circuit pattern
replaces the previous pattern used in managing the DNS manager hook, of
setting it to nil in response to CapMap changes.

Fixes tailscale/corp#38716

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-03-27 11:52:34 -04:00

362 lines
12 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package conn25
import (
"errors"
"net/netip"
"testing"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
"tailscale.com/wgengine/filter"
)
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 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 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
}{
{
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,
},
{
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)
tt.p.IPProto = ipproto.UDP
tt.p.IPVersion = 4
tt.p.StuffForTesting(40)
if want, got := tt.expectedFilterResponse, dph.HandlePacketFromWireGuard(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 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)
if dph.HandlePacketFromWireGuard(incoming) != 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)
o1 := outgoing
if dph.HandlePacketFromWireGuard(&o1) != 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) != 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)
}
}