mirror of https://github.com/tailscale/tailscale/
tstest/natlab/vnet: add start of virtual network-based NAT Lab
Updates #13038 Change-Id: I3c74120d73149c1329288621f6474bbbcaa7e1a6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>pull/13060/head
parent
6ca078c46e
commit
1ed958fe23
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Type 'C-a c' to enter monitor; q to quit."
|
||||
|
||||
set -eux
|
||||
qemu-system-x86_64 -M microvm,isa-serial=off \
|
||||
-m 1G \
|
||||
-nodefaults -no-user-config -nographic \
|
||||
-kernel $HOME/src/github.com/tailscale/gokrazy-kernel/vmlinuz \
|
||||
-append "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-dd02023b0001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet tailscale-tta=1" \
|
||||
-drive id=blk0,file=$HOME/src/tailscale.com/gokrazy/tsapp.img,format=raw \
|
||||
-device virtio-blk-device,drive=blk0 \
|
||||
-netdev stream,id=net0,addr.type=unix,addr.path=/tmp/qemu.sock \
|
||||
-device virtio-serial-device \
|
||||
-device virtio-net-device,netdev=net0,mac=52:cc:cc:cc:cc:00 \
|
||||
-chardev stdio,id=virtiocon0,mux=on \
|
||||
-device virtconsole,chardev=virtiocon0 \
|
||||
-mon chardev=virtiocon0,mode=readline \
|
||||
-audio none
|
||||
|
@ -0,0 +1,101 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The vnet binary runs a virtual network stack in userspace for qemu instances
|
||||
// to connect to and simulate various network conditions.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstest/natlab/vnet"
|
||||
)
|
||||
|
||||
var (
|
||||
listen = flag.String("listen", "/tmp/qemu.sock", "path to listen on")
|
||||
nat = flag.String("nat", "easy", "type of NAT to use")
|
||||
portmap = flag.Bool("portmap", false, "enable portmapping")
|
||||
dgram = flag.Bool("dgram", false, "enable datagram mode; for use with macOS Hypervisor.Framework and VZFileHandleNetworkDeviceAttachment")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if _, err := os.Stat(*listen); err == nil {
|
||||
os.Remove(*listen)
|
||||
}
|
||||
|
||||
var srv net.Listener
|
||||
var err error
|
||||
var conn *net.UnixConn
|
||||
if *dgram {
|
||||
addr, err := net.ResolveUnixAddr("unixgram", *listen)
|
||||
if err != nil {
|
||||
log.Fatalf("ResolveUnixAddr: %v", err)
|
||||
}
|
||||
conn, err = net.ListenUnixgram("unixgram", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("ListenUnixgram: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
} else {
|
||||
srv, err = net.Listen("unix", *listen)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var c vnet.Config
|
||||
node1 := c.AddNode(c.AddNetwork("2.1.1.1", "192.168.1.1/24", vnet.NAT(*nat)))
|
||||
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", vnet.NAT(*nat)))
|
||||
if *portmap {
|
||||
node1.Network().AddService(vnet.NATPMP)
|
||||
}
|
||||
|
||||
s, err := vnet.New(&c)
|
||||
if err != nil {
|
||||
log.Fatalf("newServer: %v", err)
|
||||
}
|
||||
|
||||
if err := s.PopulateDERPMapIPs(); err != nil {
|
||||
log.Printf("warning: ignoring failure to populate DERP map: %v", err)
|
||||
}
|
||||
|
||||
s.WriteStartingBanner(os.Stdout)
|
||||
|
||||
go func() {
|
||||
getStatus := func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
st, err := s.NodeStatus(ctx, node1)
|
||||
if err != nil {
|
||||
log.Printf("NodeStatus: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("NodeStatus: %q", st)
|
||||
}
|
||||
for {
|
||||
time.Sleep(5 * time.Second)
|
||||
getStatus()
|
||||
}
|
||||
}()
|
||||
|
||||
if conn != nil {
|
||||
s.ServeUnixConn(conn, vnet.ProtocolUnixDGRAM)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
c, err := srv.Accept()
|
||||
if err != nil {
|
||||
log.Printf("Accept: %v", err)
|
||||
continue
|
||||
}
|
||||
go s.ServeUnixConn(c.(*net.UnixConn), vnet.ProtocolQEMU)
|
||||
}
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// Note: the exported Node and Network are the configuration types;
|
||||
// the unexported node and network are the runtime types that are actually
|
||||
// used once the server is created.
|
||||
|
||||
// Config is the requested state of the natlab virtual network.
|
||||
//
|
||||
// The zero value is a valid empty configuration. Call AddNode
|
||||
// and AddNetwork to methods on the returned Node and Network
|
||||
// values to modify the config before calling NewServer.
|
||||
// Once the NewServer is called, Config is no longer used.
|
||||
type Config struct {
|
||||
nodes []*Node
|
||||
networks []*Network
|
||||
}
|
||||
|
||||
// AddNode creates a new node in the world.
|
||||
//
|
||||
// The opts may be of the following types:
|
||||
// - *Network: zero, one, or more networks to add this node to
|
||||
// - TODO: more
|
||||
//
|
||||
// On an error or unknown opt type, AddNode returns a
|
||||
// node with a carried error that gets returned later.
|
||||
func (c *Config) AddNode(opts ...any) *Node {
|
||||
num := len(c.nodes)
|
||||
n := &Node{
|
||||
mac: MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(num)}, // 52=TS then 0xcc for ccclient
|
||||
}
|
||||
c.nodes = append(c.nodes, n)
|
||||
for _, o := range opts {
|
||||
switch o := o.(type) {
|
||||
case *Network:
|
||||
if !slices.Contains(o.nodes, n) {
|
||||
o.nodes = append(o.nodes, n)
|
||||
}
|
||||
n.nets = append(n.nets, o)
|
||||
default:
|
||||
if n.err == nil {
|
||||
n.err = fmt.Errorf("unknown AddNode option type %T", o)
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// AddNetwork add a new network.
|
||||
//
|
||||
// The opts may be of the following types:
|
||||
// - string IP address, for the network's WAN IP (if any)
|
||||
// - string netip.Prefix, for the network's LAN IP (defaults to 192.168.0.0/24)
|
||||
// - NAT, the type of NAT to use
|
||||
// - NetworkService, a service to add to the network
|
||||
//
|
||||
// On an error or unknown opt type, AddNetwork returns a
|
||||
// network with a carried error that gets returned later.
|
||||
func (c *Config) AddNetwork(opts ...any) *Network {
|
||||
num := len(c.networks)
|
||||
n := &Network{
|
||||
mac: MAC{0x52, 0xee, 0xee, 0xee, 0xee, byte(num)}, // 52=TS then 0xee for 'etwork
|
||||
}
|
||||
c.networks = append(c.networks, n)
|
||||
for _, o := range opts {
|
||||
switch o := o.(type) {
|
||||
case string:
|
||||
if ip, err := netip.ParseAddr(o); err == nil {
|
||||
n.wanIP = ip
|
||||
} else if ip, err := netip.ParsePrefix(o); err == nil {
|
||||
n.lanIP = ip
|
||||
} else {
|
||||
if n.err == nil {
|
||||
n.err = fmt.Errorf("unknown string option %q", o)
|
||||
}
|
||||
}
|
||||
case NAT:
|
||||
n.natType = o
|
||||
case NetworkService:
|
||||
n.AddService(o)
|
||||
default:
|
||||
if n.err == nil {
|
||||
n.err = fmt.Errorf("unknown AddNetwork option type %T", o)
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Node is the configuration of a node in the virtual network.
|
||||
type Node struct {
|
||||
err error
|
||||
n *node // nil until NewServer called
|
||||
|
||||
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
|
||||
// but not done. We need a MAC-per-Network.
|
||||
|
||||
mac MAC
|
||||
nets []*Network
|
||||
}
|
||||
|
||||
// Network returns the first network this node is connected to,
|
||||
// or nil if none.
|
||||
func (n *Node) Network() *Network {
|
||||
if len(n.nets) == 0 {
|
||||
return nil
|
||||
}
|
||||
return n.nets[0]
|
||||
}
|
||||
|
||||
// Network is the configuration of a network in the virtual network.
|
||||
type Network struct {
|
||||
mac MAC // MAC address of the router/gateway
|
||||
natType NAT
|
||||
|
||||
wanIP netip.Addr
|
||||
lanIP netip.Prefix
|
||||
nodes []*Node
|
||||
|
||||
svcs set.Set[NetworkService]
|
||||
|
||||
// ...
|
||||
err error // carried error
|
||||
}
|
||||
|
||||
// NetworkService is a service that can be added to a network.
|
||||
type NetworkService string
|
||||
|
||||
const (
|
||||
NATPMP NetworkService = "NAT-PMP"
|
||||
PCP NetworkService = "PCP"
|
||||
UPnP NetworkService = "UPnP"
|
||||
)
|
||||
|
||||
// AddService adds a network service (such as port mapping protocols) to a
|
||||
// network.
|
||||
func (n *Network) AddService(s NetworkService) {
|
||||
if n.svcs == nil {
|
||||
n.svcs = set.Of(s)
|
||||
} else {
|
||||
n.svcs.Add(s)
|
||||
}
|
||||
}
|
||||
|
||||
// initFromConfig initializes the server from the previous calls
|
||||
// to NewNode and NewNetwork and returns an error if
|
||||
// there were any configuration issues.
|
||||
func (s *Server) initFromConfig(c *Config) error {
|
||||
netOfConf := map[*Network]*network{}
|
||||
for _, conf := range c.networks {
|
||||
if conf.err != nil {
|
||||
return conf.err
|
||||
}
|
||||
if !conf.lanIP.IsValid() {
|
||||
conf.lanIP = netip.MustParsePrefix("192.168.0.0/24")
|
||||
}
|
||||
n := &network{
|
||||
s: s,
|
||||
mac: conf.mac,
|
||||
portmap: conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
|
||||
wanIP: conf.wanIP,
|
||||
lanIP: conf.lanIP,
|
||||
nodesByIP: map[netip.Addr]*node{},
|
||||
}
|
||||
netOfConf[conf] = n
|
||||
s.networks.Add(n)
|
||||
if _, ok := s.networkByWAN[conf.wanIP]; ok {
|
||||
return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP)
|
||||
}
|
||||
s.networkByWAN[conf.wanIP] = n
|
||||
}
|
||||
for _, conf := range c.nodes {
|
||||
if conf.err != nil {
|
||||
return conf.err
|
||||
}
|
||||
n := &node{
|
||||
mac: conf.mac,
|
||||
net: netOfConf[conf.Network()],
|
||||
}
|
||||
conf.n = n
|
||||
if _, ok := s.nodeByMAC[n.mac]; ok {
|
||||
return fmt.Errorf("two nodes have the same MAC %v", n.mac)
|
||||
}
|
||||
s.nodes = append(s.nodes, n)
|
||||
s.nodeByMAC[n.mac] = n
|
||||
|
||||
// Allocate a lanIP for the node. Use the network's CIDR and use final
|
||||
// octet 101 (for first node), 102, etc. The node number comes from the
|
||||
// last octent of the MAC address (0-based)
|
||||
ip4 := n.net.lanIP.Addr().As4()
|
||||
ip4[3] = 101 + n.mac[5]
|
||||
n.lanIP = netip.AddrFrom4(ip4)
|
||||
n.net.nodesByIP[n.lanIP] = n
|
||||
}
|
||||
|
||||
// Now that nodes are populated, set up NAT:
|
||||
for _, conf := range c.networks {
|
||||
n := netOfConf[conf]
|
||||
natType := cmp.Or(conf.natType, EasyNAT)
|
||||
if err := n.InitNAT(natType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package vnet
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*Config)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
setup: func(c *Config) {
|
||||
c.AddNode(c.AddNetwork("2.1.1.1", "192.168.1.1/24", EasyNAT, NATPMP))
|
||||
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", HardNAT))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "indirect",
|
||||
setup: func(c *Config) {
|
||||
n1 := c.AddNode(c.AddNetwork("2.1.1.1", "192.168.1.1/24", HardNAT))
|
||||
n1.Network().AddService(NATPMP)
|
||||
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", NAT("hard")))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi-node-in-net",
|
||||
setup: func(c *Config) {
|
||||
net1 := c.AddNetwork("2.1.1.1", "192.168.1.1/24")
|
||||
c.AddNode(net1)
|
||||
c.AddNode(net1)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dup-wan-ip",
|
||||
setup: func(c *Config) {
|
||||
c.AddNetwork("2.1.1.1", "192.168.1.1/24")
|
||||
c.AddNetwork("2.1.1.1", "10.2.0.1/16")
|
||||
},
|
||||
wantErr: "two networks have the same WAN IP 2.1.1.1; Anycast not (yet?) supported",
|
||||
},
|
||||
{
|
||||
name: "one-to-one-nat-with-multiple-nodes",
|
||||
setup: func(c *Config) {
|
||||
net1 := c.AddNetwork("2.1.1.1", "192.168.1.1/24", One2OneNAT)
|
||||
c.AddNode(net1)
|
||||
c.AddNode(net1)
|
||||
},
|
||||
wantErr: "error creating NAT type \"one2one\" for network 2.1.1.1: can't use one2one NAT type on networks other than single-node networks",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var c Config
|
||||
tt.setup(&c)
|
||||
_, err := New(&c)
|
||||
if err == nil {
|
||||
if tt.wantErr == "" {
|
||||
return
|
||||
}
|
||||
t.Fatalf("got success; wanted error %q", tt.wantErr)
|
||||
}
|
||||
if err.Error() != tt.wantErr {
|
||||
t.Fatalf("got error %q; want %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
// 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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue