start of pluggable NAT impl types

Change-Id: I633bce41e978f385eab26478baa42e56178c489a
This commit is contained in:
Brad Fitzpatrick 2024-07-27 21:37:04 -07:00
parent 1071dc5d4d
commit 0f2ecf8a18
2 changed files with 79 additions and 18 deletions

53
natlab/natlabd/nat.go Normal file
View File

@ -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())
}

View File

@ -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) {