diff --git a/pkg/daemon/managed.go b/pkg/daemon/managed.go index bf4b65c6..3594dec0 100644 --- a/pkg/daemon/managed.go +++ b/pkg/daemon/managed.go @@ -3,10 +3,11 @@ package daemon import ( + "crypto/rand" + "encoding/binary" "encoding/json" "fmt" "log/slog" - "math/rand" "os" "path/filepath" "sort" @@ -122,7 +123,7 @@ func (me *ManagedEngine) Bootstrap() error { } // Shuffle and pick up to rules.Links peers - rand.Shuffle(len(candidates), func(i, j int) { + cryptoShuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) limit := me.rules.Links @@ -316,7 +317,7 @@ func (me *ManagedEngine) fill(members []uint32) int { candidates = append(candidates, id) } - rand.Shuffle(len(candidates), func(i, j int) { + cryptoShuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) @@ -427,3 +428,36 @@ func (me *ManagedEngine) load() error { slog.Info("managed: loaded persisted state", "network_id", me.netID, "peers", len(me.peers)) return nil } + +// cryptoShuffle shuffles n elements using crypto/rand. +// It implements Fisher-Yates with rejection-sampled cryptographically +// secure random indices. Safe for peer-candidate shuffling. +func cryptoShuffle(n int, swap func(i, j int)) { + buf := make([]byte, 8) + for i := n - 1; i > 0; i-- { + // Rejection-sample a random index in [0, i] + var idx int + for tries := 0; tries < 20; tries++ { + if _, err := rand.Read(buf[:4]); err != nil { + idx = i + break + } + v := int(binary.LittleEndian.Uint32(buf[:4])) + if v < 0 { + v = -v + } + // Reject to avoid modulo bias + limit := (v / (i + 1)) * (i + 1) + if v >= limit { + continue + } + idx = v % (i + 1) + break + } + // Fallback for hard edge case: idx remains 0, which for i>0 + // is a valid (though biased) index — swap or no-op is fine + if idx != i { + swap(idx, i) + } + } +} diff --git a/pkg/daemon/routing/discovery.go b/pkg/daemon/routing/discovery.go index bab0c6c8..bdd00fe9 100644 --- a/pkg/daemon/routing/discovery.go +++ b/pkg/daemon/routing/discovery.go @@ -3,9 +3,10 @@ package routing import ( + "crypto/rand" "encoding/json" "fmt" - "math/rand" + "math/big" "os" "path/filepath" "sort" @@ -273,5 +274,10 @@ func (s *BeaconSelectionState) ApplyRefreshDecision(d RefreshDecision) { // InitialJitter returns a duration in [0, BeaconRefreshJitter) for // avoiding thundering-herd on the registry at fleet restart. func InitialJitter() time.Duration { - return time.Duration(rand.Int63n(int64(BeaconRefreshJitter))) + n, err := rand.Int(rand.Reader, big.NewInt(int64(BeaconRefreshJitter))) + if err != nil { + // Fallback: return 0 jitter on crypto failure (safe, just bad for thundering herd) + return 0 + } + return time.Duration(n.Int64()) } diff --git a/pkg/daemon/zz_diag_hook_diaglog.go b/pkg/daemon/zz_diag_hook_diaglog.go index f2c960f6..732315df 100644 --- a/pkg/daemon/zz_diag_hook_diaglog.go +++ b/pkg/daemon/zz_diag_hook_diaglog.go @@ -4,7 +4,8 @@ package daemon import ( - "math/rand" + "crypto/rand" + "math/big" "sync/atomic" ) @@ -25,5 +26,9 @@ func diagShouldDropFrame() bool { if r == 0 { return false } - return uint64(rand.Intn(1000)) < r + n, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + return false + } + return uint64(n.Int64()) < r }