chore: rename project quicnprotochat -> quicproquo (binaries: qpq)

Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
  quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
  *.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated

HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:11:51 +01:00
parent 553de3a2b7
commit 853ca4fec0
152 changed files with 4070 additions and 788 deletions

View File

@@ -0,0 +1,78 @@
//! Parse `username@domain` federated addresses.
//!
//! A bare `username` (no `@`) is treated as local.
/// A parsed federated address.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FederatedAddress {
pub username: String,
pub domain: Option<String>,
}
impl FederatedAddress {
/// Parse a `user@domain` string. Bare `user` → domain is `None`.
pub fn parse(input: &str) -> Self {
// Split on the *last* '@' so usernames can contain '@' in theory.
match input.rsplit_once('@') {
Some((user, domain)) if !domain.is_empty() && !user.is_empty() => Self {
username: user.to_string(),
domain: Some(domain.to_string()),
},
_ => Self {
username: input.to_string(),
domain: None,
},
}
}
/// Returns true if this address refers to a local user (no domain or domain matches local).
pub fn is_local(&self, local_domain: &str) -> bool {
match &self.domain {
None => true,
Some(d) => d == local_domain,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_username() {
let addr = FederatedAddress::parse("alice");
assert_eq!(addr.username, "alice");
assert_eq!(addr.domain, None);
assert!(addr.is_local("example.com"));
}
#[test]
fn user_at_domain() {
let addr = FederatedAddress::parse("alice@remote.example.com");
assert_eq!(addr.username, "alice");
assert_eq!(addr.domain, Some("remote.example.com".into()));
assert!(!addr.is_local("local.example.com"));
assert!(addr.is_local("remote.example.com"));
}
#[test]
fn trailing_at_is_bare() {
let addr = FederatedAddress::parse("alice@");
assert_eq!(addr.username, "alice@");
assert_eq!(addr.domain, None);
}
#[test]
fn leading_at_is_bare() {
let addr = FederatedAddress::parse("@domain.com");
assert_eq!(addr.username, "@domain.com");
assert_eq!(addr.domain, None);
}
#[test]
fn multiple_at_uses_last() {
let addr = FederatedAddress::parse("user@org@domain.com");
assert_eq!(addr.username, "user@org");
assert_eq!(addr.domain, Some("domain.com".into()));
}
}

View File

@@ -0,0 +1,287 @@
//! Outbound federation client: connects to peer servers to relay messages.
//!
//! Uses a lazy connection pool (DashMap) to reuse QUIC connections to known peers.
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Context;
use dashmap::DashMap;
use quinn::Endpoint;
use crate::config::{EffectiveFederationConfig, FederationPeerConfig};
/// Outbound federation client for relaying to peer servers.
pub struct FederationClient {
/// Peer domain → address mapping from config.
peer_addresses: HashMap<String, SocketAddr>,
/// Lazy QUIC connection pool: domain → active Connection.
connections: DashMap<String, quinn::Connection>,
/// Local QUIC endpoint (shared for all outbound federation connections).
endpoint: Endpoint,
/// Local domain (for the FederationAuth.origin field).
local_domain: String,
}
impl FederationClient {
/// Create a new federation client from config.
///
/// The `endpoint` should be configured with mTLS client credentials.
pub fn new(
config: &EffectiveFederationConfig,
endpoint: Endpoint,
) -> anyhow::Result<Self> {
let mut peer_addresses = HashMap::new();
for peer in &config.peers {
let addr: SocketAddr = peer.address.parse().with_context(|| {
format!("parse federation peer address '{}' for '{}'", peer.address, peer.domain)
})?;
peer_addresses.insert(peer.domain.clone(), addr);
}
Ok(Self {
peer_addresses,
connections: DashMap::new(),
endpoint,
local_domain: config.domain.clone(),
})
}
/// Check if we have a configured peer for the given domain.
pub fn has_peer(&self, domain: &str) -> bool {
self.peer_addresses.contains_key(domain)
}
/// List all configured peer domains.
pub fn peer_domains(&self) -> Vec<String> {
self.peer_addresses.keys().cloned().collect()
}
/// Get the local domain.
pub fn local_domain(&self) -> &str {
&self.local_domain
}
/// Relay a single enqueue to a remote peer. Returns the seq assigned by the remote server.
pub async fn relay_enqueue(
&self,
domain: &str,
recipient_key: &[u8],
payload: &[u8],
channel_id: &[u8],
) -> anyhow::Result<u64> {
let conn = self.get_or_connect(domain).await?;
let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?;
let (reader, writer) = (
tokio_util::compat::TokioAsyncReadCompatExt::compat(recv),
tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send),
);
let rpc_network = capnp_rpc::twoparty::VatNetwork::new(
reader,
writer,
capnp_rpc::rpc_twoparty_capnp::Side::Client,
Default::default(),
);
let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None);
let client: quicproquo_proto::federation_capnp::federation_service::Client =
rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
let mut req = client.relay_enqueue_request();
{
let mut builder = req.get();
builder.set_recipient_key(recipient_key);
builder.set_payload(payload);
builder.set_channel_id(channel_id);
builder.set_version(1);
let mut auth = builder.init_auth();
auth.set_origin(&self.local_domain);
}
let response = req.send().promise.await
.map_err(|e| anyhow::anyhow!("federation relay_enqueue failed: {e}"))?;
let seq = response.get()
.map_err(|e| anyhow::anyhow!("read relay_enqueue response: {e}"))?
.get_seq();
Ok(seq)
}
/// Proxy a key package fetch to a remote peer.
pub async fn proxy_fetch_key_package(
&self,
domain: &str,
identity_key: &[u8],
) -> anyhow::Result<Option<Vec<u8>>> {
let conn = self.get_or_connect(domain).await?;
let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?;
let (reader, writer) = (
tokio_util::compat::TokioAsyncReadCompatExt::compat(recv),
tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send),
);
let rpc_network = capnp_rpc::twoparty::VatNetwork::new(
reader,
writer,
capnp_rpc::rpc_twoparty_capnp::Side::Client,
Default::default(),
);
let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None);
let client: quicproquo_proto::federation_capnp::federation_service::Client =
rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
let mut req = client.proxy_fetch_key_package_request();
{
let mut builder = req.get();
builder.set_identity_key(identity_key);
let mut auth = builder.init_auth();
auth.set_origin(&self.local_domain);
}
let response = req.send().promise.await
.map_err(|e| anyhow::anyhow!("federation proxy_fetch_key_package failed: {e}"))?;
let pkg = response.get()
.map_err(|e| anyhow::anyhow!("read proxy_fetch_key_package response: {e}"))?
.get_package()
.map_err(|e| anyhow::anyhow!("get package: {e}"))?;
if pkg.is_empty() {
Ok(None)
} else {
Ok(Some(pkg.to_vec()))
}
}
/// Proxy a hybrid key fetch to a remote peer.
pub async fn proxy_fetch_hybrid_key(
&self,
domain: &str,
identity_key: &[u8],
) -> anyhow::Result<Option<Vec<u8>>> {
let conn = self.get_or_connect(domain).await?;
let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?;
let (reader, writer) = (
tokio_util::compat::TokioAsyncReadCompatExt::compat(recv),
tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send),
);
let rpc_network = capnp_rpc::twoparty::VatNetwork::new(
reader,
writer,
capnp_rpc::rpc_twoparty_capnp::Side::Client,
Default::default(),
);
let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None);
let client: quicproquo_proto::federation_capnp::federation_service::Client =
rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
let mut req = client.proxy_fetch_hybrid_key_request();
{
let mut builder = req.get();
builder.set_identity_key(identity_key);
let mut auth = builder.init_auth();
auth.set_origin(&self.local_domain);
}
let response = req.send().promise.await
.map_err(|e| anyhow::anyhow!("federation proxy_fetch_hybrid_key failed: {e}"))?;
let pk = response.get()
.map_err(|e| anyhow::anyhow!("read proxy_fetch_hybrid_key response: {e}"))?
.get_hybrid_public_key()
.map_err(|e| anyhow::anyhow!("get hybrid_public_key: {e}"))?;
if pk.is_empty() {
Ok(None)
} else {
Ok(Some(pk.to_vec()))
}
}
/// Proxy a user resolution to a remote peer.
pub async fn proxy_resolve_user(
&self,
domain: &str,
username: &str,
) -> anyhow::Result<Option<Vec<u8>>> {
let conn = self.get_or_connect(domain).await?;
let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?;
let (reader, writer) = (
tokio_util::compat::TokioAsyncReadCompatExt::compat(recv),
tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send),
);
let rpc_network = capnp_rpc::twoparty::VatNetwork::new(
reader,
writer,
capnp_rpc::rpc_twoparty_capnp::Side::Client,
Default::default(),
);
let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None);
let client: quicproquo_proto::federation_capnp::federation_service::Client =
rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
let mut req = client.proxy_resolve_user_request();
{
let mut builder = req.get();
builder.set_username(username);
let mut auth = builder.init_auth();
auth.set_origin(&self.local_domain);
}
let response = req.send().promise.await
.map_err(|e| anyhow::anyhow!("federation proxy_resolve_user failed: {e}"))?;
let key = response.get()
.map_err(|e| anyhow::anyhow!("read proxy_resolve_user response: {e}"))?
.get_identity_key()
.map_err(|e| anyhow::anyhow!("get identity_key: {e}"))?;
if key.is_empty() {
Ok(None)
} else {
Ok(Some(key.to_vec()))
}
}
/// Get an existing connection or create a new one to a peer domain.
async fn get_or_connect(&self, domain: &str) -> anyhow::Result<quinn::Connection> {
// Check for cached connection that's still alive.
if let Some(conn) = self.connections.get(domain) {
if conn.close_reason().is_none() {
return Ok(conn.clone());
}
}
let addr = self.peer_addresses.get(domain).ok_or_else(|| {
anyhow::anyhow!("no federation peer configured for domain '{domain}'")
})?;
tracing::info!(domain = domain, addr = %addr, "connecting to federation peer");
let conn = self
.endpoint
.connect(*addr, domain)
.map_err(|e| anyhow::anyhow!("federation connect to {domain}: {e}"))?
.await
.with_context(|| format!("federation QUIC handshake with {domain}"))?;
self.connections.insert(domain.to_string(), conn.clone());
Ok(conn)
}
}

View File

@@ -0,0 +1,16 @@
//! Federation subsystem: server-to-server message relay over mutual TLS + QUIC.
//!
//! When federation is enabled, the server binds a second QUIC endpoint on a
//! dedicated port (default 7001) that only accepts connections from known peers
//! authenticated via mTLS. Inbound requests are handled by [`service::FederationServiceImpl`],
//! which delegates to the local [`Store`]. Outbound relay uses [`client::FederationClient`].
pub mod address;
pub mod client;
pub mod routing;
pub mod service;
pub mod tls;
pub use address::FederatedAddress;
pub use client::FederationClient;
pub use routing::Destination;

View File

@@ -0,0 +1,44 @@
//! Federation routing: determine whether a recipient is local or remote.
use std::sync::Arc;
use crate::storage::Store;
/// Where a message should be delivered.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Destination {
/// Recipient is on this server.
Local,
/// Recipient's home server is the given domain.
Remote(String),
}
/// Resolve a recipient identity key to a routing destination.
///
/// 1. Check the `identity_home_servers` table for an explicit mapping.
/// 2. If no mapping exists, assume local (backwards compatible with single-server deployments).
pub fn resolve_destination(
store: &Arc<dyn Store>,
recipient_key: &[u8],
local_domain: &str,
) -> Destination {
match store.get_identity_home_server(recipient_key) {
Ok(Some(domain)) if domain != local_domain => Destination::Remote(domain),
_ => Destination::Local,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_identity_routes_local() {
let store: Arc<dyn Store> =
Arc::new(crate::storage::FileBackedStore::open(
tempfile::tempdir().unwrap().path(),
).unwrap());
let dest = resolve_destination(&store, &[1u8; 32], "local.example.com");
assert_eq!(dest, Destination::Local);
}
}

View File

@@ -0,0 +1,201 @@
//! Inbound federation handler: implements `FederationService` Cap'n Proto interface.
//!
//! Delegates all operations to the local [`Store`], acting as a trusted relay
//! from authenticated peer servers.
use std::sync::Arc;
use capnp::capability::Promise;
use quicproquo_proto::federation_capnp::federation_service;
use tokio::sync::Notify;
use dashmap::DashMap;
use crate::storage::Store;
/// Inbound federation RPC handler.
pub struct FederationServiceImpl {
pub store: Arc<dyn Store>,
pub waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
pub local_domain: String,
}
impl federation_service::Server for FederationServiceImpl {
fn relay_enqueue(
&mut self,
params: federation_service::RelayEnqueueParams,
mut results: federation_service::RelayEnqueueResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
let recipient_key = match p.get_recipient_key() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_key: {e}"))),
};
let payload = match p.get_payload() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad payload: {e}"))),
};
let channel_id = p.get_channel_id().unwrap_or_default().to_vec();
if let Ok(a) = p.get_auth() {
if let Ok(origin) = a.get_origin() {
let origin = origin.to_str().unwrap_or("?");
tracing::debug!(origin = origin, "federation relay_enqueue");
}
}
if recipient_key.len() != 32 {
return Promise::err(capnp::Error::failed("recipient_key must be 32 bytes".into()));
}
if payload.is_empty() {
return Promise::err(capnp::Error::failed("payload must not be empty".into()));
}
let seq = match self.store.enqueue(&recipient_key, &channel_id, payload) {
Ok(s) => s,
Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))),
};
results.get().set_seq(seq);
// Wake any waiting fetchWait clients.
if let Some(waiter) = self.waiters.get(&recipient_key) {
waiter.notify_waiters();
}
tracing::info!(
recipient_prefix = %hex::encode(&recipient_key[..4]),
seq = seq,
"federation: relayed enqueue"
);
Promise::ok(())
}
fn relay_batch_enqueue(
&mut self,
params: federation_service::RelayBatchEnqueueParams,
mut results: federation_service::RelayBatchEnqueueResults,
) -> Promise<(), capnp::Error> {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
let recipient_keys = match p.get_recipient_keys() {
Ok(v) => v,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_keys: {e}"))),
};
let payload = match p.get_payload() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad payload: {e}"))),
};
let channel_id = p.get_channel_id().unwrap_or_default().to_vec();
let mut seqs = Vec::with_capacity(recipient_keys.len() as usize);
for i in 0..recipient_keys.len() {
let rk = match recipient_keys.get(i) {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad key[{i}]: {e}"))),
};
if rk.len() != 32 {
return Promise::err(capnp::Error::failed(
format!("recipient_key[{i}] must be 32 bytes"),
));
}
let seq = match self.store.enqueue(&rk, &channel_id, payload.clone()) {
Ok(s) => s,
Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))),
};
seqs.push(seq);
if let Some(waiter) = self.waiters.get(&rk) {
waiter.notify_waiters();
}
}
let mut list = results.get().init_seqs(seqs.len() as u32);
for (i, seq) in seqs.iter().enumerate() {
list.set(i as u32, *seq);
}
tracing::info!(
recipient_count = recipient_keys.len(),
"federation: relayed batch_enqueue"
);
Promise::ok(())
}
fn proxy_fetch_key_package(
&mut self,
params: federation_service::ProxyFetchKeyPackageParams,
mut results: federation_service::ProxyFetchKeyPackageResults,
) -> Promise<(), capnp::Error> {
let identity_key = match params.get().and_then(|p| p.get_identity_key()) {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
match self.store.fetch_key_package(&identity_key) {
Ok(Some(pkg)) => results.get().set_package(&pkg),
Ok(None) => results.get().set_package(&[]),
Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))),
}
Promise::ok(())
}
fn proxy_fetch_hybrid_key(
&mut self,
params: federation_service::ProxyFetchHybridKeyParams,
mut results: federation_service::ProxyFetchHybridKeyResults,
) -> Promise<(), capnp::Error> {
let identity_key = match params.get().and_then(|p| p.get_identity_key()) {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
match self.store.fetch_hybrid_key(&identity_key) {
Ok(Some(pk)) => results.get().set_hybrid_public_key(&pk),
Ok(None) => results.get().set_hybrid_public_key(&[]),
Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))),
}
Promise::ok(())
}
fn proxy_resolve_user(
&mut self,
params: federation_service::ProxyResolveUserParams,
mut results: federation_service::ProxyResolveUserResults,
) -> Promise<(), capnp::Error> {
let username = match params.get().and_then(|p| p.get_username()) {
Ok(u) => match u.to_str() {
Ok(s) => s.to_string(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad utf-8: {e}"))),
},
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
match self.store.get_user_identity_key(&username) {
Ok(Some(key)) => results.get().set_identity_key(&key),
Ok(None) => results.get().set_identity_key(&[]),
Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))),
}
Promise::ok(())
}
fn federation_health(
&mut self,
_params: federation_service::FederationHealthParams,
mut results: federation_service::FederationHealthResults,
) -> Promise<(), capnp::Error> {
results.get().set_status("ok");
results.get().set_server_domain(&self.local_domain);
Promise::ok(())
}
}

View File

@@ -0,0 +1,85 @@
//! Build mTLS server/client configs for the federation endpoint.
//!
//! Federation uses a separate CA from the public-facing QUIC endpoint.
//! Both server and client present certificates; the server verifies the client
//! cert is signed by the federation CA.
use std::path::Path;
use std::sync::Arc;
use anyhow::Context;
use quinn::ServerConfig;
use quinn_proto::crypto::rustls::QuicServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::version::TLS13;
/// Build a QUIC server config for the federation listener with mutual TLS.
///
/// `cert`/`key`: this server's federation certificate and private key.
/// `ca`: the federation CA certificate used to verify peer certificates.
pub fn build_federation_server_config(
cert_path: &Path,
key_path: &Path,
ca_path: &Path,
) -> anyhow::Result<ServerConfig> {
let cert_bytes = std::fs::read(cert_path)
.with_context(|| format!("read federation cert: {:?}", cert_path))?;
let key_bytes = std::fs::read(key_path)
.with_context(|| format!("read federation key: {:?}", key_path))?;
let ca_bytes = std::fs::read(ca_path)
.with_context(|| format!("read federation CA: {:?}", ca_path))?;
let cert_chain = vec![CertificateDer::from(cert_bytes)];
let key = PrivateKeyDer::try_from(key_bytes)
.map_err(|_| anyhow::anyhow!("invalid federation private key"))?;
// Build a root cert store with the federation CA for client verification.
let mut root_store = rustls::RootCertStore::empty();
root_store
.add(CertificateDer::from(ca_bytes))
.context("add federation CA to root store")?;
let client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
.build()
.context("build client cert verifier")?;
let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13])
.with_client_cert_verifier(client_verifier)
.with_single_cert(cert_chain, key)?;
tls.alpn_protocols = vec![b"qnpc-fed".to_vec()];
let crypto = QuicServerConfig::try_from(tls)
.map_err(|e| anyhow::anyhow!("invalid federation server TLS config: {e}"))?;
Ok(ServerConfig::with_crypto(Arc::new(crypto)))
}
/// Build a QUIC client config for connecting to a federation peer with mutual TLS.
pub fn build_federation_client_config(
cert_path: &Path,
key_path: &Path,
ca_path: &Path,
) -> anyhow::Result<rustls::ClientConfig> {
let cert_bytes = std::fs::read(cert_path)
.with_context(|| format!("read federation cert: {:?}", cert_path))?;
let key_bytes = std::fs::read(key_path)
.with_context(|| format!("read federation key: {:?}", key_path))?;
let ca_bytes = std::fs::read(ca_path)
.with_context(|| format!("read federation CA: {:?}", ca_path))?;
let cert_chain = vec![CertificateDer::from(cert_bytes)];
let key = PrivateKeyDer::try_from(key_bytes)
.map_err(|_| anyhow::anyhow!("invalid federation client private key"))?;
let mut root_store = rustls::RootCertStore::empty();
root_store
.add(CertificateDer::from(ca_bytes))
.context("add federation CA to root store")?;
let tls = rustls::ClientConfig::builder_with_protocol_versions(&[&TLS13])
.with_root_certificates(root_store)
.with_client_auth_cert(cert_chain, key)
.context("set client auth cert")?;
Ok(tls)
}