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