Updates #13038 Change-Id: I3c74120d73149c1329288621f6474bbbcaa7e1a6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
6ca078c46e
commit
1ed958fe23
@ -0,0 +1,159 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The tta server is the Tailscale Test Agent.
|
||||
//
|
||||
// It runs on each Tailscale node being integration tested and permits the test
|
||||
// harness to control the node. It connects out to the test drver (rather than
|
||||
// accepting any TCP connections inbound, which might be blocked depending on
|
||||
// the scenario being tested) and then the test driver turns the TCP connection
|
||||
// around and sends request back.
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"flag" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
"os" |
||||
"os/exec" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"tailscale.com/util/set" |
||||
"tailscale.com/version/distro" |
||||
) |
||||
|
||||
var ( |
||||
driverAddr = flag.String("driver", "test-driver.tailscale:8008", "address of the test driver; by default we use the DNS name test-driver.tailscale which is special cased in the emulated network's DNS server") |
||||
) |
||||
|
||||
type chanListener <-chan net.Conn |
||||
|
||||
func serveCmd(w http.ResponseWriter, cmd string, args ...string) { |
||||
if distro.Get() == distro.Gokrazy && !strings.Contains(cmd, "/") { |
||||
cmd = "/user/" + cmd |
||||
} |
||||
out, err := exec.Command(cmd, args...).CombinedOutput() |
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
||||
if err != nil { |
||||
w.Header().Set("Exec-Err", err.Error()) |
||||
w.WriteHeader(500) |
||||
} |
||||
w.Write(out) |
||||
} |
||||
|
||||
func main() { |
||||
if distro.Get() == distro.Gokrazy { |
||||
cmdLine, _ := os.ReadFile("/proc/cmdline") |
||||
if !bytes.Contains(cmdLine, []byte("tailscale-tta=1")) { |
||||
// "Exiting immediately with status code 0 when the
|
||||
// GOKRAZY_FIRST_START=1 environment variable is set means “don’t
|
||||
// start the program on boot”"
|
||||
return |
||||
} |
||||
} |
||||
flag.Parse() |
||||
log.Printf("Tailscale Test Agent running.") |
||||
|
||||
var mux http.ServeMux |
||||
var hs http.Server |
||||
hs.Handler = &mux |
||||
var ( |
||||
stMu sync.Mutex |
||||
newSet = set.Set[net.Conn]{} // conns in StateNew
|
||||
) |
||||
needConnCh := make(chan bool, 1) |
||||
hs.ConnState = func(c net.Conn, s http.ConnState) { |
||||
stMu.Lock() |
||||
defer stMu.Unlock() |
||||
switch s { |
||||
case http.StateNew: |
||||
newSet.Add(c) |
||||
case http.StateClosed: |
||||
newSet.Delete(c) |
||||
} |
||||
if len(newSet) == 0 { |
||||
select { |
||||
case needConnCh <- true: |
||||
default: |
||||
} |
||||
} |
||||
} |
||||
conns := make(chan net.Conn, 1) |
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
io.WriteString(w, "TTA\n") |
||||
return |
||||
}) |
||||
mux.HandleFunc("/up", func(w http.ResponseWriter, r *http.Request) { |
||||
serveCmd(w, "tailscale", "up", "--auth-key=test") |
||||
}) |
||||
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { |
||||
serveCmd(w, "tailscale", "status", "--json") |
||||
}) |
||||
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { |
||||
target := r.FormValue("target") |
||||
cmd := exec.Command("tailscale", "ping", target) |
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
||||
w.(http.Flusher).Flush() |
||||
cmd.Stdout = w |
||||
cmd.Stderr = w |
||||
if err := cmd.Run(); err != nil { |
||||
fmt.Fprintf(w, "error: %v\n", err) |
||||
} |
||||
}) |
||||
go hs.Serve(chanListener(conns)) |
||||
|
||||
var lastErr string |
||||
needConnCh <- true |
||||
for { |
||||
<-needConnCh |
||||
c, err := connect() |
||||
log.Printf("Connect: %v", err) |
||||
if err != nil { |
||||
s := err.Error() |
||||
if s != lastErr { |
||||
log.Printf("Connect failure: %v", s) |
||||
} |
||||
lastErr = s |
||||
time.Sleep(time.Second) |
||||
continue |
||||
} |
||||
conns <- c |
||||
|
||||
time.Sleep(time.Second) |
||||
} |
||||
} |
||||
|
||||
func connect() (net.Conn, error) { |
||||
c, err := net.Dial("tcp", *driverAddr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return c, nil |
||||
} |
||||
|
||||
func (cl chanListener) Accept() (net.Conn, error) { |
||||
c, ok := <-cl |
||||
if !ok { |
||||
return nil, errors.New("closed") |
||||
} |
||||
return c, nil |
||||
} |
||||
|
||||
func (cl chanListener) Close() error { |
||||
return nil |
||||
} |
||||
|
||||
func (cl chanListener) Addr() net.Addr { |
||||
return &net.TCPAddr{ |
||||
IP: net.ParseIP("52.0.0.34"), // TS..DR(iver)
|
||||
Port: 123, |
||||
} |
||||
} |
||||
@ -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