mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-27 11:41:14 +00:00
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:
committed by
Brad Fitzpatrick
parent
7df7e01d0f
commit
dd615c8fdd
@@ -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
|
||||
|
||||
@@ -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+
|
||||
|
||||
13
feature/buildfeatures/feature_iptables_disabled.go
Normal file
13
feature/buildfeatures/feature_iptables_disabled.go
Normal 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
|
||||
13
feature/buildfeatures/feature_iptables_enabled.go
Normal file
13
feature/buildfeatures/feature_iptables_enabled.go
Normal 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
|
||||
@@ -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},
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
20
util/linuxfw/iptables_disabled.go
Normal file
20
util/linuxfw/iptables_disabled.go
Normal 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")
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user