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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user