mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-30 07:42:12 +01:00 
			
		
		
		
	Now cmd/derper doesn't depend on iptables, nftables, and netlink code :) But this is really just a cleanup step I noticed on the way to making tsnet applications able to not link all the OS router code which they don't use. Updates #17313 Change-Id: Ic7b4e04e3a9639fd198e9dbeb0f7bae22a4a47a9 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			184 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			184 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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/feature"
 | |
| 	"tailscale.com/tsconst"
 | |
| 	"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 (
 | |
| 	fwmarkMask         = tsconst.LinuxFwmarkMask
 | |
| 	fwmarkMaskNum      = tsconst.LinuxFwmarkMaskNum
 | |
| 	subnetRouteMark    = tsconst.LinuxSubnetRouteMark
 | |
| 	subnetRouteMarkNum = tsconst.LinuxSubnetRouteMarkNum
 | |
| 	bypassMark         = tsconst.LinuxBypassMark
 | |
| 	bypassMarkNum      = tsconst.LinuxBypassMarkNum
 | |
| )
 | |
| 
 | |
| // 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 = bypassMarkNum
 | |
| 	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)
 | |
| }
 | |
| 
 | |
| 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)
 | |
| 	}
 | |
| }
 |