From 10c595d962a43fa1c01642e1ea295b7eb98e74a6 Mon Sep 17 00:00:00 2001 From: James 'zofrex' Sanderson Date: Thu, 4 Jan 2024 18:29:04 +0100 Subject: [PATCH] ipn/ipnlocal: refresh node key without blocking if cap enabled (#10529) Updates tailscale/corp#16016 Signed-off-by: James Sanderson Co-authored-by: Maisem Ali --- control/controlknobs/controlknobs.go | 8 ++++++++ ipn/ipnlocal/local.go | 27 +++++++++++++++++++++------ tailcfg/tailcfg.go | 7 ++++++- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index fddffde3e..f9ed69812 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -64,6 +64,11 @@ type Knobs struct { // LinuxForceNfTables is whether the node should use nftables for Linux // netfiltering, unless overridden by the user. LinuxForceNfTables atomic.Bool + + // SeamlessKeyRenewal is whether to enable the alpha functionality of + // renewing node keys without breaking connections. + // http://go/seamless-key-renewal + SeamlessKeyRenewal atomic.Bool } // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self @@ -89,6 +94,7 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability, silentDisco = has(tailcfg.NodeAttrSilentDisco) forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables) forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables) + seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal) ) if has(tailcfg.NodeAttrOneCGNATEnable) { @@ -109,6 +115,7 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability, k.SilentDisco.Store(silentDisco) k.LinuxForceIPTables.Store(forceIPTables) k.LinuxForceNfTables.Store(forceNfTables) + k.SeamlessKeyRenewal.Store(seamlessKeyRenewal) } // AsDebugJSON returns k as something that can be marshalled with json.Marshal @@ -130,5 +137,6 @@ func (k *Knobs) AsDebugJSON() map[string]any { "SilentDisco": k.SilentDisco.Load(), "LinuxForceIPTables": k.LinuxForceIPTables.Load(), "LinuxForceNfTables": k.LinuxForceNfTables.Load(), + "SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(), } } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index cc904b18e..a06a66a54 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1074,9 +1074,11 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control b.blockEngineUpdates(false) } - if st.LoginFinished() && wasBlocked { - // Auth completed, unblock the engine - b.blockEngineUpdates(false) + if st.LoginFinished() && (wasBlocked || b.seamlessRenewalEnabled()) { + if wasBlocked { + // Auth completed, unblock the engine + b.blockEngineUpdates(false) + } b.authReconfig() b.send(ipn.Notify{LoginFinished: &empty.Message{}}) } @@ -1108,7 +1110,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control b.authURL = st.URL b.authURLSticky = st.URL } - if wasBlocked && st.LoginFinished() { + if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() { // Interactive login finished successfully (URL visited). // After an interactive login, the user always wants // WantRunning. @@ -2456,8 +2458,10 @@ func (b *LocalBackend) popBrowserAuthNow() { b.logf("popBrowserAuthNow: url=%v", url != "") - b.blockEngineUpdates(true) - b.stopEngineAndWait() + if !b.seamlessRenewalEnabled() { + b.blockEngineUpdates(true) + b.stopEngineAndWait() + } b.tellClientToBrowseToURL(url) if b.State() == ipn.Running { b.enterState(ipn.Starting) @@ -4176,6 +4180,9 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) { switch newState { case ipn.NeedsLogin: systemd.Status("Needs login: %s", authURL) + if b.seamlessRenewalEnabled() { + break + } b.blockEngineUpdates(true) fallthrough case ipn.Stopped: @@ -5801,6 +5808,14 @@ func (b *LocalBackend) AdvertiseRoute(ipp netip.Prefix) error { return err } +// seamlessRenewalEnabled reports whether seamless key renewals are enabled +// (i.e. we saw our self node with the SeamlessKeyRenewal attr in a netmap). +// This enables beta functionality of renewing node keys without breaking +// connections. +func (b *LocalBackend) seamlessRenewalEnabled() bool { + return b.ControlKnobs().SeamlessKeyRenewal.Load() +} + var ( disallowedAddrs = []netip.Addr{ netip.MustParseAddr("::1"), diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index dae9cf1b1..bbcfd86aa 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -124,7 +124,8 @@ type CapabilityVersion int // - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates) // - 82: 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables, c2n /netfilter-kind // - 83: 2023-12-18: Client understands DefaultAutoUpdate -const CurrentCapabilityVersion CapabilityVersion = 83 +// - 84: 2024-01-04: Client understands SeamlessKeyRenewal +const CurrentCapabilityVersion CapabilityVersion = 84 type StableID string @@ -2190,6 +2191,10 @@ const ( // netfilter management. // This cannot be set simultaneously with NodeAttrLinuxMustUseIPTables. NodeAttrLinuxMustUseNfTables NodeCapability = "linux-netfilter?v=nftables" + + // NodeAttrSeamlessKeyRenewal makes clients enable beta functionality + // of renewing node keys without breaking connections. + NodeAttrSeamlessKeyRenewal NodeCapability = "seamless-key-renewal" ) // SetDNSRequest is a request to add a DNS record.