mirror of https://github.com/tailscale/tailscale/
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.pull/61/head
parent
77907a76a3
commit
57bbafde84
@ -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"}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue