The returned error in the signature is left over from previous implementations and was only returning nil. If we know NewFlow will succeed we can fire a create hook (implemented in a future commit) before NewFlow, which will prevent a remove hook for a flow from firing before the create hook for the same flow. Updates tailscale/corp#38630 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
375 lines
11 KiB
Go
375 lines
11 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package conn25
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"tailscale.com/net/flowtrack"
|
|
"tailscale.com/net/packet"
|
|
"tailscale.com/tstime/mono"
|
|
"tailscale.com/types/ipproto"
|
|
)
|
|
|
|
var nilPacket *packet.Parsed // nil packet to perform actions against
|
|
|
|
// mkTuple wraps flowtrack.MakeTuple with the UDP proto.
|
|
func mkTuple(src, dst string) flowtrack.Tuple {
|
|
return flowtrack.MakeTuple(
|
|
ipproto.UDP,
|
|
netip.MustParseAddrPort(src),
|
|
netip.MustParseAddrPort(dst),
|
|
)
|
|
}
|
|
|
|
func mkFlowWithActions(fromTun, fromWG flowtrack.Tuple) (FlowData, *int, *int) {
|
|
var tunCalls, wgCalls int
|
|
fd := mkFlow(fromTun, fromWG)
|
|
fd.FromTun.Action = func(_ *packet.Parsed) { tunCalls++ }
|
|
fd.FromWG.Action = func(_ *packet.Parsed) { wgCalls++ }
|
|
return fd, &tunCalls, &wgCalls
|
|
}
|
|
|
|
func mkFlow(fromTun, fromWG flowtrack.Tuple) FlowData {
|
|
return FlowData{
|
|
FromTun: TupleAndAction{
|
|
Tuple: fromTun,
|
|
Action: func(_ *packet.Parsed) {},
|
|
},
|
|
FromWG: TupleAndAction{
|
|
Tuple: fromWG,
|
|
Action: func(_ *packet.Parsed) {},
|
|
},
|
|
}
|
|
}
|
|
|
|
func mkFlows(n int) []FlowData {
|
|
flows := make([]FlowData, n)
|
|
for i := range n {
|
|
flows[i] = mkFlow(
|
|
mkTuple(fmt.Sprintf("1.0.%d.%d:1000", (i>>8)&0xff, i&0xff), "2.0.0.1:80"),
|
|
mkTuple(fmt.Sprintf("3.0.%d.%d:1000", (i>>8)&0xff, i&0xff), "4.0.0.1:80"),
|
|
)
|
|
}
|
|
return flows
|
|
}
|
|
|
|
func assertFlowHit(t *testing.T, ft *FlowTable, dir Origin, tuple flowtrack.Tuple) PacketAction {
|
|
t.Helper()
|
|
return assertFlowLookup(t, ft, dir, tuple, true)
|
|
}
|
|
|
|
func assertFlowMiss(t *testing.T, ft *FlowTable, dir Origin, tuple flowtrack.Tuple) {
|
|
t.Helper()
|
|
assertFlowLookup(t, ft, dir, tuple, false)
|
|
}
|
|
|
|
func assertFlowLookup(t *testing.T, ft *FlowTable, dir Origin, tuple flowtrack.Tuple, wantHit bool) PacketAction {
|
|
t.Helper()
|
|
var action PacketAction
|
|
var ok bool
|
|
|
|
switch dir {
|
|
case FromTun:
|
|
action, ok = ft.LookupFromTunDevice(tuple)
|
|
case FromWireGuard:
|
|
action, ok = ft.LookupFromWireGuard(tuple)
|
|
default:
|
|
t.Fatalf("invalid direction: %v", dir)
|
|
}
|
|
|
|
if wantHit && !ok {
|
|
t.Fatalf("expected flow hit for tuple: %v, dir: %v", tuple, dir)
|
|
}
|
|
if !wantHit && ok {
|
|
t.Fatalf("expected flow miss for tuple: %v, dir: %v", tuple, dir)
|
|
}
|
|
|
|
if wantHit {
|
|
return action
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestFlowTable_NewFlow_Lookup(t *testing.T) {
|
|
ft := NewFlowTable(0)
|
|
|
|
// The tuples in both directions are defined by the caller.
|
|
// The don't have to be mirror images of each other,
|
|
// to account for intentional modifications, like NAT.
|
|
fromTunTuple := mkTuple("1.2.3.4:1000", "4.3.2.1:80")
|
|
fromWGTuple := mkTuple("4.3.2.2:80", "1.2.3.4:1000")
|
|
|
|
flow1, tunCount1, wgCount1 := mkFlowWithActions(fromTunTuple, fromWGTuple)
|
|
ft.NewFlow(flow1)
|
|
|
|
// Test basic lookups, and perform actions on packet.
|
|
assertFlowHit(t, ft, FromTun, fromTunTuple)(nilPacket)
|
|
assertFlowHit(t, ft, FromWireGuard, fromWGTuple)(nilPacket)
|
|
|
|
if *tunCount1 != 1 {
|
|
t.Fatal("action for from-tun tuple key was not executed")
|
|
}
|
|
if *wgCount1 != 1 {
|
|
t.Fatal("action for from-wg tuple key was not executed")
|
|
}
|
|
|
|
// Test tuple not found.
|
|
notFoundTuple := mkTuple("1.2.3.4:1000", "4.0.4.4:80")
|
|
assertFlowMiss(t, ft, FromTun, notFoundTuple)
|
|
|
|
// Wrong direction is also not found.
|
|
assertFlowMiss(t, ft, FromWireGuard, fromTunTuple)
|
|
|
|
// Overwriting from-tun tuple removes the from-wg tuple as well.
|
|
newFromWGTuple := mkTuple("9.9.9.9:99", "8.8.8.8:88")
|
|
flow2 := mkFlow(fromTunTuple, newFromWGTuple)
|
|
ft.NewFlow(flow2)
|
|
assertFlowMiss(t, ft, FromWireGuard, fromWGTuple)
|
|
|
|
// Overwriting the from-wg tuple removes the from-tun tuple as well.
|
|
newFromTunTuple := mkTuple("8.8.8.8:88", "9.9.9.9:99")
|
|
flow3 := mkFlow(newFromTunTuple, newFromWGTuple)
|
|
ft.NewFlow(flow3)
|
|
assertFlowMiss(t, ft, FromTun, fromTunTuple)
|
|
}
|
|
|
|
// TestFlowTable_OneReplacesTwo targets a specific case
|
|
// in which a single new flow replaces two existing flows
|
|
// because each tuple of the new flow matches one tuple
|
|
// of an existing flow.
|
|
func TestFlowTable_OneReplacesTwo(t *testing.T) {
|
|
ft := NewFlowTable(0)
|
|
|
|
tunTuple1 := mkTuple("1.2.3.4:1000", "4.3.2.1:80")
|
|
wgTuple1 := mkTuple("4.3.2.2:80", "1.2.3.4:1000")
|
|
flow1, tunCount1, wgCount1 := mkFlowWithActions(tunTuple1, wgTuple1)
|
|
|
|
tunTuple2 := mkTuple("8.8.8.8:88", "9.9.9.9:99")
|
|
wgTuple2 := mkTuple("9.9.9.9:99", "8.8.8.8:88")
|
|
flow2, tunCount2, wgCount2 := mkFlowWithActions(tunTuple2, wgTuple2)
|
|
|
|
// Install the first two flows.
|
|
ft.NewFlow(flow1)
|
|
ft.NewFlow(flow2)
|
|
|
|
// Confirm they are properly installed through lookups.
|
|
assertFlowHit(t, ft, FromTun, tunTuple1)
|
|
assertFlowHit(t, ft, FromWireGuard, wgTuple1)
|
|
assertFlowHit(t, ft, FromTun, tunTuple2)
|
|
assertFlowHit(t, ft, FromWireGuard, wgTuple2)
|
|
|
|
// flow3 tuples overlap with flow1 and flow2.
|
|
flow3, tunCount3, wgCount3 := mkFlowWithActions(tunTuple1, wgTuple2)
|
|
ft.NewFlow(flow3)
|
|
|
|
// flow3 lookups hit on both of their tuples.
|
|
tunAction3 := assertFlowHit(t, ft, FromTun, tunTuple1)
|
|
wgAction3 := assertFlowHit(t, ft, FromWireGuard, wgTuple2)
|
|
|
|
// The non-overlapping tuples from flow1 and flow2 should now miss.
|
|
assertFlowMiss(t, ft, FromTun, tunTuple2)
|
|
assertFlowMiss(t, ft, FromWireGuard, wgTuple1)
|
|
|
|
// Perform both actions on a nil packet to bump counters.
|
|
tunAction3(nilPacket)
|
|
wgAction3(nilPacket)
|
|
|
|
// Only flow3 counters should have been bumped.
|
|
if *tunCount1 != 0 || *wgCount1 != 0 {
|
|
t.Fatalf("flow1 counters (tun, wg), want: (0,0), got: (%d,%d)", *tunCount1, *wgCount1)
|
|
}
|
|
if *tunCount2 != 0 || *wgCount2 != 0 {
|
|
t.Fatalf("flow2 counters (tun, wg), want: (0,0), got: (%d,%d)", *tunCount2, *wgCount2)
|
|
}
|
|
if *tunCount3 != 1 || *wgCount3 != 1 {
|
|
t.Fatalf("flow3 counters (tun, wg), want: (1,1), got: (%d,%d)", *tunCount3, *wgCount3)
|
|
}
|
|
}
|
|
|
|
func TestFlowTable_Eviction(t *testing.T) {
|
|
// Table only has two spots.
|
|
ft := NewFlowTable(2)
|
|
aTun, aWG := mkTuple("3.0.0.1:1000", "3.0.0.2:80"), mkTuple("3.0.0.2:80", "3.0.0.1:1000")
|
|
bTun, bWG := mkTuple("3.0.0.3:1000", "3.0.0.4:80"), mkTuple("3.0.0.4:80", "3.0.0.3:1000")
|
|
cTun, cWG := mkTuple("3.0.0.5:1000", "3.0.0.6:80"), mkTuple("3.0.0.6:80", "3.0.0.5:1000")
|
|
dTun, dWG := mkTuple("3.0.0.7:1000", "3.0.0.8:80"), mkTuple("3.0.0.8:80", "3.0.0.7:1000")
|
|
|
|
a := mkFlow(aTun, aWG)
|
|
b := mkFlow(bTun, bWG)
|
|
c := mkFlow(cTun, cWG)
|
|
d := mkFlow(dTun, dWG)
|
|
|
|
// Install a and b.
|
|
ft.NewFlow(a)
|
|
ft.NewFlow(b)
|
|
|
|
// Move a to the front from tun side, b is ready for eviction.
|
|
assertFlowHit(t, ft, FromTun, aTun)
|
|
|
|
// Install c.
|
|
ft.NewFlow(c)
|
|
|
|
// Check b is out.
|
|
assertFlowMiss(t, ft, FromTun, bTun)
|
|
assertFlowMiss(t, ft, FromWireGuard, bWG)
|
|
|
|
// Check c is in.
|
|
assertFlowHit(t, ft, FromTun, cTun)
|
|
assertFlowHit(t, ft, FromWireGuard, cWG)
|
|
|
|
// Move a to the front again, now from WG side.
|
|
assertFlowHit(t, ft, FromWireGuard, aWG)
|
|
|
|
// Install d.
|
|
ft.NewFlow(d)
|
|
|
|
// Check c is out.
|
|
assertFlowMiss(t, ft, FromTun, cTun)
|
|
assertFlowMiss(t, ft, FromWireGuard, cWG)
|
|
|
|
// Check d is in.
|
|
assertFlowHit(t, ft, FromTun, dTun)
|
|
assertFlowHit(t, ft, FromWireGuard, dWG)
|
|
}
|
|
|
|
func syncSubtest(t *testing.T, name string, f func(*testing.T)) {
|
|
t.Helper()
|
|
t.Run(name, func(t *testing.T) {
|
|
synctest.Test(t, f)
|
|
})
|
|
}
|
|
|
|
func TestFlowTable_removeIdle(t *testing.T) {
|
|
type flowSpec struct {
|
|
installAt time.Duration // wall-clock time of install, from test start
|
|
wantRemoved bool // do we expect this flow to be removed by the sweep
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
idleTimeout time.Duration
|
|
maxRemovedPerSweep int
|
|
flowSpecs []flowSpec
|
|
removalAt time.Duration // wall-clock time of sweep, from test start
|
|
}{
|
|
{
|
|
name: "one-expired-flow",
|
|
idleTimeout: 1 * time.Minute,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: true}, // age at sweep = 120s
|
|
},
|
|
removalAt: 2 * time.Minute,
|
|
},
|
|
{
|
|
name: "one-not-expired-flow",
|
|
idleTimeout: 1 * time.Minute,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: false}, // age at sweep = 30s
|
|
},
|
|
removalAt: 30 * time.Second,
|
|
},
|
|
{
|
|
name: "two-flows-one-expired",
|
|
idleTimeout: 1 * time.Minute,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: true}, // age at sweep = 75s
|
|
{installAt: 30 * time.Second, wantRemoved: false}, // age at sweep = 45s
|
|
},
|
|
removalAt: 75 * time.Second,
|
|
},
|
|
{
|
|
name: "two-flows-both-expired",
|
|
idleTimeout: 1 * time.Minute,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: true}, // age at sweep = 120s
|
|
{installAt: 30 * time.Second, wantRemoved: true}, // age at sweep = 90s
|
|
},
|
|
removalAt: 2 * time.Minute,
|
|
},
|
|
{
|
|
// Both flows are time-expired, but maxRemovedPerSweep=1 caps removal at 1.
|
|
// Flow 0 is at the back of the LRU (installed first) and is removed; flow 1 stays.
|
|
name: "two-flows-both-expired-but-max-count-equal-one",
|
|
idleTimeout: 1 * time.Minute,
|
|
maxRemovedPerSweep: 1,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: true}, // age at sweep = 120s, removed (under cap)
|
|
{installAt: 30 * time.Second, wantRemoved: false}, // age at sweep = 90s, kept (cap reached)
|
|
},
|
|
removalAt: 2 * time.Minute,
|
|
},
|
|
{
|
|
name: "zero-idle-timeout-means-no-expiration",
|
|
idleTimeout: 0,
|
|
flowSpecs: []flowSpec{
|
|
{installAt: 0, wantRemoved: false},
|
|
{installAt: 30 * time.Second, wantRemoved: false},
|
|
},
|
|
removalAt: 2 * time.Minute,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
syncSubtest(t, tt.name, func(t *testing.T) {
|
|
ft := NewFlowTable(
|
|
0, // turn off LRU for these tests
|
|
WithFlowIdleTimeout(tt.idleTimeout),
|
|
WithMaxRemovedFlowsPerSweep(tt.maxRemovedPerSweep),
|
|
)
|
|
|
|
start := time.Now()
|
|
flows := mkFlows(len(tt.flowSpecs))
|
|
for i, spec := range tt.flowSpecs {
|
|
time.Sleep(time.Until(start.Add(spec.installAt)))
|
|
ft.NewFlow(flows[i])
|
|
}
|
|
|
|
var wantRemovedCount int
|
|
for _, spec := range tt.flowSpecs {
|
|
if spec.wantRemoved {
|
|
wantRemovedCount++
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Until(start.Add(tt.removalAt)))
|
|
|
|
gotRemovedCount := ft.removeIdle(mono.Now())
|
|
if wantRemovedCount != gotRemovedCount {
|
|
t.Errorf("unexpected remove idle count: want %d, got %d", wantRemovedCount, gotRemovedCount)
|
|
}
|
|
|
|
for i, spec := range tt.flowSpecs {
|
|
if spec.wantRemoved {
|
|
assertFlowMiss(t, ft, FromTun, flows[i].FromTun.Tuple)
|
|
} else {
|
|
assertFlowHit(t, ft, FromTun, flows[i].FromTun.Tuple)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
syncSubtest(t, "lookup-resets-lastseen", func(t *testing.T) {
|
|
ft := NewFlowTable(0, WithFlowIdleTimeout(time.Minute))
|
|
flows := mkFlows(2)
|
|
|
|
ft.NewFlow(flows[0]) // t=0 (flow 0 install)
|
|
time.Sleep(30 * time.Second) //
|
|
ft.NewFlow(flows[1]) // t=30s (flow 1 install)
|
|
time.Sleep(60 * time.Second) //
|
|
assertFlowHit(t, ft, FromTun, flows[0].FromTun.Tuple) // t=90s (flow 0 looked up, lastSeen bumped)
|
|
time.Sleep(15 * time.Second) //
|
|
|
|
if got := ft.removeIdle(mono.Now()); got != 1 {
|
|
t.Errorf("removeIdle returned %d, want 1", got)
|
|
}
|
|
assertFlowHit(t, ft, FromTun, flows[0].FromTun.Tuple)
|
|
assertFlowMiss(t, ft, FromTun, flows[1].FromTun.Tuple)
|
|
})
|
|
}
|