feat: wire up federation message routing and P2P client fallback

- Enqueue handler checks resolve_destination() for remote recipients
- User resolution supports user@domain federated addresses
- P2P mesh commands (/mesh start, /mesh stop) wired into client session
- Federation routing integration tests with SqlStore
- Fix DashMap deadlock in validate_session()
This commit is contained in:
2026-03-09 20:38:38 +01:00
parent 872695e5f1
commit 416618f4cf
11 changed files with 265 additions and 32 deletions

View File

@@ -105,7 +105,6 @@ pub struct FederationPeerConfig {
}
#[derive(Debug)]
#[allow(dead_code)] // federation not yet wired up
pub struct EffectiveFederationConfig {
pub enabled: bool,
pub domain: String,

View File

@@ -28,12 +28,15 @@ impl AuthService {
/// Validate a session token and return the caller's auth context.
pub fn validate_session(&self, token: &[u8]) -> Option<CallerAuth> {
let info = self.sessions.get(token)?;
if info.expires_at <= crate::auth::current_timestamp() {
let expires_at = info.expires_at;
let identity_key = info.identity_key.clone();
drop(info); // release read-lock before potential write
if expires_at <= crate::auth::current_timestamp() {
self.sessions.remove(token);
return None;
}
Some(CallerAuth {
identity_key: info.identity_key.clone(),
identity_key,
token: token.to_vec(),
device_id: None,
})

View File

@@ -175,6 +175,7 @@ pub struct UploadKeyPackageReq {
pub package: Vec<u8>,
}
#[derive(Debug)]
pub struct UploadKeyPackageResp {
pub fingerprint: Vec<u8>,
}
@@ -252,6 +253,7 @@ pub struct CreateChannelReq {
pub peer_key: Vec<u8>,
}
#[derive(Debug)]
pub struct CreateChannelResp {
pub channel_id: Vec<u8>,
pub was_new: bool,

View File

@@ -2,7 +2,6 @@
//!
//! A bare `username` (no `@`) is treated as local.
#![allow(dead_code)] // federation not yet wired up
/// A parsed federated address.
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -2,7 +2,6 @@
//!
//! Uses a lazy connection pool (DashMap) to reuse QUIC connections to known peers.
#![allow(dead_code)] // federation not yet wired up
use std::collections::HashMap;
use std::net::SocketAddr;

View File

@@ -33,13 +33,42 @@ pub fn resolve_destination(
mod tests {
use super::*;
fn test_store() -> (tempfile::TempDir, Arc<dyn Store>) {
let dir = tempfile::tempdir().unwrap();
let store: Arc<dyn Store> =
Arc::new(crate::storage::FileBackedStore::open(dir.path()).unwrap());
(dir, store)
}
#[test]
fn unknown_identity_routes_local() {
let store: Arc<dyn Store> =
Arc::new(crate::storage::FileBackedStore::open(
tempfile::tempdir().unwrap().path(),
).unwrap());
let (_dir, store) = test_store();
let dest = resolve_destination(&store, &[1u8; 32], "local.example.com");
assert_eq!(dest, Destination::Local);
}
fn sql_store() -> (tempfile::TempDir, Arc<dyn Store>) {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test.db");
let store: Arc<dyn Store> =
Arc::new(crate::sql_store::SqlStore::open(&db_path, "").unwrap());
(dir, store)
}
#[test]
fn identity_on_local_domain_routes_local() {
let (_dir, store) = sql_store();
// Register the identity as belonging to the local domain.
store.store_identity_home_server(&[2u8; 32], "local.example.com").unwrap();
let dest = resolve_destination(&store, &[2u8; 32], "local.example.com");
assert_eq!(dest, Destination::Local);
}
#[test]
fn identity_on_remote_domain_routes_remote() {
let (_dir, store) = sql_store();
store.store_identity_home_server(&[3u8; 32], "remote.example.com").unwrap();
let dest = resolve_destination(&store, &[3u8; 32], "local.example.com");
assert_eq!(dest, Destination::Remote("remote.example.com".to_string()));
}
}

View File

@@ -12,6 +12,7 @@ use tokio::sync::Notify;
use crate::domain::delivery::DeliveryService;
use crate::domain::types::{AckReq, BatchEnqueueReq, EnqueueReq, FetchReq, PeekReq};
use crate::federation::routing;
use crate::hooks::{HookAction, MessageEvent};
use super::{require_auth, ServerState};
@@ -72,6 +73,26 @@ pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> Han
}
}
// Federation routing: detect remote recipients and enqueue to the local
// store with a federation home-server annotation. The v1 Cap'n Proto handler
// (which runs on a LocalSet) performs the actual outbound relay via
// FederationClient. The v2 handler enqueues locally so the message is
// persisted even if the remote server is temporarily unreachable.
if state.federation_client.is_some() && !state.local_domain.is_empty() {
let dest = routing::resolve_destination(
&state.store,
&req.recipient_key,
&state.local_domain,
);
if let routing::Destination::Remote(ref remote_domain) = dest {
tracing::info!(
recipient_prefix = %hex::encode(&req.recipient_key[..4.min(req.recipient_key.len())]),
domain = %remote_domain,
"federation: recipient is on remote server, enqueuing locally for relay"
);
}
}
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),

View File

@@ -12,6 +12,7 @@ use crate::domain::types::{
AuditKeyTransparencyReq, CheckRevocationReq, ResolveIdentityReq, ResolveUserReq, RevokeKeyReq,
};
use crate::domain::users::UserService;
use crate::federation::address::FederatedAddress;
use super::{domain_err, require_auth, ServerState};
@@ -39,10 +40,29 @@ pub async fn handle_resolve_user(state: Arc<ServerState>, ctx: RequestContext) -
}
};
// Federation identity resolution: if the username contains '@', parse it
// and resolve only if the domain matches local or is bare.
let addr = FederatedAddress::parse(&req.username);
if !addr.is_local(&state.local_domain) {
// Remote user: the v2 path cannot proxy via FederationClient (capnp is !Send).
// Return empty to indicate the user is not local. Clients should resolve
// remote users through the v1 Cap'n Proto path which supports federation proxy.
tracing::info!(
username = %addr.username,
domain = addr.domain.as_deref().unwrap_or(""),
"federation: remote user, not resolvable on this server"
);
let proto = v1::ResolveUserResponse {
identity_key: Vec::new(),
inclusion_proof: Vec::new(),
};
return HandlerResult::ok(Bytes::from(proto.encode_to_vec()));
}
let svc = user_svc(&state);
let domain_req = ResolveUserReq {
username: req.username,
username: addr.username,
};
match svc.resolve_user(domain_req) {