mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 12:16:44 +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>
294 lines
8.7 KiB
Go
294 lines
8.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package vnet
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"math/rand/v2"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
const (
|
|
One2OneNAT NAT = "one2one"
|
|
EasyNAT NAT = "easy" // address+port filtering
|
|
EasyAFNAT NAT = "easyaf" // address filtering (not port)
|
|
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)
|
|
|
|
// IsPublicPortUsed reports whether the provided WAN IP+port is in use by
|
|
// anything. (In particular, the NAT-PMP/etc port mappers might have taken
|
|
// a port.) Implementations should check this before allocating a port,
|
|
// and then they should report IsPublicPortUsed themselves for that port.
|
|
IsPublicPortUsed(netip.AddrPort) bool
|
|
}
|
|
|
|
// 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)
|
|
|
|
// IsPublicPortUsed reports whether the provided WAN IP+port is in use by
|
|
// anything. The port mapper uses this to avoid grabbing an in-use port.
|
|
IsPublicPortUsed(netip.AddrPort) bool
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
|
|
func (n *oneToOneNAT) IsPublicPortUsed(netip.AddrPort) bool {
|
|
return true // all ports are owned by the 1:1 NAT
|
|
}
|
|
|
|
type srcDstTuple struct {
|
|
src netip.AddrPort
|
|
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 {
|
|
pool IPPool
|
|
wanIP netip.Addr
|
|
|
|
out map[srcDstTuple]portMappingAndTime
|
|
in map[hardKeyIn]lanAddrAndTime
|
|
}
|
|
|
|
func init() {
|
|
registerNATType(HardNAT, func(p IPPool) (NATTable, error) {
|
|
return &hardNAT{pool: p, wanIP: p.WANIP()}, nil
|
|
})
|
|
}
|
|
|
|
func (n *hardNAT) IsPublicPortUsed(ap netip.AddrPort) bool {
|
|
if ap.Addr() != n.wanIP {
|
|
return false
|
|
}
|
|
for k := range n.in {
|
|
if k.wanPort == ap.Port() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (n *hardNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
|
|
ko := srcDstTuple{src, 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
|
|
if n.pool.IsPublicPortUsed(netip.AddrPortFrom(n.wanIP, port)) {
|
|
continue
|
|
}
|
|
|
|
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 {
|
|
pool IPPool
|
|
wanIP netip.Addr
|
|
out map[netip.AddrPort]portMappingAndTime
|
|
in map[uint16]lanAddrAndTime
|
|
lastOut map[srcDstTuple]time.Time // (lan:port, wan:port) => last packet out time
|
|
}
|
|
|
|
func init() {
|
|
registerNATType(EasyNAT, func(p IPPool) (NATTable, error) {
|
|
return &easyNAT{pool: p, wanIP: p.WANIP()}, nil
|
|
})
|
|
}
|
|
|
|
func (n *easyNAT) IsPublicPortUsed(ap netip.AddrPort) bool {
|
|
if ap.Addr() != n.wanIP {
|
|
return false
|
|
}
|
|
_, ok := n.in[ap.Port()]
|
|
return ok
|
|
}
|
|
|
|
func (n *easyNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
|
|
mak.Set(&n.lastOut, srcDstTuple{src, dst}, at)
|
|
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)
|
|
if n.pool.IsPublicPortUsed(wanAddr) {
|
|
continue
|
|
}
|
|
|
|
// 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.
|
|
}
|
|
lanDst = n.in[dst.Port()].lanAddr
|
|
|
|
// Stateful firewall: drop incoming packets that don't have traffic out.
|
|
// TODO(bradfitz): verify Linux does this in the router code, not in the NAT code.
|
|
if t, ok := n.lastOut[srcDstTuple{lanDst, src}]; !ok || at.Sub(t) > 300*time.Second {
|
|
log.Printf("Drop incoming packet from %v to %v; no recent outgoing packet", src, dst)
|
|
return netip.AddrPort{}
|
|
}
|
|
|
|
return lanDst
|
|
}
|