1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Copyright 2023 Developers of the reconcile project.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Per-peer replay protection for the authenticated modes (AGENTS.md §8).
//!
//! Every authenticated datagram carries a 16-byte replay header (`seq || stamp`, little-endian,
//! ms since epoch) inside the authenticated region. A `seq` already seen or behind the sliding
//! bitmap is rejected, as is a `stamp` deviating from local time by more than
//! [`FRESHNESS_WINDOW_DEFAULT`]. Unauthenticated mode carries no header and is exempt.
//!
//! Three rules the code must keep:
//!
//! - **Replay state outlives membership.** A decommissioned peer keeps its filter entry, or a
//! captured datagram re-adds it to `members` and re-poisons causal stability. The staleness
//! purge is sound only because no datagram can raise `stamp_at_max` without being accepted or
//! triggering `reset`.
//! - **Restart beats regression.** For `seq <= max_seq`, `stamp > stamp_at_max` means a genuine
//! restart and resets the state; otherwise the bitmap decides. *Residual*: a restart within the
//! same millisecond is indistinguishable from a replay and is dropped.
//! - **Post-restart tail guard.** `PeerState::max_stamp_seen`, never rewound by `reset`, blocks a
//! forward-path datagram with a strictly lower stamp; strict `<` because same-millisecond bursts
//! share a stamp. Relies on [`SenderCounter::next_stamp`]'s in-process floor. *Residual*: a
//! sender restarting with its clock behind its own stamps is treated as a replay until the clock
//! catches up.
//!
//! Split across siblings by concern: `wire` owns [`Seq`]/[`Stamp`]'s encoding, ordering and
//! freshness check; `bitmap` owns the sliding out-of-order acceptance window; `peer_state` owns
//! the per-peer accept/restart decision; `sender` owns [`SenderCounter`]'s monotonic issuance;
//! `filter` owns [`ReplayFilter`]'s per-peer map, staleness purge and public entry points. This
//! file keeps the public type definitions (their module location is their `cargo public-api`-
//! visible path — see AGENTS.md §11) plus the shared support (`WINDOW_SIZE`, `phys_now_ms`) every
//! sibling draws on.
use HashMap;
use IpAddr;
use AtomicU64;
use Duration;
use Utc;
use Mutex;
use PeerState;
/// Length of the replay header prepended to the authenticated portion of every datagram.
///
/// `seq (8 bytes) || stamp (8 bytes)`.
pub const REPLAY_HEADER_LEN: usize = 16;
/// Default freshness window: datagrams whose sender wall-clock stamp deviates from local physical
/// time by more than this value in either direction are rejected.
pub const FRESHNESS_WINDOW_DEFAULT: Duration = from_secs; // 5 minutes
/// Size of the out-of-order acceptance bitmap: a `seq` up to this far behind `max_seq` is accepted
/// as legitimate UDP reordering (one bit per relative sequence number); older is rejected.
const WINDOW_SIZE: u64 = 1024;
/// Read the local physical time as milliseconds since the Unix epoch.
/// A per-sender monotonic sequence number carried in the replay header. This module owns its wire
/// encoding and its ordering semantics.
;
/// A sender wall-clock stamp (milliseconds since the Unix epoch) carried in the replay header.
///
/// This module owns its wire encoding and its freshness check.
;
/// Sender-side replay state, one per node. `stamp_floor` keeps minted stamps monotonic within the
/// process — the guarantee the receiver's tail guard relies on, lost on restart (module docs).
/// Receiver-side per-peer replay filter.
///
/// Entries are purged once `now - stamp_at_max > window`, at which point no replayable datagram
/// could clear the freshness check anyway. `enabled` mirrors the owning
/// [`crate::auth::Authenticator`]'s mode, fixed at construction; a disabled filter accepts
/// everything, so no caller decides whether replay-checking applies.