From 57bbafde8422ebcb4086bccb70e938feb1f238f1 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 19 Feb 2020 23:59:51 -0500 Subject: [PATCH] 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. --- .gitignore | 2 +- cmd/relaynode/acl.json | 63 ----- cmd/relaynode/debian/install | 1 - cmd/relaynode/debian/tailscale-relay.service | 2 +- cmd/relaynode/default.deb.od | 1 + cmd/relaynode/default.dir.od | 1 - cmd/relaynode/default.rpm.od | 1 + cmd/relaynode/relaynode.go | 66 +----- cmd/relaynode/tailscale-relay.defaults | 6 - cmd/relaynode/tailscale-relay.spec.in | 2 - control/policy/policy.go | 228 ------------------- control/policy/policy_test.go | 156 ------------- 12 files changed, 11 insertions(+), 518 deletions(-) delete mode 100644 cmd/relaynode/acl.json delete mode 100644 control/policy/policy.go delete mode 100644 control/policy/policy_test.go diff --git a/.gitignore b/.gitignore index 822dd335c..5ea6b5a28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Binaries for programs and plugins +*~ *.exe -*.exe~ *.dll *.so *.dylib diff --git a/cmd/relaynode/acl.json b/cmd/relaynode/acl.json deleted file mode 100644 index 29aff21df..000000000 --- a/cmd/relaynode/acl.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - // Declare static groups of users beyond those in the identity service - "Groups": { - "group:eng": ["u1@example.com", "u2@example.com"] - }, - - // 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": ["a@example.com", "b@example.com"], - // 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"} - ] -} diff --git a/cmd/relaynode/debian/install b/cmd/relaynode/debian/install index d2b64573f..a00adff00 100644 --- a/cmd/relaynode/debian/install +++ b/cmd/relaynode/debian/install @@ -1,4 +1,3 @@ relaynode /usr/sbin tailscale-login /usr/sbin taillogin /usr/sbin -acl.json /etc/tailscale diff --git a/cmd/relaynode/debian/tailscale-relay.service b/cmd/relaynode/debian/tailscale-relay.service index 446295a30..a1226d7f0 100644 --- a/cmd/relaynode/debian/tailscale-relay.service +++ b/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] diff --git a/cmd/relaynode/default.deb.od b/cmd/relaynode/default.deb.od index 09cd35502..fddf0d7ab 100644 --- a/cmd/relaynode/default.deb.od +++ b/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 && diff --git a/cmd/relaynode/default.dir.od b/cmd/relaynode/default.dir.od index 90e963ff9..c7b1efd09 100644 --- a/cmd/relaynode/default.dir.od +++ b/cmd/relaynode/default.dir.od @@ -8,7 +8,6 @@ mkdir "$outdir" touch $outdir/.stamp sfiles=" tailscale-login - acl.json debian/*.service *.defaults " diff --git a/cmd/relaynode/default.rpm.od b/cmd/relaynode/default.rpm.od index 254ba1fcf..c2e2df350 100644 --- a/cmd/relaynode/default.rpm.od +++ b/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 diff --git a/cmd/relaynode/relaynode.go b/cmd/relaynode/relaynode.go index 1dcf209df..91ae76e3a 100644 --- a/cmd/relaynode/relaynode.go +++ b/cmd/relaynode/relaynode.go @@ -29,7 +29,6 @@ "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) diff --git a/cmd/relaynode/tailscale-relay.defaults b/cmd/relaynode/tailscale-relay.defaults index 077d112a2..e638270a9 100644 --- a/cmd/relaynode/tailscale-relay.defaults +++ b/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="" diff --git a/cmd/relaynode/tailscale-relay.spec.in b/cmd/relaynode/tailscale-relay.spec.in index 351947e36..a0e62153b 100644 --- a/cmd/relaynode/tailscale-relay.spec.in +++ b/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 diff --git a/control/policy/policy.go b/control/policy/policy.go deleted file mode 100644 index 533cae19c..000000000 --- a/control/policy/policy.go +++ /dev/null @@ -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 -} diff --git a/control/policy/policy_test.go b/control/policy/policy_test.go deleted file mode 100644 index 4d20e350d..000000000 --- a/control/policy/policy_test.go +++ /dev/null @@ -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{ - "A@b.com": []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": ["a@b.com"], "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) - } -}