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/343/head
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
|
||||
|
|
22
ipn/prefs.go
22
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
|
||||
|
|
|
@ -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…
Reference in New Issue