From cda2300682fccb31c10d219d1bc47dfab2154db0 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 28 Jul 2024 07:17:06 -0700 Subject: [PATCH] add start of Hard NAT impl untested so far Change-Id: I682b604d0e90debf9eae3f1814663f336d03f57c Signed-off-by: Brad Fitzpatrick --- natlab/natlabd/nat.go | 75 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/natlab/natlabd/nat.go b/natlab/natlabd/nat.go index 6f1ead189..c1cf12882 100644 --- a/natlab/natlabd/nat.go +++ b/natlab/natlabd/nat.go @@ -1,6 +1,7 @@ package main import ( + "math/rand/v2" "net/netip" "time" ) @@ -43,6 +44,8 @@ type oneToOneNAT struct { 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()) } @@ -50,3 +53,75 @@ func (n *oneToOneNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wa 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 +}