clawft_kernel/
mesh_swarm_revoke_log.rs1use std::path::{Path, PathBuf};
26use std::collections::HashMap;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::time::{Duration, Instant};
30
31use super::*;
32use crate::mesh_discovery::MeshPeerEvent;
33
34pub const MAX_LOGGED: usize = 1024;
36pub const MAX_CONCURRENT_REPLAYS: usize = 8;
38pub const REPLAY_COOLDOWN: Duration = Duration::from_secs(60);
40const MAX_LOG_FILE_BYTES: u64 = 8 * 1024 * 1024;
42
43#[derive(Default)]
45pub(super) struct NoticeLog {
46 pub(super) entries: Vec<SignedRevocation>,
47 file: Option<PathBuf>,
48}
49
50pub(super) struct ReplayState {
52 running: bool,
53 started: Instant,
54 cancel: Arc<AtomicBool>,
55}
56
57pub(super) type Replays = HashMap<String, ReplayState>;
58
59fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
60 use std::io::Write;
61 let tmp = path.with_extension("json.tmp");
62 let mut opts = std::fs::OpenOptions::new();
63 opts.create(true).write(true).truncate(true);
64 #[cfg(unix)]
65 {
66 use std::os::unix::fs::OpenOptionsExt;
67 opts.mode(0o600);
68 }
69 let mut f = opts.open(&tmp)?;
70 f.write_all(bytes)?;
71 f.sync_all()?;
72 std::fs::rename(&tmp, path)
73}
74
75impl RevocationExchange {
76 pub fn with_log_file(&self, path: impl Into<PathBuf>) -> usize {
83 let path = path.into();
84 let loaded: Vec<SignedRevocation> = std::fs::symlink_metadata(&path)
85 .ok()
86 .filter(|m| m.file_type().is_file() && m.len() <= MAX_LOG_FILE_BYTES)
87 .and_then(|_| std::fs::read(&path).ok())
88 .and_then(|b| serde_json::from_slice(&b).ok())
89 .unwrap_or_default();
90 let mut kept = Vec::new();
91 for s in loaded {
92 let signer_ok = s
93 .public_key
94 .as_slice()
95 .try_into()
96 .map(|pk: [u8; 32]| {
97 !self.list.is_subject_revoked(
98 RevocationKind::SignerKey,
99 &crate::workload_pkg::codec::hex_encode(&pk),
100 )
101 })
102 .unwrap_or(false);
103 if signer_ok && self.is_current(&s) && verify_revocation(&s, &self.anchors).is_ok() {
104 let key = Self::notice_key(&s);
106 let mut g = self.seen.lock().unwrap_or_else(|p| p.into_inner());
107 if g.0.insert(key) {
108 g.1.push_back(key);
109 }
110 kept.push(s);
111 }
112 }
113 let n = kept.len();
114 let mut log = self.log.lock().unwrap_or_else(|p| p.into_inner());
115 log.entries = kept;
116 log.file = Some(path);
117 n
118 }
119
120 fn is_current(&self, s: &SignedRevocation) -> bool {
123 let Ok(n) = serde_json::from_str::<RevocationNotice>(&s.payload) else {
124 return false;
125 };
126 let signer_revoked = <[u8; 32]>::try_from(s.public_key.as_slice()).is_ok_and(|pk| {
127 self.list
128 .is_subject_revoked(RevocationKind::SignerKey, &crate::workload_pkg::codec::hex_encode(&pk))
129 });
130 !signer_revoked && self.list.is_subject_revoked(n.kind, &n.id)
131 }
132
133 pub fn forget_subject(&self, kind: RevocationKind, id: &str) -> usize {
136 let Ok(id) = kind.normalize(id) else { return 0 };
137 let mut log = self.log.lock().unwrap_or_else(|p| p.into_inner());
138 let before = log.entries.len();
139 log.entries.retain(|e| {
140 serde_json::from_str::<RevocationNotice>(&e.payload).is_ok_and(|n| !(n.kind == kind && n.id == id))
141 });
142 let dropped = before - log.entries.len();
143 if dropped > 0 {
144 Self::save_log(&log);
145 }
146 dropped
147 }
148
149 fn save_log(log: &NoticeLog) {
150 if let Some(path) = &log.file
151 && let Err(e) = serde_json::to_vec(&log.entries)
152 .map_err(std::io::Error::other)
153 .and_then(|b| write_atomic(path, &b))
154 {
155 tracing::warn!(path = %path.display(), error = %e, "revocation log not saved");
156 }
157 }
158
159 pub(super) fn log_notice(&self, signed: &SignedRevocation) {
161 let mut log = self.log.lock().unwrap_or_else(|p| p.into_inner());
162 if log.entries.contains(signed) {
163 return;
164 }
165 log.entries.push(signed.clone());
166 if log.entries.len() > MAX_LOGGED {
167 log.entries.remove(0);
168 }
169 Self::save_log(&log);
170 }
171
172 pub fn logged(&self) -> Vec<SignedRevocation> {
176 let mut log = self.log.lock().unwrap_or_else(|p| p.into_inner());
177 let before = log.entries.len();
178 log.entries.retain(|e| self.is_current(e));
179 if log.entries.len() != before {
180 Self::save_log(&log);
181 }
182 log.entries.clone()
183 }
184
185 pub fn replays_started(&self) -> u64 {
187 self.replays_started.load(Ordering::Relaxed)
188 }
189
190 pub async fn replay_to(&self, peer: &str) -> usize {
195 self.replay_cancellable(peer, &AtomicBool::new(false)).await
196 }
197
198 async fn replay_cancellable(&self, peer: &str, cancel: &AtomicBool) -> usize {
199 let mut sent = 0usize;
200 for signed in self.logged() {
201 if cancel.load(Ordering::Relaxed) {
202 break;
203 }
204 let Ok(value) = serde_json::to_value(&signed) else {
205 continue;
206 };
207 if sent as f64 >= NOTICE_BURST {
208 tokio::time::sleep(Duration::from_secs_f64(1.0 / NOTICES_PER_SEC)).await;
209 if cancel.load(Ordering::Relaxed) {
210 break;
211 }
212 }
213 let msg = KernelMessage::new(
214 0,
215 MessageTarget::Topic(REVOKE_TOPIC.to_string()),
216 MessagePayload::Json(value),
217 );
218 if let Err(e) = self.runtime.route_to_remote(peer, msg).await {
219 tracing::debug!(peer, error = %e, "revocation replay stopped");
220 break;
221 }
222 sent += 1;
223 }
224 sent
225 }
226
227 fn start_replay(me: &Arc<Self>, peer: String) {
230 let cancel = {
231 let mut g = me.replays.lock().unwrap_or_else(|p| p.into_inner());
232 if let Some(r) = g.get(&peer)
233 && (r.running || r.started.elapsed() < REPLAY_COOLDOWN)
234 {
235 return;
236 }
237 if g.values().filter(|r| r.running).count() >= MAX_CONCURRENT_REPLAYS {
241 return;
242 }
243 if g.len() >= 1024 && !g.contains_key(&peer) {
245 g.retain(|_, r| r.running);
246 }
247 let cancel = Arc::new(AtomicBool::new(false));
248 g.insert(
249 peer.clone(),
250 ReplayState {
251 running: true,
252 started: Instant::now(),
253 cancel: cancel.clone(),
254 },
255 );
256 cancel
257 };
258 me.replays_started.fetch_add(1, Ordering::Relaxed);
259 let x = me.clone();
260 tokio::spawn(async move {
261 x.replay_cancellable(&peer, &cancel).await;
262 if let Some(r) = x.replays.lock().unwrap_or_else(|p| p.into_inner()).get_mut(&peer) {
263 r.running = false;
264 }
265 });
266 }
267
268 pub(super) fn spawn_rejoin_replay(me: &Arc<Self>) {
272 let Ok(handle) = tokio::runtime::Handle::try_current() else {
273 return;
274 };
275 let mut rx = me.runtime.subscribe_peer_events();
276 let weak = Arc::downgrade(me);
277 handle.spawn(async move {
278 loop {
279 let ev = match rx.recv().await {
280 Ok(e) => e,
281 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
282 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
283 };
284 let Some(x) = weak.upgrade() else { break };
285 match ev {
286 MeshPeerEvent::Joined { node_id, verified: true, .. }
287 | MeshPeerEvent::Recovered { node_id, verified: true, .. } => Self::start_replay(&x, node_id),
288 MeshPeerEvent::Left { node_id } => {
289 if let Some(r) = x.replays.lock().unwrap_or_else(|p| p.into_inner()).get(&node_id) {
290 r.cancel.store(true, Ordering::Relaxed);
291 }
292 }
293 _ => {}
294 }
295 }
296 });
297 }
298}