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:
78
crates/quicproquo-server/src/federation/address.rs
Normal file
78
crates/quicproquo-server/src/federation/address.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
287
crates/quicproquo-server/src/federation/client.rs
Normal file
287
crates/quicproquo-server/src/federation/client.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
16
crates/quicproquo-server/src/federation/mod.rs
Normal file
16
crates/quicproquo-server/src/federation/mod.rs
Normal 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;
|
||||
44
crates/quicproquo-server/src/federation/routing.rs
Normal file
44
crates/quicproquo-server/src/federation/routing.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
201
crates/quicproquo-server/src/federation/service.rs
Normal file
201
crates/quicproquo-server/src/federation/service.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
85
crates/quicproquo-server/src/federation/tls.rs
Normal file
85
crates/quicproquo-server/src/federation/tls.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user