cmd/tailscale: add --advertise-tags option.

These will be used for dynamically changing the identity of a node, so
its ACL rights can be different from your own.

Note: Not all implemented yet on the server side, but we need this so
we can request the tagged rights in the first place.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
pull/340/head
Avery Pennarun 5 years ago
parent 5650f1ecf9
commit 9d1f48032a

@ -24,6 +24,7 @@ import (
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/tailcfg"
) )
// globalStateKey is the ipn.StateKey that tailscaled loads on // 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.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.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.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") upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upCmd := &ffcli.Command{ upCmd := &ffcli.Command{
Name: "up", Name: "up",
@ -61,10 +63,8 @@ func main() {
"tailscale up" connects this machine to your Tailscale network, "tailscale up" connects this machine to your Tailscale network,
triggering authentication if necessary. triggering authentication if necessary.
The flags passed to this command set tailscaled options that are The flags passed to this command are specific to this machine. If you don't
specific to this machine, such as whether to advertise some routes to specify any flags, options are reset to their default.
other nodes in the Tailscale network. If you don't specify any flags,
options are reset to their default.
`), `),
FlagSet: upf, FlagSet: upf,
Exec: runUp, Exec: runUp,
@ -101,6 +101,7 @@ var upArgs struct {
noSingleRoutes bool noSingleRoutes bool
shieldsUp bool shieldsUp bool
advertiseRoutes string advertiseRoutes string
advertiseTags string
authKey string authKey string
} }
@ -109,7 +110,7 @@ func runUp(ctx context.Context, args []string) error {
log.Fatalf("too many non-flag arguments: %q", args) log.Fatalf("too many non-flag arguments: %q", args)
} }
var adv []wgcfg.CIDR var routes []wgcfg.CIDR
if upArgs.advertiseRoutes != "" { if upArgs.advertiseRoutes != "" {
advroutes := strings.Split(upArgs.advertiseRoutes, ",") advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes { for _, s := range advroutes {
@ -117,7 +118,18 @@ func runUp(ctx context.Context, args []string) error {
if err != nil { if err != nil {
log.Fatalf("%q is not a valid CIDR prefix: %v", s, err) 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.RouteAll = upArgs.acceptRoutes
prefs.AllowSingleHosts = !upArgs.noSingleRoutes prefs.AllowSingleHosts = !upArgs.noSingleRoutes
prefs.ShieldsUp = upArgs.shieldsUp prefs.ShieldsUp = upArgs.shieldsUp
prefs.AdvertiseRoutes = adv prefs.AdvertiseRoutes = routes
prefs.AdvertiseTags = tags
c, bc, ctx, cancel := connect(ctx) c, bc, ctx, cancel := connect(ctx)
defer cancel() defer cancel()

@ -201,6 +201,7 @@ func (b *LocalBackend) Start(opts Options) error {
b.serverURL = b.prefs.ControlURL b.serverURL = b.prefs.ControlURL
hi.RoutableIPs = append(hi.RoutableIPs, b.prefs.AdvertiseRoutes...) hi.RoutableIPs = append(hi.RoutableIPs, b.prefs.AdvertiseRoutes...)
hi.RequestTags = append(hi.RequestTags, b.prefs.AdvertiseTags...)
b.notify = opts.Notify b.notify = opts.Notify
b.netMapCache = nil b.netMapCache = nil

@ -47,14 +47,19 @@ type Prefs struct {
// AdvertiseRoutes specifies CIDR prefixes to advertise into the // AdvertiseRoutes specifies CIDR prefixes to advertise into the
// Tailscale network as reachable through the current node. // Tailscale network as reachable through the current node.
AdvertiseRoutes []wgcfg.CIDR 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 // NotepadURLs is a debugging setting that opens OAuth URLs in
// notepad.exe on Windows, rather than loading them in a browser. // notepad.exe on Windows, rather than loading them in a browser.
// //
// TODO(danderson): remove?
// apenwarr 2020-04-29: Unfortunately this is still needed sometimes. // apenwarr 2020-04-29: Unfortunately this is still needed sometimes.
// Windows' default browser setting is sometimes screwy and this helps // Windows' default browser setting is sometimes screwy and this helps
// narrow it down a bit. // users narrow it down a bit.
NotepadURLs bool NotepadURLs bool
// DisableDERP prevents DERP from being used. // DisableDERP prevents DERP from being used.
@ -109,6 +114,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.DisableDERP == p2.DisableDERP && p.DisableDERP == p2.DisableDERP &&
p.ShieldsUp == p2.ShieldsUp && p.ShieldsUp == p2.ShieldsUp &&
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) p.Persist.Equals(p2.Persist)
} }
@ -124,6 +130,18 @@ func compareIPNets(a, b []wgcfg.CIDR) bool {
return true 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 { func NewPrefs() *Prefs {
return &Prefs{ return &Prefs{
// Provide default values for options which might be missing // Provide default values for options which might be missing

@ -20,7 +20,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
} }
func TestPrefsEqual(t *testing.T) { 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) { 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", t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles) have, prefsHandles)

@ -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 type ServiceProto string
const ( const (
@ -238,6 +276,7 @@ type Hostinfo struct {
OS string // operating system the client runs on (a version.OS value) OS string // operating system the client runs on (a version.OS value)
Hostname string // name of the host the client runs on Hostname string // name of the host the client runs on
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route 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 Services []Service `json:",omitempty"` // services advertised by this machine
NetInfo *NetInfo `json:",omitempty"` NetInfo *NetInfo `json:",omitempty"`

@ -21,7 +21,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestHostinfoEqual(t *testing.T) { func TestHostinfoEqual(t *testing.T) {
hiHandles := []string{ hiHandles := []string{
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "Hostname", "RoutableIPs", "Services", "IPNVersion", "FrontendLogID", "BackendLogID", "OS", "Hostname", "RoutableIPs", "RequestTags", "Services",
"NetInfo", "NetInfo",
} }
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
@ -140,6 +140,22 @@ func TestHostinfoEqual(t *testing.T) {
true, 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{TCP, 1234, "foo"}}},
&Hostinfo{Services: []Service{Service{UDP, 2345, "bar"}}}, &Hostinfo{Services: []Service{Service{UDP, 2345, "bar"}}},

Loading…
Cancel
Save