mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-09 14:05:36 +09:00
control,ipn: add Tailnet hint for device registration
Add an ipn.Prefs.Tailnet field, passed through controlclient.Options into tailcfg.RegisterRequest.Tailnet, so a client can recommend or require a tailnet at registration (e.g. to skip the tailnet picker when the intent is already known) without impersonating MDM. Previously this value could only come from the "Tailnet" syspolicy setting. That setting still takes precedence as an administrative override; the prefs value is used only when no policy is set. Updates #22093 Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
This commit is contained in:
1 parent
57ca6e1e2a
commit
5cf2727225
8 files changed
+123
-6
No files matched your search
@@ -1051,6 +1051,10 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
case "AutoExitNode":
|
||||
// Handled by tailscale {set,up} --exit-node=auto:any.
|
||||
continue
|
||||
case "Tailnet":
|
||||
// Registration-only hint for the tailnet to join, consumed when
|
||||
// building the RegisterRequest. Not editable via a CLI flag.
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ import (
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/httpbody"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/vizerror"
|
||||
@@ -110,6 +109,7 @@ type Direct struct {
|
||||
|
||||
persist persist.PersistView
|
||||
authKey string
|
||||
tailnet string // optional recommended/required tailnet for registration; see [tailcfg.RegisterRequest.Tailnet]
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry time.Time // or zero value if none/unknown
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
@@ -140,6 +140,7 @@ type Options struct {
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
Tailnet string // optional recommended/required tailnet for registration; see [tailcfg.RegisterRequest.Tailnet]
|
||||
Clock tstime.Clock
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
@@ -376,6 +377,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
tailnet: opts.Tailnet,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
@@ -756,10 +758,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
|
||||
tailnet, err := c.polc.GetString(pkey.Tailnet, "")
|
||||
if err != nil {
|
||||
c.logf("unable to provide Tailnet field in register request. err: %v", err)
|
||||
}
|
||||
now := c.clock.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
@@ -771,7 +769,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
NodeKeySignature: nodeKeySignature,
|
||||
Tailnet: tailnet,
|
||||
Tailnet: c.tailnet,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
|
||||
@@ -104,6 +104,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
Tailnet string
|
||||
RemoteConfig bool
|
||||
DriveShares []*drive.Share
|
||||
RelayServerPort *uint16
|
||||
|
||||
@@ -442,6 +442,28 @@ func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
// Linux-only.
|
||||
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
|
||||
|
||||
// Tailnet is an optional identifier for the recommended or required tailnet
|
||||
// the node should join at registration, mirroring
|
||||
// [tailcfg.RegisterRequest.Tailnet]. It is a comma-separated list of tailnet
|
||||
// identifiers (a stable tailnet ID, stable org ID, or tailnet SID). The
|
||||
// server treats the list as a set and restricts login to any one of the
|
||||
// listed tailnets.
|
||||
//
|
||||
// A single leading "required:" prefix (e.g. "required:tn1,tn2") governs the
|
||||
// whole set: when present, the server forces login to one of the listed
|
||||
// tailnets rather than merely recommending them. The prefix is not repeated
|
||||
// per entry. If empty, no recommendation is sent and the login page offers
|
||||
// all tailnets.
|
||||
//
|
||||
// The value is passed to the server verbatim; the client does not interpret
|
||||
// it. The syspolicy "Tailnet" setting overrides this value when set, so
|
||||
// administrators can enforce a tailnet via MDM.
|
||||
//
|
||||
// It is only consumed at registration time (when building the
|
||||
// RegisterRequest), so like ControlURL it must be set in the initial prefs
|
||||
// passed to Start rather than changed later via EditPrefs.
|
||||
func (v PrefsView) Tailnet() string { return v.ж.Tailnet }
|
||||
|
||||
// RemoteConfig, if true, delegates full remote control of this node's
|
||||
// prefs and LocalAPI to the tailnet admin via the control plane. When
|
||||
// enabled, the control server can read and edit any of this node's
|
||||
@@ -521,6 +543,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
Tailnet string
|
||||
RemoteConfig bool
|
||||
DriveShares []*drive.Share
|
||||
RelayServerPort *uint16
|
||||
|
||||
@@ -2295,6 +2295,11 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
|
||||
anyChange = true
|
||||
}
|
||||
|
||||
if tailnet, err := b.polc.GetString(pkey.Tailnet, prefs.Tailnet); err == nil && prefs.Tailnet != tailnet {
|
||||
prefs.Tailnet = tailnet
|
||||
anyChange = true
|
||||
}
|
||||
|
||||
const sentinel = "HostnameDefaultValue"
|
||||
hostnameFromPolicy, _ := b.polc.GetString(pkey.Hostname, sentinel)
|
||||
switch hostnameFromPolicy {
|
||||
@@ -3325,6 +3330,7 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error {
|
||||
Persist: *persistv,
|
||||
ServerURL: serverURL,
|
||||
AuthKey: opts.AuthKey,
|
||||
Tailnet: prefs.Tailnet(),
|
||||
Hostinfo: b.hostInfoWithServicesLocked(),
|
||||
HTTPTestClient: httpTestClient,
|
||||
DiscoPublicKey: discoPublic,
|
||||
|
||||
@@ -4786,6 +4786,53 @@ func TestApplySysPolicy(t *testing.T) {
|
||||
pkey.ControlURL: "set",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tailnet",
|
||||
wantPrefs: ipn.Prefs{
|
||||
Tailnet: "required:tn123",
|
||||
},
|
||||
wantAnyChange: true,
|
||||
stringPolicies: map[pkey.Key]string{
|
||||
pkey.Tailnet: "required:tn123",
|
||||
},
|
||||
},
|
||||
{
|
||||
// The value is passed through verbatim, including a comma-separated
|
||||
// list and a single leading "required:" prefix governing the set.
|
||||
name: "Tailnet-comma-separated-required",
|
||||
wantPrefs: ipn.Prefs{
|
||||
Tailnet: "required:tn123,tn456",
|
||||
},
|
||||
wantAnyChange: true,
|
||||
stringPolicies: map[pkey.Key]string{
|
||||
pkey.Tailnet: "required:tn123,tn456",
|
||||
},
|
||||
},
|
||||
{
|
||||
// A comma-separated list without the prefix is a recommendation
|
||||
// over the set, again passed through unchanged.
|
||||
name: "Tailnet-comma-separated-recommended",
|
||||
wantPrefs: ipn.Prefs{
|
||||
Tailnet: "tn123,tn456",
|
||||
},
|
||||
wantAnyChange: true,
|
||||
stringPolicies: map[pkey.Key]string{
|
||||
pkey.Tailnet: "tn123,tn456",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tailnet-overrides-prefs",
|
||||
prefs: ipn.Prefs{
|
||||
Tailnet: "tn123",
|
||||
},
|
||||
wantPrefs: ipn.Prefs{
|
||||
Tailnet: "required:tn456",
|
||||
},
|
||||
wantAnyChange: true,
|
||||
stringPolicies: map[pkey.Key]string{
|
||||
pkey.Tailnet: "required:tn456",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable-apply-keeps-check",
|
||||
prefs: ipn.Prefs{
|
||||
|
||||
@@ -278,6 +278,28 @@ type Prefs struct {
|
||||
// Linux-only.
|
||||
NetfilterKind string
|
||||
|
||||
// Tailnet is an optional identifier for the recommended or required tailnet
|
||||
// the node should join at registration, mirroring
|
||||
// [tailcfg.RegisterRequest.Tailnet]. It is a comma-separated list of tailnet
|
||||
// identifiers (a stable tailnet ID, stable org ID, or tailnet SID). The
|
||||
// server treats the list as a set and restricts login to any one of the
|
||||
// listed tailnets.
|
||||
//
|
||||
// A single leading "required:" prefix (e.g. "required:tn1,tn2") governs the
|
||||
// whole set: when present, the server forces login to one of the listed
|
||||
// tailnets rather than merely recommending them. The prefix is not repeated
|
||||
// per entry. If empty, no recommendation is sent and the login page offers
|
||||
// all tailnets.
|
||||
//
|
||||
// The value is passed to the server verbatim; the client does not interpret
|
||||
// it. The syspolicy "Tailnet" setting overrides this value when set, so
|
||||
// administrators can enforce a tailnet via MDM.
|
||||
//
|
||||
// It is only consumed at registration time (when building the
|
||||
// RegisterRequest), so like ControlURL it must be set in the initial prefs
|
||||
// passed to Start rather than changed later via EditPrefs.
|
||||
Tailnet string `json:",omitzero"`
|
||||
|
||||
// RemoteConfig, if true, delegates full remote control of this node's
|
||||
// prefs and LocalAPI to the tailnet admin via the control plane. When
|
||||
// enabled, the control server can read and edit any of this node's
|
||||
@@ -386,6 +408,7 @@ type MaskedPrefs struct {
|
||||
AppConnectorSet bool `json:",omitempty"`
|
||||
PostureCheckingSet bool `json:",omitempty"`
|
||||
NetfilterKindSet bool `json:",omitempty"`
|
||||
TailnetSet bool `json:",omitzero"`
|
||||
RemoteConfigSet bool `json:",omitempty"`
|
||||
DriveSharesSet bool `json:",omitempty"`
|
||||
RelayServerPortSet bool `json:",omitempty"`
|
||||
@@ -622,6 +645,9 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if p.NetfilterKind != "" {
|
||||
fmt.Fprintf(&sb, "netfilterKind=%s ", p.NetfilterKind)
|
||||
}
|
||||
if p.Tailnet != "" {
|
||||
fmt.Fprintf(&sb, "tailnet=%q ", p.Tailnet)
|
||||
}
|
||||
if buildfeatures.HasClientUpdate {
|
||||
sb.WriteString(p.AutoUpdate.Pretty())
|
||||
}
|
||||
@@ -698,6 +724,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.PostureChecking == p2.PostureChecking &&
|
||||
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
|
||||
p.NetfilterKind == p2.NetfilterKind &&
|
||||
p.Tailnet == p2.Tailnet &&
|
||||
p.RemoteConfig == p2.RemoteConfig &&
|
||||
compareUint16Ptrs(p.RelayServerPort, p2.RelayServerPort) &&
|
||||
slices.Equal(p.RelayServerStaticEndpoints, p2.RelayServerStaticEndpoints)
|
||||
|
||||
@@ -67,6 +67,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"AppConnector",
|
||||
"PostureChecking",
|
||||
"NetfilterKind",
|
||||
"Tailnet",
|
||||
"RemoteConfig",
|
||||
"DriveShares",
|
||||
"RelayServerPort",
|
||||
@@ -360,6 +361,16 @@ func TestPrefsEqual(t *testing.T) {
|
||||
&Prefs{NetfilterKind: ""},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{Tailnet: "required:tn123"},
|
||||
&Prefs{Tailnet: "required:tn123"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{Tailnet: "required:tn123"},
|
||||
&Prefs{Tailnet: "tn123"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
||||
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
||||
|
||||
Reference in new issue
Block a user