Update dependencies
This commit is contained in:
130
vendor/tailscale.com/util/linuxfw/detector.go
generated
vendored
Normal file
130
vendor/tailscale.com/util/linuxfw/detector.go
generated
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
logf("GoKrazy should use nftables.")
|
||||
hostinfo.SetFirewallMode("nft-gokrazy")
|
||||
return FirewallModeNfTables
|
||||
}
|
||||
|
||||
mode := envknob.String("TS_DEBUG_FIREWALL_MODE")
|
||||
// If the envknob isn't set, fall back to the pref suggested by c2n or
|
||||
// nodeattrs.
|
||||
if mode == "" {
|
||||
mode = prefHint
|
||||
logf("using firewall mode pref %s", prefHint)
|
||||
} else if prefHint != "" {
|
||||
logf("TS_DEBUG_FIREWALL_MODE set, overriding firewall mode from %s to %s", prefHint, mode)
|
||||
}
|
||||
|
||||
var det linuxFWDetector
|
||||
if mode == "" {
|
||||
// We have no preference, so check if `iptables` is even available.
|
||||
_, err := det.iptDetect()
|
||||
if err != nil && errors.Is(err, exec.ErrNotFound) {
|
||||
logf("iptables not found: %v; falling back to nftables", err)
|
||||
mode = "nftables"
|
||||
}
|
||||
}
|
||||
|
||||
// We now use iptables as default and have "auto" and "nftables" as
|
||||
// options for people to test further.
|
||||
switch mode {
|
||||
case "auto":
|
||||
return pickFirewallModeFromInstalledRules(logf, det)
|
||||
case "nftables":
|
||||
hostinfo.SetFirewallMode("nft-forced")
|
||||
return FirewallModeNfTables
|
||||
case "iptables":
|
||||
hostinfo.SetFirewallMode("ipt-forced")
|
||||
default:
|
||||
logf("default choosing iptables")
|
||||
hostinfo.SetFirewallMode("ipt-default")
|
||||
}
|
||||
return FirewallModeIPTables
|
||||
}
|
||||
|
||||
// tableDetector abstracts helpers to detect the firewall mode.
|
||||
// It is implemented for testing purposes.
|
||||
type tableDetector interface {
|
||||
iptDetect() (int, error)
|
||||
nftDetect() (int, error)
|
||||
}
|
||||
|
||||
type linuxFWDetector struct{}
|
||||
|
||||
// iptDetect returns the number of iptables rules in the current namespace.
|
||||
func (l linuxFWDetector) iptDetect() (int, error) {
|
||||
return detectIptables()
|
||||
}
|
||||
|
||||
// nftDetect returns the number of nftables rules in the current namespace.
|
||||
func (l linuxFWDetector) nftDetect() (int, error) {
|
||||
return detectNetfilter()
|
||||
}
|
||||
|
||||
// pickFirewallModeFromInstalledRules returns the firewall mode to use based on
|
||||
// the environment and the system's capabilities.
|
||||
func pickFirewallModeFromInstalledRules(logf logger.Logf, det tableDetector) FirewallMode {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
return FirewallModeNfTables
|
||||
}
|
||||
iptAva, nftAva := true, true
|
||||
iptRuleCount, err := det.iptDetect()
|
||||
if err != nil {
|
||||
logf("detect iptables rule: %v", err)
|
||||
iptAva = false
|
||||
}
|
||||
nftRuleCount, err := det.nftDetect()
|
||||
if err != nil {
|
||||
logf("detect nftables rule: %v", err)
|
||||
nftAva = false
|
||||
}
|
||||
logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
|
||||
switch {
|
||||
case nftRuleCount > 0 && iptRuleCount == 0:
|
||||
logf("nftables is currently in use")
|
||||
hostinfo.SetFirewallMode("nft-inuse")
|
||||
return FirewallModeNfTables
|
||||
case iptRuleCount > 0 && nftRuleCount == 0:
|
||||
logf("iptables is currently in use")
|
||||
hostinfo.SetFirewallMode("ipt-inuse")
|
||||
return FirewallModeIPTables
|
||||
case nftAva:
|
||||
// if both iptables and nftables are available but
|
||||
// neither/both are currently used, use nftables.
|
||||
logf("nftables is available")
|
||||
hostinfo.SetFirewallMode("nft")
|
||||
return FirewallModeNfTables
|
||||
case iptAva:
|
||||
logf("iptables is available")
|
||||
hostinfo.SetFirewallMode("ipt")
|
||||
return FirewallModeIPTables
|
||||
default:
|
||||
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
|
||||
// runner which exists but won't do anything. Creating iptablesRunner errors only
|
||||
// if the iptables command is missing or doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll carry on.
|
||||
hostinfo.SetFirewallMode("ipt-fb")
|
||||
return FirewallModeIPTables
|
||||
}
|
||||
}
|
||||
142
vendor/tailscale.com/util/linuxfw/fake.go
generated
vendored
Normal file
142
vendor/tailscale.com/util/linuxfw/fake.go
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type fakeIPTables struct {
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
type fakeRule struct {
|
||||
table, chain string
|
||||
args []string
|
||||
}
|
||||
|
||||
func newFakeIPTables() *fakeIPTables {
|
||||
return &fakeIPTables{
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
"nat/PREROUTING": nil,
|
||||
"nat/OUTPUT": nil,
|
||||
"nat/POSTROUTING": nil,
|
||||
"mangle/FORWARD": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Insert(table, chain string, pos int, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if pos > len(rules)+1 {
|
||||
return fmt.Errorf("bad position %d in %s", pos, k)
|
||||
}
|
||||
rules = append(rules, "")
|
||||
copy(rules[pos:], rules[pos-1:])
|
||||
rules[pos-1] = strings.Join(args, " ")
|
||||
n.n[k] = rules
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Append(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
return n.Insert(table, chain, len(n.n[k])+1, args...)
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Exists(table, chain string, args ...string) (bool, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for _, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else {
|
||||
return false, fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for i, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
rules = append(rules[:i], rules[i+1:]...)
|
||||
n.n[k] = rules
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) List(table, chain string) ([]string, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
return rules, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("exitcode:1")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) NewChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
return fmt.Errorf("table/chain %s already exists", k)
|
||||
}
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if len(rules) != 0 {
|
||||
return fmt.Errorf("table/chain %s is not empty", k)
|
||||
}
|
||||
delete(n.n, k)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func NewFakeIPTablesRunner() *iptablesRunner {
|
||||
ipt4 := newFakeIPTables()
|
||||
v6Available := false
|
||||
var ipt6 iptablesInterface
|
||||
if use6, err := strconv.ParseBool(os.Getenv("TS_TEST_FAKE_NETFILTER_6")); use6 || err != nil {
|
||||
ipt6 = newFakeIPTables()
|
||||
v6Available = true
|
||||
}
|
||||
|
||||
iptr := &iptablesRunner{ipt4, ipt6, v6Available, v6Available, v6Available}
|
||||
return iptr
|
||||
}
|
||||
39
vendor/tailscale.com/util/linuxfw/helpers.go
generated
vendored
Normal file
39
vendor/tailscale.com/util/linuxfw/helpers.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
func formatMaybePrintable(b []byte) string {
|
||||
// Remove a single trailing null, if any.
|
||||
if slicesx.LastEqual(b, 0) {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
|
||||
nonprintable := strings.IndexFunc(string(b), func(r rune) bool {
|
||||
return r > unicode.MaxASCII || !unicode.IsPrint(r)
|
||||
})
|
||||
if nonprintable >= 0 {
|
||||
return "<hex>" + hex.EncodeToString(b)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func formatPortRange(r [2]uint16) string {
|
||||
if r == [2]uint16{0, 65535} {
|
||||
return fmt.Sprintf(`any`)
|
||||
} else if r[0] == r[1] {
|
||||
return fmt.Sprintf(`%d`, r[0])
|
||||
}
|
||||
return fmt.Sprintf(`%d-%d`, r[0], r[1])
|
||||
}
|
||||
73
vendor/tailscale.com/util/linuxfw/iptables.go
generated
vendored
Normal file
73
vendor/tailscale.com/util/linuxfw/iptables.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// DebugNetfilter prints debug information about iptables rules to the
|
||||
// provided log function.
|
||||
func DebugIptables(logf logger.Logf) error {
|
||||
// unused.
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectIptables returns the number of iptables rules that are present in the
|
||||
// system, ignoring the default "ACCEPT" rule present in the standard iptables
|
||||
// chains.
|
||||
//
|
||||
// It only returns an error when there is no iptables binary, or when iptables -S
|
||||
// fails. In all other cases, it returns the number of non-default rules.
|
||||
//
|
||||
// If the iptables binary is not found, it returns an underlying exec.ErrNotFound
|
||||
// error.
|
||||
func detectIptables() (int, error) {
|
||||
// run "iptables -S" to get the list of rules using iptables
|
||||
// exec.Command returns an error if the binary is not found
|
||||
cmd := exec.Command("iptables", "-S")
|
||||
output, err := cmd.Output()
|
||||
ip6cmd := exec.Command("ip6tables", "-S")
|
||||
ip6output, ip6err := ip6cmd.Output()
|
||||
var allLines []string
|
||||
outputStr := string(output)
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
ip6outputStr := string(ip6output)
|
||||
ip6lines := strings.Split(ip6outputStr, "\n")
|
||||
switch {
|
||||
case err == nil && ip6err == nil:
|
||||
allLines = append(lines, ip6lines...)
|
||||
case err == nil && ip6err != nil:
|
||||
allLines = lines
|
||||
case err != nil && ip6err == nil:
|
||||
allLines = ip6lines
|
||||
default:
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeIPTables,
|
||||
Err: fmt.Errorf("iptables command run fail: %w", multierr.New(err, ip6err)),
|
||||
}
|
||||
}
|
||||
|
||||
// count the number of non-default rules
|
||||
count := 0
|
||||
for _, line := range allLines {
|
||||
trimmedLine := strings.TrimLeftFunc(line, unicode.IsSpace)
|
||||
if line != "" && strings.HasPrefix(trimmedLine, "-A") {
|
||||
// if the line is not empty and starts with "-A", it is a rule appended not default
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// return the count of non-default rules
|
||||
return count, nil
|
||||
}
|
||||
79
vendor/tailscale.com/util/linuxfw/iptables_for_svcs.go
generated
vendored
Normal file
79
vendor/tailscale.com/util/linuxfw/iptables_for_svcs.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// This file contains functionality to insert portmapping rules for a 'service'.
|
||||
// These are currently only used by the Kubernetes operator proxies.
|
||||
// An iptables rule for such a service contains a comment with the service name.
|
||||
|
||||
// EnsurePortMapRuleForSvc adds a prerouting rule that forwards traffic received
|
||||
// on match port and NOT on the provided interface to target IP and target port.
|
||||
// Rule will only be added if it does not already exists.
|
||||
func (i *iptablesRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
table := i.getIPTByAddr(targetIP)
|
||||
args := argsForPortMapRule(svc, tun, targetIP, pm)
|
||||
exists, err := table.Exists("nat", "PREROUTING", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return table.Append("nat", "PREROUTING", args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMapRuleForSvc constructs a prerouting rule as would be created by
|
||||
// EnsurePortMapRuleForSvc with the provided args and, if such a rule exists,
|
||||
// deletes it.
|
||||
func (i *iptablesRunner) DeletePortMapRuleForSvc(svc, excludeI string, targetIP netip.Addr, pm PortMap) error {
|
||||
table := i.getIPTByAddr(targetIP)
|
||||
args := argsForPortMapRule(svc, excludeI, targetIP, pm)
|
||||
exists, err := table.Exists("nat", "PREROUTING", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return table.Delete("nat", "PREROUTING", args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSvc constructs all possible rules that would have been created by
|
||||
// EnsurePortMapRuleForSvc from the provided args and ensures that each one that
|
||||
// exists is deleted.
|
||||
func (i *iptablesRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr, pms []PortMap) error {
|
||||
for _, tip := range targetIPs {
|
||||
for _, pm := range pms {
|
||||
if err := i.DeletePortMapRuleForSvc(svc, tun, tip, pm); err != nil {
|
||||
return fmt.Errorf("error deleting rule: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func argsForPortMapRule(svc, excludeI string, targetIP netip.Addr, pm PortMap) []string {
|
||||
c := commentForSvc(svc, pm)
|
||||
return []string{
|
||||
"!", "-i", excludeI,
|
||||
"-p", pm.Protocol,
|
||||
"--dport", fmt.Sprintf("%d", pm.MatchPort),
|
||||
"-m", "comment", "--comment", c,
|
||||
"-j", "DNAT",
|
||||
"--to-destination", fmt.Sprintf("%v:%v", targetIP, pm.TargetPort),
|
||||
}
|
||||
}
|
||||
|
||||
// commentForSvc generates a comment to be added to an iptables DNAT rule for a
|
||||
// service. This is for iptables debugging/readability purposes only.
|
||||
func commentForSvc(svc string, pm PortMap) string {
|
||||
return fmt.Sprintf("%s:%s:%d -> %s:%d", svc, pm.Protocol, pm.MatchPort, pm.Protocol, pm.TargetPort)
|
||||
}
|
||||
774
vendor/tailscale.com/util/linuxfw/iptables_runner.go
generated
vendored
Normal file
774
vendor/tailscale.com/util/linuxfw/iptables_runner.go
generated
vendored
Normal file
@@ -0,0 +1,774 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// isNotExistError needs to be overridden in tests that rely on distinguishing
|
||||
// this error, because we don't have a good way how to create a new
|
||||
// iptables.Error of that type.
|
||||
var isNotExistError = func(err error) bool {
|
||||
var e *iptables.Error
|
||||
return errors.As(err, &e) && e.IsNotExist()
|
||||
}
|
||||
|
||||
type iptablesInterface interface {
|
||||
// Adding this interface for testing purposes so we can mock out
|
||||
// the iptables library, in reality this is a wrapper to *iptables.IPTables.
|
||||
Insert(table, chain string, pos int, args ...string) error
|
||||
Append(table, chain string, args ...string) error
|
||||
Exists(table, chain string, args ...string) (bool, error)
|
||||
Delete(table, chain string, args ...string) error
|
||||
List(table, chain string) ([]string, error)
|
||||
ClearChain(table, chain string) error
|
||||
NewChain(table, chain string) error
|
||||
DeleteChain(table, chain string) error
|
||||
}
|
||||
|
||||
type iptablesRunner struct {
|
||||
ipt4 iptablesInterface
|
||||
ipt6 iptablesInterface
|
||||
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
v6FilterAvailable bool
|
||||
}
|
||||
|
||||
func checkIP6TablesExists() error {
|
||||
// Some distros ship ip6tables separately from iptables.
|
||||
if _, err := exec.LookPath("ip6tables"); err != nil {
|
||||
return fmt.Errorf("path not found: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
|
||||
// If the underlying iptables library fails to initialize, that error is
|
||||
// returned. The runner probes for IPv6 support once at initialization time and
|
||||
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
|
||||
func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportsV6, supportsV6NAT, supportsV6Filter := false, false, false
|
||||
v6err := CheckIPv6(logf)
|
||||
ip6terr := checkIP6TablesExists()
|
||||
var ipt6 *iptables.IPTables
|
||||
switch {
|
||||
case v6err != nil:
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
case ip6terr != nil:
|
||||
logf("disabling tunneled IPv6 due to missing ip6tables: %v", ip6terr)
|
||||
default:
|
||||
supportsV6 = true
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
supportsV6Filter = checkSupportsV6Filter(ipt6, logf)
|
||||
supportsV6NAT = checkSupportsV6NAT(ipt6, logf)
|
||||
logf("netfilter running in iptables mode v6 = %v, v6filter = %v, v6nat = %v", supportsV6, supportsV6Filter, supportsV6NAT)
|
||||
}
|
||||
return &iptablesRunner{
|
||||
ipt4: ipt4,
|
||||
ipt6: ipt6,
|
||||
v6Available: supportsV6,
|
||||
v6NATAvailable: supportsV6NAT,
|
||||
v6FilterAvailable: supportsV6Filter}, nil
|
||||
}
|
||||
|
||||
// checkSupportsV6Filter returns whether the system has a "filter" table in the
|
||||
// IPv6 tables. Some container environments such as GitHub codespaces have
|
||||
// limited local IPv6 support, and containers containing ip6tables, but do not
|
||||
// have kernel support for IPv6 filtering.
|
||||
// We will not set ip6tables rules in these instances.
|
||||
func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool {
|
||||
if ipt == nil {
|
||||
return false
|
||||
}
|
||||
_, filterListErr := ipt.ListChains("filter")
|
||||
if filterListErr == nil {
|
||||
return true
|
||||
}
|
||||
logf("ip6tables filtering is not supported on this host: %v", filterListErr)
|
||||
return false
|
||||
}
|
||||
|
||||
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
||||
// IPv6 netfilter stack.
|
||||
//
|
||||
// The nat table was added after the initial release of ipv6
|
||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||
// traffic.
|
||||
// ipt must be initialized for IPv6.
|
||||
func checkSupportsV6NAT(ipt *iptables.IPTables, logf logger.Logf) bool {
|
||||
if ipt == nil || ipt.Proto() != iptables.ProtocolIPv6 {
|
||||
return false
|
||||
}
|
||||
_, natListErr := ipt.ListChains("nat")
|
||||
if natListErr == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO (irbekrm): the following two checks were added before the check
|
||||
// above that verifies that nat chains can be listed. It is a
|
||||
// container-friendly check (see
|
||||
// https://github.com/tailscale/tailscale/issues/11344), but also should
|
||||
// be good enough on its own in other environments. If we never observe
|
||||
// it falsely succeed, let's remove the other two checks.
|
||||
|
||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
logf("[unexpected] listing nat chains failed, but /proc/net/ip6_tables_name reports a nat table existing")
|
||||
return true
|
||||
}
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
logf("[unexpected] listing nat chains failed, but modprobe ip6table_nat succeeded")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasIPV6 reports true if the system supports IPv6.
|
||||
func (i *iptablesRunner) HasIPV6() bool {
|
||||
return i.v6Available
|
||||
}
|
||||
|
||||
// HasIPV6Filter reports true if the system supports ip6tables filter table.
|
||||
func (i *iptablesRunner) HasIPV6Filter() bool {
|
||||
return i.v6FilterAvailable
|
||||
}
|
||||
|
||||
// HasIPV6NAT reports true if the system supports IPv6 NAT.
|
||||
func (i *iptablesRunner) HasIPV6NAT() bool {
|
||||
return i.v6NATAvailable
|
||||
}
|
||||
|
||||
// getIPTByAddr returns the iptablesInterface with correct IP family
|
||||
// that we will be using for the given address.
|
||||
func (i *iptablesRunner) getIPTByAddr(addr netip.Addr) iptablesInterface {
|
||||
nf := i.ipt4
|
||||
if addr.Is6() {
|
||||
nf = i.ipt6
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
// AddLoopbackRule adds an iptables rule to permit loopback traffic to
|
||||
// a local Tailscale IP.
|
||||
func (i *iptablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsChain returns the name of the tailscale sub-chain corresponding
|
||||
// to the given "parent" chain (e.g. INPUT, FORWARD, ...).
|
||||
func tsChain(chain string) string {
|
||||
return "ts-" + strings.ToLower(chain)
|
||||
}
|
||||
|
||||
// DelLoopbackRule removes the iptables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTables gets the available iptablesInterface in iptables runner.
|
||||
func (i *iptablesRunner) getTables() []iptablesInterface {
|
||||
if i.HasIPV6Filter() {
|
||||
return []iptablesInterface{i.ipt4, i.ipt6}
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// getNATTables gets the available iptablesInterface in iptables runner.
|
||||
// If the system does not support IPv6 NAT, only the IPv4 iptablesInterface
|
||||
// is returned.
|
||||
func (i *iptablesRunner) getNATTables() []iptablesInterface {
|
||||
if i.HasIPV6NAT() {
|
||||
return i.getTables()
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// AddHooks inserts calls to tailscale's netfilter chains in
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist. If they do not, an error is returned.
|
||||
func (i *iptablesRunner) AddHooks() error {
|
||||
// divert inserts a jump to the tailscale chain in the given table/chain.
|
||||
// If the jump already exists, it is a no-op.
|
||||
divert := func(ipt iptablesInterface, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := ipt.Exists(table, chain, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := divert(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddChains creates custom Tailscale chains in netfilter via iptables
|
||||
// if the ts-chain doesn't already exist.
|
||||
func (i *iptablesRunner) AddChains() error {
|
||||
// create creates a chain in the given table if it doesn't already exist.
|
||||
// If the chain already exists, it is a no-op.
|
||||
create := func(ipt iptablesInterface, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. let's create it!
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := create(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := create(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBase adds some basic processing rules to be supplemented by
|
||||
// later calls to other helpers.
|
||||
func (i *iptablesRunner) AddBase(tunname string) error {
|
||||
if err := i.addBase4(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
if i.HasIPV6Filter() {
|
||||
if err := i.addBase6(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase4 adds some basic IPv4 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase4(tunname string) error {
|
||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
||||
// is an exception carved out for ranges used by ChromeOS, for
|
||||
// which we fall out of the Tailscale chain.
|
||||
//
|
||||
// Note, this will definitely break nodes that end up using the
|
||||
// CGNAT range for other purposes :(.
|
||||
args := []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
args = []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Explicitly allow all other inbound traffic to the tun interface
|
||||
args = []string{"-i", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
// traffic to the tailscale interface by default. We use packet
|
||||
// marks here so both filter/FORWARD and nat/POSTROUTING can match
|
||||
// on these packets of interest.
|
||||
//
|
||||
// In particular, we only want to apply SNAT rules in
|
||||
// nat/POSTROUTING to packets that originated from the Tailscale
|
||||
// interface, but we can't match on the inbound interface in
|
||||
// POSTROUTING. So instead, we match on the inbound interface in
|
||||
// filter/FORWARD, and set a packet mark that nat/POSTROUTING can
|
||||
// use to effectively run that same test again.
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) AddDNATRule(origDst, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
return table.Insert("nat", "PREROUTING", 1, "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dst.String())
|
||||
}
|
||||
|
||||
// EnsureSNATForDst sets up firewall to ensure that all traffic aimed for dst, has its source ip set to src:
|
||||
// - creates a SNAT rule if not already present
|
||||
// - ensures that any no longer valid SNAT rules for the same dst are removed
|
||||
func (i *iptablesRunner) EnsureSNATForDst(src, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
rules, err := table.List("nat", "POSTROUTING")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing rules: %v", err)
|
||||
}
|
||||
// iptables accept either address or a CIDR value for the --destination flag, but converts an address to /32
|
||||
// CIDR. Explicitly passing a /32 CIDR made it possible to test this rule.
|
||||
dstPrefix, err := dst.Prefix(32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calculating prefix of dst %v: %v", dst, err)
|
||||
}
|
||||
|
||||
// wantsArgsPrefix is the prefix of the SNAT rule for the provided destination.
|
||||
// We should only have one POSTROUTING rule with this prefix.
|
||||
wantsArgsPrefix := fmt.Sprintf("-d %s -j SNAT --to-source", dstPrefix.String())
|
||||
// wantsArgs is the actual SNAT rule that we want.
|
||||
wantsArgs := fmt.Sprintf("%s %s", wantsArgsPrefix, src.String())
|
||||
for _, r := range rules {
|
||||
args := argsFromPostRoutingRule(r)
|
||||
if strings.HasPrefix(args, wantsArgsPrefix) {
|
||||
if strings.HasPrefix(args, wantsArgs) {
|
||||
return nil
|
||||
}
|
||||
// SNAT rule matching the destination, but for a different source - delete.
|
||||
if err := table.Delete("nat", "POSTROUTING", strings.Split(args, " ")...); err != nil {
|
||||
// If we failed to delete don't crash the node- the proxy should still be functioning.
|
||||
log.Printf("[unexpected] error deleting rule %s: %v, please report it.", r, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return table.Insert("nat", "POSTROUTING", 1, "-d", dstPrefix.String(), "-j", "SNAT", "--to-source", src.String())
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) DNATNonTailscaleTraffic(tun string, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
return table.Insert("nat", "PREROUTING", 1, "!", "-i", tun, "-j", "DNAT", "--to-destination", dst.String())
|
||||
}
|
||||
|
||||
// DNATWithLoadBalancer adds iptables rules to forward all traffic received for
|
||||
// originDst to the backend dsts. Traffic will be load balanced using round robin.
|
||||
func (i *iptablesRunner) DNATWithLoadBalancer(origDst netip.Addr, dsts []netip.Addr) error {
|
||||
table := i.getIPTByAddr(dsts[0])
|
||||
if err := table.ClearChain("nat", "PREROUTING"); err != nil && !isNotExistError(err) {
|
||||
// If clearing the PREROUTING chain fails, fail the whole operation. This
|
||||
// rule is currently only used in Kubernetes containers where a
|
||||
// failed container gets restarted which should hopefully fix things.
|
||||
return fmt.Errorf("error clearing nat PREROUTING chain: %w", err)
|
||||
}
|
||||
// If dsts contain more than one address, for n := n in range(len(dsts)..2) route packets for every nth connection to dsts[n].
|
||||
for i := len(dsts); i >= 2; i-- {
|
||||
dst := dsts[i-1] // the order in which rules for addrs are installed does not matter
|
||||
if err := table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-m", "statistic", "--mode", "nth", "--every", fmt.Sprint(i), "--packet", "0", "-j", "DNAT", "--to-destination", dst.String()); err != nil {
|
||||
return fmt.Errorf("error adding DNAT rule for %s: %w", dst.String(), err)
|
||||
}
|
||||
}
|
||||
// If the packet falls through to this rule, we route to the first destination in the list unconditionally.
|
||||
return table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dsts[0].String())
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) ClampMSSToPMTU(tun string, addr netip.Addr) error {
|
||||
table := i.getIPTByAddr(addr)
|
||||
return table.Append("mangle", "FORWARD", "-o", tun, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
}
|
||||
|
||||
// addBase6 adds some basic IPv6 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase6(tunname string) error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
// Explicitly allow all other inbound traffic to the tun interface
|
||||
args := []string{"-i", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
|
||||
// (see corresponding IPv4 CGNAT rule).
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelChains removes the custom Tailscale chains from netfilter via iptables.
|
||||
func (i *iptablesRunner) DelChains() error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter via iptables.
|
||||
func (i *iptablesRunner) DelBase() error {
|
||||
del := func(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := del(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks deletes the calls to tailscale's netfilter chains
|
||||
// in the relevant main netfilter chains.
|
||||
func (i *iptablesRunner) DelHooks(logf logger.Logf) error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSNATRule adds a netfilter rule to SNAT traffic destined for
|
||||
// local subnets.
|
||||
func (i *iptablesRunner) AddSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSNATRule removes the netfilter rule to SNAT traffic destined for
|
||||
// local subnets. An error is returned if the rule does not exist.
|
||||
func (i *iptablesRunner) DelSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func statefulRuleArgs(tunname string) []string {
|
||||
return []string{"-o", tunname, "-m", "conntrack", "!", "--ctstate", "ESTABLISHED,RELATED", "-j", "DROP"}
|
||||
}
|
||||
|
||||
// AddStatefulRule adds a netfilter rule for stateful packet filtering using
|
||||
// conntrack.
|
||||
func (i *iptablesRunner) AddStatefulRule(tunname string) error {
|
||||
// Drop packets that are destined for the tailscale interface if
|
||||
// they're a new connection, per conntrack, to prevent hosts on the
|
||||
// same subnet from being able to use this device as a way to forward
|
||||
// packets on to the Tailscale network.
|
||||
//
|
||||
// The conntrack states are:
|
||||
// NEW A packet which creates a new connection.
|
||||
// ESTABLISHED A packet which belongs to an existing connection
|
||||
// (i.e., a reply packet, or outgoing packet on a
|
||||
// connection which has seen replies).
|
||||
// RELATED A packet which is related to, but not part of, an
|
||||
// existing connection, such as an ICMP error.
|
||||
// INVALID A packet which could not be identified for some
|
||||
// reason: this includes running out of memory and ICMP
|
||||
// errors which don't correspond to any known
|
||||
// connection. Generally these packets should be
|
||||
// dropped.
|
||||
//
|
||||
// We drop NEW packets to prevent connections from coming "into"
|
||||
// Tailscale from other hosts on the same network segment; we drop
|
||||
// INVALID packets as well.
|
||||
args := statefulRuleArgs(tunname)
|
||||
for _, ipt := range i.getTables() {
|
||||
// First, find the final "accept" rule.
|
||||
rules, err := ipt.List("filter", "ts-forward")
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing rules in filter/ts-forward: %w", err)
|
||||
}
|
||||
want := fmt.Sprintf("-A %s -o %s -j ACCEPT", "ts-forward", tunname)
|
||||
|
||||
pos := slices.Index(rules, want)
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("couldn't find final ACCEPT rule in filter/ts-forward")
|
||||
}
|
||||
|
||||
if err := ipt.Insert("filter", "ts-forward", pos, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelStatefulRule removes the netfilter rule for stateful packet filtering
|
||||
// using conntrack.
|
||||
func (i *iptablesRunner) DelStatefulRule(tunname string) error {
|
||||
args := statefulRuleArgs(tunname)
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := ipt.Delete("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in filter/ts-forward: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMagicsockPortRule generates the string slice containing the arguments
|
||||
// to describe a rule accepting traffic on a particular port to iptables. It is
|
||||
// separated out here to avoid repetition in AddMagicsockPortRule and
|
||||
// RemoveMagicsockPortRule, since it is important that the same rule is passed
|
||||
// to Append() and Delete().
|
||||
func buildMagicsockPortRule(port uint16) []string {
|
||||
return []string{"-p", "udp", "--dport", strconv.FormatUint(uint64(port), 10), "-j", "ACCEPT"}
|
||||
}
|
||||
|
||||
// AddMagicsockPortRule adds a rule to iptables to allow incoming traffic on
|
||||
// the specified UDP port, so magicsock can accept incoming connections.
|
||||
// network must be either "udp4" or "udp6" - this determines whether the rule
|
||||
// is added for IPv4 or IPv6.
|
||||
func (i *iptablesRunner) AddMagicsockPortRule(port uint16, network string) error {
|
||||
var ipt iptablesInterface
|
||||
switch network {
|
||||
case "udp4":
|
||||
ipt = i.ipt4
|
||||
case "udp6":
|
||||
ipt = i.ipt6
|
||||
default:
|
||||
return fmt.Errorf("unsupported network %s", network)
|
||||
}
|
||||
|
||||
args := buildMagicsockPortRule(port)
|
||||
|
||||
if err := ipt.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelMagicsockPortRule removes a rule added by AddMagicsockPortRule to accept
|
||||
// incoming traffic on a particular UDP port.
|
||||
// network must be either "udp4" or "udp6" - this determines whether the rule
|
||||
// is removed for IPv4 or IPv6.
|
||||
func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error {
|
||||
var ipt iptablesInterface
|
||||
switch network {
|
||||
case "udp4":
|
||||
ipt = i.ipt4
|
||||
case "udp6":
|
||||
ipt = i.ipt6
|
||||
default:
|
||||
return fmt.Errorf("unsupported network %s", network)
|
||||
}
|
||||
|
||||
args := buildMagicsockPortRule(port)
|
||||
|
||||
if err := ipt.Delete("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("removing %v in filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPTablesCleanUp removes all Tailscale added iptables rules.
|
||||
// Any errors that occur are logged to the provided logf.
|
||||
func IPTablesCleanUp(logf logger.Logf) {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Gokrazy uses nftables and doesn't have the "iptables" command.
|
||||
// Avoid log spam on cleanup. (#12277)
|
||||
return
|
||||
}
|
||||
err := clearRules(iptables.ProtocolIPv4, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear iptables: %v", err)
|
||||
}
|
||||
|
||||
err = clearRules(iptables.ProtocolIPv6, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear ip6tables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
||||
// exist, it's a no-op since the desired state is already achieved but we log the
|
||||
// error because error code from the iptables module resists unwrapping.
|
||||
func delTSHook(ipt iptablesInterface, table, chain string, logf logger.Logf) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := ipt.Delete(table, chain, args...); err != nil && !isNotExistError(err) {
|
||||
return fmt.Errorf("deleting %v in %s/%s: %v", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delChain flushes and deletes a chain. If the chain does not exist, it's a no-op
|
||||
// since the desired state is already achieved. otherwise, it returns an error.
|
||||
func delChain(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. nothing to do.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := ipt.DeleteChain(table, chain); err != nil {
|
||||
return fmt.Errorf("deleting %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRules clears all the iptables rules created by Tailscale
|
||||
// for the given protocol. If error occurs, it's logged but not returned.
|
||||
func clearRules(proto iptables.Protocol, logf logger.Logf) error {
|
||||
ipt, err := iptables.NewWithProtocol(proto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
// argsFromPostRoutingRule accepts a rule as returned by iptables.List and, if it is a rule from POSTROUTING chain,
|
||||
// returns the args part, else returns the original rule.
|
||||
func argsFromPostRoutingRule(r string) string {
|
||||
args, _ := strings.CutPrefix(r, "-A POSTROUTING ")
|
||||
return args
|
||||
}
|
||||
182
vendor/tailscale.com/util/linuxfw/linuxfw.go
generated
vendored
Normal file
182
vendor/tailscale.com/util/linuxfw/linuxfw.go
generated
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Package linuxfw returns the kind of firewall being used by the kernel.
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/netlink"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MatchDecision is the decision made by the firewall for a packet matched by a rule.
|
||||
// It is used to decide whether to accept or masquerade a packet in addMatchSubnetRouteMarkRule.
|
||||
type MatchDecision int
|
||||
|
||||
const (
|
||||
Accept MatchDecision = iota
|
||||
Masq
|
||||
)
|
||||
|
||||
type FWModeNotSupportedError struct {
|
||||
Mode FirewallMode
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Error() string {
|
||||
return fmt.Sprintf("firewall mode %q not supported: %v", e.Mode, e.Err)
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Is(target error) bool {
|
||||
_, ok := target.(FWModeNotSupportedError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type FirewallMode string
|
||||
|
||||
const (
|
||||
FirewallModeIPTables FirewallMode = "iptables"
|
||||
FirewallModeNfTables FirewallMode = "nftables"
|
||||
)
|
||||
|
||||
// The following bits are added to packet marks for Tailscale use.
|
||||
//
|
||||
// We tried to pick bits sufficiently out of the way that it's
|
||||
// unlikely to collide with existing uses. We have 4 bytes of mark
|
||||
// bits to play with. We leave the lower byte alone on the assumption
|
||||
// that sysadmins would use those. Kubernetes uses a few bits in the
|
||||
// second byte, so we steer clear of that too.
|
||||
//
|
||||
// Empirically, most of the documentation on packet marks on the
|
||||
// internet gives the impression that the marks are 16 bits
|
||||
// wide. Based on this, we theorize that the upper two bytes are
|
||||
// relatively unused in the wild, and so we consume bits 16:23 (the
|
||||
// third byte).
|
||||
//
|
||||
// The constants are in the iptables/iproute2 string format for
|
||||
// matching and setting the bits, so they can be directly embedded in
|
||||
// commands.
|
||||
const (
|
||||
// The mask for reading/writing the 'firewall mask' bits on a packet.
|
||||
// See the comment on the const block on why we only use the third byte.
|
||||
//
|
||||
// We claim bits 16:23 entirely. For now we only use the lower four
|
||||
// bits, leaving the higher 4 bits for future use.
|
||||
TailscaleFwmarkMask = "0xff0000"
|
||||
TailscaleFwmarkMaskNum = 0xff0000
|
||||
|
||||
// Packet is from Tailscale and to a subnet route destination, so
|
||||
// is allowed to be routed through this machine.
|
||||
TailscaleSubnetRouteMark = "0x40000"
|
||||
TailscaleSubnetRouteMarkNum = 0x40000
|
||||
|
||||
// Packet was originated by tailscaled itself, and must not be
|
||||
// routed over the Tailscale network.
|
||||
TailscaleBypassMark = "0x80000"
|
||||
TailscaleBypassMarkNum = 0x80000
|
||||
)
|
||||
|
||||
// getTailscaleFwmarkMaskNeg returns the negation of TailscaleFwmarkMask in bytes.
|
||||
func getTailscaleFwmarkMaskNeg() []byte {
|
||||
return []byte{0xff, 0x00, 0xff, 0xff}
|
||||
}
|
||||
|
||||
// getTailscaleFwmarkMask returns the TailscaleFwmarkMask in bytes.
|
||||
func getTailscaleFwmarkMask() []byte {
|
||||
return []byte{0x00, 0xff, 0x00, 0x00}
|
||||
}
|
||||
|
||||
// getTailscaleSubnetRouteMark returns the TailscaleSubnetRouteMark in bytes.
|
||||
func getTailscaleSubnetRouteMark() []byte {
|
||||
return []byte{0x00, 0x04, 0x00, 0x00}
|
||||
}
|
||||
|
||||
// checkIPv6ForTest can be set in tests.
|
||||
var checkIPv6ForTest func(logger.Logf) error
|
||||
|
||||
// checkIPv6 checks whether the system appears to have a working IPv6
|
||||
// network stack. It returns an error explaining what looks wrong or
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func CheckIPv6(logf logger.Logf) error {
|
||||
if f := checkIPv6ForTest; f != nil {
|
||||
return f(logf)
|
||||
}
|
||||
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
bs, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
if err != nil {
|
||||
// Be conservative if we can't find the IPv6 configuration knob.
|
||||
return err
|
||||
}
|
||||
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_ipv6 has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_ipv6 is set")
|
||||
}
|
||||
|
||||
// Older kernels don't support IPv6 policy routing. Some kernels
|
||||
// support policy routing but don't have this knob, so absence of
|
||||
// the knob is not fatal.
|
||||
bs, err = os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
|
||||
if err == nil {
|
||||
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_policy has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_policy is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := CheckIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = TailscaleBypassMarkNum
|
||||
rule.Table = 52
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
netlink.RuleDel(rule)
|
||||
// And clean up on exit.
|
||||
defer netlink.RuleDel(rule)
|
||||
return netlink.RuleAdd(rule)
|
||||
}
|
||||
40
vendor/tailscale.com/util/linuxfw/linuxfw_unsupported.go
generated
vendored
Normal file
40
vendor/tailscale.com/util/linuxfw/linuxfw_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// NOTE: linux_{arm64, amd64} are the only two currently supported archs due to missing
|
||||
// support in upstream dependencies.
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && !(arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("linuxfw:unsupported")
|
||||
|
||||
// DebugNetfilter is not supported on non-Linux platforms.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
}
|
||||
|
||||
// DetectNetfilter is not supported on non-Linux platforms.
|
||||
func detectNetfilter() (int, error) {
|
||||
return 0, ErrUnsupported
|
||||
}
|
||||
|
||||
// DebugIptables is not supported on non-Linux platforms.
|
||||
func debugIptables(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
}
|
||||
|
||||
// DetectIptables is not supported on non-Linux platforms.
|
||||
func detectIptables() (int, error) {
|
||||
return 0, ErrUnsupported
|
||||
}
|
||||
292
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
Normal file
292
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/google/nftables/xt"
|
||||
"github.com/josharian/native"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// DebugNetfilter prints debug information about netfilter rules to the
|
||||
// provided log function.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chains, err := conn.ListChains()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot list chains: %w", err)
|
||||
}
|
||||
|
||||
if len(chains) == 0 {
|
||||
logf("netfilter: no chains")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, chain := range chains {
|
||||
logf("netfilter: table=%s chain=%s", chain.Table.Name, chain.Name)
|
||||
|
||||
rules, err := conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sort.Slice(rules, func(i, j int) bool {
|
||||
return rules[i].Position < rules[j].Position
|
||||
})
|
||||
|
||||
for i, rule := range rules {
|
||||
logf("netfilter: rule[%d]: pos=%d flags=%d", i, rule.Position, rule.Flags)
|
||||
for _, ex := range rule.Exprs {
|
||||
switch v := ex.(type) {
|
||||
case *expr.Meta:
|
||||
key := cmp.Or(metaKeyNames[v.Key], "UNKNOWN")
|
||||
logf("netfilter: Meta: key=%s source_register=%v register=%d", key, v.SourceRegister, v.Register)
|
||||
|
||||
case *expr.Cmp:
|
||||
op := cmp.Or(cmpOpNames[v.Op], "UNKNOWN")
|
||||
logf("netfilter: Cmp: op=%s register=%d data=%s", op, v.Register, formatMaybePrintable(v.Data))
|
||||
|
||||
case *expr.Counter:
|
||||
// don't print
|
||||
|
||||
case *expr.Verdict:
|
||||
kind := cmp.Or(verdictNames[v.Kind], "UNKNOWN")
|
||||
logf("netfilter: Verdict: kind=%s data=%s", kind, v.Chain)
|
||||
|
||||
case *expr.Target:
|
||||
logf("netfilter: Target: name=%s info=%s", v.Name, printTargetInfo(v.Name, v.Info))
|
||||
|
||||
case *expr.Match:
|
||||
logf("netfilter: Match: name=%s info=%+v", v.Name, printMatchInfo(v.Name, v.Info))
|
||||
|
||||
case *expr.Payload:
|
||||
logf("netfilter: Payload: op=%s src=%d dst=%d base=%s offset=%d len=%d",
|
||||
payloadOperationTypeNames[v.OperationType],
|
||||
v.SourceRegister, v.DestRegister,
|
||||
payloadBaseNames[v.Base],
|
||||
v.Offset, v.Len)
|
||||
// TODO(andrew): csum
|
||||
|
||||
case *expr.Bitwise:
|
||||
var xor string
|
||||
for _, b := range v.Xor {
|
||||
if b != 0 {
|
||||
xor = fmt.Sprintf(" xor=%v", v.Xor)
|
||||
break
|
||||
}
|
||||
}
|
||||
logf("netfilter: Bitwise: src=%d dst=%d len=%d mask=%v%s",
|
||||
v.SourceRegister, v.DestRegister, v.Len, v.Mask, xor)
|
||||
|
||||
default:
|
||||
logf("netfilter: unknown %T: %+v", v, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectNetfilter returns the number of nftables rules present in the system.
|
||||
func detectNetfilter() (int, error) {
|
||||
// Frist try creating a dummy postrouting chain. Emperically, we have
|
||||
// noticed that on some devices there is partial nftables support and the
|
||||
// kernel rejects some chains that are valid on other devices. This is a
|
||||
// workaround to detect that case.
|
||||
//
|
||||
// This specifically allows us to run in on GKE nodes using COS images which
|
||||
// have partial nftables support (as of 2023-10-18). When we try to create a
|
||||
// dummy postrouting chain, we get an error like:
|
||||
// add chain: conn.Receive: netlink receive: no such file or directory
|
||||
nft, err := newNfTablesRunner(logger.Discard)
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: fmt.Errorf("cannot create nftables runner: %w", err),
|
||||
}
|
||||
}
|
||||
if err := nft.createDummyPostroutingChains(); err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
chains, err := conn.ListChains()
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: fmt.Errorf("cannot list chains: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
var validRules int
|
||||
for _, chain := range chains {
|
||||
rules, err := conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
validRules += len(rules)
|
||||
}
|
||||
|
||||
return validRules, nil
|
||||
}
|
||||
|
||||
func printMatchInfo(name string, info xt.InfoAny) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{`)
|
||||
|
||||
var handled bool = true
|
||||
switch v := info.(type) {
|
||||
// TODO(andrew): we should support these common types
|
||||
//case *xt.ConntrackMtinfo3:
|
||||
//case *xt.ConntrackMtinfo2:
|
||||
case *xt.Tcp:
|
||||
fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
|
||||
if v.Option != 0 {
|
||||
fmt.Fprintf(&sb, " Option:%d", v.Option)
|
||||
}
|
||||
if v.FlagsMask != 0 {
|
||||
fmt.Fprintf(&sb, " FlagsMask:%d", v.FlagsMask)
|
||||
}
|
||||
if v.FlagsCmp != 0 {
|
||||
fmt.Fprintf(&sb, " FlagsCmp:%d", v.FlagsCmp)
|
||||
}
|
||||
if v.InvFlags != 0 {
|
||||
fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
|
||||
}
|
||||
|
||||
case *xt.Udp:
|
||||
fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
|
||||
if v.InvFlags != 0 {
|
||||
fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
|
||||
}
|
||||
|
||||
case *xt.AddrType:
|
||||
var sprefix, dprefix string
|
||||
if v.InvertSource {
|
||||
sprefix = "!"
|
||||
}
|
||||
if v.InvertDest {
|
||||
dprefix = "!"
|
||||
}
|
||||
// TODO(andrew): translate source/dest
|
||||
fmt.Fprintf(&sb, "Source:%s%d Dest:%s%d", sprefix, v.Source, dprefix, v.Dest)
|
||||
|
||||
case *xt.AddrTypeV1:
|
||||
// TODO(andrew): translate source/dest
|
||||
fmt.Fprintf(&sb, "Source:%d Dest:%d", v.Source, v.Dest)
|
||||
|
||||
var flags []string
|
||||
for flag, name := range addrTypeFlagNames {
|
||||
if v.Flags&flag != 0 {
|
||||
flags = append(flags, name)
|
||||
}
|
||||
}
|
||||
if len(flags) > 0 {
|
||||
sort.Strings(flags)
|
||||
fmt.Fprintf(&sb, "Flags:%s", strings.Join(flags, ","))
|
||||
}
|
||||
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
if handled {
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
unknown, ok := info.(*xt.Unknown)
|
||||
if !ok {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
data := []byte(*unknown)
|
||||
|
||||
// Things where upstream has no type
|
||||
handled = true
|
||||
switch name {
|
||||
case "pkttype":
|
||||
if len(data) != 8 {
|
||||
handled = false
|
||||
break
|
||||
}
|
||||
|
||||
pkttype := int(native.Endian.Uint32(data[0:4]))
|
||||
invert := int(native.Endian.Uint32(data[4:8]))
|
||||
var invertPrefix string
|
||||
if invert != 0 {
|
||||
invertPrefix = "!"
|
||||
}
|
||||
|
||||
pkttypeName := packetTypeNames[pkttype]
|
||||
if pkttypeName != "" {
|
||||
fmt.Fprintf(&sb, "PktType:%s%s", invertPrefix, pkttypeName)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "PktType:%s%d", invertPrefix, pkttype)
|
||||
}
|
||||
|
||||
default:
|
||||
handled = true
|
||||
}
|
||||
|
||||
if !handled {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func printTargetInfo(name string, info xt.InfoAny) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{`)
|
||||
|
||||
unknown, ok := info.(*xt.Unknown)
|
||||
if !ok {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
data := []byte(*unknown)
|
||||
|
||||
// Things where upstream has no type
|
||||
switch name {
|
||||
case "LOG":
|
||||
if len(data) != 32 {
|
||||
fmt.Fprintf(&sb, `Error:"bad size; want 32, got %d"`, len(data))
|
||||
break
|
||||
}
|
||||
|
||||
level := data[0]
|
||||
logflags := data[1]
|
||||
prefix := unix.ByteSliceToString(data[2:])
|
||||
fmt.Fprintf(&sb, "Level:%d LogFlags:%d Prefix:%q", level, logflags, prefix)
|
||||
default:
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
245
vendor/tailscale.com/util/linuxfw/nftables_for_svcs.go
generated
vendored
Normal file
245
vendor/tailscale.com/util/linuxfw/nftables_for_svcs.go
generated
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/binaryutil"
|
||||
"github.com/google/nftables/expr"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// This file contains functionality that is currently (09/2024) used to set up
|
||||
// routing for the Tailscale Kubernetes operator egress proxies. A tailnet
|
||||
// service (identified by tailnet IP or FQDN) that gets exposed to cluster
|
||||
// workloads gets a separate prerouting chain created for it for each IP family
|
||||
// of the chain's target addresses. Each service's prerouting chain contains one
|
||||
// or more portmapping rules. A portmapping rule DNATs traffic received on a
|
||||
// particular port to a port of the tailnet service. Creating a chain per
|
||||
// service makes it easier to delete a service when no longer needed and helps
|
||||
// with readability.
|
||||
|
||||
// EnsurePortMapRuleForSvc:
|
||||
// - ensures that nat table exists
|
||||
// - ensures that there is a prerouting chain for the given service and IP family of the target address in the nat table
|
||||
// - ensures that there is a portmapping rule mathcing the given portmap (only creates the rule if it does not already exist)
|
||||
func (n *nftablesRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
t, ch, err := n.ensureChainForSvc(svc, targetIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ensuring chain for %s: %w", svc, err)
|
||||
}
|
||||
meta := svcPortMapRuleMeta(svc, targetIP, pm)
|
||||
rule, err := n.findRuleByMetadata(t, ch, meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up rule: %w", err)
|
||||
}
|
||||
if rule != nil {
|
||||
return nil
|
||||
}
|
||||
p, err := protoFromString(pm.Protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting protocol %s: %w", pm.Protocol, err)
|
||||
}
|
||||
|
||||
rule = portMapRule(t, ch, tun, targetIP, pm.MatchPort, pm.TargetPort, p, meta)
|
||||
n.conn.InsertRule(rule)
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
// DeletePortMapRuleForSvc deletes a portmapping rule in the given service/IP family chain.
|
||||
// It finds the matching rule using metadata attached to the rule.
|
||||
// The caller is expected to call DeleteSvc if the whole service (the chain)
|
||||
// needs to be deleted, so we don't deal with the case where this is the only
|
||||
// rule in the chain here.
|
||||
func (n *nftablesRunner) DeletePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
table, err := n.getNFTByAddr(targetIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %s: %w", targetIP, err)
|
||||
}
|
||||
t, err := getTableIfExists(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if nat table exists: %w", err)
|
||||
}
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
ch, err := getChainFromTable(n.conn, t, svc)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return fmt.Errorf("error checking if chain %s exists: %w", svc, err)
|
||||
}
|
||||
if errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return nil // service chain does not exist, so neither does the portmapping rule
|
||||
}
|
||||
meta := svcPortMapRuleMeta(svc, targetIP, pm)
|
||||
rule, err := n.findRuleByMetadata(t, ch, meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if rule == nil {
|
||||
return nil
|
||||
}
|
||||
if err := n.conn.DelRule(rule); err != nil {
|
||||
return fmt.Errorf("error deleting rule: %w", err)
|
||||
}
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
// DeleteSvc deletes the chains for the given service if any exist.
|
||||
func (n *nftablesRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr, pm []PortMap) error {
|
||||
for _, tip := range targetIPs {
|
||||
table, err := n.getNFTByAddr(tip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %s: %w", tip, err)
|
||||
}
|
||||
t, err := getTableIfExists(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if nat table exists: %w", err)
|
||||
}
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
ch, err := getChainFromTable(n.conn, t, svc)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return fmt.Errorf("error checking if chain %s exists: %w", svc, err)
|
||||
}
|
||||
if errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return nil
|
||||
}
|
||||
n.conn.DelChain(ch)
|
||||
}
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
func portMapRule(t *nftables.Table, ch *nftables.Chain, tun string, targetIP netip.Addr, matchPort, targetPort uint16, proto uint8, meta []byte) *nftables.Rule {
|
||||
var fam uint32
|
||||
if targetIP.Is4() {
|
||||
fam = unix.NFPROTO_IPV4
|
||||
} else {
|
||||
fam = unix.NFPROTO_IPV6
|
||||
}
|
||||
rule := &nftables.Rule{
|
||||
Table: t,
|
||||
Chain: ch,
|
||||
UserData: meta,
|
||||
Exprs: []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpNeq,
|
||||
Register: 1,
|
||||
Data: []byte(tun),
|
||||
},
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{proto},
|
||||
},
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: binaryutil.BigEndian.PutUint16(matchPort),
|
||||
},
|
||||
&expr.Immediate{
|
||||
Register: 1,
|
||||
Data: targetIP.AsSlice(),
|
||||
},
|
||||
&expr.Immediate{
|
||||
Register: 2,
|
||||
Data: binaryutil.BigEndian.PutUint16(targetPort),
|
||||
},
|
||||
&expr.NAT{
|
||||
Type: expr.NATTypeDestNAT,
|
||||
Family: fam,
|
||||
RegAddrMin: 1,
|
||||
RegAddrMax: 1,
|
||||
RegProtoMin: 2,
|
||||
RegProtoMax: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
// svcPortMapRuleMeta generates metadata for a rule.
|
||||
// This metadata can then be used to find the rule.
|
||||
// https://github.com/google/nftables/issues/48
|
||||
func svcPortMapRuleMeta(svcName string, targetIP netip.Addr, pm PortMap) []byte {
|
||||
return []byte(fmt.Sprintf("svc:%s,targetIP:%s:matchPort:%v,targetPort:%v,proto:%v", svcName, targetIP.String(), pm.MatchPort, pm.TargetPort, pm.Protocol))
|
||||
}
|
||||
|
||||
func (n *nftablesRunner) findRuleByMetadata(t *nftables.Table, ch *nftables.Chain, meta []byte) (*nftables.Rule, error) {
|
||||
if n.conn == nil || t == nil || ch == nil || len(meta) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rules, err := n.conn.GetRules(t, ch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing rules: %w", err)
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if reflect.DeepEqual(rule.UserData, meta) {
|
||||
return rule, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *nftablesRunner) ensureChainForSvc(svc string, targetIP netip.Addr) (*nftables.Table, *nftables.Chain, error) {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
table, err := n.getNFTByAddr(targetIP)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error setting up nftables for IP family of %v: %w", targetIP, err)
|
||||
}
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error ensuring nat table: %w", err)
|
||||
}
|
||||
svcCh, err := getOrCreateChain(n.conn, chainInfo{
|
||||
table: nat,
|
||||
name: svc,
|
||||
chainType: nftables.ChainTypeNAT,
|
||||
chainHook: nftables.ChainHookPrerouting,
|
||||
chainPriority: nftables.ChainPriorityNATDest,
|
||||
chainPolicy: &polAccept,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error ensuring prerouting chain: %w", err)
|
||||
}
|
||||
return nat, svcCh, nil
|
||||
}
|
||||
|
||||
// // PortMap is the port mapping for a service rule.
|
||||
type PortMap struct {
|
||||
// MatchPort is the local port to which the rule should apply.
|
||||
MatchPort uint16
|
||||
// TargetPort is the port to which the traffic should be forwarded.
|
||||
TargetPort uint16
|
||||
// Protocol is the protocol to match packets on. Only TCP and UDP are
|
||||
// supported.
|
||||
Protocol string
|
||||
}
|
||||
|
||||
func protoFromString(s string) (uint8, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "tcp":
|
||||
return unix.IPPROTO_TCP, nil
|
||||
case "udp":
|
||||
return unix.IPPROTO_UDP, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unrecognized protocol: %q", s)
|
||||
}
|
||||
}
|
||||
2055
vendor/tailscale.com/util/linuxfw/nftables_runner.go
generated
vendored
Normal file
2055
vendor/tailscale.com/util/linuxfw/nftables_runner.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
95
vendor/tailscale.com/util/linuxfw/nftables_types.go
generated
vendored
Normal file
95
vendor/tailscale.com/util/linuxfw/nftables_types.go
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/google/nftables/xt"
|
||||
)
|
||||
|
||||
var metaKeyNames = map[expr.MetaKey]string{
|
||||
expr.MetaKeyLEN: "LEN",
|
||||
expr.MetaKeyPROTOCOL: "PROTOCOL",
|
||||
expr.MetaKeyPRIORITY: "PRIORITY",
|
||||
expr.MetaKeyMARK: "MARK",
|
||||
expr.MetaKeyIIF: "IIF",
|
||||
expr.MetaKeyOIF: "OIF",
|
||||
expr.MetaKeyIIFNAME: "IIFNAME",
|
||||
expr.MetaKeyOIFNAME: "OIFNAME",
|
||||
expr.MetaKeyIIFTYPE: "IIFTYPE",
|
||||
expr.MetaKeyOIFTYPE: "OIFTYPE",
|
||||
expr.MetaKeySKUID: "SKUID",
|
||||
expr.MetaKeySKGID: "SKGID",
|
||||
expr.MetaKeyNFTRACE: "NFTRACE",
|
||||
expr.MetaKeyRTCLASSID: "RTCLASSID",
|
||||
expr.MetaKeySECMARK: "SECMARK",
|
||||
expr.MetaKeyNFPROTO: "NFPROTO",
|
||||
expr.MetaKeyL4PROTO: "L4PROTO",
|
||||
expr.MetaKeyBRIIIFNAME: "BRIIIFNAME",
|
||||
expr.MetaKeyBRIOIFNAME: "BRIOIFNAME",
|
||||
expr.MetaKeyPKTTYPE: "PKTTYPE",
|
||||
expr.MetaKeyCPU: "CPU",
|
||||
expr.MetaKeyIIFGROUP: "IIFGROUP",
|
||||
expr.MetaKeyOIFGROUP: "OIFGROUP",
|
||||
expr.MetaKeyCGROUP: "CGROUP",
|
||||
expr.MetaKeyPRANDOM: "PRANDOM",
|
||||
}
|
||||
|
||||
var cmpOpNames = map[expr.CmpOp]string{
|
||||
expr.CmpOpEq: "EQ",
|
||||
expr.CmpOpNeq: "NEQ",
|
||||
expr.CmpOpLt: "LT",
|
||||
expr.CmpOpLte: "LTE",
|
||||
expr.CmpOpGt: "GT",
|
||||
expr.CmpOpGte: "GTE",
|
||||
}
|
||||
|
||||
var verdictNames = map[expr.VerdictKind]string{
|
||||
expr.VerdictReturn: "RETURN",
|
||||
expr.VerdictGoto: "GOTO",
|
||||
expr.VerdictJump: "JUMP",
|
||||
expr.VerdictBreak: "BREAK",
|
||||
expr.VerdictContinue: "CONTINUE",
|
||||
expr.VerdictDrop: "DROP",
|
||||
expr.VerdictAccept: "ACCEPT",
|
||||
expr.VerdictStolen: "STOLEN",
|
||||
expr.VerdictQueue: "QUEUE",
|
||||
expr.VerdictRepeat: "REPEAT",
|
||||
expr.VerdictStop: "STOP",
|
||||
}
|
||||
|
||||
var payloadOperationTypeNames = map[expr.PayloadOperationType]string{
|
||||
expr.PayloadLoad: "LOAD",
|
||||
expr.PayloadWrite: "WRITE",
|
||||
}
|
||||
|
||||
var payloadBaseNames = map[expr.PayloadBase]string{
|
||||
expr.PayloadBaseLLHeader: "ll-header",
|
||||
expr.PayloadBaseNetworkHeader: "network-header",
|
||||
expr.PayloadBaseTransportHeader: "transport-header",
|
||||
}
|
||||
|
||||
var packetTypeNames = map[int]string{
|
||||
0 /* PACKET_HOST */ : "unicast",
|
||||
1 /* PACKET_BROADCAST */ : "broadcast",
|
||||
2 /* PACKET_MULTICAST */ : "multicast",
|
||||
}
|
||||
|
||||
var addrTypeFlagNames = map[xt.AddrTypeFlags]string{
|
||||
xt.AddrTypeUnspec: "unspec",
|
||||
xt.AddrTypeUnicast: "unicast",
|
||||
xt.AddrTypeLocal: "local",
|
||||
xt.AddrTypeBroadcast: "broadcast",
|
||||
xt.AddrTypeAnycast: "anycast",
|
||||
xt.AddrTypeMulticast: "multicast",
|
||||
xt.AddrTypeBlackhole: "blackhole",
|
||||
xt.AddrTypeUnreachable: "unreachable",
|
||||
xt.AddrTypeProhibit: "prohibit",
|
||||
xt.AddrTypeThrow: "throw",
|
||||
xt.AddrTypeNat: "nat",
|
||||
xt.AddrTypeXresolve: "xresolve",
|
||||
}
|
||||
Reference in New Issue
Block a user