various: implement stateful firewalling on Linux (#12025)

Updates https://github.com/tailscale/corp/issues/19623


Change-Id: I7980e1fb736e234e66fa000d488066466c96ec85

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
This commit is contained in:
Andrew Lytvynov
2024-05-06 15:22:17 -07:00
committed by GitHub
parent 5ef178fdca
commit c28f5767bf
17 changed files with 632 additions and 47 deletions

View File

@@ -12,6 +12,7 @@ import (
"net/netip"
"os"
"os/exec"
"slices"
"strconv"
"strings"
@@ -36,6 +37,7 @@ type iptablesInterface interface {
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
@@ -530,6 +532,67 @@ func (i *iptablesRunner) DelSNATRule() error {
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
}
// 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