feature/conn25: unify FlowTable storage to prepare for expiry

Previously we had two maps keyed on a direction-specific tuple, with
distinct values containing the data (action) for that direction.
Values pointed at each other across maps to ensure they were removed
at the same time in the case of tuple overwrite, but LRU eviction
was per-map. So if LRU was turned on, it was possible for one
direction's data (action) to be evicted and leave the other direction
dangling.

NewFlow replaces the two direction-specific flow constructors, and
lookups return the direction-specific PacketAction directly.

Now the values in each map point to the same element, with data for both
directions in the element. A linked list also points to the elements to
implement LRU. The previous flowtrack.Cache is removed.

The single LRU structure will allow us to implement idle time expiration
by walking the list backward starting with the least recently used flow, and
stopping after a fixed number of flows, or at the first non-expired flow.

We add commented-out unused placeholder fields for tracking the
"last seen" timestamp, and an on-removal hook, to document the intent for
the follow-up expiry work.

Updates tailscale/corp#38630

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2026-05-26 10:09:48 -04:00
committed by mzbenami
parent 26952d53fa
commit 5877809097
3 changed files with 318 additions and 183 deletions
+20 -14
View File
@@ -108,17 +108,17 @@ func (dh *datapathHandler) HandlePacketFromWireGuard(p *packet.Parsed) filter.Re
// Check if this is an existing (return) flow on a client.
// If found, perform the action for the existing client flow and return.
existing, ok := dh.clientFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
action, ok := dh.clientFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
action(p)
return filter.Accept
}
// Check if this is an existing connector outbound flow.
// If found, perform the action for the existing connector outbound flow and return.
existing, ok = dh.connectorFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
action, ok = dh.connectorFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
action(p)
return filter.Accept
}
@@ -145,15 +145,18 @@ func (dh *datapathHandler) HandlePacketFromWireGuard(p *packet.Parsed) filter.Re
// This is a new outbound flow on a connector. Install a DNAT TransitIP-to-RealIP action
// for the outgoing direction, and an SNAT RealIP-to-TransitIP action for the
// return direction.
outgoing := FlowData{
outgoing := TupleAndAction{
Tuple: flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst),
Action: dh.dnatAction(realIP),
}
incoming := FlowData{
incoming := TupleAndAction{
Tuple: flowtrack.MakeTuple(p.IPProto, netip.AddrPortFrom(realIP, p.Dst.Port()), p.Src),
Action: dh.snatAction(transitIP),
}
if err := dh.connectorFlowTable.NewFlowFromWireGuard(outgoing, incoming); err != nil {
if err := dh.connectorFlowTable.NewFlow(FlowData{
FromTun: incoming,
FromWG: outgoing,
}); err != nil {
dh.debugLogf("error installing flow, passing packet unmodified: %v", err)
return filter.Accept
}
@@ -174,17 +177,17 @@ func (dh *datapathHandler) HandlePacketFromTunDevice(p *packet.Parsed) filter.Re
// Check if this is an existing client outbound flow.
// If found, perform the action for the existing client flow and return.
existing, ok := dh.clientFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
action, ok := dh.clientFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
action(p)
return filter.Accept
}
// Check if this is an existing connector return flow.
// If found, perform the action for the existing connector return flow and return.
existing, ok = dh.connectorFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
action, ok = dh.connectorFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
action(p)
return filter.Accept
}
@@ -211,15 +214,18 @@ func (dh *datapathHandler) HandlePacketFromTunDevice(p *packet.Parsed) filter.Re
// This is a new outbound client flow. Install a DNAT MagicIP-to-TransitIP action
// for the outgoing direction, and an SNAT TransitIP-to-MagicIP action for the
// return direction.
outgoing := FlowData{
outgoing := TupleAndAction{
Tuple: flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst),
Action: dh.dnatAction(transitIP),
}
incoming := FlowData{
incoming := TupleAndAction{
Tuple: flowtrack.MakeTuple(p.IPProto, netip.AddrPortFrom(transitIP, p.Dst.Port()), p.Src),
Action: dh.snatAction(magicIP),
}
if err := dh.clientFlowTable.NewFlowFromTunDevice(outgoing, incoming); err != nil {
if err := dh.clientFlowTable.NewFlow(FlowData{
FromTun: outgoing,
FromWG: incoming,
}); err != nil {
dh.debugLogf("error installing flow from tun device, passing packet unmodified: %v", err)
return filter.Accept
}