mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	The AddSNATRuleForDst rule was adding a new rule each time it was called including: - if a rule already existed - if a rule matching the destination, but with different desired source already existed This was causing issues especially for the in-progress egress HA proxies work, where the rules are now refreshed more frequently, so more redundant rules were being created. This change: - only creates the rule if it doesn't already exist - if a rule for the same dst, but different source is found, delete it - also ensures that egress proxies refresh firewall rules if the node's tailnet IP changes Updates tailscale/tailscale#13406 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
		
			
				
	
	
		
			263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build linux
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/netip"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"tailscale.com/util/linuxfw"
 | |
| )
 | |
| 
 | |
| // ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
 | |
| func ensureIPForwarding(root, clusterProxyTargetIP, tailnetTargetIP, tailnetTargetFQDN string, routes *string) error {
 | |
| 	var (
 | |
| 		v4Forwarding, v6Forwarding bool
 | |
| 	)
 | |
| 	if clusterProxyTargetIP != "" {
 | |
| 		proxyIP, err := netip.ParseAddr(clusterProxyTargetIP)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("invalid cluster destination IP: %v", err)
 | |
| 		}
 | |
| 		if proxyIP.Is4() {
 | |
| 			v4Forwarding = true
 | |
| 		} else {
 | |
| 			v6Forwarding = true
 | |
| 		}
 | |
| 	}
 | |
| 	if tailnetTargetIP != "" {
 | |
| 		proxyIP, err := netip.ParseAddr(tailnetTargetIP)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("invalid tailnet destination IP: %v", err)
 | |
| 		}
 | |
| 		if proxyIP.Is4() {
 | |
| 			v4Forwarding = true
 | |
| 		} else {
 | |
| 			v6Forwarding = true
 | |
| 		}
 | |
| 	}
 | |
| 	// Currently we only proxy traffic to the IPv4 address of the tailnet
 | |
| 	// target.
 | |
| 	if tailnetTargetFQDN != "" {
 | |
| 		v4Forwarding = true
 | |
| 	}
 | |
| 	if routes != nil && *routes != "" {
 | |
| 		for _, route := range strings.Split(*routes, ",") {
 | |
| 			cidr, err := netip.ParsePrefix(route)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("invalid subnet route: %v", err)
 | |
| 			}
 | |
| 			if cidr.Addr().Is4() {
 | |
| 				v4Forwarding = true
 | |
| 			} else {
 | |
| 				v6Forwarding = true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return enableIPForwarding(v4Forwarding, v6Forwarding, root)
 | |
| }
 | |
| 
 | |
| func enableIPForwarding(v4Forwarding, v6Forwarding bool, root string) error {
 | |
| 	var paths []string
 | |
| 	if v4Forwarding {
 | |
| 		paths = append(paths, filepath.Join(root, "proc/sys/net/ipv4/ip_forward"))
 | |
| 	}
 | |
| 	if v6Forwarding {
 | |
| 		paths = append(paths, filepath.Join(root, "proc/sys/net/ipv6/conf/all/forwarding"))
 | |
| 	}
 | |
| 
 | |
| 	// In some common configurations (e.g. default docker,
 | |
| 	// kubernetes), the container environment denies write access to
 | |
| 	// most sysctls, including IP forwarding controls. Check the
 | |
| 	// sysctl values before trying to change them, so that we
 | |
| 	// gracefully do nothing if the container's already been set up
 | |
| 	// properly by e.g. a k8s initContainer.
 | |
| 	for _, path := range paths {
 | |
| 		bs, err := os.ReadFile(path)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("reading %q: %w", path, err)
 | |
| 		}
 | |
| 		if v := strings.TrimSpace(string(bs)); v != "1" {
 | |
| 			if err := os.WriteFile(path, []byte("1"), 0644); err != nil {
 | |
| 				return fmt.Errorf("enabling %q: %w", path, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func installEgressForwardingRule(_ context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
 | |
| 	dst, err := netip.ParseAddr(dstStr)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	var local netip.Addr
 | |
| 	for _, pfx := range tsIPs {
 | |
| 		if !pfx.IsSingleIP() {
 | |
| 			continue
 | |
| 		}
 | |
| 		if pfx.Addr().Is4() != dst.Is4() {
 | |
| 			continue
 | |
| 		}
 | |
| 		local = pfx.Addr()
 | |
| 		break
 | |
| 	}
 | |
| 	if !local.IsValid() {
 | |
| 		return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
 | |
| 	}
 | |
| 	if err := nfr.DNATNonTailscaleTraffic("tailscale0", dst); err != nil {
 | |
| 		return fmt.Errorf("installing egress proxy rules: %w", err)
 | |
| 	}
 | |
| 	if err := nfr.EnsureSNATForDst(local, dst); err != nil {
 | |
| 		return fmt.Errorf("installing egress proxy rules: %w", err)
 | |
| 	}
 | |
| 	if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
 | |
| 		return fmt.Errorf("installing egress proxy rules: %w", err)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // installTSForwardingRuleForDestination accepts a destination address and a
 | |
| // list of node's tailnet addresses, sets up rules to forward traffic for
 | |
| // destination to the tailnet IP matching the destination IP family.
 | |
| // Destination can be Pod IP of this node.
 | |
| func installTSForwardingRuleForDestination(_ context.Context, dstFilter string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
 | |
| 	dst, err := netip.ParseAddr(dstFilter)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	var local netip.Addr
 | |
| 	for _, pfx := range tsIPs {
 | |
| 		if !pfx.IsSingleIP() {
 | |
| 			continue
 | |
| 		}
 | |
| 		if pfx.Addr().Is4() != dst.Is4() {
 | |
| 			continue
 | |
| 		}
 | |
| 		local = pfx.Addr()
 | |
| 		break
 | |
| 	}
 | |
| 	if !local.IsValid() {
 | |
| 		return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstFilter, tsIPs)
 | |
| 	}
 | |
| 	if err := nfr.AddDNATRule(dst, local); err != nil {
 | |
| 		return fmt.Errorf("installing rule for forwarding traffic to tailnet IP: %w", err)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func installIngressForwardingRule(_ context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
 | |
| 	dst, err := netip.ParseAddr(dstStr)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	var local netip.Addr
 | |
| 	proxyHasIPv4Address := false
 | |
| 	for _, pfx := range tsIPs {
 | |
| 		if !pfx.IsSingleIP() {
 | |
| 			continue
 | |
| 		}
 | |
| 		if pfx.Addr().Is4() {
 | |
| 			proxyHasIPv4Address = true
 | |
| 		}
 | |
| 		if pfx.Addr().Is4() != dst.Is4() {
 | |
| 			continue
 | |
| 		}
 | |
| 		local = pfx.Addr()
 | |
| 		break
 | |
| 	}
 | |
| 	if proxyHasIPv4Address && dst.Is6() {
 | |
| 		log.Printf("Warning: proxy backend ClusterIP is an IPv6 address and the proxy has a IPv4 tailnet address. You might need to disable IPv4 address allocation for the proxy for forwarding to work. See https://github.com/tailscale/tailscale/issues/12156")
 | |
| 	}
 | |
| 	if !local.IsValid() {
 | |
| 		return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
 | |
| 	}
 | |
| 	if err := nfr.AddDNATRule(local, dst); err != nil {
 | |
| 		return fmt.Errorf("installing ingress proxy rules: %w", err)
 | |
| 	}
 | |
| 	if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
 | |
| 		return fmt.Errorf("installing ingress proxy rules: %w", err)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func installIngressForwardingRuleForDNSTarget(_ context.Context, backendAddrs []net.IP, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
 | |
| 	var (
 | |
| 		tsv4       netip.Addr
 | |
| 		tsv6       netip.Addr
 | |
| 		v4Backends []netip.Addr
 | |
| 		v6Backends []netip.Addr
 | |
| 	)
 | |
| 	for _, pfx := range tsIPs {
 | |
| 		if pfx.IsSingleIP() && pfx.Addr().Is4() {
 | |
| 			tsv4 = pfx.Addr()
 | |
| 			continue
 | |
| 		}
 | |
| 		if pfx.IsSingleIP() && pfx.Addr().Is6() {
 | |
| 			tsv6 = pfx.Addr()
 | |
| 			continue
 | |
| 		}
 | |
| 	}
 | |
| 	// TODO: log if more than one backend address is found and firewall is
 | |
| 	// in nftables mode that only the first IP will be used.
 | |
| 	for _, ip := range backendAddrs {
 | |
| 		if ip.To4() != nil {
 | |
| 			v4Backends = append(v4Backends, netip.AddrFrom4([4]byte(ip.To4())))
 | |
| 		}
 | |
| 		if ip.To16() != nil {
 | |
| 			v6Backends = append(v6Backends, netip.AddrFrom16([16]byte(ip.To16())))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Enable IP forwarding here as opposed to at the start of containerboot
 | |
| 	// as the IPv4/IPv6 requirements might have changed.
 | |
| 	// For Kubernetes operator proxies, forwarding for both IPv4 and IPv6 is
 | |
| 	// enabled by an init container, so in practice enabling forwarding here
 | |
| 	// is only needed if this proxy has been configured by manually setting
 | |
| 	// TS_EXPERIMENTAL_DEST_DNS_NAME env var for a containerboot instance.
 | |
| 	if err := enableIPForwarding(len(v4Backends) != 0, len(v6Backends) != 0, ""); err != nil {
 | |
| 		log.Printf("[unexpected] failed to ensure IP forwarding: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	updateFirewall := func(dst netip.Addr, backendTargets []netip.Addr) error {
 | |
| 		if err := nfr.DNATWithLoadBalancer(dst, backendTargets); err != nil {
 | |
| 			return fmt.Errorf("installing DNAT rules for ingress backends %+#v: %w", backendTargets, err)
 | |
| 		}
 | |
| 		// The backend might advertize MSS higher than that of the
 | |
| 		// tailscale interfaces. Clamp MSS of packets going out via
 | |
| 		// tailscale0 interface to its MTU to prevent broken connections
 | |
| 		// in environments where path MTU discovery is not working.
 | |
| 		if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
 | |
| 			return fmt.Errorf("adding rule to clamp traffic via tailscale0: %v", err)
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if len(v4Backends) != 0 {
 | |
| 		if !tsv4.IsValid() {
 | |
| 			log.Printf("backend targets %v contain at least one IPv4 address, but this node's Tailscale IPs do not contain a valid IPv4 address: %v", backendAddrs, tsIPs)
 | |
| 		} else if err := updateFirewall(tsv4, v4Backends); err != nil {
 | |
| 			return fmt.Errorf("Installing IPv4 firewall rules: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if len(v6Backends) != 0 && !tsv6.IsValid() {
 | |
| 		if !tsv6.IsValid() {
 | |
| 			log.Printf("backend targets %v contain at least one IPv6 address, but this node's Tailscale IPs do not contain a valid IPv6 address: %v", backendAddrs, tsIPs)
 | |
| 		} else if !nfr.HasIPV6NAT() {
 | |
| 			log.Printf("backend targets %v contain at least one IPv6 address, but the chosen firewall mode does not support IPv6 NAT", backendAddrs)
 | |
| 		} else if err := updateFirewall(tsv6, v6Backends); err != nil {
 | |
| 			return fmt.Errorf("Installing IPv6 firewall rules: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |