mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-13 00:59:55 +00:00

untested so far Change-Id: Id5a265693d77a4552342c08e643e4a7ed5545e6a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"math/rand/v2"
|
|
"net/netip"
|
|
"time"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
var _ NATTable = (*oneToOneNAT)(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
|
|
}
|
|
|
|
var _ NATTable = (*hardNAT)(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.
|
|
port := src.Port() // start with the port the client used? (TODO: does FreeBSD do this? make knob?)
|
|
for {
|
|
ki := hardKeyIn{wanPort: port, src: dst}
|
|
if _, ok := n.in[ki]; ok {
|
|
// Collision. Try again.
|
|
port = rand.N(uint16(32<<10)) + 32<<10 // pick some "ephemeral" port
|
|
continue
|
|
}
|
|
n.in[ki] = lanAddrAndTime{lanAddr: src, at: at}
|
|
n.out[ko] = portMappingAndTime{port: port, at: at}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type easyKeyOut struct {
|
|
src netip.AddrPort
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var _ NATTable = (*easyNAT)(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)
|
|
if _, ok := n.in[port]; !ok {
|
|
// Found a free port.
|
|
n.out[src] = portMappingAndTime{port: port, at: at}
|
|
n.in[port] = lanAddrAndTime{lanAddr: src, at: at}
|
|
return netip.AddrPortFrom(n.wanIP, port)
|
|
}
|
|
}
|
|
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
|
|
}
|