// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package linuxfw returns the kind of firewall being used by the kernel. //go:build linux package linuxfw import ( "bytes" "errors" "fmt" "os" "os/exec" "strconv" "strings" "github.com/tailscale/netlink" "tailscale.com/types/logger" ) // 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 ( // The mask for reading/writing the 'firewall mask' bits on a packet. // See the comment on the const block on why we only use the third byte. // // We claim bits 16:23 entirely. For now we only use the lower four // bits, leaving the higher 4 bits for future use. TailscaleFwmarkMask = "0xff0000" TailscaleFwmarkMaskNeg = "0xff00ffff" TailscaleFwmarkMaskNum = 0xff0000 // Packet is from Tailscale and to a subnet route destination, so // is allowed to be routed through this machine. TailscaleSubnetRouteMark = "0x40000" TailscaleSubnetRouteMarkNum = 0x40000 // This one is same value but padded to even number of digit, so // hex decoding can work correctly. TailscaleSubnetRouteMarkHexStr = "0x040000" // Packet was originated by tailscaled itself, and must not be // routed over the Tailscale network. TailscaleBypassMark = "0x80000" TailscaleBypassMarkNum = 0x80000 ) // errCode extracts and returns the process exit code from err, or // zero if err is nil. func errCode(err error) int { if err == nil { return 0 } var e *exec.ExitError if ok := errors.As(err, &e); ok { return e.ExitCode() } s := err.Error() if strings.HasPrefix(s, "exitcode:") { code, err := strconv.Atoi(s[9:]) if err == nil { return code } } return -42 } // 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 { _, 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) } // Some distros ship ip6tables separately from iptables. if _, err := exec.LookPath("ip6tables"); err != nil { return err } return nil } // 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. func checkSupportsV6NAT() bool { bs, err := os.ReadFile("/proc/net/ip6_tables_names") if err != nil { // Can't read the file. Assume SNAT works. return true } if bytes.Contains(bs, []byte("nat\n")) { return true } // In nftables mode, that proc file will be empty. Try another thing: if exec.Command("modprobe", "ip6table_nat").Run() == nil { return true } return false } 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 = TailscaleBypassMarkNum 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) }