config, types: move randomize_client_port from server config to policy file

Tailscale models the randomize-client-port toggle as a top-level
field on the ACL policy. Headscale now matches that shape: the
server-config randomize_client_port key is removed, the toggle
lives in the policy file as randomizeClientPort, and per-node
opt-in via nodeAttrs is also supported.

Operators upgrading from a config-set randomize_client_port hit
depr.fatalWithHint at startup, which prints the deprecation message
and points at the new policy field rather than silently dropping
the toggle. The default carries over (false) so operators who never
set it are unaffected. config-example.yaml ships a REMOVED stanza
showing the migration.

types/node.go drops the cfg.RandomizeClientPort read from
TailNode -- the cap is now policy-driven through compileNodeAttrs
and the tail_test.go expectations follow.
This commit is contained in:
Kristoffer Dalby 2026-05-11 14:49:36 +00:00
parent 6fcff9e352
commit 3f73ed5404
5 changed files with 57 additions and 35 deletions

View file

@ -130,9 +130,8 @@ type Config struct {
OIDC OIDCConfig
LogTail LogTailConfig
RandomizeClientPort bool
Taildrop TaildropConfig
LogTail LogTailConfig
Taildrop TaildropConfig
CLI CLIConfig
@ -428,7 +427,6 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("oidc.email_verified_required", true)
viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false)
viper.SetDefault("taildrop.enabled", true)
viper.SetDefault("node.expiry", "0")
@ -530,6 +528,16 @@ func validateServerConfig() error {
depr.fatal("oidc.strip_email_domain")
depr.fatal("oidc.map_legacy_users")
// Removed since v0.29.0: `randomize_client_port` moved to the ACL
// policy as a top-level `randomizeClientPort` field, matching the
// Tailscale-hosted control plane schema. Per-node `nodeAttrs`
// entries granting `https://tailscale.com/cap/randomize-client-port`
// also work.
depr.fatalWithHint("randomize_client_port",
`Set "randomizeClientPort": true at the top level of your policy file `+
`(see policy.path / policy.mode), or grant the cap per-node via a `+
`"nodeAttrs" entry. See CHANGELOG.md (BREAKING / Configuration).`)
// Deprecated: ephemeral_node_inactivity_timeout -> node.ephemeral.inactivity_timeout
depr.warnNoAlias("node.ephemeral.inactivity_timeout", "ephemeral_node_inactivity_timeout")
@ -1120,7 +1128,6 @@ func LoadServerConfig() (*Config, error) {
derpConfig := derpConfig()
logTailConfig := logtailConfig()
randomizeClientPort := viper.GetBool("randomize_client_port")
oidcClientSecret := viper.GetString("oidc.client_secret")
@ -1219,8 +1226,7 @@ func LoadServerConfig() (*Config, error) {
},
},
LogTail: logTailConfig,
RandomizeClientPort: randomizeClientPort,
LogTail: logTailConfig,
Taildrop: TaildropConfig{
Enabled: viper.GetBool("taildrop.enabled"),
},
@ -1330,6 +1336,22 @@ func (d *deprecator) fatal(oldKey string) {
}
}
// fatalWithHint behaves like fatal but appends a remediation pointer to
// the message so operators see exactly what to do without leaving the
// terminal. Use it when the removed key has a clean replacement on the
// policy side.
func (d *deprecator) fatalWithHint(oldKey, hint string) {
if viper.IsSet(oldKey) {
d.fatals.Add(
fmt.Sprintf(
"The %q configuration key has been removed. %s",
oldKey,
hint,
),
)
}
}
// fatalIfNewKeyIsNotUsed deprecates and adds an entry to the fatal list of options if the oldKey is set and the new key is _not_ set.
// If the new key is set, a warning is emitted instead.
func (d *deprecator) fatalIfNewKeyIsNotUsed(newKey, oldKey string) {