mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-07 22:42:02 -05: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>
354 lines
11 KiB
Go
354 lines
11 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package netutil
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"tailscale.com/net/netmon"
|
|
)
|
|
|
|
// protocolsRequiredForForwarding reports whether IPv4 and/or IPv6 protocols are
|
|
// required to forward the specified routes.
|
|
// The state param must be specified.
|
|
func protocolsRequiredForForwarding(routes []netip.Prefix, state *netmon.State) (v4, v6 bool) {
|
|
if len(routes) == 0 {
|
|
// Nothing to route, so no need to warn.
|
|
return false, false
|
|
}
|
|
|
|
localIPs := make(map[netip.Addr]bool)
|
|
for _, addrs := range state.InterfaceIPs {
|
|
for _, pfx := range addrs {
|
|
localIPs[pfx.Addr()] = true
|
|
}
|
|
}
|
|
|
|
for _, r := range routes {
|
|
// It's possible to advertise a route to one of the local
|
|
// machine's local IPs. IP forwarding isn't required for this
|
|
// to work, so we shouldn't warn for such exports.
|
|
if r.IsSingleIP() && localIPs[r.Addr()] {
|
|
continue
|
|
}
|
|
if r.Addr().Is4() {
|
|
v4 = true
|
|
} else {
|
|
v6 = true
|
|
}
|
|
}
|
|
return v4, v6
|
|
}
|
|
|
|
// CheckIPForwarding reports whether IP forwarding is enabled correctly
|
|
// for subnet routing and exit node functionality on any interface.
|
|
// The state param must not be nil.
|
|
// The routes should only be advertised routes, and should not contain the
|
|
// nodes Tailscale IPs.
|
|
// It returns an error if it is unable to determine if IP forwarding is enabled.
|
|
// It returns a warning describing configuration issues if IP forwarding is
|
|
// non-functional or partly functional.
|
|
func CheckIPForwarding(routes []netip.Prefix, state *netmon.State) (warn, err error) {
|
|
if runtime.GOOS != "linux" {
|
|
switch runtime.GOOS {
|
|
case "dragonfly", "freebsd", "netbsd", "openbsd":
|
|
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS), nil
|
|
case "illumos", "solaris":
|
|
_, err := ipForwardingEnabledSunOS(ipv4, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, "")
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
if state == nil {
|
|
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration; no link state")
|
|
}
|
|
const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
|
|
wantV4, wantV6 := protocolsRequiredForForwarding(routes, state)
|
|
if !wantV4 && !wantV6 {
|
|
return nil, nil
|
|
}
|
|
|
|
v4e, err := ipForwardingEnabledLinux(ipv4, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
|
}
|
|
v6e, err := ipForwardingEnabledLinux(ipv6, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
|
}
|
|
|
|
if v4e && v6e {
|
|
// IP forwarding is enabled systemwide, all is well.
|
|
return nil, nil
|
|
}
|
|
|
|
if !wantV4 {
|
|
if !v6e {
|
|
return nil, fmt.Errorf("IPv6 forwarding is disabled, subnet routing/exit nodes may not work.%s", kbLink)
|
|
}
|
|
return nil, nil
|
|
}
|
|
// IP forwarding isn't enabled globally, but it might be enabled
|
|
// on a per-interface basis. Check if it's on for all interfaces,
|
|
// and warn appropriately if it's not.
|
|
// Note: you might be wondering why we check only the state of
|
|
// ipv6.conf.all.forwarding, rather than per-interface forwarding
|
|
// configuration. According to kernel documentation, it seems
|
|
// that to actually forward packets, you need to enable
|
|
// forwarding globally, and the per-interface forwarding
|
|
// setting only alters other things such as how router
|
|
// advertisements are handled. The kernel itself warns that
|
|
// enabling forwarding per-interface and not globally will
|
|
// probably not work, so I feel okay calling those configs
|
|
// broken until we have proof otherwise.
|
|
var (
|
|
anyEnabled bool
|
|
warnings []string
|
|
)
|
|
if wantV6 && !v6e {
|
|
warnings = append(warnings, "IPv6 forwarding is disabled.")
|
|
}
|
|
for _, iface := range state.Interface {
|
|
if iface.Name == "lo" {
|
|
continue
|
|
}
|
|
v4e, err := ipForwardingEnabledLinux(ipv4, iface.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
|
} else if !v4e {
|
|
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, ipForwardSysctlKey(dotFormat, ipv4, iface.Name)))
|
|
} else {
|
|
anyEnabled = true
|
|
}
|
|
}
|
|
if !anyEnabled {
|
|
// IP forwarding is completely disabled, just say that rather
|
|
// than enumerate all the interfaces on the system.
|
|
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink), nil
|
|
}
|
|
if len(warnings) > 0 {
|
|
// If partially enabled, enumerate the bits that won't work.
|
|
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink), nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// CheckReversePathFiltering reports whether reverse path filtering is either
|
|
// disabled or set to 'loose' mode for exit node functionality on any
|
|
// interface.
|
|
//
|
|
// The routes should only be advertised routes, and should not contain the
|
|
// node's Tailscale IPs.
|
|
//
|
|
// This function returns an error if it is unable to determine whether reverse
|
|
// path filtering is enabled, or a warning describing configuration issues if
|
|
// reverse path fitering is non-functional or partly functional.
|
|
func CheckReversePathFiltering(state *netmon.State) (warn []string, err error) {
|
|
if runtime.GOOS != "linux" {
|
|
return nil, nil
|
|
}
|
|
|
|
if state == nil {
|
|
return nil, errors.New("no link state")
|
|
}
|
|
|
|
// The kernel uses the maximum value for rp_filter between the 'all'
|
|
// setting and each per-interface config, so we need to fetch both.
|
|
allSetting, err := reversePathFilterValueLinux("all")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading global rp_filter value: %w", err)
|
|
}
|
|
|
|
const (
|
|
filtOff = 0
|
|
filtStrict = 1
|
|
filtLoose = 2
|
|
)
|
|
|
|
// Because the kernel use the max rp_filter value, each interface will use 'loose', so we
|
|
// can abort early.
|
|
if allSetting == filtLoose {
|
|
return nil, nil
|
|
}
|
|
|
|
for _, iface := range state.Interface {
|
|
if iface.IsLoopback() {
|
|
continue
|
|
}
|
|
|
|
iSetting, err := reversePathFilterValueLinux(iface.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading interface rp_filter value for %q: %w", iface.Name, err)
|
|
}
|
|
// Perform the same max() that the kernel does
|
|
if allSetting > iSetting {
|
|
iSetting = allSetting
|
|
}
|
|
if iSetting == filtStrict {
|
|
warn = append(warn, fmt.Sprintf("interface %q has strict reverse-path filtering enabled", iface.Name))
|
|
}
|
|
}
|
|
return warn, nil
|
|
}
|
|
|
|
// ipForwardSysctlKey returns the sysctl key for the given protocol and iface.
|
|
// When the dotFormat parameter is true the output is formatted as `net.ipv4.ip_forward`,
|
|
// else it is `net/ipv4/ip_forward`
|
|
func ipForwardSysctlKey(format sysctlFormat, p protocol, iface string) string {
|
|
if iface == "" {
|
|
if format == dotFormat {
|
|
if p == ipv4 {
|
|
return "net.ipv4.ip_forward"
|
|
}
|
|
return "net.ipv6.conf.all.forwarding"
|
|
}
|
|
if p == ipv4 {
|
|
return "net/ipv4/ip_forward"
|
|
}
|
|
return "net/ipv6/conf/all/forwarding"
|
|
}
|
|
|
|
var k string
|
|
if p == ipv4 {
|
|
k = "net/ipv4/conf/%s/forwarding"
|
|
} else {
|
|
k = "net/ipv6/conf/%s/forwarding"
|
|
}
|
|
if format == dotFormat {
|
|
// Swap the delimiters.
|
|
iface = strings.ReplaceAll(iface, ".", "/")
|
|
k = strings.ReplaceAll(k, "/", ".")
|
|
}
|
|
return fmt.Sprintf(k, iface)
|
|
}
|
|
|
|
// rpFilterSysctlKey returns the sysctl key for the given iface.
|
|
//
|
|
// Format controls whether the output is formatted as
|
|
// `net.ipv4.conf.iface.rp_filter` or `net/ipv4/conf/iface/rp_filter`.
|
|
func rpFilterSysctlKey(format sysctlFormat, iface string) string {
|
|
// No iface means all interfaces
|
|
if iface == "" {
|
|
iface = "all"
|
|
}
|
|
|
|
k := "net/ipv4/conf/%s/rp_filter"
|
|
if format == dotFormat {
|
|
// Swap the delimiters.
|
|
iface = strings.ReplaceAll(iface, ".", "/")
|
|
k = strings.ReplaceAll(k, "/", ".")
|
|
}
|
|
return fmt.Sprintf(k, iface)
|
|
}
|
|
|
|
type sysctlFormat int
|
|
|
|
const (
|
|
dotFormat sysctlFormat = iota
|
|
slashFormat
|
|
)
|
|
|
|
type protocol int
|
|
|
|
const (
|
|
ipv4 protocol = iota
|
|
ipv6
|
|
)
|
|
|
|
// ipForwardingEnabledLinux reports whether the IP Forwarding is enabled for the
|
|
// given interface.
|
|
// The iface param determines which interface to check against, "" means to check
|
|
// global config.
|
|
// This is Linux-specific: it only reads from /proc/sys and doesn't shell out to
|
|
// sysctl (which on Linux just reads from /proc/sys anyway).
|
|
func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
|
|
k := ipForwardSysctlKey(slashFormat, p, iface)
|
|
bs, err := os.ReadFile(filepath.Join("/proc/sys", k))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// If IPv6 is disabled, sysctl keys like "net.ipv6.conf.all.forwarding" just don't
|
|
// exist on disk. But first diagnose whether procfs is even mounted before assuming
|
|
// absence means false.
|
|
if fi, err := os.Stat("/proc/sys"); err != nil {
|
|
return false, fmt.Errorf("failed to check sysctl %v; no procfs? %w", k, err)
|
|
} else if !fi.IsDir() {
|
|
return false, fmt.Errorf("failed to check sysctl %v; /proc/sys isn't a directory, is %v", k, fi.Mode())
|
|
}
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
|
|
if err != nil {
|
|
return false, fmt.Errorf("couldn't parse %s: %w", k, err)
|
|
}
|
|
// 0 = disabled, 1 = enabled, 2 = enabled (but uncommon)
|
|
// https://github.com/tailscale/tailscale/issues/8375
|
|
if val < 0 || val > 2 {
|
|
return false, fmt.Errorf("unexpected value %d for %s", val, k)
|
|
}
|
|
on := val == 1 || val == 2
|
|
return on, nil
|
|
}
|
|
|
|
// reversePathFilterValueLinux reports the reverse path filter setting on Linux
|
|
// for the given interface.
|
|
//
|
|
// The iface param determines which interface to check against; the empty
|
|
// string means to check the global config.
|
|
//
|
|
// This function tries to look up the value directly from `/proc/sys`, and
|
|
// falls back to using the `sysctl` command on failure.
|
|
func reversePathFilterValueLinux(iface string) (int, error) {
|
|
k := rpFilterSysctlKey(slashFormat, iface)
|
|
bs, err := os.ReadFile(filepath.Join("/proc/sys", k))
|
|
if err != nil {
|
|
// Fall back to the sysctl command
|
|
k := rpFilterSysctlKey(dotFormat, iface)
|
|
bs, err = exec.Command("sysctl", "-n", k).Output()
|
|
if err != nil {
|
|
return -1, fmt.Errorf("couldn't check %s (%v)", k, err)
|
|
}
|
|
}
|
|
v, err := strconv.Atoi(string(bytes.TrimSpace(bs)))
|
|
if err != nil {
|
|
return -1, fmt.Errorf("couldn't parse %s (%v)", k, err)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func ipForwardingEnabledSunOS(p protocol, iface string) (bool, error) {
|
|
var proto string
|
|
if p == ipv4 {
|
|
proto = "ipv4"
|
|
} else if p == ipv6 {
|
|
proto = "ipv6"
|
|
} else {
|
|
return false, fmt.Errorf("unknown protocol")
|
|
}
|
|
|
|
ipadmCmd := "\"ipadm show-prop " + proto + " -p forwarding -o CURRENT -c\""
|
|
bs, err := exec.Command("ipadm", "show-prop", proto, "-p", "forwarding", "-o", "CURRENT", "-c").Output()
|
|
if err != nil {
|
|
return false, fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", ipadmCmd, err)
|
|
}
|
|
if string(bs) != "on\n" {
|
|
return false, fmt.Errorf("IP forwarding is set to off. Subnet routes won't work. Try 'routeadm -u -e %s-forwarding'", proto)
|
|
}
|
|
return true, nil
|
|
}
|