mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	Updates #7123 Change-Id: Ie9be6814831f661ad5636afcd51d063a0d7a907d Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			236 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| 
 | |
| package linuxfw
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"strings"
 | |
| 	"unicode"
 | |
| 
 | |
| 	"github.com/coreos/go-iptables/iptables"
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"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 {
 | |
| 	// 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", errors.Join(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
 | |
| }
 | |
| 
 | |
| // 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 errors.Join(errs...)
 | |
| }
 |