tailscale/tstest/natlab/vnet/conf.go
Brad Fitzpatrick ff1d0aa027 tstest/natlab/vnet: start adding tests
And refactor some of vnet.go for testability.

The only behavioral change (with a new test) is that ethernet
broadcasts no longer get sent back to the sender.

Updates #13038

Change-Id: Ic2e7e7d6d8805b7b7f2b5c52c2c5ba97101cef14
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-08-27 18:32:48 -07:00

412 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package vnet
import (
"cmp"
"fmt"
"net/netip"
"os"
"slices"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcapgo"
"tailscale.com/types/logger"
"tailscale.com/util/must"
"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
pcapFile string
blendReality bool
}
// SetPCAPFile sets the filename to write a pcap file to,
// or empty to disable pcap file writing.
func (c *Config) SetPCAPFile(file string) {
c.pcapFile = file
}
// NumNodes returns the number of nodes in the configuration.
func (c *Config) NumNodes() int {
return len(c.nodes)
}
// SetBlendReality sets whether to blend the real controlplane.tailscale.com and
// DERP servers into the virtual network. This is mostly useful for interactive
// testing when working on natlab.
func (c *Config) SetBlendReality(v bool) {
c.blendReality = v
}
// FirstNetwork returns the first network in the config, or nil if none.
func (c *Config) FirstNetwork() *Network {
if len(c.networks) == 0 {
return nil
}
return c.networks[0]
}
func nodeMac(n int) MAC {
// 52=TS then 0xcc for cccclient
return MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(n)}
}
// 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) + 1
n := &Node{
num: num,
mac: nodeMac(num),
}
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)
case TailscaledEnv:
n.env = append(n.env, o)
case NodeOption:
switch o {
case HostFirewall:
n.hostFW = true
case VerboseSyslog:
n.verboseSyslog = true
default:
if n.err == nil {
n.err = fmt.Errorf("unknown NodeOption %q", o)
}
}
default:
if n.err == nil {
n.err = fmt.Errorf("unknown AddNode option type %T", o)
}
}
}
return n
}
// NodeOption is an option that can be passed to Config.AddNode.
type NodeOption string
const (
HostFirewall NodeOption = "HostFirewall"
VerboseSyslog NodeOption = "VerboseSyslog"
)
// TailscaledEnv is а option that can be passed to Config.AddNode
// to set an environment variable for tailscaled.
type TailscaledEnv struct {
Key, Value string
}
// 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) + 1
n := &Network{
num: num,
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.wanIP4 = ip
} else if ip, err := netip.ParsePrefix(o); err == nil {
// If the prefix is IPv4, treat it as the router's internal IPv4 address + CIDR.
// If the prefix is IPv6, treat it as the router's WAN IPv6 + CIDR (typically a /64).
if ip.Addr().Is4() {
n.lanIP4 = ip
} else if ip.Addr().Is6() {
n.wanIP6 = 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
num int // 1-based node number
n *node // nil until NewServer called
env []TailscaledEnv
hostFW bool
verboseSyslog bool
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
// but not done. We need a MAC-per-Network.
mac MAC
nets []*Network
}
// Num returns the 1-based node number.
func (n *Node) Num() int {
return n.num
}
// String returns the string "nodeN" where N is the 1-based node number.
func (n *Node) String() string {
return fmt.Sprintf("node%d", n.num)
}
// MAC returns the MAC address of the node.
func (n *Node) MAC() MAC {
return n.mac
}
func (n *Node) Env() []TailscaledEnv {
return n.env
}
func (n *Node) HostFirewall() bool {
return n.hostFW
}
func (n *Node) VerboseSyslog() bool {
return n.verboseSyslog
}
func (n *Node) SetVerboseSyslog(v bool) {
n.verboseSyslog = v
}
// IsV6Only reports whether this node is only connected to IPv6 networks.
func (n *Node) IsV6Only() bool {
for _, net := range n.nets {
if net.CanV4() {
return false
}
}
for _, net := range n.nets {
if net.CanV6() {
return true
}
}
return false
}
// 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 {
num int // 1-based
mac MAC // MAC address of the router/gateway
natType NAT
wanIP6 netip.Prefix // global unicast router in host bits; CIDR is /64 delegated to LAN
wanIP4 netip.Addr // IPv4 WAN IP, if any
lanIP4 netip.Prefix
nodes []*Node
svcs set.Set[NetworkService]
// ...
err error // carried error
}
func (n *Network) CanV4() bool {
return n.lanIP4.IsValid() || n.wanIP4.IsValid()
}
func (n *Network) CanV6() bool {
return n.wanIP6.IsValid()
}
func (n *Network) CanTakeMoreNodes() bool {
if n.natType == One2OneNAT {
return len(n.nodes) == 0
}
return len(n.nodes) < 150
}
// 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{}
if c.pcapFile != "" {
pcf, err := os.OpenFile(c.pcapFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
nw, err := pcapgo.NewNgWriter(pcf, layers.LinkTypeEthernet)
if err != nil {
return err
}
pw := &pcapWriter{
f: pcf,
w: nw,
}
s.pcapWriter = pw
}
for i, conf := range c.networks {
if conf.err != nil {
return conf.err
}
if !conf.lanIP4.IsValid() && !conf.wanIP6.IsValid() {
conf.lanIP4 = netip.MustParsePrefix("192.168.0.0/24")
}
n := &network{
num: conf.num,
s: s,
mac: conf.mac,
portmap: conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
wanIP6: conf.wanIP6,
v4: conf.lanIP4.IsValid(),
v6: conf.wanIP6.IsValid(),
wanIP4: conf.wanIP4,
lanIP4: conf.lanIP4,
nodesByIP4: map[netip.Addr]*node{},
nodesByMAC: map[MAC]*node{},
logf: logger.WithPrefix(s.logf, fmt.Sprintf("[net-%v] ", conf.mac)),
}
netOfConf[conf] = n
s.networks.Add(n)
if conf.wanIP4.IsValid() {
if conf.wanIP4.Is6() {
return fmt.Errorf("invalid IPv6 address in wanIP")
}
if _, ok := s.networkByWAN.Lookup(conf.wanIP4); ok {
return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP4)
}
s.networkByWAN.Insert(netip.PrefixFrom(conf.wanIP4, 32), n)
}
if conf.wanIP6.IsValid() {
if conf.wanIP6.Addr().Is4() {
return fmt.Errorf("invalid IPv4 address in wanIP6")
}
if _, ok := s.networkByWAN.LookupPrefix(conf.wanIP6); ok {
return fmt.Errorf("two networks have the same WAN IPv6 %v; Anycast not (yet?) supported", conf.wanIP6)
}
s.networkByWAN.Insert(conf.wanIP6, n)
}
n.lanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
Name: fmt.Sprintf("network%d-lan", i+1),
LinkType: layers.LinkTypeIPv4,
}))
n.wanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
Name: fmt.Sprintf("network%d-wan", i+1),
LinkType: layers.LinkTypeIPv4,
}))
}
for _, conf := range c.nodes {
if conf.err != nil {
return conf.err
}
n := &node{
num: conf.num,
mac: conf.mac,
net: netOfConf[conf.Network()],
verboseSyslog: conf.VerboseSyslog(),
}
n.interfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
Name: n.String(),
LinkType: layers.LinkTypeEthernet,
}))
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
if n.net.v4 {
// 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.lanIP4.Addr().As4()
ip4[3] = 100 + n.mac[5]
n.lanIP = netip.AddrFrom4(ip4)
n.net.nodesByIP4[n.lanIP] = n
}
n.net.nodesByMAC[n.mac] = 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
}