diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go index 77389bc47..1314d04a9 100644 --- a/cmd/tailscale/tailscale.go +++ b/cmd/tailscale/tailscale.go @@ -24,6 +24,7 @@ import ( "tailscale.com/ipn" "tailscale.com/paths" "tailscale.com/safesocket" + "tailscale.com/tailcfg" ) // globalStateKey is the ipn.StateKey that tailscaled loads on @@ -51,6 +52,7 @@ func main() { upf.BoolVar(&upArgs.noSingleRoutes, "no-single-routes", false, "don't install routes to single nodes") upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections") upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)") + upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)") upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key") upCmd := &ffcli.Command{ Name: "up", @@ -61,10 +63,8 @@ func main() { "tailscale up" connects this machine to your Tailscale network, triggering authentication if necessary. -The flags passed to this command set tailscaled options that are -specific to this machine, such as whether to advertise some routes to -other nodes in the Tailscale network. If you don't specify any flags, -options are reset to their default. +The flags passed to this command are specific to this machine. If you don't +specify any flags, options are reset to their default. `), FlagSet: upf, Exec: runUp, @@ -101,6 +101,7 @@ var upArgs struct { noSingleRoutes bool shieldsUp bool advertiseRoutes string + advertiseTags string authKey string } @@ -109,7 +110,7 @@ func runUp(ctx context.Context, args []string) error { log.Fatalf("too many non-flag arguments: %q", args) } - var adv []wgcfg.CIDR + var routes []wgcfg.CIDR if upArgs.advertiseRoutes != "" { advroutes := strings.Split(upArgs.advertiseRoutes, ",") for _, s := range advroutes { @@ -117,7 +118,18 @@ func runUp(ctx context.Context, args []string) error { if err != nil { log.Fatalf("%q is not a valid CIDR prefix: %v", s, err) } - adv = append(adv, cidr) + routes = append(routes, cidr) + } + } + + var tags []string + if upArgs.advertiseTags != "" { + tags = strings.Split(upArgs.advertiseTags, ",") + for _, tag := range tags { + err := tailcfg.CheckTag(tag) + if err != nil { + log.Fatalf("tag: %q: %s", tag, err) + } } } @@ -129,7 +141,8 @@ func runUp(ctx context.Context, args []string) error { prefs.RouteAll = upArgs.acceptRoutes prefs.AllowSingleHosts = !upArgs.noSingleRoutes prefs.ShieldsUp = upArgs.shieldsUp - prefs.AdvertiseRoutes = adv + prefs.AdvertiseRoutes = routes + prefs.AdvertiseTags = tags c, bc, ctx, cancel := connect(ctx) defer cancel() diff --git a/ipn/local.go b/ipn/local.go index 90f20d9ba..eb52f277b 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -201,6 +201,7 @@ func (b *LocalBackend) Start(opts Options) error { b.serverURL = b.prefs.ControlURL hi.RoutableIPs = append(hi.RoutableIPs, b.prefs.AdvertiseRoutes...) + hi.RequestTags = append(hi.RequestTags, b.prefs.AdvertiseTags...) b.notify = opts.Notify b.netMapCache = nil diff --git a/ipn/prefs.go b/ipn/prefs.go index cfad1ab17..cdab720eb 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -47,14 +47,19 @@ type Prefs struct { // AdvertiseRoutes specifies CIDR prefixes to advertise into the // Tailscale network as reachable through the current node. AdvertiseRoutes []wgcfg.CIDR + // AdvertiseTags specifies groups that this node wants to join, for + // purposes of ACL enforcement. These can be referenced from the ACL + // security policy. Note that advertising a tag doesn't guarantee that + // the control server will allow you to take on the rights for that + // tag. + AdvertiseTags []string // NotepadURLs is a debugging setting that opens OAuth URLs in // notepad.exe on Windows, rather than loading them in a browser. // - // TODO(danderson): remove? // apenwarr 2020-04-29: Unfortunately this is still needed sometimes. // Windows' default browser setting is sometimes screwy and this helps - // narrow it down a bit. + // users narrow it down a bit. NotepadURLs bool // DisableDERP prevents DERP from being used. @@ -109,6 +114,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.DisableDERP == p2.DisableDERP && p.ShieldsUp == p2.ShieldsUp && compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && + compareStrings(p.AdvertiseTags, p2.AdvertiseTags) && p.Persist.Equals(p2.Persist) } @@ -124,6 +130,18 @@ func compareIPNets(a, b []wgcfg.CIDR) bool { return true } +func compareStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + func NewPrefs() *Prefs { return &Prefs{ // Provide default values for options which might be missing diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index a1e666179..a1b0a7913 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -20,7 +20,7 @@ func fieldsOf(t reflect.Type) (fields []string) { } func TestPrefsEqual(t *testing.T) { - prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseRoutes", "NotepadURLs", "DisableDERP", "Persist"} + prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseRoutes", "AdvertiseTags", "NotepadURLs", "DisableDERP", "Persist"} if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n", have, prefsHandles) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index f37a59992..669143b9b 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -207,6 +207,44 @@ func (m MachineStatus) String() string { } } +func isNum(b byte) bool { + return b >= '0' && b <= '9' +} + +func isAlpha(b byte) bool { + return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') +} + +// CheckTag valids whether a given string can be used as an ACL tag. +// For now we allow only ascii alphanumeric tags, and they need to start +// with a letter. No unicode shenanigans allowed, and we reserve punctuation +// marks other than '-' for a possible future URI scheme. +// +// Because we're ignoring unicode entirely, we can treat utf-8 as a series of +// bytes. Anything >= 128 is disqualified anyway. +// +// We might relax these rules later. +func CheckTag(tag string) error { + if !strings.HasPrefix(tag, "tag:") { + return errors.New("tags must start with 'tag:'") + } + tag = tag[4:] + if tag == "" { + return errors.New("tag names must not be empty") + } + if !isAlpha(tag[0]) { + return errors.New("tag names must start with a letter, after 'tag:'") + } + + for _, b := range []byte(tag) { + if !isNum(b) && !isAlpha(b) && b != '-' { + return errors.New("tag names can only contain numbers, letters, or dashes") + } + } + + return nil +} + type ServiceProto string const ( @@ -238,6 +276,7 @@ type Hostinfo struct { OS string // operating system the client runs on (a version.OS value) Hostname string // name of the host the client runs on RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route + RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim Services []Service `json:",omitempty"` // services advertised by this machine NetInfo *NetInfo `json:",omitempty"` diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 5587f4839..c402f6dea 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -21,7 +21,7 @@ func fieldsOf(t reflect.Type) (fields []string) { func TestHostinfoEqual(t *testing.T) { hiHandles := []string{ - "IPNVersion", "FrontendLogID", "BackendLogID", "OS", "Hostname", "RoutableIPs", "Services", + "IPNVersion", "FrontendLogID", "BackendLogID", "OS", "Hostname", "RoutableIPs", "RequestTags", "Services", "NetInfo", } if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { @@ -140,6 +140,22 @@ func TestHostinfoEqual(t *testing.T) { true, }, + { + &Hostinfo{RequestTags: []string{"abc", "def"}}, + &Hostinfo{RequestTags: []string{"abc", "def"}}, + true, + }, + { + &Hostinfo{RequestTags: []string{"abc", "def"}}, + &Hostinfo{RequestTags: []string{"abc", "123"}}, + false, + }, + { + &Hostinfo{RequestTags: []string{}}, + &Hostinfo{RequestTags: []string{"abc"}}, + false, + }, + { &Hostinfo{Services: []Service{Service{TCP, 1234, "foo"}}}, &Hostinfo{Services: []Service{Service{UDP, 2345, "bar"}}},