util/linuxfw, feature/buildfeatures: add ts_omit_iptables to make IPTables optional

Updates #12614

Change-Id: Ic0eba982aa8468a55c63e1b763345f032a55b4e2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-26 16:19:12 -07:00
committed by Brad Fitzpatrick
parent 7df7e01d0f
commit dd615c8fdd
18 changed files with 282 additions and 221 deletions

View File

@@ -98,7 +98,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/disco from tailscale.com/derp/derpserver
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/feature from tailscale.com/tsweb
tailscale.com/feature from tailscale.com/tsweb+
L tailscale.com/feature/buildfeatures from tailscale.com/util/linuxfw
tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/hostinfo from tailscale.com/net/netmon+
tailscale.com/ipn from tailscale.com/client/local

View File

@@ -2,7 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
github.com/gaissmai/bart from tailscale.com/net/ipset+
github.com/gaissmai/bart/internal/bitset from github.com/gaissmai/bart+
@@ -420,13 +419,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os/exec from github.com/coreos/go-iptables/iptables+
os/exec from tailscale.com/clientupdate+
os/signal from tailscale.com/cmd/tailscaled
os/user from archive/tar+
path from archive/tar+
path/filepath from archive/tar+
reflect from archive/tar+
regexp from github.com/coreos/go-iptables/iptables+
regexp from internal/profile+
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from github.com/klauspost/compress/zstd+

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_iptables
package buildfeatures
// HasIPTables is whether the binary was built with support for modular feature "Linux iptables support".
// Specifically, it's whether the binary was NOT built with the "ts_omit_iptables" build tag.
// It's a const so it can be used for dead code elimination.
const HasIPTables = false

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_iptables
package buildfeatures
// HasIPTables is whether the binary was built with support for modular feature "Linux iptables support".
// Specifically, it's whether the binary was NOT built with the "ts_omit_iptables" build tag.
// It's a const so it can be used for dead code elimination.
const HasIPTables = true

View File

@@ -112,6 +112,7 @@ var Features = map[FeatureTag]FeatureMeta{
Desc: "Generic Receive Offload support (performance)",
Deps: []FeatureTag{"netstack"},
},
"iptables": {"IPTables", "Linux iptables support", nil},
"kube": {"Kube", "Kubernetes integration", nil},
"linuxdnsfight": {"LinuxDNSFight", "Linux support for detecting DNS fights (inotify watching of /etc/resolv.conf)", nil},
"oauthkey": {"OAuthKey", "OAuth secret-to-authkey resolution support", nil},

View File

@@ -421,6 +421,8 @@ func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
// NetfilterKind specifies what netfilter implementation to use.
//
// It can be "iptables", "nftables", or "" to auto-detect.
//
// Linux-only.
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }

View File

@@ -264,6 +264,8 @@ type Prefs struct {
// NetfilterKind specifies what netfilter implementation to use.
//
// It can be "iptables", "nftables", or "" to auto-detect.
//
// Linux-only.
NetfilterKind string

View File

@@ -10,6 +10,8 @@ import (
"os/exec"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/hostinfo"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
@@ -42,10 +44,12 @@ func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
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"
if buildfeatures.HasIPTables {
_, err := det.iptDetect()
if err != nil && errors.Is(err, exec.ErrNotFound) {
logf("iptables not found: %v; falling back to nftables", err)
mode = "nftables"
}
}
}
@@ -59,11 +63,16 @@ func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
return FirewallModeNfTables
case "iptables":
hostinfo.SetFirewallMode("ipt-forced")
default:
return FirewallModeIPTables
}
if buildfeatures.HasIPTables {
logf("default choosing iptables")
hostinfo.SetFirewallMode("ipt-default")
return FirewallModeIPTables
}
return FirewallModeIPTables
logf("default choosing nftables")
hostinfo.SetFirewallMode("nft-default")
return FirewallModeNfTables
}
// tableDetector abstracts helpers to detect the firewall mode.
@@ -80,19 +89,33 @@ func (l linuxFWDetector) iptDetect() (int, error) {
return detectIptables()
}
var hookDetectNetfilter feature.Hook[func() (int, error)]
// ErrUnsupported is the error returned from all functions on non-Linux
// platforms.
var ErrUnsupported = errors.New("linuxfw:unsupported")
// nftDetect returns the number of nftables rules in the current namespace.
func (l linuxFWDetector) nftDetect() (int, error) {
return detectNetfilter()
if f, ok := hookDetectNetfilter.GetOk(); ok {
return f()
}
return 0, ErrUnsupported
}
// 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 !buildfeatures.HasIPTables {
hostinfo.SetFirewallMode("nft-noipt")
return FirewallModeNfTables
}
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 {

View File

@@ -128,7 +128,7 @@ func (n *fakeIPTables) DeleteChain(table, chain string) error {
}
}
func NewFakeIPTablesRunner() *iptablesRunner {
func NewFakeIPTablesRunner() NetfilterRunner {
ipt4 := newFakeIPTables()
v6Available := false
var ipt6 iptablesInterface

View File

@@ -1,21 +1,34 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && (arm64 || amd64) && !ts_omit_iptables
// TODO(#8502): add support for more architectures
//go:build linux && (arm64 || amd64)
package linuxfw
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"unicode"
"github.com/coreos/go-iptables/iptables"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
"tailscale.com/version/distro"
)
func init() {
isNotExistError = func(err error) bool {
var e *iptables.Error
return errors.As(err, &e) && e.IsNotExist()
}
}
// DebugNetfilter prints debug information about iptables rules to the
// provided log function.
func DebugIptables(logf logger.Logf) error {
@@ -71,3 +84,153 @@ func detectIptables() (int, error) {
// return the count of non-default rules
return count, 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
}
func init() {
hookIPTablesCleanup.Set(ipTablesCleanUp)
}
// ipTablesCleanUp removes all Tailscale added iptables rules.
// Any errors that occur are logged to the provided logf.
func ipTablesCleanUp(logf logger.Logf) {
switch distro.Get() {
case distro.Gokrazy, distro.JetKVM:
// These use nftables and don'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)
}
}
// 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...)
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !(arm64 || amd64)) || ts_omit_iptables
package linuxfw
import (
"errors"
"tailscale.com/types/logger"
)
func detectIptables() (int, error) {
return 0, nil
}
func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
return nil, errors.New("iptables disabled in build")
}

View File

@@ -10,6 +10,10 @@ import (
"testing"
)
func newFakeIPTablesRunner() *iptablesRunner {
return NewFakeIPTablesRunner().(*iptablesRunner)
}
func Test_iptablesRunner_EnsurePortMapRuleForSvc(t *testing.T) {
v4Addr := netip.MustParseAddr("10.0.0.4")
v6Addr := netip.MustParseAddr("fd7a:115c:a1e0::701:b62a")
@@ -45,7 +49,7 @@ func Test_iptablesRunner_EnsurePortMapRuleForSvc(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
table := iptr.getIPTByAddr(tt.targetIP)
for _, ruleset := range tt.precreateSvcRules {
mustPrecreatePortMapRule(t, ruleset, table)
@@ -103,7 +107,7 @@ func Test_iptablesRunner_DeletePortMapRuleForSvc(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
table := iptr.getIPTByAddr(tt.targetIP)
for _, ruleset := range tt.precreateSvcRules {
mustPrecreatePortMapRule(t, ruleset, table)
@@ -127,7 +131,7 @@ func Test_iptablesRunner_DeleteSvc(t *testing.T) {
v4Addr := netip.MustParseAddr("10.0.0.4")
v6Addr := netip.MustParseAddr("fd7a:115c:a1e0::701:b62a")
testPM := PortMap{Protocol: "tcp", MatchPort: 4003, TargetPort: 80}
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
// create two rules that will consitute svc1
s1R1 := argsForPortMapRule("svc1", "tailscale0", v4Addr, testPM)
@@ -189,7 +193,7 @@ func Test_iptablesRunner_EnsureDNATRuleForSvc(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
table := iptr.getIPTByAddr(tt.targetIP)
for _, ruleset := range tt.precreateSvcRules {
mustPrecreateDNATRule(t, ruleset, table)
@@ -248,7 +252,7 @@ func Test_iptablesRunner_DeleteDNATRuleForSvc(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
table := iptr.getIPTByAddr(tt.targetIP)
for _, ruleset := range tt.precreateSvcRules {
mustPrecreateDNATRule(t, ruleset, table)

View File

@@ -6,31 +6,22 @@
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()
}
var isNotExistError = func(err error) bool { return false }
type iptablesInterface interface {
// Adding this interface for testing purposes so we can mock out
@@ -62,98 +53,6 @@ func checkIP6TablesExists() error {
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
@@ -685,26 +584,6 @@ func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error
return nil
}
// IPTablesCleanUp removes all Tailscale added iptables rules.
// Any errors that occur are logged to the provided logf.
func IPTablesCleanUp(logf logger.Logf) {
switch distro.Get() {
case distro.Gokrazy, distro.JetKVM:
// These use nftables and don'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.
@@ -733,40 +612,6 @@ func delChain(ipt iptablesInterface, table, chain string) error {
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 {

View File

@@ -20,7 +20,7 @@ func init() {
}
func TestAddAndDeleteChains(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
err := iptr.AddChains()
if err != nil {
t.Fatal(err)
@@ -59,7 +59,7 @@ func TestAddAndDeleteChains(t *testing.T) {
}
func TestAddAndDeleteHooks(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
// don't need to test what happens if the chains don't exist, because
// this is handled by fake iptables, in realife iptables would return error.
if err := iptr.AddChains(); err != nil {
@@ -113,7 +113,7 @@ func TestAddAndDeleteHooks(t *testing.T) {
}
func TestAddAndDeleteBase(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
tunname := "tun0"
if err := iptr.AddChains(); err != nil {
t.Fatal(err)
@@ -176,7 +176,7 @@ func TestAddAndDeleteBase(t *testing.T) {
}
func TestAddAndDelLoopbackRule(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
// We don't need to test for malformed addresses, AddLoopbackRule
// takes in a netip.Addr, which is already valid.
fakeAddrV4 := netip.MustParseAddr("192.168.0.2")
@@ -247,7 +247,7 @@ func TestAddAndDelLoopbackRule(t *testing.T) {
}
func TestAddAndDelSNATRule(t *testing.T) {
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
if err := iptr.AddChains(); err != nil {
t.Fatal(err)
@@ -292,7 +292,7 @@ func TestAddAndDelSNATRule(t *testing.T) {
func TestEnsureSNATForDst_ipt(t *testing.T) {
ip1, ip2, ip3 := netip.MustParseAddr("100.99.99.99"), netip.MustParseAddr("100.88.88.88"), netip.MustParseAddr("100.77.77.77")
iptr := NewFakeIPTablesRunner()
iptr := newFakeIPTablesRunner()
// 1. A new rule gets added
mustCreateSNATRule_ipt(t, iptr, ip1, ip2)

View File

@@ -14,6 +14,7 @@ import (
"strings"
"github.com/tailscale/netlink"
"tailscale.com/feature"
"tailscale.com/types/logger"
)
@@ -180,3 +181,13 @@ func CheckIPRuleSupportsV6(logf logger.Logf) error {
defer netlink.RuleDel(rule)
return netlink.RuleAdd(rule)
}
var hookIPTablesCleanup feature.Hook[func(logger.Logf)]
// IPTablesCleanUp removes all Tailscale added iptables rules.
// Any errors that occur are logged to the provided logf.
func IPTablesCleanUp(logf logger.Logf) {
if f, ok := hookIPTablesCleanup.GetOk(); ok {
f(logf)
}
}

View File

@@ -1,40 +0,0 @@
// 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
}

View File

@@ -103,6 +103,10 @@ func DebugNetfilter(logf logger.Logf) error {
return nil
}
func init() {
hookDetectNetfilter.Set(detectNetfilter)
}
// 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

View File

@@ -94,7 +94,7 @@ type Config struct {
SNATSubnetRoutes bool // SNAT traffic to local subnets
StatefulFiltering bool // Apply stateful filtering to inbound connections
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
}
func (a *Config) Equal(b *Config) bool {