Files
tailscale/feature/conn25/flowtable.go
Michael Ben-Ami ce7789071f feature/conn25: add NATing support with flow caching
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>
2026-03-18 11:49:47 -04:00

150 lines
4.2 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package conn25
import (
"errors"
"sync"
"tailscale.com/net/flowtrack"
"tailscale.com/net/packet"
)
// PacketAction may modify the packet.
type PacketAction func(*packet.Parsed)
// FlowData is an entry stored in the [FlowTable].
type FlowData struct {
Tuple flowtrack.Tuple
Action PacketAction
}
// Origin is used to track the direction of a flow.
type Origin uint8
const (
// FromTun indicates the flow is from the tun device.
FromTun Origin = iota
// FromWireGuard indicates the flow is from the WireGuard tunnel.
FromWireGuard
)
type cachedFlow struct {
flow FlowData
paired flowtrack.Tuple // tuple for the other direction
}
// FlowTable stores and retrieves [FlowData] that can be looked up
// by 5-tuple. New entries specify the tuple to use for both directions
// of traffic flow. The underlying cache is LRU, and the maximum number
// of entries is specified in calls to [NewFlowTable]. FlowTable has
// its own mutex and is safe for concurrent use.
type FlowTable struct {
mu sync.Mutex
fromTunCache *flowtrack.Cache[cachedFlow] // guarded by mu
fromWGCache *flowtrack.Cache[cachedFlow] // guarded by mu
}
// NewFlowTable returns a [FlowTable] maxEntries maximum entries.
// A maxEntries of 0 indicates no maximum. See also [FlowTable].
func NewFlowTable(maxEntries int) *FlowTable {
return &FlowTable{
fromTunCache: &flowtrack.Cache[cachedFlow]{
MaxEntries: maxEntries,
},
fromWGCache: &flowtrack.Cache[cachedFlow]{
MaxEntries: maxEntries,
},
}
}
// LookupFromTunDevice looks up a [FlowData] entry that is valid to run for packets
// observed as coming from the tun device. The tuple must match the direction it was
// stored with.
func (t *FlowTable) LookupFromTunDevice(k flowtrack.Tuple) (FlowData, bool) {
return t.lookup(k, FromTun)
}
// LookupFromWireGuard looks up a [FlowData] entry that is valid to run for packets
// observed as coming from the WireGuard tunnel. The tuple must match the direction it was
// stored with.
func (t *FlowTable) LookupFromWireGuard(k flowtrack.Tuple) (FlowData, bool) {
return t.lookup(k, FromWireGuard)
}
func (t *FlowTable) lookup(k flowtrack.Tuple, want Origin) (FlowData, bool) {
var cache *flowtrack.Cache[cachedFlow]
switch want {
case FromTun:
cache = t.fromTunCache
case FromWireGuard:
cache = t.fromWGCache
default:
return FlowData{}, false
}
t.mu.Lock()
defer t.mu.Unlock()
v, ok := cache.Get(k)
if !ok {
return FlowData{}, false
}
return v.flow, true
}
// NewFlowFromTunDevice installs (or overwrites) both the forward and return entries.
// The forward tuple is tagged as FromTun, and the return tuple is tagged as FromWireGuard.
// If overwriting, it removes the old paired tuple for the forward key to avoid stale reverse mappings.
func (t *FlowTable) NewFlowFromTunDevice(fwd, rev FlowData) error {
return t.newFlow(FromTun, fwd, rev)
}
// NewFlowFromWireGuard installs (or overwrites) both the forward and return entries.
// The forward tuple is tagged as FromWireGuard, and the return tuple is tagged as FromTun.
// If overwriting, it removes the old paired tuple for the forward key to avoid stale reverse mappings.
func (t *FlowTable) NewFlowFromWireGuard(fwd, rev FlowData) error {
return t.newFlow(FromWireGuard, fwd, rev)
}
func (t *FlowTable) newFlow(fwdOrigin Origin, fwd, rev FlowData) error {
if fwd.Action == nil || rev.Action == nil {
return errors.New("nil action received for flow")
}
var fwdCache, revCache *flowtrack.Cache[cachedFlow]
switch fwdOrigin {
case FromTun:
fwdCache, revCache = t.fromTunCache, t.fromWGCache
case FromWireGuard:
fwdCache, revCache = t.fromWGCache, t.fromTunCache
default:
return errors.New("newFlow called with unknown direction")
}
t.mu.Lock()
defer t.mu.Unlock()
// If overwriting an existing entry, remove its previously-paired mapping so
// we don't leave stale tuples around.
if old, ok := fwdCache.Get(fwd.Tuple); ok {
revCache.Remove(old.paired)
}
if old, ok := revCache.Get(rev.Tuple); ok {
fwdCache.Remove(old.paired)
}
fwdCache.Add(fwd.Tuple, cachedFlow{
flow: fwd,
paired: rev.Tuple,
})
revCache.Add(rev.Tuple, cachedFlow{
flow: rev,
paired: fwd.Tuple,
})
return nil
}