mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-14 23:17:29 +00:00
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:
@@ -88,9 +88,10 @@ type Config struct {
|
||||
SubnetRoutes []netip.Prefix
|
||||
|
||||
// Linux-only things below, ignored on other platforms.
|
||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
|
||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
|
||||
}
|
||||
|
||||
func (a *Config) Equal(b *Config) bool {
|
||||
|
@@ -38,17 +38,18 @@ const (
|
||||
)
|
||||
|
||||
type linuxRouter struct {
|
||||
closed atomic.Bool
|
||||
logf func(fmt string, args ...any)
|
||||
tunname string
|
||||
netMon *netmon.Monitor
|
||||
unregNetMon func()
|
||||
addrs map[netip.Prefix]bool
|
||||
routes map[netip.Prefix]bool
|
||||
localRoutes map[netip.Prefix]bool
|
||||
snatSubnetRoutes bool
|
||||
netfilterMode preftype.NetfilterMode
|
||||
netfilterKind string
|
||||
closed atomic.Bool
|
||||
logf func(fmt string, args ...any)
|
||||
tunname string
|
||||
netMon *netmon.Monitor
|
||||
unregNetMon func()
|
||||
addrs map[netip.Prefix]bool
|
||||
routes map[netip.Prefix]bool
|
||||
localRoutes map[netip.Prefix]bool
|
||||
snatSubnetRoutes bool
|
||||
statefulFiltering bool
|
||||
netfilterMode preftype.NetfilterMode
|
||||
netfilterKind string
|
||||
|
||||
// ruleRestorePending is whether a timer has been started to
|
||||
// restore deleted ip rules.
|
||||
@@ -390,6 +391,7 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
||||
}
|
||||
r.addrs = newAddrs
|
||||
|
||||
// Ensure that the SNAT rule is added or removed as needed.
|
||||
switch {
|
||||
case cfg.SNATSubnetRoutes == r.snatSubnetRoutes:
|
||||
// state already correct, nothing to do.
|
||||
@@ -404,6 +406,21 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
||||
}
|
||||
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
|
||||
|
||||
// As above, for stateful filtering
|
||||
switch {
|
||||
case cfg.StatefulFiltering == r.statefulFiltering:
|
||||
// state already correct, nothing to do.
|
||||
case cfg.StatefulFiltering:
|
||||
if err := r.addStatefulRule(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
default:
|
||||
if err := r.delStatefulRule(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
r.statefulFiltering = cfg.StatefulFiltering
|
||||
|
||||
// Issue 11405: enable IP forwarding on gokrazy.
|
||||
advertisingRoutes := len(cfg.SubnetRoutes) > 0
|
||||
if distro.Get() == distro.Gokrazy && advertisingRoutes {
|
||||
@@ -1327,6 +1344,26 @@ func (r *linuxRouter) delSNATRule() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// addStatefulRule adds a netfilter rule to perform stateful filtering from
|
||||
// subnets onto the tailnet.
|
||||
func (r *linuxRouter) addStatefulRule() error {
|
||||
if r.netfilterMode == netfilterOff {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.nfr.AddStatefulRule(r.tunname)
|
||||
}
|
||||
|
||||
// delStatefulRule removes the netfilter rule to perform stateful filtering
|
||||
// from subnets onto the tailnet.
|
||||
func (r *linuxRouter) delStatefulRule() error {
|
||||
if r.netfilterMode == netfilterOff {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.nfr.DelStatefulRule(r.tunname)
|
||||
}
|
||||
|
||||
// cidrDiff calls add and del as needed to make the set of prefixes in
|
||||
// old and new match. Returns a map reflecting the actual new state
|
||||
// (which may be somewhere in between old and new if some commands
|
||||
|
@@ -94,11 +94,49 @@ ip route add 192.168.16.0/24 dev tailscale0 table 52` + basic,
|
||||
{
|
||||
name: "addr and routes and subnet routes with netfilter",
|
||||
in: &Config{
|
||||
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
||||
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
||||
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
||||
SNATSubnetRoutes: true,
|
||||
NetfilterMode: netfilterOn,
|
||||
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
||||
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
||||
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
||||
SNATSubnetRoutes: true,
|
||||
StatefulFiltering: true,
|
||||
NetfilterMode: netfilterOn,
|
||||
},
|
||||
want: `
|
||||
up
|
||||
ip addr add 100.101.102.104/10 dev tailscale0
|
||||
ip route add 10.0.0.0/8 dev tailscale0 table 52
|
||||
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
|
||||
`v4/filter/FORWARD -j ts-forward
|
||||
v4/filter/INPUT -j ts-input
|
||||
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000/0xff0000
|
||||
v4/filter/ts-forward -m mark --mark 0x40000/0xff0000 -j ACCEPT
|
||||
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
||||
v4/filter/ts-forward -o tailscale0 -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP
|
||||
v4/filter/ts-forward -o tailscale0 -j ACCEPT
|
||||
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
|
||||
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
|
||||
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
||||
v4/nat/POSTROUTING -j ts-postrouting
|
||||
v4/nat/ts-postrouting -m mark --mark 0x40000/0xff0000 -j MASQUERADE
|
||||
v6/filter/FORWARD -j ts-forward
|
||||
v6/filter/INPUT -j ts-input
|
||||
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000/0xff0000
|
||||
v6/filter/ts-forward -m mark --mark 0x40000/0xff0000 -j ACCEPT
|
||||
v6/filter/ts-forward -o tailscale0 -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP
|
||||
v6/filter/ts-forward -o tailscale0 -j ACCEPT
|
||||
v6/nat/POSTROUTING -j ts-postrouting
|
||||
v6/nat/ts-postrouting -m mark --mark 0x40000/0xff0000 -j MASQUERADE
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "addr and routes and subnet routes with netfilter but no stateful filtering",
|
||||
in: &Config{
|
||||
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
||||
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
||||
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
||||
SNATSubnetRoutes: true,
|
||||
StatefulFiltering: false,
|
||||
NetfilterMode: netfilterOn,
|
||||
},
|
||||
want: `
|
||||
up
|
||||
@@ -411,6 +449,22 @@ func insertRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRul
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRuleAt(n *fakeIPTablesRunner, curIPT map[string][]string, chain string, pos int, newRule string) {
|
||||
rules, ok := curIPT[chain]
|
||||
if !ok {
|
||||
n.t.Fatalf("no %s chain exists", chain)
|
||||
}
|
||||
|
||||
// If the given position is after the end of the chain, error.
|
||||
if pos > len(rules) {
|
||||
n.t.Fatalf("position %d > len(chain %s) %d", pos, chain, len(chain))
|
||||
}
|
||||
|
||||
// Insert the rule at the given position
|
||||
rules = slices.Insert(rules, pos, newRule)
|
||||
curIPT[chain] = rules
|
||||
}
|
||||
|
||||
func appendRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRule string) error {
|
||||
// Get current rules for filter/ts-input chain with according IP version
|
||||
curTSInputRules, ok := curIPT[chain]
|
||||
@@ -611,6 +665,33 @@ func (n *fakeIPTablesRunner) DelSNATRule() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddStatefulRule(tunname string) error {
|
||||
newRule := fmt.Sprintf("-o %s -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP", tunname)
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
// Mimic the real runner and insert after the 'accept all' rule
|
||||
wantRule := fmt.Sprintf("-o %s -j ACCEPT", tunname)
|
||||
|
||||
const chain = "filter/ts-forward"
|
||||
pos := slices.Index(ipt[chain], wantRule)
|
||||
if pos < 0 {
|
||||
n.t.Fatalf("no rule %q in chain %s", wantRule, chain)
|
||||
}
|
||||
|
||||
insertRuleAt(n, ipt, chain, pos, newRule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelStatefulRule(tunname string) error {
|
||||
delRule := fmt.Sprintf("-o %s -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP", tunname)
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
if err := deleteRule(n, ipt, "filter/ts-forward", delRule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMagicsockPortRule builds a fake rule to use in AddMagicsockPortRule and
|
||||
// DelMagicsockPortRule below.
|
||||
func buildMagicsockPortRule(port uint16) string {
|
||||
|
@@ -23,8 +23,8 @@ func mustCIDRs(ss ...string) []netip.Prefix {
|
||||
func TestConfigEqual(t *testing.T) {
|
||||
testedFields := []string{
|
||||
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
||||
"SubnetRoutes", "SNATSubnetRoutes", "NetfilterMode",
|
||||
"NetfilterKind",
|
||||
"SubnetRoutes", "SNATSubnetRoutes", "StatefulFiltering",
|
||||
"NetfilterMode", "NetfilterKind",
|
||||
}
|
||||
configType := reflect.TypeFor[Config]()
|
||||
configFields := []string{}
|
||||
@@ -125,6 +125,16 @@ func TestConfigEqual(t *testing.T) {
|
||||
&Config{SNATSubnetRoutes: false},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Config{StatefulFiltering: false},
|
||||
&Config{StatefulFiltering: true},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{StatefulFiltering: false},
|
||||
&Config{StatefulFiltering: false},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{NetfilterMode: preftype.NetfilterOff},
|
||||
|
Reference in New Issue
Block a user