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

Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/796/head
David Anderson 4 years ago committed by Dave Anderson
parent f0ef561049
commit 0d80904fc2

@ -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,58 +439,60 @@ func (r *linuxRouter) addIPRules() error {
rg := newRunGroup(nil, r.cmd) rg := newRunGroup(nil, r.cmd)
// NOTE(apenwarr): We leave spaces between each pref number. for _, family := range []string{"-4", "-6"} {
// This is so the sysadmin can override by inserting rules in // NOTE(apenwarr): We leave spaces between each pref number.
// between if they want. // This is so the sysadmin can override by inserting rules in
// between if they want.
// NOTE(apenwarr): This sequence seems complicated, right?
// If we could simply have a rule that said "match packets that // NOTE(apenwarr): This sequence seems complicated, right?
// *don't* have this fwmark", then we would only need to add one // If we could simply have a rule that said "match packets that
// link to table 52 and we'd be done. Unfortunately, older kernels // *don't* have this fwmark", then we would only need to add one
// and 'ip rule' implementations (including busybox), don't support // link to table 52 and we'd be done. Unfortunately, older kernels
// checking for the lack of a fwmark, only the presence. The technique // and 'ip rule' implementations (including busybox), don't support
// below works even on very old kernels. // checking for the lack of a fwmark, only the presence. The technique
// below works even on very old kernels.
// Packets from us, tagged with our fwmark, first try the kernel's
// main routing table. // Packets from us, tagged with our fwmark, first try the kernel's
rg.Run( // main routing table.
"ip", "rule", "add", rg.Run(
"pref", tailscaleRouteTable+"10", "ip", family, "rule", "add",
"fwmark", tailscaleBypassMark, "pref", tailscaleRouteTable+"10",
"table", "main", "fwmark", tailscaleBypassMark,
) "table", "main",
// ...and then we try the 'default' table, for correctness, )
// even though it's been empty on every Linux system I've ever seen. // ...and then we try the 'default' table, for correctness,
rg.Run( // even though it's been empty on every Linux system I've ever seen.
"ip", "rule", "add", rg.Run(
"pref", tailscaleRouteTable+"30", "ip", family, "rule", "add",
"fwmark", tailscaleBypassMark, "pref", tailscaleRouteTable+"30",
"table", "default", "fwmark", tailscaleBypassMark,
) "table", "default",
// If neither of those matched (no default route on this system?) )
// then packets from us should be aborted rather than falling through // If neither of those matched (no default route on this system?)
// to the tailscale routes, because that would create routing loops. // then packets from us should be aborted rather than falling through
rg.Run( // to the tailscale routes, because that would create routing loops.
"ip", "rule", "add", rg.Run(
"pref", tailscaleRouteTable+"50", "ip", family, "rule", "add",
"fwmark", tailscaleBypassMark, "pref", tailscaleRouteTable+"50",
"type", "unreachable", "fwmark", tailscaleBypassMark,
) "type", "unreachable",
// If we get to this point, capture all packets and send them )
// through to the tailscale route table. For apps other than us // If we get to this point, capture all packets and send them
// (ie. with no fwmark set), this is the first routing table, so // through to the tailscale route table. For apps other than us
// it takes precedence over all the others, ie. VPN routes always // (ie. with no fwmark set), this is the first routing table, so
// beat non-VPN routes. // it takes precedence over all the others, ie. VPN routes always
// // beat non-VPN routes.
// NOTE(apenwarr): tables >255 are not supported in busybox, so we //
// can't use a table number that aligns with the rule preferences. // NOTE(apenwarr): tables >255 are not supported in busybox, so we
rg.Run( // can't use a table number that aligns with the rule preferences.
"ip", "rule", "add", rg.Run(
"pref", tailscaleRouteTable+"70", "ip", family, "rule", "add",
"table", tailscaleRouteTable, "pref", tailscaleRouteTable+"70",
) "table", tailscaleRouteTable,
// If that didn't match, then non-fwmark packets fall through to the )
// usual rules (pref 32766 and 32767, ie. main and default). // If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default).
}
return rg.ErrAcc return rg.ErrAcc
} }
@ -505,73 +512,91 @@ 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)
// When deleting rules, we want to be a bit specific (mention which for _, family := range []string{"-4", "-6"} {
// table we were routing to) but not *too* specific (fwmarks, etc). // When deleting rules, we want to be a bit specific (mention which
// That leaves us some flexibility to change these values in later // table we were routing to) but not *too* specific (fwmarks, etc).
// versions without having ongoing hacks for every possible // That leaves us some flexibility to change these values in later
// combination. // versions without having ongoing hacks for every possible
// combination.
// Delete old-style tailscale rules
// (never released in a stable version, so we can drop this // Delete old-style tailscale rules
// support eventually). // (never released in a stable version, so we can drop this
rg.Run( // support eventually).
"ip", "rule", "del", rg.Run(
"pref", "10000", "ip", family, "rule", "del",
"table", "main", "pref", "10000",
) "table", "main",
)
// Delete new-style tailscale rules.
rg.Run( // Delete new-style tailscale rules.
"ip", "rule", "del", rg.Run(
"pref", tailscaleRouteTable+"10", "ip", family, "rule", "del",
"table", "main", "pref", tailscaleRouteTable+"10",
) "table", "main",
rg.Run( )
"ip", "rule", "del", rg.Run(
"pref", tailscaleRouteTable+"30", "ip", family, "rule", "del",
"table", "default", "pref", tailscaleRouteTable+"30",
) "table", "default",
rg.Run( )
"ip", "rule", "del", rg.Run(
"pref", tailscaleRouteTable+"50", "ip", family, "rule", "del",
"type", "unreachable", "pref", tailscaleRouteTable+"50",
) "type", "unreachable",
rg.Run( )
"ip", "rule", "del", rg.Run(
"pref", tailscaleRouteTable+"70", "ip", family, "rule", "del",
"table", tailscaleRouteTable, "pref", tailscaleRouteTable+"70",
) "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 {
return err for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
if err := create(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := create(ipt, "filter", "ts-forward"); err != nil {
return err
}
if err := create(ipt, "nat", "ts-postrouting"); err != nil {
return err
}
} }
if err := create("filter", "ts-forward"); err != nil { 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 return err
} }
if err := create("nat", "ts-postrouting"); err != nil { 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,14 +686,16 @@ 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} {
return err if err := del(ipt, "filter", "ts-input"); err != nil {
} return err
if err := del("filter", "ts-forward"); err != nil { }
return err if err := del(ipt, "filter", "ts-forward"); err != nil {
} return err
if err := del("nat", "ts-postrouting"); err != nil { }
return err if err := del(ipt, "nat", "ts-postrouting"); err != nil {
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,14 +716,16 @@ 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} {
return err if err := del(ipt, "filter", "ts-input"); err != nil {
} return err
if err := del("filter", "ts-forward"); err != nil { }
return err if err := del(ipt, "filter", "ts-forward"); err != nil {
} return err
if err := del("nat", "ts-postrouting"); err != nil { }
return err if err := del(ipt, "nat", "ts-postrouting"); err != nil {
return err
}
} }
return nil return nil
@ -682,31 +735,33 @@ 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} {
return err if err := divert(ipt, "filter", "INPUT"); err != nil {
} return err
if err := divert("filter", "FORWARD"); err != nil { }
return err if err := divert(ipt, "filter", "FORWARD"); err != nil {
} return err
if err := divert("nat", "POSTROUTING"); err != nil { }
return err if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
return err
}
} }
return nil return nil
} }
@ -714,10 +769,10 @@ func (r *linuxRouter) addNetfilterHooks() error {
// 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,14 +784,16 @@ func (r *linuxRouter) delNetfilterHooks() error {
return nil return nil
} }
if err := del("filter", "INPUT"); err != nil { for _, ipt := range []netfilterRunner{r.ipt4, r.ipt6} {
return err if err := del(ipt, "filter", "INPUT"); err != nil {
} return err
if err := del("filter", "FORWARD"); err != nil { }
return err if err := del(ipt, "filter", "FORWARD"); err != nil {
} return err
if err := del("nat", "POSTROUTING"); err != nil { }
return err if err := del(ipt, "nat", "POSTROUTING"); err != nil {
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
} }

@ -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. t *testing.T
type fakeOS struct { n map[string][]string
t *testing.T
up bool
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,67 +332,32 @@ func NewFakeOS(t *testing.T) *fakeOS {
} }
} }
var errExec = errors.New("execution failed") func (n *fakeNetfilter) Insert(table, chain string, pos int, args ...string) error {
func (o *fakeOS) String() string {
var b strings.Builder
if o.up {
b.WriteString("up\n")
} else {
b.WriteString("down\n")
}
for _, ip := range o.ips {
fmt.Fprintf(&b, "ip addr add %s\n", ip)
}
for _, route := range o.routes {
fmt.Fprintf(&b, "ip route add %s\n", route)
}
for _, rule := range o.rules {
fmt.Fprintf(&b, "ip rule add %s\n", rule)
}
var chains []string
for chain := range o.netfilter {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter[chain] {
fmt.Fprintf(&b, "%s %s\n", chain, rule)
}
}
return b.String()[:len(b.String())-1]
}
func (o *fakeOS) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok { if rules, ok := n.n[k]; ok {
if pos > len(rules)+1 { if pos > len(rules)+1 {
o.t.Errorf("bad position %d in %s", pos, k) n.t.Errorf("bad position %d in %s", pos, k)
return errExec return errExec
} }
rules = append(rules, "") rules = append(rules, "")
copy(rules[pos:], rules[pos-1:]) copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ") rules[pos-1] = strings.Join(args, " ")
o.netfilter[k] = rules n.n[k] = rules
} else { } else {
o.t.Errorf("unknown table/chain %s", k) n.t.Errorf("unknown table/chain %s", k)
return errExec return errExec
} }
return nil return nil
} }
func (o *fakeOS) Append(table, chain string, args ...string) error { func (n *fakeNetfilter) Append(table, chain string, args ...string) error {
k := table + "/" + chain k := table + "/" + chain
return o.Insert(table, chain, len(o.netfilter[k])+1, args...) return n.Insert(table, chain, len(n.n[k])+1, args...)
} }
func (o *fakeOS) Exists(table, chain string, args ...string) (bool, error) { func (n *fakeNetfilter) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok { if rules, ok := n.n[k]; ok {
for _, rule := range rules { for _, rule := range rules {
if rule == strings.Join(args, " ") { if rule == strings.Join(args, " ") {
return true, nil return true, nil
@ -368,75 +365,132 @@ func (o *fakeOS) Exists(table, chain string, args ...string) (bool, error) {
} }
return false, nil return false, nil
} else { } else {
o.t.Errorf("unknown table/chain %s", k) n.t.Errorf("unknown table/chain %s", k)
return false, errExec return false, errExec
} }
} }
func (o *fakeOS) Delete(table, chain string, args ...string) error { func (n *fakeNetfilter) Delete(table, chain string, args ...string) error {
k := table + "/" + chain k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok { if rules, ok := n.n[k]; ok {
for i, rule := range rules { for i, rule := range rules {
if rule == strings.Join(args, " ") { if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...) rules = append(rules[:i], rules[i+1:]...)
o.netfilter[k] = rules n.n[k] = rules
return nil return nil
} }
} }
o.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k) n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec return errExec
} else { } else {
o.t.Errorf("unknown table/chain %s", k) n.t.Errorf("unknown table/chain %s", k)
return errExec return errExec
} }
} }
func (o *fakeOS) ListChains(table string) (ret []string, err error) { func (n *fakeNetfilter) ClearChain(table, chain string) 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 k := table + "/" + chain
if _, ok := o.netfilter[k]; ok { if _, ok := n.n[k]; ok {
o.netfilter[k] = nil n.n[k] = nil
return nil return nil
} else { } else {
o.t.Logf("note: ClearChain: unknown table/chain %s", k) n.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1") return errors.New("exitcode:1")
} }
} }
func (o *fakeOS) NewChain(table, chain string) error { func (n *fakeNetfilter) NewChain(table, chain string) error {
k := table + "/" + chain k := table + "/" + chain
if _, ok := o.netfilter[k]; ok { if _, ok := n.n[k]; ok {
o.t.Errorf("table/chain %s already exists", k) n.t.Errorf("table/chain %s already exists", k)
return errExec return errExec
} }
o.netfilter[k] = nil n.n[k] = nil
return nil return nil
} }
func (o *fakeOS) DeleteChain(table, chain string) error { func (n *fakeNetfilter) DeleteChain(table, chain string) error {
k := table + "/" + chain k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok { if rules, ok := n.n[k]; ok {
if len(rules) != 0 { if len(rules) != 0 {
o.t.Errorf("%s is not empty", k) n.t.Errorf("%s is not empty", k)
return errExec return errExec
} }
delete(o.netfilter, k) delete(n.n, k)
return nil return nil
} else { } else {
o.t.Errorf("%s does not exist", k) n.t.Errorf("%s does not exist", k)
return errExec 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")
func (o *fakeOS) String() string {
var b strings.Builder
if o.up {
b.WriteString("up\n")
} else {
b.WriteString("down\n")
}
for _, ip := range o.ips {
fmt.Fprintf(&b, "ip addr add %s\n", ip)
}
for _, route := range o.routes {
fmt.Fprintf(&b, "ip route add %s\n", route)
}
for _, rule := range o.rules {
fmt.Fprintf(&b, "ip rule add %s\n", rule)
}
var chains []string
for chain := range o.netfilter4.n {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter4.n[chain] {
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]
}
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] {

@ -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

Loading…
Cancel
Save