summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
authorAnton Tolchanov <anton@tailscale.com>2025-09-25 14:19:15 +0100
committerAnton Tolchanov <anton@tailscale.com>2025-09-25 14:19:15 +0100
commit00088aea1e30a0833c4875f84f1e75d1ad777357 (patch)
treefac1ecd118621206330ecc6581e813ba5726b02f /ipn
parent99f0c03e9ba1b84007e6eb4ad05256ef9854da66 (diff)
downloadtailscale-knyar/sshcap.tar.xz
tailscale-knyar/sshcap.zip
WIP expiring capsknyar/sshcap
Diffstat (limited to 'ipn')
-rw-r--r--ipn/ipnlocal/c2n_test.go3
-rw-r--r--ipn/ipnlocal/expiry.go86
-rw-r--r--ipn/ipnlocal/expiry_test.go27
-rw-r--r--ipn/ipnlocal/local.go1
4 files changed, 90 insertions, 27 deletions
diff --git a/ipn/ipnlocal/c2n_test.go b/ipn/ipnlocal/c2n_test.go
index 75a57dee5..9586549c3 100644
--- a/ipn/ipnlocal/c2n_test.go
+++ b/ipn/ipnlocal/c2n_test.go
@@ -268,6 +268,8 @@ func TestRedactNetmapPrivateKeys(t *testing.T) {
f(tailcfg.DisplayMessage{}, "Severity"): false,
f(tailcfg.DisplayMessage{}, "Text"): false,
f(tailcfg.DisplayMessage{}, "Title"): false,
+ f(tailcfg.ExtraCapMapValue{}, "Expiry"): false,
+ f(tailcfg.ExtraCapMapValue{}, "Value"): false,
f(tailcfg.FilterRule{}, "CapGrant"): false,
f(tailcfg.FilterRule{}, "DstPorts"): false,
f(tailcfg.FilterRule{}, "IPProto"): false,
@@ -353,6 +355,7 @@ func TestRedactNetmapPrivateKeys(t *testing.T) {
f(tailcfg.Node{}, "DiscoKey"): false,
f(tailcfg.Node{}, "Endpoints"): false,
f(tailcfg.Node{}, "ExitNodeDNSResolvers"): false,
+ f(tailcfg.Node{}, "ExtraCapMap"): false,
f(tailcfg.Node{}, "Expired"): false,
f(tailcfg.Node{}, "HomeDERP"): false,
f(tailcfg.Node{}, "Hostinfo"): false,
diff --git a/ipn/ipnlocal/expiry.go b/ipn/ipnlocal/expiry.go
index 849e28610..961c9ff4c 100644
--- a/ipn/ipnlocal/expiry.go
+++ b/ipn/ipnlocal/expiry.go
@@ -153,6 +153,39 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
}
}
+func (em *expiryManager) expireNodeCaps(netmap *netmap.NetworkMap, localNow time.Time) {
+ controlNow := localNow.Add(em.clockDelta.Load())
+ if controlNow.Before(flagExpiredPeersEpoch) {
+ em.logf("netmap: expireNodeCaps: [unexpected] delta-adjusted current time is before hardcoded epoch; skipping")
+ return
+ }
+ expireCaps := func(n *tailcfg.Node) (changed bool) {
+ if len(n.ExtraCapMap) == 0 {
+ return false
+ }
+ for capName, cap := range n.ExtraCapMap {
+ if !cap.Expiry.IsZero() && cap.Expiry.Before(controlNow) {
+ delete(n.ExtraCapMap, capName)
+ changed = true
+ }
+ }
+ return changed
+ }
+ if netmap.SelfNode.Valid() {
+ // TODO(anton): don't clone if there's nothing to change.
+ self := netmap.SelfNode.AsStruct()
+ if expireCaps(self) {
+ netmap.SelfNode = self.View()
+ }
+ }
+ for i, peer := range netmap.Peers {
+ p := peer.AsStruct()
+ if expireCaps(p) {
+ netmap.Peers[i] = p.View()
+ }
+ }
+}
+
// nextPeerExpiry returns the time that the next node in the netmap expires
// (including the self node), based on their KeyExpiry. It skips nodes that are
// already marked as Expired. If there are no nodes expiring in the future,
@@ -174,43 +207,42 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim
}
var nextExpiry time.Time // zero if none
- for _, peer := range nm.Peers {
- if peer.KeyExpiry().IsZero() {
- continue // tagged node
- } else if peer.Expired() {
- // Peer already expired; Expired is set by the
- // flagExpiredPeers function, above.
- continue
- } else if peer.KeyExpiry().Before(controlNow) {
- // This peer already expired, and peer.Expired
- // isn't set for some reason. Skip this node.
- continue
+ update := func(expiry time.Time) {
+ if expiry.IsZero() {
+ return
}
-
// nextExpiry being zero is a sentinel that we haven't yet set
// an expiry; otherwise, only update if this node's expiry is
// sooner than the currently-stored one (since we want the
// soonest-occurring expiry time).
- if nextExpiry.IsZero() || peer.KeyExpiry().Before(nextExpiry) {
- nextExpiry = peer.KeyExpiry()
+ if nextExpiry.IsZero() || expiry.Before(nextExpiry) {
+ nextExpiry = expiry
+ }
+ }
+ handleNode := func(n tailcfg.NodeView) {
+ if n.KeyExpiry().IsZero() {
+ // tagged node
+ } else if n.Expired() {
+ // Already expired; Expired is set by the
+ // flagExpiredPeers function, above.
+ } else if n.KeyExpiry().Before(controlNow) {
+ // Already expired, but Expired
+ // isn't set for some reason. Skip it.
+ } else {
+ update(n.KeyExpiry())
+ }
+ // Also handle expiring caps.
+ for _, c := range n.ExtraCapMap().All() {
+ update(c.Expiry())
}
}
+ for _, peer := range nm.Peers {
+ handleNode(peer)
+ }
// Ensure that we also fire this timer if our own node key expires.
if nm.SelfNode.Valid() {
- selfExpiry := nm.SelfNode.KeyExpiry()
-
- if selfExpiry.IsZero() {
- // No expiry for self node
- } else if selfExpiry.Before(controlNow) {
- // Self node already expired; we don't want to return a
- // time in the past, so skip this.
- } else if nextExpiry.IsZero() || selfExpiry.Before(nextExpiry) {
- // Self node expires after now, but before the soonest
- // peer in the netmap; update our next expiry to this
- // time.
- nextExpiry = selfExpiry
- }
+ handleNode(nm.SelfNode)
}
// As an additional defense in depth, never return a time that is
diff --git a/ipn/ipnlocal/expiry_test.go b/ipn/ipnlocal/expiry_test.go
index 2c646ca72..d65788a28 100644
--- a/ipn/ipnlocal/expiry_test.go
+++ b/ipn/ipnlocal/expiry_test.go
@@ -15,6 +15,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus/eventbustest"
+ "tailscale.com/util/mak"
)
func TestFlagExpiredPeers(t *testing.T) {
@@ -238,6 +239,32 @@ func TestNextPeerExpiry(t *testing.T) {
},
want: noExpiry,
},
+ {
+ name: "self_attribute",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInMoreFuture, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInMoreFuture})
+ }),
+ }),
+ SelfNode: n(2, "self", noExpiry, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInFuture})
+ }).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "peer_attribute",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInMoreFuture, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInFuture})
+ }),
+ }),
+ SelfNode: n(2, "self", timeInMoreFuture).View(),
+ },
+ want: timeInFuture,
+ },
}
for _, tt := range tests {
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index ce42ae75a..9f154912f 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -1575,6 +1575,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
if st.NetMap != nil {
now := b.clock.Now()
b.em.flagExpiredPeers(st.NetMap, now)
+ b.em.expireNodeCaps(st.NetMap, now)
// Always stop the existing netmap timer if we have a netmap;
// it's possible that we have no nodes expiring, so we should