Federated relay tier: peer flooding, per-member isolation, registry admission and relay discovery

This commit is contained in:
George Coles
2026-09-15 06:49:43 -04:00
parent d30e612be8
commit 2c97fd523f
11 changed files with 759 additions and 177 deletions
+41 -115
View File
@@ -23,7 +23,7 @@ use crate::message::{
AggregateBody, Envelope, QueryBody, ResponseBody, ResponseItem, TYPE_AGGREGATE, TYPE_QUERY,
TYPE_RESPONSE, build_response, timestamp_is_fresh,
};
use crate::registry::{SignedRegistry, authorized_keys, load_registry, verify_registry};
use crate::registry::Watcher;
#[derive(Default)]
struct Aggregates {
@@ -37,8 +37,7 @@ pub struct Node {
index: LocalIndex,
members: RwLock<Vec<Member>>,
members_mtime: Mutex<Option<SystemTime>>,
registry: Mutex<Option<SignedRegistry>>,
registry_mtime: Mutex<Option<SystemTime>>,
registry: Option<Arc<Watcher>>,
aggregates: Mutex<Aggregates>,
seen: Mutex<HashSet<String>>,
pending: Mutex<HashMap<String, Vec<(String, ResponseBody)>>>,
@@ -122,14 +121,28 @@ impl Node {
.timeout(Duration::from_secs(15))
.build()
.context("building http client")?;
let node = Arc::new(Self {
let registry = match (
config.node.registry.as_deref(),
config.node.ma_key.as_deref(),
) {
(Some(source), Some(ma_key)) => Some(Watcher::new(
source,
ma_key,
Some(config.registry_cache_path()),
)),
(Some(_), None) => return Err(anyhow!("registry configured without ma_key")),
_ => None,
};
if let Some(watcher) = &registry {
watcher.load_initial();
}
Ok(Arc::new(Self {
config,
key,
index,
members: RwLock::new(members),
members_mtime: Mutex::new(members_mtime),
registry: Mutex::new(None),
registry_mtime: Mutex::new(None),
registry,
aggregates: Mutex::new(Aggregates::default()),
seen: Mutex::new(HashSet::new()),
pending: Mutex::new(HashMap::new()),
@@ -137,90 +150,7 @@ impl Node {
client,
sent: AtomicU64::new(0),
received: AtomicU64::new(0),
});
node.load_initial_registry();
Ok(node)
}
fn load_initial_registry(&self) {
let Some(source) = &self.config.node.registry else {
return;
};
let result = if source.starts_with("http://") || source.starts_with("https://") {
load_registry(&self.config.registry_cache_path())
} else {
let path = std::path::PathBuf::from(source);
let loaded = load_registry(&path);
if loaded.is_ok() {
let mtime = fs::metadata(&path)
.and_then(|metadata| metadata.modified())
.ok();
*self.registry_mtime.lock().expect("registry mtime lock") = mtime;
}
loaded
};
if let Ok(signed) = result {
if let Err(error) = self.verify_and_apply_registry(signed) {
eprintln!("cached registry rejected: {error}");
}
}
}
fn verify_and_apply_registry(&self, signed: SignedRegistry) -> Result<()> {
let ma_key = self
.config
.node
.ma_key
.as_deref()
.ok_or_else(|| anyhow!("registry configured without ma_key"))?;
verify_registry(&signed, ma_key)?;
{
let current = self.registry.lock().expect("registry lock");
if let Some(existing) = current.as_ref() {
if signed.doc.version <= existing.doc.version {
return Err(anyhow!("registry version rollback rejected"));
}
}
}
*self.registry.lock().expect("registry lock") = Some(signed);
Ok(())
}
fn refresh_registry(&self) {
let Some(source) = &self.config.node.registry else {
return;
};
if source.starts_with("http://") || source.starts_with("https://") {
return;
}
let path = std::path::PathBuf::from(source);
let mtime = fs::metadata(&path)
.and_then(|metadata| metadata.modified())
.ok();
{
let last = self.registry_mtime.lock().expect("registry mtime lock");
if *last == mtime {
return;
}
}
let Ok(signed) = load_registry(&path) else {
return;
};
*self.registry_mtime.lock().expect("registry mtime lock") = mtime;
if let Err(error) = self.verify_and_apply_registry(signed) {
eprintln!("registry update rejected: {error}");
}
}
async fn fetch_registry(&self) -> Result<()> {
let Some(url) = &self.config.node.registry else {
return Ok(());
};
let response = self.client.get(url).send().await?;
let signed: SignedRegistry = response.json().await?;
self.verify_and_apply_registry(signed.clone())?;
crate::registry::save_registry(&self.config.registry_cache_path(), &signed)?;
Ok(())
}))
}
fn refresh_members(&self) {
@@ -311,23 +241,31 @@ impl Node {
.with_context(|| format!("binding {}", node.config.node.listen))?;
let addr = listener.local_addr()?;
let mut tasks = Vec::new();
if let Some(source) = &node.config.node.registry {
if source.starts_with("http://") || source.starts_with("https://") {
if let Err(error) = node.fetch_registry().await {
if let Some(watcher) = node.registry.clone() {
if watcher.is_url() {
if let Err(error) = watcher.fetch(&node.client).await {
eprintln!("initial registry fetch failed: {error}");
}
let node_for_registry = node.clone();
let client = node.client.clone();
tasks.push(tokio::spawn(async move {
loop {
if let Err(error) = node_for_registry.fetch_registry().await {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(error) = watcher.fetch(&client).await {
eprintln!("registry fetch failed: {error}");
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
}));
}
}
for relay in node.config.node.relays.clone() {
let relays = if node.config.node.relays.is_empty() {
node.registry
.as_ref()
.map(|watcher| watcher.relays())
.unwrap_or_default()
} else {
node.config.node.relays.clone()
};
for relay in relays {
tasks.push(tokio::spawn(poll_relay(node.clone(), relay)));
}
let app = router(node.clone());
@@ -456,18 +394,11 @@ impl Node {
return;
}
self.refresh_members();
self.refresh_registry();
let (class, listed) = if self.config.node.registry.is_some() {
let registry = self.registry.lock().expect("registry lock");
let authorized = registry
.as_ref()
.map(|signed| authorized_keys(signed, now_ts()));
match authorized {
Some(map) => match map.get(&envelope.key) {
Some((id, class)) if id == &envelope.from => (Some(class.clone()), true),
_ => (None, false),
},
None => (None, false),
let (class, listed) = if let Some(watcher) = &self.registry {
watcher.refresh_if_changed();
match watcher.authorized(&envelope.key, now_ts()) {
Some((id, class)) if id == envelope.from => (Some(class), true),
_ => (None, false),
}
} else {
let members = self.members.read().expect("members lock");
@@ -761,12 +692,7 @@ async fn local_query(
async fn local_status(State(node): State<Arc<Node>>) -> Response {
let aggregates = node.aggregate_for(&current_period(), None);
let registry_version = node
.registry
.lock()
.expect("registry lock")
.as_ref()
.map(|signed| signed.doc.version);
let registry_version = node.registry.as_ref().and_then(|watcher| watcher.version());
Json(json!({
"name": node.config.node.name,
"id": node.config.node.id,