mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
08a38f21c9
We have a filter in tailscaled itself now, which is more robust against weird network topologies (such as the one Docker creates). Signed-off-by: David Anderson <danderson@tailscale.com>
531 lines
14 KiB
Go
531 lines
14 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package router
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"inet.af/netaddr"
|
|
)
|
|
|
|
func mustCIDR(s string) netaddr.IPPrefix {
|
|
pfx, err := netaddr.ParseIPPrefix(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return pfx
|
|
}
|
|
|
|
func mustCIDRs(ss ...string) []netaddr.IPPrefix {
|
|
var ret []netaddr.IPPrefix
|
|
for _, s := range ss {
|
|
ret = append(ret, mustCIDR(s))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func TestRouterStates(t *testing.T) {
|
|
basic := `
|
|
ip rule add pref 8810 fwmark 0x20000 table main
|
|
ip rule add pref 8830 fwmark 0x20000 table default
|
|
ip rule add pref 8850 fwmark 0x20000 type unreachable
|
|
ip rule add pref 8888 table 88
|
|
`
|
|
states := []struct {
|
|
name string
|
|
in *Config
|
|
want string
|
|
}{
|
|
{
|
|
name: "no config",
|
|
in: nil,
|
|
want: `
|
|
up` + basic,
|
|
},
|
|
{
|
|
name: "local addr only",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.103/10"),
|
|
NetfilterMode: NetfilterOff,
|
|
},
|
|
want: `
|
|
up
|
|
ip addr add 100.101.102.103/10 dev tailscale0` + basic,
|
|
},
|
|
|
|
{
|
|
name: "addr and routes",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.103/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "192.168.16.0/24"),
|
|
NetfilterMode: NetfilterOff,
|
|
},
|
|
want: `
|
|
up
|
|
ip addr add 100.101.102.103/10 dev tailscale0
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88
|
|
ip route add 192.168.16.0/24 dev tailscale0 table 88` + basic,
|
|
},
|
|
|
|
{
|
|
name: "addr and routes and subnet routes",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.103/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "192.168.16.0/24"),
|
|
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
|
NetfilterMode: NetfilterOff,
|
|
},
|
|
want: `
|
|
up
|
|
ip addr add 100.101.102.103/10 dev tailscale0
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88
|
|
ip route add 192.168.16.0/24 dev tailscale0 table 88` + 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,
|
|
},
|
|
want: `
|
|
up
|
|
ip addr add 100.101.102.104/10 dev tailscale0
|
|
ip route add 10.0.0.0/8 dev tailscale0 table 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/FORWARD -j ts-forward
|
|
filter/INPUT -j ts-input
|
|
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
nat/POSTROUTING -j ts-postrouting
|
|
nat/ts-postrouting -m mark --mark 0x10000 -j MASQUERADE
|
|
`,
|
|
},
|
|
{
|
|
name: "addr and routes with netfilter",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
|
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 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/FORWARD -j ts-forward
|
|
filter/INPUT -j ts-input
|
|
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
nat/POSTROUTING -j ts-postrouting
|
|
`,
|
|
},
|
|
|
|
{
|
|
name: "addr and routes and subnet routes with netfilter but no SNAT",
|
|
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: false,
|
|
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 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/FORWARD -j ts-forward
|
|
filter/INPUT -j ts-input
|
|
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
nat/POSTROUTING -j ts-postrouting
|
|
`,
|
|
},
|
|
{
|
|
name: "addr and routes with netfilter",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
|
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 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/FORWARD -j ts-forward
|
|
filter/INPUT -j ts-input
|
|
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
nat/POSTROUTING -j ts-postrouting
|
|
`,
|
|
},
|
|
|
|
{
|
|
name: "addr and routes with half netfilter",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
|
NetfilterMode: NetfilterNoDivert,
|
|
},
|
|
want: `
|
|
up
|
|
ip addr add 100.101.102.104/10 dev tailscale0
|
|
ip route add 10.0.0.0/8 dev tailscale0 table 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
`,
|
|
},
|
|
{
|
|
name: "addr and routes with netfilter2",
|
|
in: &Config{
|
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
|
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 88
|
|
ip route add 100.100.100.100/32 dev tailscale0 table 88` + basic +
|
|
`filter/FORWARD -j ts-forward
|
|
filter/INPUT -j ts-input
|
|
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x10000
|
|
filter/ts-forward -m mark --mark 0x10000 -j ACCEPT
|
|
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
|
filter/ts-forward -o tailscale0 -j ACCEPT
|
|
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
|
|
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
|
nat/POSTROUTING -j ts-postrouting
|
|
`,
|
|
},
|
|
}
|
|
|
|
fake := NewFakeOS(t)
|
|
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake, fake)
|
|
if err != nil {
|
|
t.Fatalf("failed to create router: %v", err)
|
|
}
|
|
if err := router.Up(); err != nil {
|
|
t.Fatalf("failed to up router: %v", err)
|
|
}
|
|
|
|
testState := func(t *testing.T, i int) {
|
|
t.Helper()
|
|
if err := router.Set(states[i].in); err != nil {
|
|
t.Fatalf("failed to set router config: %v", err)
|
|
}
|
|
got := fake.String()
|
|
want := strings.TrimSpace(states[i].want)
|
|
if diff := cmp.Diff(got, want); diff != "" {
|
|
t.Fatalf("unexpected OS state (-got+want):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
for i, state := range states {
|
|
t.Run(state.name, func(t *testing.T) { testState(t, i) })
|
|
}
|
|
|
|
// Cycle through a bunch of states in pseudorandom order, to
|
|
// verify that we transition cleanly from state to state no matter
|
|
// the order.
|
|
for randRun := 0; randRun < 5*len(states); randRun++ {
|
|
i := rand.Intn(len(states))
|
|
state := states[i]
|
|
t.Run(state.name, func(t *testing.T) { testState(t, i) })
|
|
}
|
|
}
|
|
|
|
// fakeOS implements netfilterRunner and commandRunner, but captures
|
|
// changes without touching the OS.
|
|
type fakeOS struct {
|
|
t *testing.T
|
|
up bool
|
|
ips []string
|
|
routes []string
|
|
rules []string
|
|
netfilter map[string][]string
|
|
}
|
|
|
|
func NewFakeOS(t *testing.T) *fakeOS {
|
|
return &fakeOS{
|
|
t: t,
|
|
netfilter: map[string][]string{
|
|
"filter/INPUT": nil,
|
|
"filter/OUTPUT": nil,
|
|
"filter/FORWARD": nil,
|
|
"nat/PREROUTING": nil,
|
|
"nat/OUTPUT": nil,
|
|
"nat/POSTROUTING": nil,
|
|
},
|
|
}
|
|
}
|
|
|
|
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.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
|
|
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 {
|
|
unexpected := func() error {
|
|
o.t.Errorf("unexpected invocation %q", strings.Join(args, " "))
|
|
return errors.New("unrecognized invocation")
|
|
}
|
|
if args[0] != "ip" {
|
|
return unexpected()
|
|
}
|
|
|
|
rest := strings.Join(args[3:], " ")
|
|
|
|
var l *[]string
|
|
switch args[1] {
|
|
case "link":
|
|
got := strings.Join(args[2:], " ")
|
|
switch got {
|
|
case "set dev tailscale0 up":
|
|
o.up = true
|
|
case "set dev tailscale0 down":
|
|
o.up = false
|
|
default:
|
|
return unexpected()
|
|
}
|
|
return nil
|
|
case "addr":
|
|
l = &o.ips
|
|
case "route":
|
|
l = &o.routes
|
|
case "rule":
|
|
l = &o.rules
|
|
default:
|
|
return unexpected()
|
|
}
|
|
|
|
switch args[2] {
|
|
case "add":
|
|
for _, el := range *l {
|
|
if el == rest {
|
|
o.t.Errorf("can't add %q, already present", rest)
|
|
return errors.New("already exists")
|
|
}
|
|
}
|
|
*l = append(*l, rest)
|
|
sort.Strings(*l)
|
|
case "del":
|
|
found := false
|
|
for i, el := range *l {
|
|
if el == rest {
|
|
found = true
|
|
*l = append((*l)[:i], (*l)[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
o.t.Logf("note: can't delete %q, not present", rest)
|
|
// 'ip rule del' exits with code 2 when a row is
|
|
// missing. We don't want to consider that an error,
|
|
// for cleanup purposes.
|
|
|
|
// TODO(apenwarr): this is a hack.
|
|
// I'd like to return an exec.ExitError(2) here, but
|
|
// I can't, because the ExitCode is implemented in
|
|
// os.ProcessState, which is an opaque object I can't
|
|
// instantiate or modify. Go's 75 levels of abstraction
|
|
// between me and an 8-bit int are really paying off
|
|
// here, as you can see.
|
|
return errors.New("exitcode:2")
|
|
}
|
|
default:
|
|
return unexpected()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *fakeOS) output(args ...string) ([]byte, error) {
|
|
want := "ip rule list priority 10000"
|
|
got := strings.Join(args, " ")
|
|
if got != want {
|
|
o.t.Errorf("unexpected command that wants output: %v", got)
|
|
return nil, errExec
|
|
}
|
|
|
|
var ret []string
|
|
for _, rule := range o.rules {
|
|
if strings.Contains(rule, "10000") {
|
|
ret = append(ret, rule)
|
|
}
|
|
}
|
|
return []byte(strings.Join(ret, "\n")), nil
|
|
}
|