Federated relay tier: peer flooding, per-member isolation, registry admission and relay discovery
This commit is contained in:
+41
-115
@@ -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) = ®istry {
|
||||
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(¤t_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,
|
||||
|
||||
Reference in New Issue
Block a user