mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-09 14:16:14 +02:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
464 lines
12 KiB
Go
464 lines
12 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package vnet
|
||
|
||
import (
|
||
"cmp"
|
||
"fmt"
|
||
"iter"
|
||
"net/netip"
|
||
"os"
|
||
"slices"
|
||
"time"
|
||
|
||
"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 (c *Config) Nodes() iter.Seq2[int, *Node] {
|
||
return slices.All(c.nodes)
|
||
}
|
||
|
||
func nodeMac(n int) MAC {
|
||
// 52=TS then 0xcc for cccclient
|
||
return MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(n)}
|
||
}
|
||
|
||
func routerMac(n int) MAC {
|
||
// 52=TS then 0xee for 'etwork
|
||
return MAC{0x52, 0xee, 0xee, 0xee, 0xee, 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)
|
||
}
|
||
}
|
||
case MAC:
|
||
n.mac = 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: routerMac(num),
|
||
}
|
||
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
|
||
breakWAN4 bool // whether to break WAN IPv4 connectivity
|
||
|
||
svcs set.Set[NetworkService]
|
||
|
||
latency time.Duration // latency applied to interface writes
|
||
lossRate float64 // chance of packet loss (0.0 to 1.0)
|
||
|
||
// ...
|
||
err error // carried error
|
||
}
|
||
|
||
// SetLatency sets the simulated network latency for this network.
|
||
func (n *Network) SetLatency(d time.Duration) {
|
||
n.latency = d
|
||
}
|
||
|
||
// SetPacketLoss sets the packet loss rate for this network 0.0 (no loss) to 1.0 (total loss).
|
||
func (n *Network) SetPacketLoss(rate float64) {
|
||
if rate < 0 {
|
||
rate = 0
|
||
} else if rate > 1 {
|
||
rate = 1
|
||
}
|
||
n.lossRate = rate
|
||
}
|
||
|
||
// SetBlackholedIPv4 sets whether the network should blackhole all IPv4 traffic
|
||
// out to the Internet. (DHCP etc continues to work on the LAN.)
|
||
func (n *Network) SetBlackholedIPv4(v bool) {
|
||
n.breakWAN4 = v
|
||
}
|
||
|
||
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,
|
||
breakWAN4: conf.breakWAN4,
|
||
latency: conf.latency,
|
||
lossRate: conf.lossRate,
|
||
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
|
||
}
|