tailscale/tstest/natlab/vnet/nat.go
Brad Fitzpatrick 1ed958fe23 tstest/natlab/vnet: add start of virtual network-based NAT Lab
Updates #13038

Change-Id: I3c74120d73149c1329288621f6474bbbcaa7e1a6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-08-07 09:37:15 -07:00

240 lines
7.0 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package vnet
import (
"errors"
"math/rand/v2"
"net/netip"
"time"
"tailscale.com/util/mak"
)
const (
One2OneNAT NAT = "one2one"
EasyNAT NAT = "easy"
HardNAT NAT = "hard"
)
// IPPool is the interface that a NAT implementation uses to get information
// about a network.
//
// Outside of tests, this is typically a *network.
type IPPool interface {
// WANIP returns the primary WAN IP address.
//
// TODO: add another method for networks with multiple WAN IP addresses.
WANIP() netip.Addr
// SoleLanIP reports whether this network has a sole LAN client
// and if so, its IP address.
SoleLANIP() (_ netip.Addr, ok bool)
// TODO: port availability stuff for interacting with portmapping
}
// newTableFunc is a constructor for a NAT table.
// The provided IPPool is typically (outside of tests) a *network.
type newTableFunc func(IPPool) (NATTable, error)
// NAT is a type of NAT that's known to natlab.
//
// For example, "easy" for Linux-style NAT, "hard" for FreeBSD-style NAT, etc.
type NAT string
// natTypes are the known NAT types.
var natTypes = map[NAT]newTableFunc{}
// registerNATType registers a NAT type.
func registerNATType(name NAT, f newTableFunc) {
if _, ok := natTypes[name]; ok {
panic("duplicate NAT type: " + name)
}
natTypes[name] = f
}
// NATTable is what a NAT implementation is expected to do.
//
// This project tests Tailscale as it faces various combinations various NAT
// implementations (e.g. Linux easy style NAT vs FreeBSD hard/endpoint dependent
// NAT vs Cloud 1:1 NAT, etc)
//
// Implementations of NATTable need not handle concurrency; the natlab serializes
// all calls into a NATTable.
//
// The provided `at` value will typically be time.Now, except for tests.
// Implementations should not use real time and should only compare
// previously provided time values.
type NATTable interface {
// PickOutgoingSrc returns the source address to use for an outgoing packet.
//
// The result should either be invalid (to drop the packet) or a WAN (not
// private) IP address.
//
// Typically, the src is a LAN source IP address, but it might also be a WAN
// IP address if the packet is being forwarded for a source machine that has
// a public IP address.
PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort)
// PickIncomingDst returns the destination address to use for an incoming
// packet. The incoming src address is always a public WAN IP.
//
// The result should either be invalid (to drop the packet) or the IP
// address of a machine on the local network address, usually a private
// LAN IP.
PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort)
}
// oneToOneNAT is a 1:1 NAT, like a typical EC2 VM.
type oneToOneNAT struct {
lanIP netip.Addr
wanIP netip.Addr
}
func init() {
registerNATType(One2OneNAT, func(p IPPool) (NATTable, error) {
lanIP, ok := p.SoleLANIP()
if !ok {
return nil, errors.New("can't use one2one NAT type on networks other than single-node networks")
}
return &oneToOneNAT{lanIP: lanIP, wanIP: p.WANIP()}, nil
})
}
func (n *oneToOneNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
return netip.AddrPortFrom(n.wanIP, src.Port())
}
func (n *oneToOneNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
return netip.AddrPortFrom(n.lanIP, dst.Port())
}
type hardKeyOut struct {
lanIP netip.Addr
dst netip.AddrPort
}
type hardKeyIn struct {
wanPort uint16
src netip.AddrPort
}
type portMappingAndTime struct {
port uint16
at time.Time
}
type lanAddrAndTime struct {
lanAddr netip.AddrPort
at time.Time
}
// hardNAT is an "Endpoint Dependent" NAT, like FreeBSD/pfSense/OPNsense.
// This is shown as "MappingVariesByDestIP: true" by netcheck, and what
// Tailscale calls "Hard NAT".
type hardNAT struct {
wanIP netip.Addr
out map[hardKeyOut]portMappingAndTime
in map[hardKeyIn]lanAddrAndTime
}
func init() {
registerNATType(HardNAT, func(p IPPool) (NATTable, error) {
return &hardNAT{wanIP: p.WANIP()}, nil
})
}
func (n *hardNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
ko := hardKeyOut{src.Addr(), dst}
if pm, ok := n.out[ko]; ok {
// Existing flow.
// TODO: bump timestamp
return netip.AddrPortFrom(n.wanIP, pm.port)
}
// No existing mapping exists. Create one.
// TODO: clean up old expired mappings
// Instead of proper data structures that would be efficient, we instead
// just loop a bunch and look for a free port. This project is only used
// by tests and doesn't care about performance, this is good enough.
for {
port := rand.N(uint16(32<<10)) + 32<<10 // pick some "ephemeral" port
ki := hardKeyIn{wanPort: port, src: dst}
if _, ok := n.in[ki]; ok {
// Port already in use.
continue
}
mak.Set(&n.in, ki, lanAddrAndTime{lanAddr: src, at: at})
mak.Set(&n.out, ko, portMappingAndTime{port: port, at: at})
return netip.AddrPortFrom(n.wanIP, port)
}
}
func (n *hardNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
if dst.Addr() != n.wanIP {
return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
}
ki := hardKeyIn{wanPort: dst.Port(), src: src}
if pm, ok := n.in[ki]; ok {
// Existing flow.
return pm.lanAddr
}
return netip.AddrPort{} // drop; no mapping
}
// easyNAT is an "Endpoint Independent" NAT, like Linux and most home routers
// (many of which are Linux).
//
// This is shown as "MappingVariesByDestIP: false" by netcheck, and what
// Tailscale calls "Easy NAT".
//
// Unlike Linux, this implementation is capped at 32k entries and doesn't resort
// to other allocation strategies when all 32k WAN ports are taken.
type easyNAT struct {
wanIP netip.Addr
out map[netip.AddrPort]portMappingAndTime
in map[uint16]lanAddrAndTime
}
func init() {
registerNATType(EasyNAT, func(p IPPool) (NATTable, error) {
return &easyNAT{wanIP: p.WANIP()}, nil
})
}
func (n *easyNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
if pm, ok := n.out[src]; ok {
// Existing flow.
// TODO: bump timestamp
return netip.AddrPortFrom(n.wanIP, pm.port)
}
// Loop through all 32k high (ephemeral) ports, starting at a random
// position and looping back around to the start.
start := rand.N(uint16(32 << 10))
for off := range uint16(32 << 10) {
port := 32<<10 + (start+off)%(32<<10)
if _, ok := n.in[port]; !ok {
wanAddr := netip.AddrPortFrom(n.wanIP, port)
// Found a free port.
mak.Set(&n.out, src, portMappingAndTime{port: port, at: at})
mak.Set(&n.in, port, lanAddrAndTime{lanAddr: src, at: at})
return wanAddr
}
}
return netip.AddrPort{} // failed to allocate a mapping; TODO: fire an alert?
}
func (n *easyNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
if dst.Addr() != n.wanIP {
return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
}
return n.in[dst.Port()].lanAddr
}