Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package linuxfw
import ( "fmt" "log" "net/netip" "os/exec" "slices" "strconv" "strings"
"tailscale.com/net/tsaddr" "tailscale.com/types/logger")
// 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 { return false }
type iptablesInterface interface { // Adding this interface for testing purposes so we can mock out // the iptables library, in reality this is a wrapper to *iptables.IPTables. Insert(table, chain string, pos int, args ...string) error Append(table, chain string, args ...string) error Exists(table, chain string, args ...string) (bool, error) Delete(table, chain string, args ...string) error List(table, chain string) ([]string, error) ClearChain(table, chain string) error NewChain(table, chain string) error DeleteChain(table, chain string) error}
type iptablesRunner struct { ipt4 iptablesInterface ipt6 iptablesInterface
v6Available bool v6NATAvailable bool v6FilterAvailable bool}
func checkIP6TablesExists() error { // Some distros ship ip6tables separately from iptables. if _, err := exec.LookPath("ip6tables"); err != nil { return fmt.Errorf("path not found: %w", err) } return nil}
// HasIPV6 reports true if the system supports IPv6.func (i *iptablesRunner) HasIPV6() bool { return i.v6Available}
// HasIPV6Filter reports true if the system supports ip6tables filter table.func (i *iptablesRunner) HasIPV6Filter() bool { return i.v6FilterAvailable}
// HasIPV6NAT reports true if the system supports IPv6 NAT.func (i *iptablesRunner) HasIPV6NAT() bool { return i.v6NATAvailable}
// getIPTByAddr returns the iptablesInterface with correct IP family// that we will be using for the given address.func (i *iptablesRunner) getIPTByAddr(addr netip.Addr) iptablesInterface { nf := i.ipt4 if addr.Is6() { nf = i.ipt6 } return nf}
// AddLoopbackRule adds an iptables rule to permit loopback traffic to// a local Tailscale IP.func (i *iptablesRunner) AddLoopbackRule(addr netip.Addr) error { if err := i.getIPTByAddr(addr).Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil { return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err) }
return nil}
// tsChain returns the name of the tailscale sub-chain corresponding// to the given "parent" chain (e.g. INPUT, FORWARD, ...).func tsChain(chain string) string { return "ts-" + strings.ToLower(chain)}
// DelLoopbackRule removes the iptables rule permitting loopback// traffic to a Tailscale IP. A missing rule is not an error: an address// left on the interface by a previous tailscaled instance never went// through AddLoopbackRule in this one, so removing it must not be// blocked by the absence of its loopback rule.func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error { ipt := i.getIPTByAddr(addr) args := []string{"-i", "lo", "-s", addr.String(), "-j", "ACCEPT"} exists, err := ipt.Exists("filter", "ts-input", args...) if err != nil { return fmt.Errorf("checking loopback allow rule for %q: %w", addr, err) } if !exists { return nil } if err := ipt.Delete("filter", "ts-input", args...); err != nil { return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err) }
return nil}
// getTables gets the available iptablesInterface in iptables runner.func (i *iptablesRunner) getTables() []iptablesInterface { if i.HasIPV6Filter() { return []iptablesInterface{i.ipt4, i.ipt6} } return []iptablesInterface{i.ipt4}}
// getNATTables gets the available iptablesInterface in iptables runner.// If the system does not support IPv6 NAT, only the IPv4 iptablesInterface// is returned.func (i *iptablesRunner) getNATTables() []iptablesInterface { if i.HasIPV6NAT() { return i.getTables() } return []iptablesInterface{i.ipt4}}
// AddHooks inserts calls to tailscale's netfilter chains in// the relevant main netfilter chains. The tailscale chains must// already exist. If they do not, an error is returned.func (i *iptablesRunner) AddHooks() error { // divert inserts a jump to the tailscale chain in the given table/chain. // If the jump already exists, it is a no-op. divert := func(ipt iptablesInterface, table, chain string) error { tsChain := tsChain(chain)
args := []string{"-j", tsChain} exists, err := ipt.Exists(table, chain, args...) if err != nil { return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err) } if exists { return nil } if err := ipt.Insert(table, chain, 1, args...); err != nil { return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err) } return nil }
for _, ipt := range i.getTables() { if err := divert(ipt, "filter", "INPUT"); err != nil { return err } if err := divert(ipt, "filter", "FORWARD"); err != nil { return err } }
for _, ipt := range i.getNATTables() { if err := divert(ipt, "nat", "POSTROUTING"); err != nil { return err } } return nil}
// AddChains creates custom Tailscale chains in netfilter via iptables// if the ts-chain doesn't already exist.func (i *iptablesRunner) AddChains() error { // create creates a chain in the given table if it doesn't already exist. // If the chain already exists, it is a no-op. create := func(ipt iptablesInterface, table, chain string) error { err := ipt.ClearChain(table, chain) if isNotExistError(err) { // nonexistent chain. let's create it! return ipt.NewChain(table, chain) } if err != nil { return fmt.Errorf("setting up %s/%s: %w", table, chain, err) } return nil }
for _, ipt := range i.getTables() { if err := create(ipt, "filter", "ts-input"); err != nil { return err } if err := create(ipt, "filter", "ts-forward"); err != nil { return err } }
for _, ipt := range i.getNATTables() { if err := create(ipt, "nat", "ts-postrouting"); err != nil { return err } }
return nil}
// AddBase adds some basic processing rules to be supplemented by// later calls to other helpers.func (i *iptablesRunner) AddBase(tunname string) error { if err := i.addBase4(tunname); err != nil { return err } if i.HasIPV6Filter() { if err := i.addBase6(tunname); err != nil { return err } } return nil}
// addBase4 adds some basic IPv4 processing rules to be// supplemented by later calls to other helpers.func (i *iptablesRunner) addBase4(tunname string) error { // Explicitly allow all inbound traffic to the tun interface args := []string{"-i", tunname, "-j", "ACCEPT"} if err := i.ipt4.Append("filter", "ts-input", args...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err) }
// Forward all traffic from the Tailscale interface, and drop // traffic to the tailscale interface by default. We use packet // marks here so both filter/FORWARD and nat/POSTROUTING can match // on these packets of interest. // // In particular, we only want to apply SNAT rules in // nat/POSTROUTING to packets that originated from the Tailscale // interface, but we can't match on the inbound interface in // POSTROUTING. So instead, we match on the inbound interface in // filter/FORWARD, and set a packet mark that nat/POSTROUTING can // use to effectively run that same test again. args = []string{"-i", tunname, "-j", "MARK", "--set-mark", subnetRouteMark + "/" + fwmarkMask} if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err) } args = []string{"-m", "mark", "--mark", subnetRouteMark + "/" + fwmarkMask, "-j", "ACCEPT"} if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err) } args = []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"} if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err) } args = []string{"-o", tunname, "-j", "ACCEPT"} if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err) }
return nil}
func (i *iptablesRunner) AddDNATRule(origDst, dst netip.Addr) error { table := i.getIPTByAddr(dst) return table.Insert("nat", "PREROUTING", 1, "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dst.String())}
// EnsureSNATForDst sets up firewall to ensure that all traffic aimed for dst, has its source ip set to src:// - creates a SNAT rule if not already present// - ensures that any no longer valid SNAT rules for the same dst are removedfunc (i *iptablesRunner) EnsureSNATForDst(src, dst netip.Addr) error { table := i.getIPTByAddr(dst) rules, err := table.List("nat", "POSTROUTING") if err != nil { return fmt.Errorf("error listing rules: %v", err) } // iptables accept either address or a CIDR value for the --destination flag, but converts an address to /32 // CIDR. Explicitly passing a /32 CIDR made it possible to test this rule. dstPrefix, err := dst.Prefix(32) if err != nil { return fmt.Errorf("error calculating prefix of dst %v: %v", dst, err) }
// wantsArgsPrefix is the prefix of the SNAT rule for the provided destination. // We should only have one POSTROUTING rule with this prefix. wantsArgsPrefix := fmt.Sprintf("-d %s -j SNAT --to-source", dstPrefix.String()) // wantsArgs is the actual SNAT rule that we want. wantsArgs := fmt.Sprintf("%s %s", wantsArgsPrefix, src.String()) for _, r := range rules { args := argsFromPostRoutingRule(r) if strings.HasPrefix(args, wantsArgsPrefix) { if strings.HasPrefix(args, wantsArgs) { return nil } // SNAT rule matching the destination, but for a different source - delete. if err := table.Delete("nat", "POSTROUTING", strings.Split(args, " ")...); err != nil { // If we failed to delete don't crash the node- the proxy should still be functioning. log.Printf("[unexpected] error deleting rule %s: %v, please report it.", r, err) } break } } return table.Insert("nat", "POSTROUTING", 1, "-d", dstPrefix.String(), "-j", "SNAT", "--to-source", src.String())}
func (i *iptablesRunner) DNATNonTailscaleTraffic(tun string, dst netip.Addr) error { table := i.getIPTByAddr(dst) return table.Insert("nat", "PREROUTING", 1, "!", "-i", tun, "-j", "DNAT", "--to-destination", dst.String())}
// DNATWithLoadBalancer adds iptables rules to forward all traffic received for// originDst to the backend dsts. Traffic will be load balanced using round robin.func (i *iptablesRunner) DNATWithLoadBalancer(origDst netip.Addr, dsts []netip.Addr) error { table := i.getIPTByAddr(dsts[0]) if err := table.ClearChain("nat", "PREROUTING"); err != nil && !isNotExistError(err) { // If clearing the PREROUTING chain fails, fail the whole operation. This // rule is currently only used in Kubernetes containers where a // failed container gets restarted which should hopefully fix things. return fmt.Errorf("error clearing nat PREROUTING chain: %w", err) } // If dsts contain more than one address, for n := n in range(len(dsts)..2) route packets for every nth connection to dsts[n]. for i := len(dsts); i >= 2; i-- { dst := dsts[i-1] // the order in which rules for addrs are installed does not matter if err := table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-m", "statistic", "--mode", "nth", "--every", fmt.Sprint(i), "--packet", "0", "-j", "DNAT", "--to-destination", dst.String()); err != nil { return fmt.Errorf("error adding DNAT rule for %s: %w", dst.String(), err) } } // If the packet falls through to this rule, we route to the first destination in the list unconditionally. return table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dsts[0].String())}
func (i *iptablesRunner) ClampMSSToPMTU(tun string, addr netip.Addr) error { table := i.getIPTByAddr(addr) // Clamp MSS on forwarded TCP handshakes in both directions: the SYN // leaving via tun towards the tailnet peer, and the SYN-ACK arriving on // tun and being forwarded back out towards the originating endpoint. A // single -o tun rule only clamps one side of the handshake, leaving the // endpoint on the other side advertising an MSS that is too large for the // tun MTU, which black-holes large segments when PMTU discovery is broken. if err := table.Append("mangle", "FORWARD", "-o", tun, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"); err != nil { return err } return table.Append("mangle", "FORWARD", "-i", tun, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")}
// addBase6 adds some basic IPv6 processing rules to be// supplemented by later calls to other helpers.func (i *iptablesRunner) addBase6(tunname string) error { // TODO: only allow traffic from Tailscale's ULA range to come // from tailscale0.
// Explicitly allow all other inbound traffic to the tun interface args := []string{"-i", tunname, "-j", "ACCEPT"} if err := i.ipt6.Append("filter", "ts-input", args...); err != nil { return fmt.Errorf("adding %v in v6/filter/ts-input: %w", args, err) }
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", subnetRouteMark + "/" + fwmarkMask} if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err) } args = []string{"-m", "mark", "--mark", subnetRouteMark + "/" + fwmarkMask, "-j", "ACCEPT"} if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err) } // TODO: drop forwarded traffic to tailscale0 from tailscale's ULA // (see corresponding IPv4 CGNAT rule). args = []string{"-o", tunname, "-j", "ACCEPT"} if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil { return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err) }
return nil}
// DelChains removes the custom Tailscale chains from netfilter via iptables.func (i *iptablesRunner) DelChains() error { for _, ipt := range i.getTables() { if err := delChain(ipt, "filter", "ts-input"); err != nil { return err } if err := delChain(ipt, "filter", "ts-forward"); err != nil { return err } }
for _, ipt := range i.getNATTables() { if err := delChain(ipt, "nat", "ts-postrouting"); err != nil { return err } }
return nil}
// DelBase empties but does not remove custom Tailscale chains from// netfilter via iptables.func (i *iptablesRunner) DelBase() error { del := func(ipt iptablesInterface, table, chain string) error { if err := ipt.ClearChain(table, chain); err != nil { if isNotExistError(err) { // nonexistent chain. That's fine, since it's // the desired state anyway. return nil } return fmt.Errorf("flushing %s/%s: %w", table, chain, err) } return nil }
for _, ipt := range i.getTables() { if err := del(ipt, "filter", "ts-input"); err != nil { return err } if err := del(ipt, "filter", "ts-forward"); err != nil { return err } } for _, ipt := range i.getNATTables() { if err := del(ipt, "nat", "ts-postrouting"); err != nil { return err } }
return nil}
// DelHooks deletes the calls to tailscale's netfilter chains// in the relevant main netfilter chains.func (i *iptablesRunner) DelHooks(logf logger.Logf) error { for _, ipt := range i.getTables() { if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil { return err } if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil { return err } } for _, ipt := range i.getNATTables() { if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil { return err } }
return nil}
// AddSNATRule adds a netfilter rule to SNAT traffic destined for// local subnets.func (i *iptablesRunner) AddSNATRule() error { args := []string{"-m", "mark", "--mark", subnetRouteMark + "/" + fwmarkMask, "-j", "MASQUERADE"} for _, ipt := range i.getNATTables() { if err := ipt.Append("nat", "ts-postrouting", args...); err != nil { return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err) } } return nil}
// DelSNATRule removes the netfilter rule to SNAT traffic destined for// local subnets. An error is returned if the rule does not exist.func (i *iptablesRunner) DelSNATRule() error { args := []string{"-m", "mark", "--mark", subnetRouteMark + "/" + fwmarkMask, "-j", "MASQUERADE"} for _, ipt := range i.getNATTables() { if err := ipt.Delete("nat", "ts-postrouting", args...); err != nil { return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err) } } return nil}
func statefulRuleArgs(tunname string) []string { return []string{"-o", tunname, "-m", "conntrack", "!", "--ctstate", "ESTABLISHED,RELATED", "-j", "DROP"}}
// AddStatefulRule adds a netfilter rule for stateful packet filtering using// conntrack.func (i *iptablesRunner) AddStatefulRule(tunname string) error { // Drop packets that are destined for the tailscale interface if // they're a new connection, per conntrack, to prevent hosts on the // same subnet from being able to use this device as a way to forward // packets on to the Tailscale network. // // The conntrack states are: // NEW A packet which creates a new connection. // ESTABLISHED A packet which belongs to an existing connection // (i.e., a reply packet, or outgoing packet on a // connection which has seen replies). // RELATED A packet which is related to, but not part of, an // existing connection, such as an ICMP error. // INVALID A packet which could not be identified for some // reason: this includes running out of memory and ICMP // errors which don't correspond to any known // connection. Generally these packets should be // dropped. // // We drop NEW packets to prevent connections from coming "into" // Tailscale from other hosts on the same network segment; we drop // INVALID packets as well. args := statefulRuleArgs(tunname) for _, ipt := range i.getTables() { // First, find the final "accept" rule. rules, err := ipt.List("filter", "ts-forward") if err != nil { return fmt.Errorf("listing rules in filter/ts-forward: %w", err) } want := fmt.Sprintf("-A %s -o %s -j ACCEPT", "ts-forward", tunname)
pos := slices.Index(rules, want) if pos < 0 { return fmt.Errorf("couldn't find final ACCEPT rule in filter/ts-forward") }
if err := ipt.Insert("filter", "ts-forward", pos, args...); err != nil { return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err) } } return nil}
// DelStatefulRule removes the netfilter rule for stateful packet filtering// using conntrack.func (i *iptablesRunner) DelStatefulRule(tunname string) error { args := statefulRuleArgs(tunname) for _, ipt := range i.getTables() { if err := ipt.Delete("filter", "ts-forward", args...); err != nil { return fmt.Errorf("deleting %v in filter/ts-forward: %w", args, err) } } return nil}
// AddConnmarkSaveRule adds conntrack marking rules to save and restore marks.// These rules run in mangle/PREROUTING (to restore marks from conntrack) and// mangle/OUTPUT (to save marks to conntrack) before rp_filter checks, enabling// proper routing table lookups for exit nodes and subnet routers.func (i *iptablesRunner) AddConnmarkSaveRule() error { // Check if rules already exist (idempotency) for _, ipt := range i.getTables() { rules, err := ipt.List("mangle", "PREROUTING") if err != nil { continue } // Look for existing connmark restore rule for _, rule := range rules { if strings.Contains(rule, "CONNMARK") && strings.Contains(rule, "restore-mark") && strings.Contains(rule, "ctmask 0xff0000") { // Rules already exist, skip adding return nil } } }
// mangle/PREROUTING: Restore mark from conntrack for ESTABLISHED/RELATED connections // This runs BEFORE routing decision and rp_filter check // The connmark check ensures we only restore when Tailscale has marked the connection, // preventing us from wiping mark bits set by other systems when ct mark is zero. for _, ipt := range i.getTables() { args := []string{ "-m", "conntrack", "--ctstate", "ESTABLISHED,RELATED", "-m", "connmark", "!", "--mark", "0x0/" + fwmarkMask, // Only restore if ct mark has Tailscale bits set "-j", "CONNMARK", "--restore-mark", "--nfmask", fwmarkMask, "--ctmask", fwmarkMask, } if err := ipt.Insert("mangle", "PREROUTING", 1, args...); err != nil { return fmt.Errorf("adding %v in mangle/PREROUTING: %w", args, err) } }
// mangle/OUTPUT: Save mark to conntrack for NEW connections with non-zero marks for _, ipt := range i.getTables() { args := []string{ "-m", "conntrack", "--ctstate", "NEW", "-m", "mark", "!", "--mark", "0x0/" + fwmarkMask, "-j", "CONNMARK", "--save-mark", "--nfmask", fwmarkMask, "--ctmask", fwmarkMask, } if err := ipt.Insert("mangle", "OUTPUT", 1, args...); err != nil { return fmt.Errorf("adding %v in mangle/OUTPUT: %w", args, err) } }
return nil}
// DelConnmarkSaveRule removes conntrack marking rules added by AddConnmarkSaveRule.func (i *iptablesRunner) DelConnmarkSaveRule() error { for _, ipt := range i.getTables() { // Delete PREROUTING rule args := []string{ "-m", "conntrack", "--ctstate", "ESTABLISHED,RELATED", "-m", "connmark", "!", "--mark", "0x0/" + fwmarkMask, "-j", "CONNMARK", "--restore-mark", "--nfmask", fwmarkMask, "--ctmask", fwmarkMask, } if err := ipt.Delete("mangle", "PREROUTING", args...); err != nil { if !isNotExistError(err) { return fmt.Errorf("deleting connmark rule in mangle/PREROUTING: %w", err) } // Rule doesn't exist - this is fine for idempotency }
// Delete OUTPUT rule args = []string{ "-m", "conntrack", "--ctstate", "NEW", "-m", "mark", "!", "--mark", "0x0/" + fwmarkMask, "-j", "CONNMARK", "--save-mark", "--nfmask", fwmarkMask, "--ctmask", fwmarkMask, } if err := ipt.Delete("mangle", "OUTPUT", args...); err != nil { if !isNotExistError(err) { return fmt.Errorf("deleting connmark rule in mangle/OUTPUT: %w", err) } // Rule doesn't exist - this is fine for idempotency } } return nil}
// buildMagicsockPortRule generates the string slice containing the arguments// to describe a rule accepting traffic on a particular port to iptables. It is// separated out here to avoid repetition in AddMagicsockPortRule and// RemoveMagicsockPortRule, since it is important that the same rule is passed// to Append() and Delete().func buildMagicsockPortRule(port uint16) []string { return []string{"-p", "udp", "--dport", strconv.FormatUint(uint64(port), 10), "-j", "ACCEPT"}}
// AddMagicsockPortRule adds a rule to iptables to allow incoming traffic on// the specified UDP port, so magicsock can accept incoming connections.// network must be either "udp4" or "udp6" - this determines whether the rule// is added for IPv4 or IPv6.func (i *iptablesRunner) AddMagicsockPortRule(port uint16, network string) error { var ipt iptablesInterface switch network { case "udp4": ipt = i.ipt4 case "udp6": ipt = i.ipt6 default: return fmt.Errorf("unsupported network %s", network) }
args := buildMagicsockPortRule(port)
if err := ipt.Append("filter", "ts-input", args...); err != nil { return fmt.Errorf("adding %v in filter/ts-input: %w", args, err) }
return nil}
// DelMagicsockPortRule removes a rule added by AddMagicsockPortRule to accept// incoming traffic on a particular UDP port.// network must be either "udp4" or "udp6" - this determines whether the rule// is removed for IPv4 or IPv6.func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error { var ipt iptablesInterface switch network { case "udp4": ipt = i.ipt4 case "udp6": ipt = i.ipt6 default: return fmt.Errorf("unsupported network %s", network) }
args := buildMagicsockPortRule(port)
if err := ipt.Delete("filter", "ts-input", args...); err != nil { return fmt.Errorf("removing %v in filter/ts-input: %w", args, err) }
return nil}
// buildExternalCGNATRules abstracts out logic for constructing firewall rules// for handling non-Tailscale CGNAT traffic, since these rules need to be// identical across [AddExternalCGNATRules] and [DelExternalCGNATRules].func buildExternalCGNATRules(mode CGNATMode, tunname string) ([][]string, error) { switch mode { case CGNATModeDrop: // Only allow CGNAT range traffic to come from the Tailscale interface. // There is an exception carved out for ranges used by ChromeOS, for // which we fall out of the Tailscale chain. return [][]string{ {"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}, {"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}, }, nil case CGNATModeReturn: // Fall out of the Tailscale chain for CGNAT traffic that doesn't // originate from the Tailscale interface. return [][]string{ {"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "RETURN"}, }, nil default: return nil, fmt.Errorf("unsupported mode %q", mode) }}
// AddExternalCGNATRules adds rules to the ts-input chain to deal with// traffic from the CGNAT range that arrives on non-Tailscale network// interfaces.func (i *iptablesRunner) AddExternalCGNATRules(mode CGNATMode, tunname string) error { rules, err := buildExternalCGNATRules(mode, tunname) if err != nil { return fmt.Errorf("build cgnat mode rule: %v", err) } for _, rule := range rules { if err := i.ipt4.Append("filter", "ts-input", rule...); err != nil { return fmt.Errorf("adding %v in v4/filter/ts-input: %w", rule, err) } } return nil}
// DelExternalCGNATRules removes the rules created by AddExternalCGNATRules,// if they exist.func (i *iptablesRunner) DelExternalCGNATRules(mode CGNATMode, tunname string) error { rules, err := buildExternalCGNATRules(mode, tunname) if err != nil { return fmt.Errorf("build cgnat mode rule: %v", err) } for _, rule := range rules { if found, err := i.ipt4.Exists("filter", "ts-input", rule...); err != nil { return fmt.Errorf("checking for %v in v4/filter/ts-input: %w", rule, err) } else if !found { // Don't need to delete a rule that isn't there. continue } if err := i.ipt4.Delete("filter", "ts-input", rule...); err != nil { return fmt.Errorf("deleting %v in v4/filter/ts-input: %w", rule, err) } } return nil}
// 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.func delTSHook(ipt iptablesInterface, table, chain string, logf logger.Logf) error { tsChain := tsChain(chain) args := []string{"-j", tsChain} if err := ipt.Delete(table, chain, args...); err != nil && !isNotExistError(err) { return fmt.Errorf("deleting %v in %s/%s: %v", args, table, chain, err) } return nil}
// delChain flushes and deletes a chain. If the chain does not exist, it's a no-op// since the desired state is already achieved. otherwise, it returns an error.func delChain(ipt iptablesInterface, table, chain string) error { if err := ipt.ClearChain(table, chain); err != nil { if isNotExistError(err) { // nonexistent chain. nothing to do. return nil } return fmt.Errorf("flushing %s/%s: %w", table, chain, err) } if err := ipt.DeleteChain(table, chain); err != nil { return fmt.Errorf("deleting %s/%s: %w", table, chain, err) } return nil}
// 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 { args, _ := strings.CutPrefix(r, "-A POSTROUTING ") return args}