wgengine/router: set up basic IPv6 routing/firewalling.

Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2020-09-22 00:49:44 +00:00 committed by Dave Anderson
parent f0ef561049
commit 0d80904fc2
3 changed files with 463 additions and 331 deletions

View File

@ -6,7 +6,6 @@ package router
import ( import (
"fmt" "fmt"
"os/exec"
"strings" "strings"
"github.com/coreos/go-iptables/iptables" "github.com/coreos/go-iptables/iptables"
@ -90,6 +89,7 @@ type linuxRouter struct {
dns *dns.Manager dns *dns.Manager
ipt4 netfilterRunner ipt4 netfilterRunner
ipt6 netfilterRunner
cmd commandRunner cmd commandRunner
} }
@ -104,12 +104,16 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (
return nil, err return nil, err
} }
return newUserspaceRouterAdvanced(logf, tunname, ipt4, osCommandRunner{}) ipt6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, err
}
return newUserspaceRouterAdvanced(logf, tunname, ipt4, ipt6, osCommandRunner{})
} }
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netfilterRunner, cmd commandRunner) (Router, error) { func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, netfilter6 netfilterRunner, cmd commandRunner) (Router, error) {
_, err := exec.Command("ip", "rule").Output() ipRuleAvailable := (cmd.run("ip", "rule") == nil)
ipRuleAvailable := (err == nil)
mconfig := dns.ManagerConfig{ mconfig := dns.ManagerConfig{
Logf: logf, Logf: logf,
@ -121,7 +125,8 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
ipRuleAvailable: ipRuleAvailable, ipRuleAvailable: ipRuleAvailable,
tunname: tunname, tunname: tunname,
netfilterMode: NetfilterOff, netfilterMode: NetfilterOff,
ipt4: netfilter, ipt4: netfilter4,
ipt6: netfilter6,
cmd: cmd, cmd: cmd,
dns: dns.NewManager(mconfig), dns: dns.NewManager(mconfig),
}, nil }, nil
@ -434,6 +439,7 @@ func (r *linuxRouter) addIPRules() error {
rg := newRunGroup(nil, r.cmd) rg := newRunGroup(nil, r.cmd)
for _, family := range []string{"-4", "-6"} {
// NOTE(apenwarr): We leave spaces between each pref number. // NOTE(apenwarr): We leave spaces between each pref number.
// This is so the sysadmin can override by inserting rules in // This is so the sysadmin can override by inserting rules in
// between if they want. // between if they want.
@ -449,7 +455,7 @@ func (r *linuxRouter) addIPRules() error {
// Packets from us, tagged with our fwmark, first try the kernel's // Packets from us, tagged with our fwmark, first try the kernel's
// main routing table. // main routing table.
rg.Run( rg.Run(
"ip", "rule", "add", "ip", family, "rule", "add",
"pref", tailscaleRouteTable+"10", "pref", tailscaleRouteTable+"10",
"fwmark", tailscaleBypassMark, "fwmark", tailscaleBypassMark,
"table", "main", "table", "main",
@ -457,7 +463,7 @@ func (r *linuxRouter) addIPRules() error {
// ...and then we try the 'default' table, for correctness, // ...and then we try the 'default' table, for correctness,
// even though it's been empty on every Linux system I've ever seen. // even though it's been empty on every Linux system I've ever seen.
rg.Run( rg.Run(
"ip", "rule", "add", "ip", family, "rule", "add",
"pref", tailscaleRouteTable+"30", "pref", tailscaleRouteTable+"30",
"fwmark", tailscaleBypassMark, "fwmark", tailscaleBypassMark,
"table", "default", "table", "default",
@ -466,7 +472,7 @@ func (r *linuxRouter) addIPRules() error {
// then packets from us should be aborted rather than falling through // then packets from us should be aborted rather than falling through
// to the tailscale routes, because that would create routing loops. // to the tailscale routes, because that would create routing loops.
rg.Run( rg.Run(
"ip", "rule", "add", "ip", family, "rule", "add",
"pref", tailscaleRouteTable+"50", "pref", tailscaleRouteTable+"50",
"fwmark", tailscaleBypassMark, "fwmark", tailscaleBypassMark,
"type", "unreachable", "type", "unreachable",
@ -480,12 +486,13 @@ func (r *linuxRouter) addIPRules() error {
// NOTE(apenwarr): tables >255 are not supported in busybox, so we // NOTE(apenwarr): tables >255 are not supported in busybox, so we
// can't use a table number that aligns with the rule preferences. // can't use a table number that aligns with the rule preferences.
rg.Run( rg.Run(
"ip", "rule", "add", "ip", family, "rule", "add",
"pref", tailscaleRouteTable+"70", "pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable, "table", tailscaleRouteTable,
) )
// If that didn't match, then non-fwmark packets fall through to the // If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default). // usual rules (pref 32766 and 32767, ie. main and default).
}
return rg.ErrAcc return rg.ErrAcc
} }
@ -505,6 +512,7 @@ func (r *linuxRouter) delIPRules() error {
// unknown rules during deletion. // unknown rules during deletion.
rg := newRunGroup([]int{2, 254}, r.cmd) rg := newRunGroup([]int{2, 254}, r.cmd)
for _, family := range []string{"-4", "-6"} {
// When deleting rules, we want to be a bit specific (mention which // When deleting rules, we want to be a bit specific (mention which
// table we were routing to) but not *too* specific (fwmarks, etc). // table we were routing to) but not *too* specific (fwmarks, etc).
// That leaves us some flexibility to change these values in later // That leaves us some flexibility to change these values in later
@ -515,63 +523,80 @@ func (r *linuxRouter) delIPRules() error {
// (never released in a stable version, so we can drop this // (never released in a stable version, so we can drop this
// support eventually). // support eventually).
rg.Run( rg.Run(
"ip", "rule", "del", "ip", family, "rule", "del",
"pref", "10000", "pref", "10000",
"table", "main", "table", "main",
) )
// Delete new-style tailscale rules. // Delete new-style tailscale rules.
rg.Run( rg.Run(
"ip", "rule", "del", "ip", family, "rule", "del",
"pref", tailscaleRouteTable+"10", "pref", tailscaleRouteTable+"10",
"table", "main", "table", "main",
) )
rg.Run( rg.Run(
"ip", "rule", "del", "ip", family, "rule", "del",
"pref", tailscaleRouteTable+"30", "pref", tailscaleRouteTable+"30",
"table", "default", "table", "default",
) )
rg.Run( rg.Run(
"ip", "rule", "del", "ip", family, "rule", "del",
"pref", tailscaleRouteTable+"50", "pref", tailscaleRouteTable+"50",
"type", "unreachable", "type", "unreachable",
) )
rg.Run( rg.Run(
"ip", "rule", "del", "ip", family, "rule", "del",
"pref", tailscaleRouteTable+"70", "pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable, "table", tailscaleRouteTable,
) )
}
return rg.ErrAcc return rg.ErrAcc
} }
// addNetfilterChains creates custom Tailscale chains in netfilter. // addNetfilterChains creates custom Tailscale chains in netfilter.
func (r *linuxRouter) addNetfilterChains() error { func (r *linuxRouter) addNetfilterChains() error {
create := func(table, chain string) error { create := func(ipt netfilterRunner, table, chain string) error {
err := r.ipt4.ClearChain(table, chain) err := ipt.ClearChain(table, chain)
if errCode(err) == 1 { if errCode(err) == 1 {
// nonexistent chain. let's create it! // nonexistent chain. let's create it!
return r.ipt4.NewChain(table, chain) return ipt.NewChain(table, chain)
} }
if err != nil { if err != nil {
return fmt.Errorf("setting up %s/%s: %w", table, chain, err) return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
} }
return nil return nil
} }
if err := create("filter", "ts-input"); err != nil {
for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := create(ipt, "filter", "ts-input"); err != nil {
return err return err
} }
if err := create("filter", "ts-forward"); err != nil { if err := create(ipt, "filter", "ts-forward"); err != nil {
return err return err
} }
if err := create("nat", "ts-postrouting"); err != nil { if err := create(ipt, "nat", "ts-postrouting"); err != nil {
return err
}
}
return nil
}
// addNetfilterBase adds some basic processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase() error {
if err := r.addNetfilterBase4(); err != nil {
return err
}
if err := r.addNetfilterBase6(); err != nil {
return err return err
} }
return nil return nil
} }
// addNetfilterBase adds with some basic processing rules to be supplemented // addNetfilterBase4 adds some basic IPv4 processing rules to be
// by later calls to other helpers. // supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase() error { func (r *linuxRouter) addNetfilterBase4() error {
// Only allow CGNAT range traffic to come from tailscale0. There // Only allow CGNAT range traffic to come from tailscale0. There
// is an exception carved out for ranges used by ChromeOS, for // is an exception carved out for ranges used by ChromeOS, for
// which we fall out of the Tailscale chain. // which we fall out of the Tailscale chain.
@ -580,11 +605,11 @@ func (r *linuxRouter) addNetfilterBase() error {
// CGNAT range for other purposes :(. // CGNAT range for other purposes :(.
args := []string{"!", "-i", r.tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"} args := []string{"!", "-i", r.tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil { if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
} }
args = []string{"!", "-i", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"} args = []string{"!", "-i", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil { if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
} }
// Forward all traffic from the Tailscale interface, and drop // Forward all traffic from the Tailscale interface, and drop
@ -600,19 +625,43 @@ func (r *linuxRouter) addNetfilterBase() error {
// use to effectively run that same test again. // use to effectively run that same test again.
args = []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark} args = []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil { if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
} }
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"} args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil { if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
} }
args = []string{"-o", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"} args = []string{"-o", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil { if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
} }
args = []string{"-o", r.tunname, "-j", "ACCEPT"} args = []string{"-o", r.tunname, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil { if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err) return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
return nil
}
// addNetfilterBase4 adds some basic IPv6 processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase6() error {
// TODO: only allow traffic from Tailscale's ULA range to come
// from tailscale0.
args := []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.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", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.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", r.tunname, "-j", "ACCEPT"}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
} }
return nil return nil
@ -620,8 +669,8 @@ func (r *linuxRouter) addNetfilterBase() error {
// delNetfilterChains removes the custom Tailscale chains from netfilter. // delNetfilterChains removes the custom Tailscale chains from netfilter.
func (r *linuxRouter) delNetfilterChains() error { func (r *linuxRouter) delNetfilterChains() error {
del := func(table, chain string) error { del := func(ipt netfilterRunner, table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil { if err := ipt.ClearChain(table, chain); err != nil {
if errCode(err) == 1 { if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's // nonexistent chain. That's fine, since it's
// the desired state anyway. // the desired state anyway.
@ -629,7 +678,7 @@ func (r *linuxRouter) delNetfilterChains() error {
} }
return fmt.Errorf("flushing %s/%s: %w", table, chain, err) return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
} }
if err := r.ipt4.DeleteChain(table, chain); err != nil { if err := ipt.DeleteChain(table, chain); err != nil {
// this shouldn't fail, because if the chain didn't // this shouldn't fail, because if the chain didn't
// exist, we would have returned after ClearChain. // exist, we would have returned after ClearChain.
return fmt.Errorf("deleting %s/%s: %v", table, chain, err) return fmt.Errorf("deleting %s/%s: %v", table, chain, err)
@ -637,15 +686,17 @@ func (r *linuxRouter) delNetfilterChains() error {
return nil return nil
} }
if err := del("filter", "ts-input"); err != nil { for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err return err
} }
if err := del("filter", "ts-forward"); err != nil { if err := del(ipt, "filter", "ts-forward"); err != nil {
return err return err
} }
if err := del("nat", "ts-postrouting"); err != nil { if err := del(ipt, "nat", "ts-postrouting"); err != nil {
return err return err
} }
}
return nil return nil
} }
@ -653,8 +704,8 @@ func (r *linuxRouter) delNetfilterChains() error {
// delNetfilterBase empties but does not remove custom Tailscale chains from // delNetfilterBase empties but does not remove custom Tailscale chains from
// netfilter. // netfilter.
func (r *linuxRouter) delNetfilterBase() error { func (r *linuxRouter) delNetfilterBase() error {
del := func(table, chain string) error { del := func(ipt netfilterRunner, table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil { if err := ipt.ClearChain(table, chain); err != nil {
if errCode(err) == 1 { if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's // nonexistent chain. That's fine, since it's
// the desired state anyway. // the desired state anyway.
@ -665,15 +716,17 @@ func (r *linuxRouter) delNetfilterBase() error {
return nil return nil
} }
if err := del("filter", "ts-input"); err != nil { for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err return err
} }
if err := del("filter", "ts-forward"); err != nil { if err := del(ipt, "filter", "ts-forward"); err != nil {
return err return err
} }
if err := del("nat", "ts-postrouting"); err != nil { if err := del(ipt, "nat", "ts-postrouting"); err != nil {
return err return err
} }
}
return nil return nil
} }
@ -682,42 +735,44 @@ func (r *linuxRouter) delNetfilterBase() error {
// the relevant main netfilter chains. The tailscale chains must // the relevant main netfilter chains. The tailscale chains must
// already exist. // already exist.
func (r *linuxRouter) addNetfilterHooks() error { func (r *linuxRouter) addNetfilterHooks() error {
divert := func(table, chain string) error { divert := func(ipt netfilterRunner, table, chain string) error {
tsChain := tsChain(chain) tsChain := tsChain(chain)
args := []string{"-j", tsChain} args := []string{"-j", tsChain}
exists, err := r.ipt4.Exists(table, chain, args...) exists, err := ipt.Exists(table, chain, args...)
if err != nil { if err != nil {
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err) return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
} }
if exists { if exists {
return nil return nil
} }
if err := r.ipt4.Insert(table, chain, 1, args...); err != 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 fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
} }
return nil return nil
} }
if err := divert("filter", "INPUT"); err != nil { for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := divert(ipt, "filter", "INPUT"); err != nil {
return err return err
} }
if err := divert("filter", "FORWARD"); err != nil { if err := divert(ipt, "filter", "FORWARD"); err != nil {
return err return err
} }
if err := divert("nat", "POSTROUTING"); err != nil { if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
return err return err
} }
}
return nil return nil
} }
// delNetfilterHooks deletes the calls to tailscale's netfilter chains // delNetfilterHooks deletes the calls to tailscale's netfilter chains
// in the relevant main netfilter chains. // in the relevant main netfilter chains.
func (r *linuxRouter) delNetfilterHooks() error { func (r *linuxRouter) delNetfilterHooks() error {
del := func(table, chain string) error { del := func(ipt netfilterRunner, table, chain string) error {
tsChain := tsChain(chain) tsChain := tsChain(chain)
args := []string{"-j", tsChain} args := []string{"-j", tsChain}
if err := r.ipt4.Delete(table, chain, args...); err != nil { if err := ipt.Delete(table, chain, args...); err != nil {
// TODO(apenwarr): check for errCode(1) here. // TODO(apenwarr): check for errCode(1) here.
// Unfortunately the error code from the iptables // Unfortunately the error code from the iptables
// module resists unwrapping, unlike with other // module resists unwrapping, unlike with other
@ -729,15 +784,17 @@ func (r *linuxRouter) delNetfilterHooks() error {
return nil return nil
} }
if err := del("filter", "INPUT"); err != nil { for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := del(ipt, "filter", "INPUT"); err != nil {
return err return err
} }
if err := del("filter", "FORWARD"); err != nil { if err := del(ipt, "filter", "FORWARD"); err != nil {
return err return err
} }
if err := del("nat", "POSTROUTING"); err != nil { if err := del(ipt, "nat", "POSTROUTING"); err != nil {
return err return err
} }
}
return nil return nil
} }
@ -750,7 +807,10 @@ func (r *linuxRouter) addSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"} args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil { if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err) return fmt.Errorf("adding %v in v4/nat/ts-postrouting: %w", args, err)
}
if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err)
} }
return nil return nil
} }
@ -764,7 +824,10 @@ func (r *linuxRouter) delSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"} args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil { if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err) return fmt.Errorf("deleting %v in v4/nat/ts-postrouting: %w", args, err)
}
if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err)
} }
return nil return nil
} }

View File

@ -34,10 +34,14 @@ func mustCIDRs(ss ...string) []netaddr.IPPrefix {
func TestRouterStates(t *testing.T) { func TestRouterStates(t *testing.T) {
basic := ` basic := `
ip rule add pref 5210 fwmark 0x80000 table main ip rule add -4 pref 5210 fwmark 0x80000 table main
ip rule add pref 5230 fwmark 0x80000 table default ip rule add -4 pref 5230 fwmark 0x80000 table default
ip rule add pref 5250 fwmark 0x80000 type unreachable ip rule add -4 pref 5250 fwmark 0x80000 type unreachable
ip rule add pref 5270 table 52 ip rule add -4 pref 5270 table 52
ip rule add -6 pref 5210 fwmark 0x80000 table main
ip rule add -6 pref 5230 fwmark 0x80000 table default
ip rule add -6 pref 5250 fwmark 0x80000 type unreachable
ip rule add -6 pref 5270 table 52
` `
states := []struct { states := []struct {
name string name string
@ -104,17 +108,24 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward `v4/filter/FORWARD -j ts-forward
filter/INPUT -j ts-input v4/filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting v4/nat/POSTROUTING -j ts-postrouting
nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE v4/nat/ts-postrouting -m mark --mark 0x40000 -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
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
v6/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
`, `,
}, },
{ {
@ -129,16 +140,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward `v4/filter/FORWARD -j ts-forward
filter/INPUT -j ts-input v4/filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`, `,
}, },
@ -156,16 +173,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward `v4/filter/FORWARD -j ts-forward
filter/INPUT -j ts-input v4/filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`, `,
}, },
{ {
@ -180,16 +203,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward `v4/filter/FORWARD -j ts-forward
filter/INPUT -j ts-input v4/filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`, `,
}, },
@ -205,13 +234,16 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 `v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
`, `,
}, },
{ {
@ -226,22 +258,28 @@ up
ip addr add 100.101.102.104/10 dev tailscale0 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 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic + ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward `v4/filter/FORWARD -j ts-forward
filter/INPUT -j ts-input v4/filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000 v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT v4/filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`, `,
}, },
} }
fake := NewFakeOS(t) fake := NewFakeOS(t)
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake, fake) router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake.netfilter4, fake.netfilter6, fake)
if err != nil { if err != nil {
t.Fatalf("failed to create router: %v", err) t.Fatalf("failed to create router: %v", err)
} }
@ -275,21 +313,15 @@ nat/POSTROUTING -j ts-postrouting
} }
} }
// fakeOS implements netfilterRunner and commandRunner, but captures type fakeNetfilter struct {
// changes without touching the OS.
type fakeOS struct {
t *testing.T t *testing.T
up bool n map[string][]string
ips []string
routes []string
rules []string
netfilter map[string][]string
} }
func NewFakeOS(t *testing.T) *fakeOS { func newNetfilter(t *testing.T) *fakeNetfilter {
return &fakeOS{ return &fakeNetfilter{
t: t, t: t,
netfilter: map[string][]string{ n: map[string][]string{
"filter/INPUT": nil, "filter/INPUT": nil,
"filter/OUTPUT": nil, "filter/OUTPUT": nil,
"filter/FORWARD": nil, "filter/FORWARD": nil,
@ -300,6 +332,118 @@ func NewFakeOS(t *testing.T) *fakeOS {
} }
} }
func (n *fakeNetfilter) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if pos > len(rules)+1 {
n.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
n.n[k] = rules
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (n *fakeNetfilter) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return n.Insert(table, chain, len(n.n[k])+1, args...)
}
func (n *fakeNetfilter) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
n.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (n *fakeNetfilter) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
n.n[k] = rules
return nil
}
}
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (n *fakeNetfilter) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.n[k] = nil
return nil
} else {
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (n *fakeNetfilter) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.t.Errorf("table/chain %s already exists", k)
return errExec
}
n.n[k] = nil
return nil
}
func (n *fakeNetfilter) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if len(rules) != 0 {
n.t.Errorf("%s is not empty", k)
return errExec
}
delete(n.n, k)
return nil
} else {
n.t.Errorf("%s does not exist", k)
return errExec
}
}
// fakeOS implements commandRunner and provides v4 and v6
// netfilterRunners, but captures changes without touching the OS.
type fakeOS struct {
t *testing.T
up bool
ips []string
routes []string
rules []string
netfilter4 *fakeNetfilter
netfilter6 *fakeNetfilter
}
func NewFakeOS(t *testing.T) *fakeOS {
return &fakeOS{
t: t,
netfilter4: newNetfilter(t),
netfilter6: newNetfilter(t),
}
}
var errExec = errors.New("execution failed") var errExec = errors.New("execution failed")
func (o *fakeOS) String() string { func (o *fakeOS) String() string {
@ -323,120 +467,30 @@ func (o *fakeOS) String() string {
} }
var chains []string var chains []string
for chain := range o.netfilter { for chain := range o.netfilter4.n {
chains = append(chains, chain) chains = append(chains, chain)
} }
sort.Strings(chains) sort.Strings(chains)
for _, chain := range chains { for _, chain := range chains {
for _, rule := range o.netfilter[chain] { for _, rule := range o.netfilter4.n[chain] {
fmt.Fprintf(&b, "%s %s\n", chain, rule) fmt.Fprintf(&b, "v4/%s %s\n", chain, rule)
} }
} }
chains = nil
for chain := range o.netfilter6.n {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter6.n[chain] {
fmt.Fprintf(&b, "v6/%s %s\n", chain, rule)
}
}
return b.String()[:len(b.String())-1] return b.String()[:len(b.String())-1]
} }
func (o *fakeOS) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if pos > len(rules)+1 {
o.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
o.netfilter[k] = rules
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (o *fakeOS) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return o.Insert(table, chain, len(o.netfilter[k])+1, args...)
}
func (o *fakeOS) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
o.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (o *fakeOS) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
o.netfilter[k] = rules
return nil
}
}
o.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (o *fakeOS) ListChains(table string) (ret []string, err error) {
for chain := range o.netfilter {
pfx := table + "/"
if strings.HasPrefix(chain, pfx) {
ret = append(ret, chain[len(pfx):])
}
}
return ret, nil
}
func (o *fakeOS) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.netfilter[k] = nil
return nil
} else {
o.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (o *fakeOS) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.t.Errorf("table/chain %s already exists", k)
return errExec
}
o.netfilter[k] = nil
return nil
}
func (o *fakeOS) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if len(rules) != 0 {
o.t.Errorf("%s is not empty", k)
return errExec
}
delete(o.netfilter, k)
return nil
} else {
o.t.Errorf("%s does not exist", k)
return errExec
}
}
func (o *fakeOS) run(args ...string) error { func (o *fakeOS) run(args ...string) error {
unexpected := func() error { unexpected := func() error {
o.t.Errorf("unexpected invocation %q", strings.Join(args, " ")) o.t.Errorf("unexpected invocation %q", strings.Join(args, " "))
@ -446,7 +500,20 @@ func (o *fakeOS) run(args ...string) error {
return unexpected() return unexpected()
} }
if len(args) == 2 && args[1] == "rule" {
// naked invocation of `ip rule` is a feature test. Return
// successfully.
return nil
}
family := ""
rest := strings.Join(args[3:], " ") rest := strings.Join(args[3:], " ")
if args[1] == "-4" || args[1] == "-6" {
family = args[1]
copy(args[1:], args[2:])
args = args[:len(args)-1]
rest = family + " " + strings.Join(args[3:], " ")
}
var l *[]string var l *[]string
switch args[1] { switch args[1] {

View File

@ -24,6 +24,8 @@ type commandRunner interface {
type osCommandRunner struct{} type osCommandRunner struct{}
// errCode extracts and returns the process exit code from err, or
// zero if err is nil.
func errCode(err error) int { func errCode(err error) int {
if err == nil { if err == nil {
return 0 return 0