mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
73b3c8fc8c
This adds support for sending packets to 33:33:00:00:01 at IPv6 multicast address ff02::1 to send to all nodes. Nothing in Tailscale depends on this (yet?), but it makes debugging in VMs behind natlab easier (e.g. you can ping all nodes), and other things might depend on this in the future. Mostly I'm trying to flesh out the IPv6 support in natlab now that we can write vnet tests. Updates #13038 Change-Id: If590031fcf075690ca35c7b230a38c3e72e621eb Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
423 lines
11 KiB
Go
423 lines
11 KiB
Go
// 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)}
|
||
}
|
||
|
||
var lanSLAACBase = netip.MustParseAddr("fe80::50cc:ccff:fecc:cc01")
|
||
|
||
// nodeLANIP6 returns a node number's Link Local SLAAC IPv6 address,
|
||
// such as fe80::50cc:ccff:fecc:cc03 for node 3.
|
||
func nodeLANIP6(n int) netip.Addr {
|
||
a := lanSLAACBase.As16()
|
||
a[15] = byte(n)
|
||
return netip.AddrFrom16(a)
|
||
}
|
||
|
||
// 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)
|
||
// if IPv4, or its WAN IPv6 + CIDR (e.g. "2000:52::1/64")
|
||
// - 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
|
||
}
|