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 4 years ago
parent 5650f1ecf9
commit 9d1f48032a

@ -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()

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

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

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

@ -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"`

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

Loading…
Cancel
Save