mirror of
https://github.com/tailscale/tailscale.git
synced 2026-03-28 19:21:13 -04:00
Introduce a datapathHandler that implements hooks that will receive packets from the tstun.Wrapper. This commit does not wire those up just yet. Perform DNAT from Magic IP to Transit IP on outbound flows on clients, and reverse SNAT in the reverse direction. Perform DNAT from Transit IP to final destination IP on outbound flows on connectors, and reverse SNAT in the reverse direction. Introduce FlowTable to cache validated flows by 5-tuple for fast lookups after the first packet. Flow expiration is not covered, and is intended as future work before the feature is officially released. Fixes tailscale/corp#34249 Fixes tailscale/corp#35995 Co-authored-by: Fran Bull <fran@tailscale.com> Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
362 lines
12 KiB
Go
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, nil)
|
|
|
|
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, nil)
|
|
|
|
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, nil)
|
|
|
|
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, nil)
|
|
|
|
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)
|
|
}
|
|
}
|