From 0f2ecf8a18b60b8807f22858a020437c7d038470 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 27 Jul 2024 21:37:04 -0700 Subject: [PATCH] start of pluggable NAT impl types Change-Id: I633bce41e978f385eab26478baa42e56178c489a --- natlab/natlabd/nat.go | 53 +++++++++++++++++++++++++++++++++++++++ natlab/natlabd/natlabd.go | 44 +++++++++++++++++++------------- 2 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 natlab/natlabd/nat.go diff --git a/natlab/natlabd/nat.go b/natlab/natlabd/nat.go new file mode 100644 index 000000000..6c79d4627 --- /dev/null +++ b/natlab/natlabd/nat.go @@ -0,0 +1,53 @@ +package main + +import ( + "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) +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. + // + // The at value will typically be time.Now, except for tests. + // Implementations should not use real time and should only compare + // previously provided time values. + 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. + // + // The at value will typically be time.Now, except for tests. + // Implementations should not use real time and should only compare + // previously provided time values. + 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 (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()) +} diff --git a/natlab/natlabd/natlabd.go b/natlab/natlabd/natlabd.go index 3701be1fd..4c0c7a770 100644 --- a/natlab/natlabd/natlabd.go +++ b/natlab/natlabd/natlabd.go @@ -15,6 +15,8 @@ import ( "os/exec" "strconv" "sync" + "sync/atomic" + "time" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -67,20 +69,27 @@ func main() { wanIP: netip.MustParseAddr("2.1.1.1"), lanIP: netip.MustParsePrefix("192.168.1.1/24"), } + node1 := &node{ + mac: MAC{0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xee}, + net: net1, + lanIP: netip.MustParseAddr("192.168.1.101"), + } + s.nodes[node1.mac] = node1 + net1.SetNATTable(&oneToOneNAT{wanIP: net1.wanIP, lanIP: node1.lanIP}) net2 := &network{ s: s, mac: MAC{0x52, 0x54, 0x00, 0x01, 0x01, 0x2}, wanIP: netip.MustParseAddr("2.2.2.1"), lanIP: netip.MustParsePrefix("10.2.0.1/16"), } - s.nodes[MAC{0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xee}] = &node{ - net: net1, - lanIP: netip.MustParseAddr("192.168.1.101"), - } - s.nodes[MAC{0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xef}] = &node{ + node2 := &node{ + mac: MAC{0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xef}, net: net2, lanIP: netip.MustParseAddr("10.2.0.102"), } + s.nodes[node2.mac] = node2 + net2.SetNATTable(&oneToOneNAT{wanIP: net2.wanIP, lanIP: node2.lanIP}) + if err := s.checkWorld(); err != nil { log.Fatalf("checkWorld: %v", err) } @@ -124,6 +133,9 @@ func (s *Server) registerNetwork(n *network) error { } s.networks.Add(n) + if n.natTable.Load() == nil { + return errors.New("network has no NATTable") + } if !n.wanIP.IsValid() { return errors.New("network has no WAN IP") } @@ -179,6 +191,10 @@ func (s *Server) checkWorld() error { return nil } +func (n *network) SetNATTable(nt NATTable) { + n.natTable.Store(&nt) +} + func (n *network) initStack() error { n.ns = stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ @@ -407,6 +423,8 @@ type network struct { lanIP netip.Prefix // with host bits set (e.g. 192.168.2.1/24) nodesByIP map[netip.Addr]*node + natTable atomic.Pointer[NATTable] // odd pointer to interface so we can change impl types + ns *stack.Stack linkEP *channel.Endpoint @@ -751,7 +769,7 @@ func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) { return } - log.Printf("Got packet: %v", packet) + //log.Printf("Got packet: %v", packet) } func (s *Server) createDHCPResponse(request gopacket.Packet) ([]byte, error) { @@ -1029,23 +1047,13 @@ func (s *Server) createDNSResponse(pkt gopacket.Packet) ([]byte, error) { // // It returns the souce WAN ip:port to use. func (n *network) doNATOut(src, dst netip.AddrPort) (newSrc netip.AddrPort) { - // TODO(bradfitz): real implementations (multiple styles) later - return netip.AddrPortFrom(n.wanIP, src.Port()) + return (*n.natTable.Load()).PickOutgoingSrc(src, dst, time.Now()) } // doNATIn performs NAT on an incoming packet from WAN src to WAN dst, returning // a new destination LAN ip:port to use. func (n *network) doNATIn(src, dst netip.AddrPort) (newDst netip.AddrPort) { - // TODO(bradfitz): this is temporary. real implementations later. - var theNode *node - for _, node := range n.nodesByIP { - theNode = node - break - } - if theNode == nil { - return - } - return netip.AddrPortFrom(theNode.lanIP, dst.Port()) + return (*n.natTable.Load()).PickIncomingDst(src, dst, time.Now()) } func (n *network) createARPResponse(pkt gopacket.Packet) ([]byte, error) {