bastion 0.55.3
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s

This commit is contained in:
Privatae
2026-10-25 23:16:45 +00:00
parent 77b7853ae1
commit 8822d5df85
136 changed files with 8638 additions and 1641 deletions
+80
View File
@@ -0,0 +1,80 @@
package main
import (
"context"
"sync"
"time"
)
// A beat out of turn, for every pairing on this box.
//
// Each running pairing beats its own Host on a 60-second ticker, and every
// beat carries the box's pairings as `peers` with the state of each (peers.go).
// That is how every Host on the machine learns that a pairing went active or was
// revoked. A pairing_grant_sync or pairing_grant_drop changes that state on disk
// at once, but nothing beat: every Host kept the old state for up to a minute.
// So a pairing approved a second after a beat still read as pending for the
// next minute, and a Host asking to connect to it in that time was refused. The
// anchor Host can read its own confirmed grant, but no other Host on the
// machine has one, and a revoke reached the other Hosts no sooner.
//
// So a change to a pairing's state asks every running pairing's beat loop to beat
// now. The signal carries nothing: the beat reads the pairings from disk as it
// always does, so it reports the state the change wrote. One pending signal per
// loop is kept and the rest are dropped, so a burst of changes is one extra beat
// per Host, never one per change.
var beatNow = struct {
sync.Mutex
subs map[chan struct{}]struct{}
}{subs: map[chan struct{}]struct{}{}}
// subscribeBeatNow registers one beat loop. The channel holds at most one
// pending signal; stop unregisters it.
func subscribeBeatNow() (<-chan struct{}, func()) {
ch := make(chan struct{}, 1)
beatNow.Lock()
beatNow.subs[ch] = struct{}{}
beatNow.Unlock()
return ch, func() {
beatNow.Lock()
delete(beatNow.subs, ch)
beatNow.Unlock()
}
}
// kickBeats asks every registered beat loop to beat now. It never blocks: a
// loop that already has a signal pending, or is mid-beat, beats once more.
func kickBeats() {
beatNow.Lock()
defer beatNow.Unlock()
for ch := range beatNow.subs {
select {
case ch <- struct{}{}:
default:
}
}
}
// runBeatLoop beats every `every`, and also whenever kickBeats asks. beforeTick
// runs before a ticked beat only (the tunnel's path failover, which belongs to
// the clock, not to a pairing change); it may be nil. It returns when ctx ends.
func runBeatLoop(ctx context.Context, every time.Duration, beforeTick, beat func()) {
kick, stop := subscribeBeatNow()
defer stop()
t := time.NewTicker(every)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
if beforeTick != nil {
beforeTick()
}
beat()
case <-kick:
beat()
}
}
}