ipnlocal: allow overriding os.Hostname() via syspolicy (#14676)

Updates tailscale/corp#25936

This defines a new syspolicy 'Hostname' and allows an IT administrator to override the value we normally read from os.Hostname(). This is particularly useful on Android and iOS devices, where the hostname we get from the OS is really just the device model (a platform restriction to prevent fingerprinting).

If we don't implement this, all devices on the customer's side will look like `google-pixel-7a-1`, `google-pixel-7a-2`, `google-pixel-7a-3`, etc. and it is not feasible for the customer to use the API or worse the admin console to manually fix these names.

Apply code review comment by @nickkhyl

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
Co-authored-by: Nick Khyl <1761190+nickkhyl@users.noreply.github.com>
This commit is contained in:
Andrea Gottardo 2025-01-17 14:52:47 -08:00 committed by GitHub
parent 97a44d6453
commit c79b736a85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 0 deletions

View File

@ -1703,6 +1703,37 @@ func applySysPolicy(prefs *ipn.Prefs, lastSuggestedExitNode tailcfg.StableNodeID
anyChange = true
}
const sentinel = "HostnameDefaultValue"
hostnameFromPolicy, _ := syspolicy.GetString(syspolicy.Hostname, sentinel)
switch hostnameFromPolicy {
case sentinel:
// An empty string for this policy value means that the admin wants to delete
// the hostname stored in the ipn.Prefs. To make that work, we need to
// distinguish between an empty string and a policy that was not set.
// We cannot do that with the current implementation of syspolicy.GetString.
// It currently does not return an error if a policy was not configured.
// Instead, it returns the default value provided as the second argument.
// This behavior makes it impossible to distinguish between a policy that
// was not set and a policy that was set to an empty default value.
// Checking for sentinel here is a workaround to distinguish between
// the two cases. If we get it, we do nothing because the policy was not set.
//
// TODO(angott,nickkhyl): clean up this behavior once syspolicy.GetString starts
// properly returning errors.
case "":
// The policy was set to an empty string, which means the admin intends
// to clear the hostname stored in preferences.
prefs.Hostname = ""
anyChange = true
default:
// The policy was set to a non-empty string, which means the admin wants
// to override the hostname stored in preferences.
if prefs.Hostname != hostnameFromPolicy {
prefs.Hostname = hostnameFromPolicy
anyChange = true
}
}
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
if shouldAutoExitNode() && lastSuggestedExitNode != "" {

View File

@ -123,6 +123,11 @@ const (
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
MachineCertificateSubject Key = "MachineCertificateSubject"
// Hostname is the hostname of the device that is running Tailscale.
// When this policy is set, it overrides the hostname that the client
// would otherwise obtain from the OS, e.g. by calling os.Hostname().
Hostname Key = "Hostname"
// Keys with a string array value.
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
@ -148,6 +153,7 @@ var implicitDefinitions = []*setting.Definition{
setting.NewDefinition(ExitNodeID, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(ExitNodeIP, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(Hostname, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),