From b94b91c1689954542a783fa58386008fecb623bf Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Fri, 18 Nov 2022 14:36:45 +0500 Subject: [PATCH] cmd/tailscale/cli: add ability to set short names for profiles This adds a `--nickname` flag to `tailscale login|set`. Updates #713 Signed-off-by: Maisem Ali --- cmd/tailscale/cli/cli_test.go | 13 +++++++++++++ cmd/tailscale/cli/login.go | 2 +- cmd/tailscale/cli/set.go | 8 ++++++++ cmd/tailscale/cli/up.go | 17 +++++++++++++++-- ipn/ipnlocal/local.go | 20 ++++++++++++++++++++ ipn/ipnlocal/profiles.go | 10 ++++++++++ ipn/prefs.go | 2 +- 7 files changed, 68 insertions(+), 4 deletions(-) diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index 682e5b86e..1e2f7099f 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -480,6 +480,19 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) { distro: "", // not Synology want: accidentalUpPrefix + " --hostname=foo --accept-routes", }, + { + name: "profile_name_ignored_in_up", + flags: []string{"--hostname=foo"}, + curPrefs: &ipn.Prefs{ + ControlURL: "https://login.tailscale.com", + CorpDNS: true, + AllowSingleHosts: true, + NetfilterMode: preftype.NetfilterOn, + ProfileName: "foo", + }, + goos: "linux", + want: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/tailscale/cli/login.go b/cmd/tailscale/cli/login.go index 2ff2c161b..51f29b0d5 100644 --- a/cmd/tailscale/cli/login.go +++ b/cmd/tailscale/cli/login.go @@ -27,6 +27,6 @@ This command is currently in alpha and may change in the future.`, if err := localClient.SwitchToEmptyProfile(ctx); err != nil { return err } - return runUp(ctx, args, loginArgs) + return runUp(ctx, "login", args, loginArgs) }, } diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index 0bc3a5463..8f7d6a32b 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -43,11 +43,13 @@ type setArgsT struct { advertiseDefaultRoute bool opUser string acceptedRisks string + profileName string } func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { setf := newFlagSet("set") + setf.StringVar(&setArgs.profileName, "nickname", "", "nickname for the login profile") setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes") setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel") setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node") @@ -81,6 +83,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { maskedPrefs := &ipn.MaskedPrefs{ Prefs: ipn.Prefs{ + ProfileName: setArgs.profileName, RouteAll: setArgs.acceptRoutes, CorpDNS: setArgs.acceptDNS, ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess, @@ -132,6 +135,11 @@ func runSet(ctx context.Context, args []string) (retErr error) { return err } } + checkPrefs := curPrefs.Clone() + checkPrefs.ApplyEdits(maskedPrefs) + if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil { + return err + } _, err = localClient.EditPrefs(ctx, maskedPrefs) return err diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index d1a7db41a..2c4147034 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -60,7 +60,7 @@ settings.) `), FlagSet: upFlagSet, Exec: func(ctx context.Context, args []string) error { - return runUp(ctx, args, upArgsGlobal) + return runUp(ctx, "up", args, upArgsGlobal) }, } @@ -122,6 +122,10 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet { } upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever") + if cmd == "login" { + upf.StringVar(&upArgs.profileName, "nickname", "", "short name for the login profile") + } + if cmd == "up" { // Some flags are only for "up", not "login". upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)") @@ -129,6 +133,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet { upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication") registerAcceptRiskFlag(upf, &upArgs.acceptedRisks) } + return upf } @@ -163,6 +168,7 @@ type upArgsT struct { json bool timeout time.Duration acceptedRisks string + profileName string } func (a upArgsT) getAuthKey() (string, error) { @@ -343,6 +349,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo prefs.Hostname = upArgs.hostname prefs.ForceDaemon = upArgs.forceDaemon prefs.OperatorUser = upArgs.opUser + prefs.ProfileName = upArgs.profileName if goos == "linux" { prefs.NoSNAT = !upArgs.snat @@ -437,7 +444,7 @@ func presentSSHToggleRisk(wantSSH, haveSSH bool, acceptedRisks string) error { return presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, acceptedRisks) } -func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) { +func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retErr error) { var egg bool if len(args) > 0 { egg = fmt.Sprint(args) == "[up down down left right left right b a]" @@ -496,6 +503,11 @@ func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) { if err != nil { return err } + if cmd == "up" { + // "tailscale up" should not be able to change the + // profile name. + prefs.ProfileName = curPrefs.ProfileName + } env := upCheckEnv{ goos: effectiveGOOS(), @@ -764,6 +776,7 @@ func init() { addPrefFlagMapping("unattended", "ForceDaemon") addPrefFlagMapping("operator", "OperatorUser") addPrefFlagMapping("ssh", "RunSSH") + addPrefFlagMapping("nickname", "ProfileName") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a19713c5d..7d1f98b3e 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2109,6 +2109,9 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error { // Keep this one just for testing. errs = append(errs, errors.New("bad hostname [test]")) } + if err := b.checkProfileNameLocked(p); err != nil { + errs = append(errs, err) + } if err := b.checkSSHPrefsLocked(p); err != nil { errs = append(errs, err) } @@ -2226,6 +2229,23 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) { return stripKeysFromPrefs(newPrefs), nil } +func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error { + if p.ProfileName == "" { + // It is always okay to clear the profile name. + return nil + } + id := b.pm.ProfileIDForName(p.ProfileName) + if id == "" { + // No profile with that name exists. That's fine. + return nil + } + if id != b.pm.CurrentProfile().ID { + // Name is already in use by another profile. + return fmt.Errorf("profile name %q already in use", p.ProfileName) + } + return nil +} + // SetPrefs saves new user preferences and propagates them throughout // the system. Implements Backend. func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) { diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index e6fada02d..5d14c1d94 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -96,6 +96,16 @@ func (pm *profileManager) findProfilesByUserID(userID tailcfg.UserID) []*ipn.Log return out } +// ProfileIDForName returns the profile ID for the profile with the +// given name. It returns "" if no such profile exists. +func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID { + p := pm.findProfileByName(name) + if p == nil { + return "" + } + return p.ID +} + func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { for _, p := range pm.knownProfiles { if p.Name == name { diff --git a/ipn/prefs.go b/ipn/prefs.go index 7ec112150..988c72152 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -190,7 +190,7 @@ type Prefs struct { // operate tailscaled without being root or using sudo. OperatorUser string `json:",omitempty"` - // ProfileName is the desired name of the profile. If empty, then the users + // ProfileName is the desired name of the profile. If empty, then the user's // LoginName is used. It is only used for display purposes in the client UI // and CLI. ProfileName string `json:",omitempty"`