mirror of
https://github.com/tailscale/tailscale.git
synced 2026-07-15 01:53:08 -04:00
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>
438 lines
14 KiB
Go
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)
|
|
}
|
|
}
|