use std::sync::LazyLock;
use dashmap::DashMap;
use freenet_stdlib::prelude::DelegateKey;
use tokio::sync::mpsc;
use crate::client_events::{ClientId, HostResult};
pub(crate) const MAX_APPS_PER_DELEGATE: usize = 128;
pub(crate) const LOCAL_RESERVED_SLOTS: usize = 32;
pub(crate) const MAX_DELEGATES_PER_CLIENT: usize = 256;
pub(crate) const REGISTRATION_TTL: std::time::Duration = std::time::Duration::from_secs(30 * 60);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AppIdentity {
Local,
Remote,
}
impl AppIdentity {
fn may_receive(self) -> bool {
matches!(self, Self::Local)
}
}
struct AppRegistration {
client_id: ClientId,
identity: AppIdentity,
withhold_logged: bool,
sender: mpsc::Sender<HostResult>,
last_seen: tokio::time::Instant,
}
impl AppRegistration {
fn latch_withhold_report(&mut self) -> bool {
if self.withhold_logged {
return false;
}
self.withhold_logged = true;
true
}
}
static DELEGATE_APPS: LazyLock<DashMap<DelegateKey, Vec<AppRegistration>>> =
LazyLock::new(DashMap::default);
static CLIENT_REGISTRATION_COUNTS: LazyLock<DashMap<ClientId, usize>> =
LazyLock::new(DashMap::default);
pub(crate) fn register_app(
delegate_key: &DelegateKey,
client_id: ClientId,
identity: AppIdentity,
sender: mpsc::Sender<HostResult>,
) -> bool {
let now = tokio::time::Instant::now();
let mut apps = DELEGATE_APPS.entry(delegate_key.clone()).or_default();
if let Some(existing) = apps.iter_mut().find(|a| a.client_id == client_id) {
existing.sender = sender;
existing.identity = identity;
existing.last_seen = now;
return true;
}
if apps.len() >= MAX_APPS_PER_DELEGATE {
tracing::warn!(
delegate = %delegate_key,
%client_id,
cap = MAX_APPS_PER_DELEGATE,
"Rejecting app registration: delegate at max apps"
);
return false;
}
if !identity.may_receive() {
let remote_occupancy = apps.iter().filter(|a| !a.identity.may_receive()).count();
if remote_occupancy >= MAX_APPS_PER_DELEGATE - LOCAL_RESERVED_SLOTS {
tracing::warn!(
delegate = %delegate_key,
%client_id,
cap = MAX_APPS_PER_DELEGATE - LOCAL_RESERVED_SLOTS,
"Rejecting non-local app registration: at the off-host cap, which \
exists so a local app can always register (GHSA-824h-7x5x-wfmf)"
);
return false;
}
}
let mut count = CLIENT_REGISTRATION_COUNTS.entry(client_id).or_insert(0);
if *count >= MAX_DELEGATES_PER_CLIENT {
tracing::warn!(
delegate = %delegate_key,
%client_id,
cap = MAX_DELEGATES_PER_CLIENT,
"Rejecting app registration: client at max delegate registrations"
);
return false;
}
apps.push(AppRegistration {
client_id,
identity,
withhold_logged: false,
sender,
last_seen: now,
});
*count += 1;
true
}
pub(crate) fn route_to_apps(delegate_key: &DelegateKey, message: HostResult) -> usize {
let Some(mut apps) = DELEGATE_APPS.get_mut(delegate_key) else {
return 0;
};
let mut delivered = 0usize;
let mut closed_clients: Vec<ClientId> = Vec::new();
apps.retain_mut(|app| {
if !app.identity.may_receive() {
if app.sender.is_closed() {
closed_clients.push(app.client_id);
return false;
}
if app.latch_withhold_report() {
tracing::info!(
delegate = %delegate_key,
client_id = %app.client_id,
"Withholding delegate notifications from a non-local \
registration (GHSA-824h-7x5x-wfmf); the client is connected \
but off-host. Logged once per registration."
);
}
return true;
}
match app.sender.try_send(message.clone()) {
Ok(()) => {
delivered += 1;
true
}
Err(mpsc::error::TrySendError::Full(_)) => {
tracing::warn!(
delegate = %delegate_key,
client_id = %app.client_id,
"App notification channel full — delegate ApplicationMessage dropped"
);
true
}
Err(mpsc::error::TrySendError::Closed(_)) => {
closed_clients.push(app.client_id);
false
}
}
});
let now_empty = apps.is_empty();
drop(apps);
for client_id in closed_clients {
decrement_client_count(client_id);
}
if now_empty {
DELEGATE_APPS.remove_if(delegate_key, |_, v| v.is_empty());
}
delivered
}
pub(crate) fn remove_client(client_id: ClientId) {
let mut removed_any = false;
DELEGATE_APPS.retain(|_, apps| {
let before = apps.len();
apps.retain(|a| a.client_id != client_id);
removed_any |= apps.len() != before;
!apps.is_empty()
});
if removed_any {
CLIENT_REGISTRATION_COUNTS.remove(&client_id);
}
}
pub(crate) fn remove_delegate(delegate_key: &DelegateKey) {
if let Some((_, apps)) = DELEGATE_APPS.remove(delegate_key) {
for app in apps {
decrement_client_count(app.client_id);
}
}
}
pub(crate) fn sweep_expired() {
let now = tokio::time::Instant::now();
let mut expired: Vec<ClientId> = Vec::new();
DELEGATE_APPS.retain(|_, apps| {
apps.retain(|a| {
let keep = now.saturating_duration_since(a.last_seen) < REGISTRATION_TTL;
if !keep {
expired.push(a.client_id);
}
keep
});
!apps.is_empty()
});
for client_id in expired {
decrement_client_count(client_id);
}
}
fn decrement_client_count(client_id: ClientId) {
if let Some(mut count) = CLIENT_REGISTRATION_COUNTS.get_mut(&client_id) {
*count = count.saturating_sub(1);
if *count == 0 {
drop(count);
CLIENT_REGISTRATION_COUNTS.remove_if(&client_id, |_, v| *v == 0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use freenet_stdlib::prelude::CodeHash;
use std::sync::atomic::{AtomicUsize, Ordering};
static NS: AtomicUsize = AtomicUsize::new(1);
struct Namespace(usize);
impl Namespace {
fn new() -> Self {
Namespace(NS.fetch_add(1, Ordering::Relaxed))
}
fn key(&self, n: usize) -> DelegateKey {
let mut bytes = [0u8; 32];
bytes[0..8].copy_from_slice(&(self.0 as u64).to_le_bytes());
bytes[8..16].copy_from_slice(&(n as u64).to_le_bytes());
DelegateKey::new(bytes, CodeHash::new(bytes))
}
fn client(&self, n: usize) -> ClientId {
ClientId((self.0 << 24) | (n & 0xFF_FFFF))
}
}
fn host_msg() -> HostResult {
use freenet_stdlib::client_api::HostResponse;
use freenet_stdlib::prelude::{ApplicationMessage, OutboundDelegateMsg};
Ok(HostResponse::DelegateResponse {
key: DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32])),
values: vec![OutboundDelegateMsg::ApplicationMessage(
ApplicationMessage::new(vec![1]),
)],
})
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn register_and_route_delivers_to_app() {
let ns = Namespace::new();
let dk = ns.key(0);
let (tx, mut rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, ns.client(0), AppIdentity::Local, tx));
let delivered = route_to_apps(&dk, host_msg());
assert_eq!(delivered, 1);
assert!(rx.try_recv().is_ok(), "app must receive the message");
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn route_to_unknown_delegate_delivers_nothing() {
let ns = Namespace::new();
let delivered = route_to_apps(&ns.key(0), host_msg());
assert_eq!(delivered, 0);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn per_delegate_cap_rejects_excess_apps() {
let ns = Namespace::new();
let dk = ns.key(0);
for i in 0..MAX_APPS_PER_DELEGATE {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, ns.client(i), AppIdentity::Local, tx));
}
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(
!register_app(
&dk,
ns.client(MAX_APPS_PER_DELEGATE),
AppIdentity::Local,
tx
),
"registration past per-delegate cap must be rejected"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn per_client_cap_rejects_excess_delegates() {
let ns = Namespace::new();
let client = ns.client(0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i), client, AppIdentity::Local, tx));
}
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(
!register_app(
&ns.key(MAX_DELEGATES_PER_CLIENT),
client,
AppIdentity::Local,
tx
),
"registration past per-client cap must be rejected"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn remove_client_frees_registrations() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, client, AppIdentity::Local, tx));
remove_client(client);
assert_eq!(route_to_apps(&dk, host_msg()), 0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i + 1), client, AppIdentity::Local, tx));
}
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn closed_channel_is_pruned_on_route() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, client, AppIdentity::Local, tx));
drop(rx); assert_eq!(route_to_apps(&dk, host_msg()), 0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (t, _r) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i + 1), client, AppIdentity::Local, t));
}
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn ttl_sweep_evicts_stale_registration() {
let ns = Namespace::new();
let dk = ns.key(0);
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, ns.client(0), AppIdentity::Local, tx));
tokio::time::advance(REGISTRATION_TTL + std::time::Duration::from_secs(1)).await;
sweep_expired();
assert_eq!(
route_to_apps(&dk, host_msg()),
0,
"stale registration must be swept after TTL"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn reregister_refreshes_ttl() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, mut rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, client, AppIdentity::Local, tx.clone()));
tokio::time::advance(REGISTRATION_TTL - std::time::Duration::from_secs(10)).await;
assert!(register_app(&dk, client, AppIdentity::Local, tx));
tokio::time::advance(std::time::Duration::from_secs(20)).await;
sweep_expired();
assert_eq!(
route_to_apps(&dk, host_msg()),
1,
"refreshed registration must survive"
);
assert!(rx.try_recv().is_ok());
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn route_skips_non_local_registrations() {
let ns = Namespace::new();
let dk = ns.key(0);
let (app_tx, mut app_rx) = mpsc::channel::<HostResult>(4);
let (cli_tx, mut cli_rx) = mpsc::channel::<HostResult>(4);
let (remote_tx, mut remote_rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, ns.client(0), AppIdentity::Local, app_tx));
assert!(register_app(&dk, ns.client(1), AppIdentity::Local, cli_tx));
assert!(register_app(
&dk,
ns.client(2),
AppIdentity::Remote,
remote_tx
));
let delivered = route_to_apps(&dk, host_msg());
assert_eq!(delivered, 2, "both local registrations must receive it");
assert!(
app_rx.try_recv().is_ok(),
"the local web app must receive it"
);
assert!(
cli_rx.try_recv().is_ok(),
"the local tokenless CLI must keep receiving it — riverctl, atlasctl \
and fdev all register in this shape"
);
assert!(
remote_rx.try_recv().is_err(),
"an off-host registration must never receive a delegate's output"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn skipped_remote_registration_is_retained_not_evicted() {
let ns = Namespace::new();
let dk = ns.key(0);
let (remote_tx, _remote_rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(
&dk,
ns.client(0),
AppIdentity::Remote,
remote_tx
));
assert_eq!(route_to_apps(&dk, host_msg()), 0);
assert!(
DELEGATE_APPS.get(&dk).is_some_and(|apps| apps.len() == 1),
"the registration must survive a skipped delivery"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn dead_non_local_registration_is_pruned_not_parked() {
let ns = Namespace::new();
let dk = ns.key(0);
let (remote_tx, remote_rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(
&dk,
ns.client(0),
AppIdentity::Remote,
remote_tx
));
drop(remote_rx);
assert_eq!(
route_to_apps(&dk, host_msg()),
0,
"a non-local registration is never a delivery target"
);
assert!(
DELEGATE_APPS.get(&dk).is_none_or(|apps| apps.is_empty()),
"a dead non-local registration must be reaped, or an off-host caller \
can hold every slot for the whole TTL and starve the local app"
);
assert!(
CLIENT_REGISTRATION_COUNTS
.get(&ns.client(0))
.is_none_or(|c| *c == 0),
"pruning must release the client's registration count"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn open_remote_connections_cannot_crowd_out_a_local_app() {
let ns = Namespace::new();
let dk = ns.key(0);
let mut keepalive = Vec::new();
let mut accepted = 0usize;
for i in 0..MAX_APPS_PER_DELEGATE {
let (tx, rx) = mpsc::channel::<HostResult>(1);
keepalive.push(rx);
if register_app(&dk, ns.client(i), AppIdentity::Remote, tx) {
accepted += 1;
}
}
assert_eq!(
accepted,
MAX_APPS_PER_DELEGATE - LOCAL_RESERVED_SLOTS,
"off-host registrations must stop at the reserved boundary"
);
let (local_tx, mut local_rx) = mpsc::channel::<HostResult>(4);
assert!(
register_app(
&dk,
ns.client(MAX_APPS_PER_DELEGATE + 1),
AppIdentity::Local,
local_tx
),
"a local app must always be able to register, however many off-host \
clients are parked on this delegate"
);
assert_eq!(route_to_apps(&dk, host_msg()), 1);
assert!(local_rx.try_recv().is_ok());
drop(keepalive);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn withhold_is_reported_once_per_registration() {
let ns = Namespace::new();
let dk = ns.key(0);
let (remote_tx, _remote_rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(
&dk,
ns.client(0),
AppIdentity::Remote,
remote_tx
));
for _ in 0..5 {
assert_eq!(route_to_apps(&dk, host_msg()), 0);
}
let spent = DELEGATE_APPS
.get_mut(&dk)
.map(|mut apps| apps.iter_mut().all(|a| !a.latch_withhold_report()))
.unwrap_or(false);
assert!(
spent,
"the report must be claimable only once per registration, or every \
contract-state change emits a shipped log line per parked connection"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn delivery_keys_only_on_locality_not_on_app_identity() {
let ns = Namespace::new();
let dk = ns.key(0);
let (a_tx, mut a_rx) = mpsc::channel::<HostResult>(4);
let (b_tx, mut b_rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, ns.client(0), AppIdentity::Local, a_tx));
assert!(register_app(&dk, ns.client(1), AppIdentity::Local, b_tx));
assert_eq!(route_to_apps(&dk, host_msg()), 2);
assert!(a_rx.try_recv().is_ok());
assert!(b_rx.try_recv().is_ok());
}
}