Browse Source

cmd/relaynode: drop local --acl-file in favour of central packet filter.

relaynode itself is not long for this world, deprecated in favour of
tailscale/tailscaled. But now that the control server supports central
distribution of packet filters, let's actually take advantage of it in
a final, backward compatible release of relaynode.
Avery Pennarun 6 years ago
parent
commit
57bbafde84

+ 1 - 1
.gitignore

@@ -1,6 +1,6 @@
 # Binaries for programs and plugins
+*~
 *.exe
-*.exe~
 *.dll
 *.so
 *.dylib

+ 0 - 63
cmd/relaynode/acl.json

@@ -1,63 +0,0 @@
-{
-  // Declare static groups of users beyond those in the identity service
-  "Groups": {
-    "group:eng": ["[email protected]", "[email protected]"]
-  },
-
-  // Declare convenient hostname aliases to use in place of IP addresses
-  "Hosts": {
-    "h222": "100.2.2.2"
-  },
-
-  // Access control list
-  "ACLs": [
-    {
-        "Action": "accept",
-        // Match any of several users
-        "Users": ["[email protected]", "[email protected]"],
-        // Match any port on h222, and port 22 of 10.1.2.3
-        "Ports": ["h222:*", "10.1.2.3:22"]
-    },
-    {
-        "Action": "accept",
-        // Match any user at all
-        "Users": ["*"],
-        // Match port 80 on one machine, ports 53 and 5353 on a second one,
-        // and ports 8000 through 8080 (a port range) on a third one.
-        "Ports": ["h222:80", "10.8.8.8:53,5353", "10.2.3.4:8000-8080"]
-    },
-    {
-        "Action": "accept",
-        // Match all users in the "Admin" role (network administrators)
-        "Users": ["role:Admin", "group:eng"],
-        // Allow access to port 22 on all servers
-        "Ports": ["*:22"]
-    },
-    {
-        "Action": "accept",
-        "Users": ["role:User"],
-        // Match only windows and linux workstations (not implemented yet)
-        "OS": ["windows", "linux"],
-        // Only desktop machines are allowed to access this server
-        "Ports": ["10.1.1.1:443"]
-    },
-    {
-        "Action": "accept",
-        "Users": ["*"],
-        // Match machines which have never been authorized, or which expired.
-        // (not implemented yet)
-        "MachineAuth": ["unauthorized", "expired"],
-        // Logged-in users on unauthorized machines can access the email server.
-        // Open the TLS ports for SMTP, IMAP, and HTTP.
-        "Ports": ["10.1.2.3:465", "10.1.2.3:993", "10.1.2.3:443"]
-    },
-
-    // Match absolutely everything. Comment out this section if you want
-    // the above ACLs to apply.
-    { "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
-
-    // Leave this line here so that every rule can end in a comma.
-    // It has no effect since it has no matching rules.
-    {"Action": "accept"}
-  ]
-}

+ 0 - 1
cmd/relaynode/debian/install

@@ -1,4 +1,3 @@
 relaynode			/usr/sbin
 tailscale-login			/usr/sbin
 taillogin			/usr/sbin
-acl.json			/etc/tailscale

+ 1 - 1
cmd/relaynode/debian/tailscale-relay.service

@@ -5,7 +5,7 @@ ConditionPathExists=/var/lib/tailscale/relay.conf
 
 [Service]
 EnvironmentFile=/etc/default/tailscale-relay
-ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $ACL_FILE $FLAGS
+ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $FLAGS
 Restart=on-failure
 
 [Install]

+ 1 - 0
cmd/relaynode/default.deb.od

@@ -11,6 +11,7 @@ arch=$(dpkg --print-architecture)
 )
 cp -a "$S/$dir/debian" "$dir/debtmp/"
 rm -f "$dir/debtmp/debian/$package.debhelper.log"
+rm -f "$dir/${package}_${version}_${arch}.deb"
 (
 	cd "$dir/debtmp" &&
 	debian/rules build &&

+ 0 - 1
cmd/relaynode/default.dir.od

@@ -8,7 +8,6 @@ mkdir "$outdir"
 touch $outdir/.stamp
 sfiles="
 	tailscale-login
-	acl.json
 	debian/*.service
 	*.defaults
 "

+ 1 - 0
cmd/relaynode/default.rpm.od

@@ -10,5 +10,6 @@ rpmbase=$HOME/rpmbuild
 
 mkdir -p "$rpmbase/SOURCES/"
 cp "$dir/$pkg.tar.gz" "$rpmbase/SOURCES/"
+rm -f "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm"
 rpmbuild -bb "$dir/$pkg.spec"
 mv "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm" $3

+ 7 - 59
cmd/relaynode/relaynode.go

@@ -29,7 +29,6 @@ import (
 	"github.com/tailscale/wireguard-go/wgcfg"
 	"tailscale.com/atomicfile"
 	"tailscale.com/control/controlclient"
-	"tailscale.com/control/policy"
 	"tailscale.com/logpolicy"
 	"tailscale.com/version"
 	"tailscale.com/wgengine"
@@ -52,7 +51,6 @@ func main() {
 	rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
 	droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
 	routes := getopt.StringLong("routes", 0, "", "list of IP ranges this node can relay")
-	aclfile := getopt.StringLong("acl-file", 0, "", "restrict traffic relaying according to json ACL file")
 	debug := getopt.StringLong("debug", 0, "", "Address of debug server")
 	getopt.Parse()
 	if len(getopt.Args()) > 0 {
@@ -83,18 +81,11 @@ func main() {
 	}
 
 	e = wgengine.NewWatchdog(e)
-	var lastacljson string
-	var p *policy.Policy
 
-	if *aclfile == "" {
-		e.SetFilter(nil)
-	} else {
-		lastacljson = readOrFatal(*aclfile)
-		p = installFilterOrFatal(e, *aclfile, lastacljson, nil)
-	}
+	// Default filter blocks everything, until Start() is called.
+	e.SetFilter(filter.NewAllowNone())
 
 	var lastNetMap *controlclient.NetworkMap
-	var lastUserMap map[string][]filter.IP
 	statusFunc := func(new controlclient.Status) {
 		if new.URL != "" {
 			fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
@@ -122,6 +113,9 @@ func main() {
 				return
 			}
 
+			log.Printf("packet filter: %v\n", m.PacketFilter)
+			e.SetFilter(filter.New(m.PacketFilter))
+
 			wgcfg, err := m.WGCfg(uflags, m.DNS)
 			if err != nil {
 				log.Fatalf("Error getting wg config: %v\n", err)
@@ -130,14 +124,6 @@ func main() {
 			if err != nil {
 				log.Fatalf("Error reconfiguring engine: %v\n", err)
 			}
-			lastUserMap = m.UserMap()
-			if p != nil {
-				matches, err := p.Expand(lastUserMap)
-				if err != nil {
-					log.Fatalf("Error expanding ACLs: %v\n", err)
-				}
-				e.SetFilter(filter.New(matches))
-			}
 		}
 	}
 
@@ -203,31 +189,8 @@ func main() {
 	signal.Notify(sigCh, os.Interrupt)
 	signal.Notify(sigCh, syscall.SIGTERM)
 
-	t := time.NewTicker(5 * time.Second)
-loop:
-	for {
-		select {
-		case <-t.C:
-			// For the sake of curiosity, request a status
-			// update periodically.
-			e.RequestStatus()
-
-			// check if aclfile has changed.
-			// TODO(apenwarr): use fsnotify instead of polling?
-			if *aclfile != "" {
-				json := readOrFatal(*aclfile)
-				if json != lastacljson {
-					logf("ACL file (%v) changed. Reloading filter.\n", *aclfile)
-					lastacljson = json
-					p = installFilterOrFatal(e, *aclfile, json, lastUserMap)
-				}
-			}
-		case <-sigCh:
-			logf("signal received, exiting")
-			t.Stop()
-			break loop
-		}
-	}
+	<-sigCh
+	logf("signal received, exiting")
 
 	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
 	defer cancel()
@@ -267,21 +230,6 @@ func readOrFatal(filename string) string {
 	return string(b)
 }
 
-func installFilterOrFatal(e wgengine.Engine, filename, acljson string, usermap map[string][]filter.IP) *policy.Policy {
-	p, err := policy.Parse(acljson)
-	if err != nil {
-		log.Fatalf("%v: json filter: %v\n", filename, err)
-	}
-
-	matches, err := p.Expand(usermap)
-	if err != nil {
-		log.Fatalf("%v: json filter: %v\n", filename, err)
-	}
-
-	e.SetFilter(filter.New(matches))
-	return p
-}
-
 func runDebugServer(addr string) {
 	mux := http.NewServeMux()
 	mux.HandleFunc("/debug/pprof/", pprof.Index)

+ 0 - 6
cmd/relaynode/tailscale-relay.defaults

@@ -4,11 +4,5 @@
 # settings.
 PORT="--port=41641"
 
-# Comment out this line to allow all traffic to be relayed.
-# Or edit the given file to allow specific traffic.
-# The example file is unlikely to match any users on your network, so it
-# will block all incoming traffic by default.
-ACL_FILE="--acl-file=/etc/tailscale/acl.json"
-
 # Extra flags you might want to pass to relaynode.
 FLAGS=""

+ 0 - 2
cmd/relaynode/tailscale-relay.spec.in

@@ -28,14 +28,12 @@ mkdir -p $D/usr/sbin $D/lib/systemd/system $D/etc/default $D/etc/tailscale
 cp taillogin tailscale-login relaynode $D/usr/sbin
 cp tailscale-relay.service $D/lib/systemd/system/
 cp tailscale-relay.defaults $D/etc/default/tailscale-relay
-cp acl.json $D/etc/tailscale/acl.json
 
 %clean
 
 %files
 %defattr(-,root,root)
 %config(noreplace) /etc/default/tailscale-relay
-%config(noreplace) /etc/tailscale/acl.json
 /lib/systemd/system/tailscale-relay.service
 /usr/sbin/taillogin
 /usr/sbin/tailscale-login

+ 0 - 228
control/policy/policy.go

@@ -1,228 +0,0 @@
-// 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 policy
-
-import (
-	"bytes"
-	"errors"
-	"fmt"
-	"net"
-	"strconv"
-	"strings"
-
-	"github.com/tailscale/hujson"
-	"tailscale.com/wgengine/filter"
-)
-
-type IP = filter.IP
-
-const IPAny = filter.IPAny
-
-type row struct {
-	Action string
-	Users  []string
-	Ports  []string
-}
-
-type Policy struct {
-	ACLs   []row
-	Groups map[string][]string
-	Hosts  map[string]IP
-}
-
-func lineAndColumn(b []byte, ofs int64) (line, col int) {
-	line = 1
-	for _, c := range b[:ofs] {
-		if c == '\n' {
-			col = 1
-			line++
-		} else {
-			col++
-		}
-	}
-	return line, col
-}
-
-func betterUnmarshal(b []byte, obj interface{}) error {
-	bio := bytes.NewReader(b)
-	d := hujson.NewDecoder(bio)
-	d.DisallowUnknownFields()
-	err := d.Decode(obj)
-	if err != nil {
-		switch ee := err.(type) {
-		case *hujson.SyntaxError:
-			row, col := lineAndColumn(b, ee.Offset)
-			return fmt.Errorf("line %d col %d: %v", row, col, ee)
-		default:
-			return fmt.Errorf("parser: %v", err)
-		}
-	}
-	return nil
-}
-
-func Parse(acljson string) (*Policy, error) {
-	p := &Policy{}
-	err := betterUnmarshal([]byte(acljson), p)
-	if err != nil {
-		return nil, err
-	}
-
-	// Check syntax with an empty usermap to start with.
-	// The caller might not have a valid usermap at startup, but we still
-	// want to check that the acljson doesn't have any syntax errors
-	// as early as possible. When the usermap updates later, it won't
-	// add any new syntax errors.
-	//
-	// TODO(apenwarr): change unmarshal code to detect syntax errors above.
-	//  Right now some of the sub-objects aren't parsed until .Expand().
-	emptyUserMap := make(map[string][]IP)
-	_, err = p.Expand(emptyUserMap)
-	if err != nil {
-		return nil, err
-	}
-
-	return p, nil
-}
-
-func parseHostPortRange(hostport string) (host string, ports []filter.PortRange, err error) {
-	hl := strings.Split(hostport, ":")
-	if len(hl) != 2 {
-		return "", nil, errors.New("hostport must have exactly one colon(:)")
-	}
-	host = hl[0]
-	portlist := hl[1]
-
-	if portlist == "*" {
-		// Special case: permit hostname:* as a port wildcard.
-		ports = append(ports, filter.PortRangeAny)
-		return host, ports, nil
-	}
-
-	pl := strings.Split(portlist, ",")
-	for _, pp := range pl {
-		if len(pp) == 0 {
-			return "", nil, fmt.Errorf("invalid port list: %#v", portlist)
-		}
-
-		pr := strings.Split(pp, "-")
-		if len(pr) > 2 {
-			return "", nil, fmt.Errorf("port range %#v: too many dashes(-)", pp)
-		}
-
-		var first, last uint64
-		first, err := strconv.ParseUint(pr[0], 10, 16)
-		if err != nil {
-			return "", nil, fmt.Errorf("port range %#v: invalid first integer", pp)
-		}
-
-		if len(pr) >= 2 {
-			last, err = strconv.ParseUint(pr[1], 10, 16)
-			if err != nil {
-				return "", nil, fmt.Errorf("port range %#v: invalid last integer", pp)
-			}
-		} else {
-			last = first
-		}
-
-		if first == 0 {
-			return "", nil, fmt.Errorf("port range %#v: first port must be >0, or use '*' for wildcard", pp)
-		}
-
-		if first > last {
-			return "", nil, fmt.Errorf("port range %#v: first port must be >= last port", pp)
-		}
-
-		ports = append(ports, filter.PortRange{uint16(first), uint16(last)})
-	}
-
-	return host, ports, nil
-}
-
-func (p *Policy) Expand(usermap map[string][]IP) (filter.Matches, error) {
-	lcusermap := make(map[string][]IP)
-	for k, v := range usermap {
-		k = strings.ToLower(k)
-		lcusermap[k] = v
-	}
-
-	for k, userlist := range p.Groups {
-		k = strings.ToLower(k)
-		if !strings.HasPrefix(k, "group:") {
-			return nil, fmt.Errorf("group[%#v]: group names must start with 'group:'", k)
-		}
-		for _, u := range userlist {
-			uips := lcusermap[u]
-			lcusermap[k] = append(lcusermap[k], uips...)
-		}
-	}
-
-	hosts := p.Hosts
-
-	var out filter.Matches
-	for _, acl := range p.ACLs {
-		if acl.Action != "accept" {
-			return nil, fmt.Errorf("action=%#v is not supported", acl.Action)
-		}
-
-		var srcs []IP
-		for _, user := range acl.Users {
-			user = strings.ToLower(user)
-			if user == "*" {
-				srcs = append(srcs, IPAny)
-				continue
-			} else if strings.Contains(user, "@") ||
-				strings.HasPrefix(user, "role:") ||
-				strings.HasPrefix(user, "group:") {
-				// fine if the requested user doesn't exist.
-				// we don't want to crash ACL parsing just
-				// because a previously authed user gets
-				// deleted. We'll silently ignore it and
-				// no firewall rules are needed.
-				// TODO(apenwarr): maybe print a warning?
-				for _, ip := range lcusermap[user] {
-					if ip != IPAny {
-						srcs = append(srcs, ip)
-					}
-				}
-			} else {
-				return nil, fmt.Errorf("wgengine/filter: invalid username: %q: needs '@domain' or 'group:' or 'role:'", user)
-			}
-		}
-
-		var dsts []filter.IPPortRange
-		for _, hostport := range acl.Ports {
-			host, ports, err := parseHostPortRange(hostport)
-			if err != nil {
-				return nil, fmt.Errorf("ports=%#v: %v", hostport, err)
-			}
-			ip := net.ParseIP(host)
-			ipv, ok := hosts[host]
-			if ok {
-				// matches an alias; ipv is now valid
-			} else if ip != nil && ip.IsUnspecified() {
-				// For clarity, reject 0.0.0.0 as an input
-				return nil, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", hostport)
-			} else if ip == nil && host == "*" {
-				// User explicitly requested wildcard dst ip
-				ipv = IPAny
-			} else {
-				if ip != nil {
-					ip = ip.To4()
-				}
-				if ip == nil || len(ip) != 4 {
-					return nil, fmt.Errorf("ports=%#v: %#v: invalid IPv4 address", hostport, host)
-				}
-				ipv = filter.NewIP(ip)
-			}
-
-			for _, pr := range ports {
-				dsts = append(dsts, filter.IPPortRange{ipv, pr})
-			}
-		}
-
-		out = append(out, filter.Match{DstPorts: dsts, SrcIPs: srcs})
-	}
-	return out, nil
-}

+ 0 - 156
control/policy/policy_test.go

@@ -1,156 +0,0 @@
-// 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 policy
-
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"tailscale.com/wgengine/filter"
-)
-
-type PortRange = filter.PortRange
-type IPPortRange = filter.IPPortRange
-
-var syntax_errors = []string{
-	`{ "ACLs": []! }`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "xPorts": ["100.122.98.50:22"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "drop", "Users": [], "Ports": ["100.122.98.50:22"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Users": [], "Ports": ["100.122.98.50:22"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4:0"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["0.0.0.0:12"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["*:0"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4:5:6"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4.5:12"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4::12"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4:0-0"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4:1-10,2-"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4:1-10,*"]}
-	]}`,
-
-	`{ "ACLs": [
-	  {"Action": "accept", "Users": [], "Ports": ["1.2.3.4,5.6.7.8:1-10"]}
-	]}`,
-
-	`{ "Hosts": {"mailserver": "not-an-ip"} }`,
-
-	`{ "Hosts": {"mailserver": "1.2.3.4:55"} }`,
-
-	`{ "xGroups": {
-	  "bob": ["user1", "user2"]
-	 }}`,
-}
-
-func TestSyntaxErrors(t *testing.T) {
-	for _, s := range syntax_errors {
-		_, err := Parse(s)
-		if err == nil {
-			t.Fatalf("Parse passed when it shouldn't. json:\n---\n%v\n---", s)
-		}
-	}
-}
-
-func ippr(ip IP, start, end uint16) []IPPortRange {
-	return []IPPortRange{
-		IPPortRange{ip, PortRange{start, end}},
-	}
-}
-
-func TestPolicy(t *testing.T) {
-	// Check ACL table parsing
-
-	usermap := map[string][]IP{
-		"[email protected]":    []IP{0x08010101, 0x08020202},
-		"role:admin": []IP{0x02020202},
-		"user1@org":  []IP{0x99010101, 0x99010102},
-		// user2 is intentionally missing
-		"user3@org": []IP{0x99030303},
-		"user4@org": []IP{},
-	}
-	want := filter.Matches{
-		{SrcIPs: []IP{0x08010101, 0x08020202}, DstPorts: []IPPortRange{
-			IPPortRange{0x01020304, PortRange{22, 22}},
-			IPPortRange{0x05060708, PortRange{23, 24}},
-			IPPortRange{0x05060708, PortRange{27, 28}},
-		}},
-		{SrcIPs: []IP{0x02020202}, DstPorts: ippr(0x08010101, 22, 22)},
-		{SrcIPs: []IP{0}, DstPorts: []IPPortRange{
-			IPPortRange{0x647a6232, PortRange{0, 65535}},
-			IPPortRange{0, PortRange{443, 443}},
-		}},
-		{SrcIPs: []IP{0x99010101, 0x99010102, 0x99030303}, DstPorts: ippr(0x01020304, 999, 999)},
-	}
-
-	p, err := Parse(`
-{
-    // Test comment
-    "Hosts": {
-    	"h1": "1.2.3.4", /* test comment */
-    	"h2": "5.6.7.8"
-    },
-    "Groups": {
-    	"group:eng": ["user1@org", "user2@org", "user3@org", "user4@org"]
-    },
-    "ACLs": [
-	{"Action": "accept", "Users": ["[email protected]"], "Ports": ["h1:22", "h2:23-24,27-28"]},
-	{"Action": "accept", "Users": ["role:Admin"], "Ports": ["8.1.1.1:22"]},
-	{"Action": "accept", "Users": ["*"], "Ports": ["100.122.98.50:*", "*:443"]},
-	{"Action": "accept", "Users": ["group:eng"], "Ports": ["h1:999"]},
-    ]}
-`)
-	if err != nil {
-		t.Fatalf("Parse failed: %v", err)
-	}
-	matches, err := p.Expand(usermap)
-	if err != nil {
-		t.Fatalf("Expand failed: %v", err)
-	}
-	if diff := cmp.Diff(want, matches); diff != "" {
-		t.Fatalf("Expand mismatch (-want +got):\n%s", diff)
-	}
-}