cmd/tailscale/cli: allow tag without "tag:" prefix in 'tailscale up'

If a user passes --advertise-tags=foo,bar (with no colons in any
segment), automatically prepend "tag:" client-side so it goes on the
wire as "tag:foo,tag:bar". Segments that already contain a colon are
left untouched and must be fully-qualified ("tag:foo"), which keeps
the door open for future colon-bearing syntax.

This was originally added in cd07437ad (2020-10-28) and then reverted
in 1be01ddc6 (2020-11-10) over forward-compatibility concerns. But
then it was realized in 2026-04-29 that this was always safe for
future extensiblity anyway (tags can't contain colons-- tag:foo:bar is
invalid anyway, per the 2020 CheckTag restrictions). So if we wanted
to perhaps some hypothetical --advertise-tags=tagset:setfoo or "group:foo",
we'd still have syntax to do, as it can't conflict with tag:group:foo.

Avery signed off on this on Slack: "Ok, I withdraw my objection to
auto-qualifying tag names in advertise-tags and I hope I won't regret
it :)"

Updates #861

Change-Id: I06935b0d3ae909894c95c9c2e185b7d6a219ff32
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2026-04-30 01:37:50 +00:00 committed by Brad Fitzpatrick
parent f343b496c3
commit 815bb291c9
2 changed files with 45 additions and 7 deletions

View File

@ -779,11 +779,43 @@ func TestPrefsFromUpArgs(t *testing.T) {
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
},
{
name: "error_tag_prefix",
name: "error_tag_bad_prefix",
args: upArgsT{
advertiseTags: "foo",
advertiseTags: "notatag:foo",
},
wantErr: `tag: "notatag:foo": tags must start with 'tag:'`,
},
{
name: "tag_auto_prefix",
args: upArgsFromOSArgs("linux", "--advertise-tags=foo,bar"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
CorpDNS: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
NoSNAT: false,
NoStatefulFiltering: "true",
NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
},
},
},
{
name: "tag_mixed_prefix",
args: upArgsFromOSArgs("linux", "--advertise-tags=tag:foo,bar"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
CorpDNS: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
NoSNAT: false,
NoStatefulFiltering: "true",
NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
},
},
wantErr: `tag: "foo": tags must start with 'tag:'`,
},
{
name: "error_long_hostname",

View File

@ -113,7 +113,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request (e.g. \"tag:eng,tag:montreal,tag:ssh\"); the \"tag:\" prefix is optional and added automatically when omitted (e.g. \"eng,montreal,ssh\")")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
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\") or empty string to not advertise routes")
upf.BoolVar(&upArgs.advertiseConnector, "advertise-connector", false, "advertise this node as an app connector")
@ -309,9 +309,15 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
var tags []string
if upArgs.advertiseTags != "" {
tags = strings.Split(upArgs.advertiseTags, ",")
for _, tag := range tags {
err := tailcfg.CheckTag(tag)
if err != nil {
for i, tag := range tags {
// Allow users to omit the "tag:" prefix; if the tag has no
// colon at all, add it for them. Tags with a colon must be
// fully qualified ("tag:foo") and are validated as-is.
if !strings.Contains(tag, ":") {
tag = "tag:" + tag
tags[i] = tag
}
if err := tailcfg.CheckTag(tag); err != nil {
return nil, fmt.Errorf("tag: %q: %s", tag, err)
}
}