mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-10 15:39:07 +02:00
[management] Pin the property that makes per-peer nonce state unnecessary
A nonce carries the window it was minted in, not the instant, and is accepted for that window and the one before it. So a peer re-stamped at least once per window can never be left holding one outside the accepted pair, whenever it was last served and however much life its own nonce had left. That is the whole reason management tracks nothing per peer, and it was resting on an argument rather than a test. The phases are part of the property, not decoration: accounts are deliberately given a refresh phase of their own, so the guarantee has to hold off the window boundary too. The negative case shows why that matters — a cadence of exactly two windows lands inside the grace window when it is aligned to the boundary and leaves a gap when it is not.
This commit is contained in:
@@ -51,3 +51,55 @@ func TestChallenger_HonoursAShortenedWindow(t *testing.T) {
|
||||
assert.ErrorIs(t, c.verifyNonce(nonce, peerKey, issued.Add(3*short)), ErrNonceExpired,
|
||||
"a shortened window must actually expire the nonce sooner")
|
||||
}
|
||||
|
||||
// heldNonceAlwaysAccepted walks ten windows and, at every step, checks the nonce the peer
|
||||
// would be holding if it were re-stamped every period starting at offset. It returns how
|
||||
// many of those checks would have rejected the peer.
|
||||
func heldNonceAlwaysAccepted(c *Challenger, peerKey []byte, period, offset time.Duration) int {
|
||||
start := time.Unix(0, 0).UTC().Add(offset)
|
||||
var rejected int
|
||||
for elapsed := time.Duration(0); elapsed < 10*Window; elapsed += 7 * time.Minute {
|
||||
lastRefresh := start.Add(elapsed / period * period)
|
||||
if c.verifyNonce(c.Nonce(peerKey, lastRefresh), peerKey, start.Add(elapsed)) != nil {
|
||||
rejected++
|
||||
}
|
||||
}
|
||||
return rejected
|
||||
}
|
||||
|
||||
// TestNonce_StaysValidWhenRestampedWithinAWindow is the reason management keeps no per-peer
|
||||
// nonce state. A nonce carries the window it was minted in, not the instant, and is accepted
|
||||
// for that window and the next. A peer re-stamped at least once per window therefore can
|
||||
// never be holding one that has fallen outside the accepted pair, whoever it is and whenever
|
||||
// it was last served. There is nothing to track and nothing to search for: the guarantee
|
||||
// comes from the cadence alone.
|
||||
//
|
||||
// The offsets matter: each account is given a phase of its own so a restart does not fan out
|
||||
// to every account at once, so the property has to hold off the window boundary too.
|
||||
func TestNonce_StaysValidWhenRestampedWithinAWindow(t *testing.T) {
|
||||
c := &Challenger{secret: []byte("secret"), window: Window}
|
||||
peerKey := []byte("peer-key")
|
||||
|
||||
for _, period := range []time.Duration{Window / 3, Window / 2, Window} {
|
||||
for _, offset := range []time.Duration{0, Window / 7, Window / 2, Window - time.Minute} {
|
||||
t.Run(period.String()+"+"+offset.String(), func(t *testing.T) {
|
||||
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, period, offset),
|
||||
"a peer re-stamped every %v is never left holding an expired nonce", period)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonce_ExpiresWhenRestampedTooSlowly(t *testing.T) {
|
||||
// The counterpart, which is what gives the test above its teeth. Note the aligned case
|
||||
// does not fail: a cadence of exactly two windows that lands on the boundaries is
|
||||
// covered by the grace window. Off the boundary it is not, and real refreshes are off
|
||||
// the boundary by design.
|
||||
c := &Challenger{secret: []byte("secret"), window: Window}
|
||||
peerKey := []byte("peer-key")
|
||||
|
||||
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, 0),
|
||||
"two windows exactly on the boundary happens to be covered by the grace window")
|
||||
assert.NotZero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, Window/2),
|
||||
"the same cadence off the boundary must leave the peer rejected for a stretch")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user