1use std::collections::BTreeMap;
27use std::sync::Mutex;
28use std::time::{Duration, Instant};
29
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33use super::identity::{InstanceId, NodeId};
34use super::protocol::Advertisement;
35use super::trust::TrustState;
36
37pub const MAX_NODES: usize = 256;
40
41pub const PRESENCE_TTL: Duration = Duration::from_secs(60);
43
44pub const RETENTION: Duration = Duration::from_secs(15 * 60);
46
47#[derive(
50 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
51)]
52#[serde(rename_all = "snake_case")]
53pub enum MeshSource {
54 UdpMulticast,
56 UdpBroadcast,
58 Rendezvous,
60 Synthetic,
62}
63
64impl MeshSource {
65 pub fn as_str(&self) -> &'static str {
67 match self {
68 MeshSource::UdpMulticast => "udp_multicast",
69 MeshSource::UdpBroadcast => "udp_broadcast",
70 MeshSource::Rendezvous => "rendezvous",
71 MeshSource::Synthetic => "synthetic",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78#[serde(rename_all = "snake_case")]
79pub enum Presence {
80 Present,
82 Absent,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
89pub struct Sighting {
90 pub source: MeshSource,
92 pub path: String,
94 pub at: String,
96}
97
98impl crate::order::Ordered for Sighting {
104 fn order_key(&self) -> crate::order::OrderKey<'_> {
105 crate::order::OrderKey::plain(&self.path, &self.at).ranked(self.source as i64)
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
111pub struct NodeRecord {
112 pub node_id: NodeId,
114 #[serde(default)]
118 pub runtime: String,
119 pub public_key: String,
121 pub display_name: String,
123 pub instance_id: InstanceId,
125 pub trust: TrustState,
127 pub presence: Presence,
129 pub endpoints: Vec<String>,
131 pub capabilities: Vec<String>,
133 pub repositories: Vec<String>,
135 pub protocol_version: u32,
137 pub version: String,
139 pub sources: Vec<Sighting>,
141 pub first_seen: String,
143 pub last_seen: String,
145 pub restarts: u64,
147}
148
149#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
151pub struct Tallies {
152 pub nodes: usize,
154 pub trusted: usize,
156 pub present: usize,
158 pub accepted: u64,
160 pub replayed: u64,
162 pub expired: u64,
164 pub evicted: u64,
166}
167
168struct Slot {
169 record: NodeRecord,
170 last_seen: Instant,
171 last_seq: u64,
173 raw: serde_json::Value,
176}
177
178struct Inner {
179 slots: BTreeMap<(NodeId, String), Slot>,
181 tallies: Tallies,
182}
183
184pub struct MeshRegistry {
187 inner: Mutex<Inner>,
188}
189
190impl Default for MeshRegistry {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl MeshRegistry {
197 pub fn new() -> Self {
199 MeshRegistry {
200 inner: Mutex::new(Inner {
201 slots: BTreeMap::new(),
202 tallies: Tallies::default(),
203 }),
204 }
205 }
206
207 pub fn known_key(&self, node: &NodeId) -> Option<String> {
209 let inner = self.inner.lock().expect("mesh registry lock");
210 inner
211 .slots
212 .iter()
213 .find(|((n, _), _)| n == node)
214 .map(|(_, s)| s.record.public_key.clone())
215 }
216
217 pub fn trust_of(&self, node: &NodeId) -> Option<TrustState> {
220 let inner = self.inner.lock().expect("mesh registry lock");
221 let states: Vec<&TrustState> = inner
222 .slots
223 .iter()
224 .filter(|((n, _), _)| n == node)
225 .map(|(_, s)| &s.record.trust)
226 .collect();
227 states
228 .iter()
229 .find(|t| matches!(t, TrustState::Rejected(_)))
230 .or(states.first())
231 .map(|t| (*t).clone())
232 }
233
234 pub fn observe(
238 &self,
239 adv: &Advertisement,
240 node_id: NodeId,
241 trust: TrustState,
242 source: MeshSource,
243 path: &str,
244 raw: serde_json::Value,
245 ) -> bool {
246 let now = Instant::now();
247 let stamp = crate::peers::rfc3339(std::time::SystemTime::now());
248 let mut inner = self.inner.lock().expect("mesh registry lock");
249 self.expire_locked(&mut inner, now);
250 let key = (node_id.clone(), adv.rt.clone());
251 if let Some(slot) = inner.slots.get_mut(&key) {
252 let same_instance = slot.record.instance_id == adv.inst;
253 if same_instance && adv.seq <= slot.last_seq {
254 inner.tallies.replayed += 1;
255 return false;
256 }
257 if !same_instance {
258 slot.record.restarts += 1;
259 slot.record.instance_id = adv.inst.clone();
260 }
261 slot.last_seq = adv.seq;
262 slot.last_seen = now;
263 slot.record.display_name = adv.name.clone();
264 slot.record.endpoints = adv.ep.clone();
265 slot.record.capabilities = adv.caps.clone();
266 slot.record.repositories = adv.repos.clone();
267 slot.record.protocol_version = adv.v;
268 slot.record.version = adv.ver.clone();
269 slot.record.trust = trust;
270 slot.record.presence = Presence::Present;
271 slot.record.last_seen = stamp.clone();
272 slot.raw = raw;
273 let sighting = Sighting {
274 source,
275 path: path.into(),
276 at: stamp,
277 };
278 match slot.record.sources.iter_mut().find(|s| s.source == source) {
279 Some(existing) => *existing = sighting,
280 None => slot.record.sources.push(sighting),
281 }
282 crate::order::canonical(&mut slot.record.sources);
283 inner.tallies.accepted += 1;
284 return true;
285 }
286 if inner.slots.len() >= MAX_NODES {
292 let victim = inner
293 .slots
294 .iter()
295 .filter(|(_, s)| !matches!(&s.record.trust, TrustState::Trusted(by) if by == "allowlist"))
296 .min_by_key(|(_, s)| s.last_seen)
297 .map(|(id, _)| id.clone());
298 match victim {
299 Some(id) => {
300 inner.slots.remove(&id);
301 inner.tallies.evicted += 1;
302 }
303 None => return false,
304 }
305 }
306 inner.slots.insert(
307 key,
308 Slot {
309 record: NodeRecord {
310 node_id,
311 runtime: adv.rt.clone(),
312 public_key: adv.pk.clone(),
313 display_name: adv.name.clone(),
314 instance_id: adv.inst.clone(),
315 trust,
316 presence: Presence::Present,
317 endpoints: adv.ep.clone(),
318 capabilities: adv.caps.clone(),
319 repositories: adv.repos.clone(),
320 protocol_version: adv.v,
321 version: adv.ver.clone(),
322 sources: vec![Sighting {
323 source,
324 path: path.into(),
325 at: stamp.clone(),
326 }],
327 first_seen: stamp.clone(),
328 last_seen: stamp,
329 restarts: 0,
330 },
331 last_seen: now,
332 last_seq: adv.seq,
333 raw,
334 },
335 );
336 inner.tallies.accepted += 1;
337 true
338 }
339
340 pub fn candidates_for(&self, limit: usize, repos: &[String]) -> Vec<serde_json::Value> {
351 let now = Instant::now();
352 let mut inner = self.inner.lock().expect("mesh registry lock");
353 self.expire_locked(&mut inner, now);
354 inner
355 .slots
356 .values()
357 .filter(|s| {
358 now.duration_since(s.last_seen) <= PRESENCE_TTL
359 && s.record.trust.is_trusted()
360 && s.record.repositories.iter().any(|r| repos.contains(r))
361 && !s.raw.is_null()
362 })
363 .take(limit)
364 .map(|s| s.raw.clone())
365 .collect()
366 }
367
368 pub fn list(&self) -> Vec<NodeRecord> {
370 let now = Instant::now();
371 let mut inner = self.inner.lock().expect("mesh registry lock");
372 self.expire_locked(&mut inner, now);
373 inner
374 .slots
375 .values()
376 .map(|slot| {
377 let mut record = slot.record.clone();
378 record.presence = if now.duration_since(slot.last_seen) <= PRESENCE_TTL {
379 Presence::Present
380 } else {
381 Presence::Absent
382 };
383 record
384 })
385 .collect()
386 }
387
388 pub fn tallies(&self) -> Tallies {
390 let now = Instant::now();
391 let mut inner = self.inner.lock().expect("mesh registry lock");
392 self.expire_locked(&mut inner, now);
393 let mut tallies = inner.tallies;
394 tallies.nodes = inner.slots.len();
395 tallies.trusted = inner
396 .slots
397 .values()
398 .filter(|s| s.record.trust.is_trusted())
399 .count();
400 tallies.present = inner
401 .slots
402 .values()
403 .filter(|s| now.duration_since(s.last_seen) <= PRESENCE_TTL)
404 .count();
405 tallies
406 }
407
408 fn expire_locked(&self, inner: &mut Inner, now: Instant) {
409 let before = inner.slots.len();
410 inner
411 .slots
412 .retain(|_, slot| now.duration_since(slot.last_seen) <= RETENTION);
413 inner.tallies.expired += (before - inner.slots.len()) as u64;
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use crate::mesh::identity::NodeIdentity;
421 use crate::mesh::protocol::advertise;
422
423 fn node() -> (NodeIdentity, Advertisement) {
424 let dir = tempfile::tempdir().unwrap();
425 let id = NodeIdentity::load_or_create(&dir.path().join("node.json")).unwrap();
426 let env = advertise(
427 &id,
428 1,
429 &["127.0.0.1:8741".into()],
430 &["http".into()],
431 &[],
432 "0.5.0",
433 );
434 (id, env.adv)
435 }
436
437 #[test]
438 fn two_sources_converge_into_one_record() {
439 let registry = MeshRegistry::new();
440 let (id, adv) = node();
441 let mut second = adv.clone();
442 second.seq = 2;
443 assert!(registry.observe(
444 &adv,
445 id.public.node_id.clone(),
446 TrustState::Observed,
447 MeshSource::UdpMulticast,
448 "192.168.1.5:7741",
449 serde_json::Value::Null
450 ));
451 assert!(registry.observe(
452 &second,
453 id.public.node_id.clone(),
454 TrustState::Observed,
455 MeshSource::Rendezvous,
456 "http://127.0.0.1:8741",
457 serde_json::Value::Null
458 ));
459 let listed = registry.list();
460 assert_eq!(listed.len(), 1, "one node, not one per source");
461 let sources: Vec<MeshSource> = listed[0].sources.iter().map(|s| s.source).collect();
462 assert_eq!(
463 sources,
464 vec![MeshSource::UdpMulticast, MeshSource::Rendezvous]
465 );
466 }
467
468 #[test]
469 fn a_replay_is_dropped_and_counted() {
470 let registry = MeshRegistry::new();
471 let (id, adv) = node();
472 assert!(registry.observe(
473 &adv,
474 id.public.node_id.clone(),
475 TrustState::Observed,
476 MeshSource::UdpMulticast,
477 "a",
478 serde_json::Value::Null
479 ));
480 assert!(!registry.observe(
481 &adv,
482 id.public.node_id.clone(),
483 TrustState::Observed,
484 MeshSource::UdpMulticast,
485 "a",
486 serde_json::Value::Null
487 ));
488 assert_eq!(registry.tallies().replayed, 1);
489 }
490
491 #[test]
492 fn a_restart_keeps_the_record_and_counts() {
493 let registry = MeshRegistry::new();
494 let (id, adv) = node();
495 registry.observe(
496 &adv,
497 id.public.node_id.clone(),
498 TrustState::Observed,
499 MeshSource::UdpMulticast,
500 "a",
501 serde_json::Value::Null,
502 );
503 let dir = tempfile::tempdir().unwrap();
505 let restarted = NodeIdentity::load_or_create(&dir.path().join("other.json")).unwrap();
506 let mut again = adv.clone();
507 again.inst = restarted.public.instance_id.clone();
508 again.seq = 1;
509 assert!(registry.observe(
510 &again,
511 id.public.node_id.clone(),
512 TrustState::Observed,
513 MeshSource::UdpMulticast,
514 "a",
515 serde_json::Value::Null
516 ));
517 let listed = registry.list();
518 assert_eq!(listed.len(), 1, "a restart is not a second node");
519 assert_eq!(listed[0].restarts, 1);
520 }
521
522 #[test]
523 fn the_table_is_bounded_and_trusted_nodes_survive_pressure() {
524 let registry = MeshRegistry::new();
525 let (id, adv) = node();
526 registry.observe(
527 &adv,
528 id.public.node_id.clone(),
529 TrustState::Trusted("allowlist".into()),
530 MeshSource::Synthetic,
531 "t",
532 serde_json::Value::Null,
533 );
534 for i in 0..(MAX_NODES + 10) {
535 let (other_id, mut other) = node();
536 other.seq = 1;
537 other.name = format!("n{i}");
538 registry.observe(
539 &other,
540 other_id.public.node_id.clone(),
541 TrustState::Observed,
542 MeshSource::Synthetic,
543 "s",
544 serde_json::Value::Null,
545 );
546 }
547 let listed = registry.list();
548 assert!(listed.len() <= MAX_NODES);
549 assert!(
550 listed.iter().any(|r| r.node_id == id.public.node_id),
551 "the trusted node was not evicted by untrusted pressure"
552 );
553 assert!(registry.tallies().evicted > 0);
554 }
555
556 #[test]
557 fn listing_is_in_node_id_order() {
558 let registry = MeshRegistry::new();
559 for _ in 0..5 {
560 let (id, adv) = node();
561 registry.observe(
562 &adv,
563 id.public.node_id.clone(),
564 TrustState::Observed,
565 MeshSource::Synthetic,
566 "s",
567 serde_json::Value::Null,
568 );
569 }
570 let listed = registry.list();
571 let mut ids: Vec<String> = listed.iter().map(|r| r.node_id.to_string()).collect();
572 let sorted = {
573 let mut s = ids.clone();
574 s.sort();
575 s
576 };
577 assert_eq!(ids, sorted, "the registry lists in one deterministic order");
578 ids.dedup();
579 assert_eq!(ids.len(), listed.len());
580 }
581}