feat: add delivery sequence numbers + major server/client refactor
Delivery sequence numbers (MLS epoch ordering fix):
- schemas/node.capnp: add Envelope{seq,data} struct; enqueue returns seq:UInt64;
fetch/fetchWait return List(Envelope) instead of List(Data)
- storage.rs: Store trait enqueue returns u64; fetch/fetch_limited return
Vec<(u64, Vec<u8>)>; FileBackedStore gains QueueMapV3 with per-inbox seq
counters and V2→V3 on-disk migration
- migrations/002_add_seq.sql: seq column, delivery_seq_counters table, index
- sql_store.rs: atomic UPSERT counter via RETURNING, ORDER BY seq, SCHEMA_VERSION→3
- node_service/delivery.rs: builds Envelope list; returns seq from enqueue
- client/rpc.rs: enqueue→u64, fetch_all/fetch_wait→Vec<(u64,Vec<u8>)>
- client/commands.rs: sort-by-seq before MLS processing; retry loop in cmd_recv
and receive_pending_plaintexts for correct epoch ordering
Server refactor:
- Split monolithic main.rs into node_service/{mod,delivery,auth_ops,key_ops,p2p_ops}
- Add auth.rs (token validation, rate limiting), config.rs, metrics.rs, tls.rs
- Add SQL migrations runner (001_initial.sql, 002_add_seq.sql)
- OPAQUE PAKE login/registration, sealed-sender mode, queue depth limit (1000)
Client refactor:
- Split lib.rs into client/{commands,rpc,state,retry,hex,mod}
- Add cmd_whoami, cmd_health, cmd_check_key, cmd_ping subcommands
- Add cmd_register_user, cmd_login (OPAQUE), cmd_refresh_keypackage
- Hybrid PQ envelope (X25519 + ML-KEM-768) on all send/recv paths
- E2E test suite expanded
Other:
- quicnprotochat-gui: Tauri 2 desktop GUI skeleton (backend + HTML UI)
- quicnprotochat-p2p: iroh-based P2P transport stub
- quicnprotochat-core: app_message, hybrid_crypto modules; GroupMember API updates
- .github/workflows/size-lint.yml: binary size regression check
- docs: protocol comparison, roadmap updates, fully-operational checklist
This commit is contained in:
246
crates/quicnprotochat-server/src/node_service/mod.rs
Normal file
246
crates/quicnprotochat-server/src/node_service/mod.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use capnp_rpc::RpcSystem;
|
||||
use dashmap::DashMap;
|
||||
use opaque_ke::ServerSetup;
|
||||
use quicnprotochat_core::opaque_auth::OpaqueSuite;
|
||||
use quicnprotochat_proto::node_capnp::node_service;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
|
||||
|
||||
use crate::auth::{
|
||||
current_timestamp, AuthConfig, PendingLogin, RateEntry, SessionInfo,
|
||||
PENDING_LOGIN_TTL_SECS, RATE_LIMIT_WINDOW_SECS,
|
||||
};
|
||||
use crate::storage::Store;
|
||||
|
||||
mod auth_ops;
|
||||
mod delivery;
|
||||
mod key_ops;
|
||||
mod p2p_ops;
|
||||
|
||||
impl node_service::Server for NodeServiceImpl {
|
||||
fn upload_key_package(
|
||||
&mut self,
|
||||
params: node_service::UploadKeyPackageParams,
|
||||
results: node_service::UploadKeyPackageResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_upload_key_package(params, results)
|
||||
}
|
||||
|
||||
fn fetch_key_package(
|
||||
&mut self,
|
||||
params: node_service::FetchKeyPackageParams,
|
||||
results: node_service::FetchKeyPackageResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_fetch_key_package(params, results)
|
||||
}
|
||||
|
||||
fn enqueue(
|
||||
&mut self,
|
||||
params: node_service::EnqueueParams,
|
||||
results: node_service::EnqueueResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_enqueue(params, results)
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
&mut self,
|
||||
params: node_service::FetchParams,
|
||||
results: node_service::FetchResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_fetch(params, results)
|
||||
}
|
||||
|
||||
fn fetch_wait(
|
||||
&mut self,
|
||||
params: node_service::FetchWaitParams,
|
||||
results: node_service::FetchWaitResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_fetch_wait(params, results)
|
||||
}
|
||||
|
||||
fn health(
|
||||
&mut self,
|
||||
params: node_service::HealthParams,
|
||||
results: node_service::HealthResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_health(params, results)
|
||||
}
|
||||
|
||||
fn upload_hybrid_key(
|
||||
&mut self,
|
||||
params: node_service::UploadHybridKeyParams,
|
||||
results: node_service::UploadHybridKeyResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_upload_hybrid_key(params, results)
|
||||
}
|
||||
|
||||
fn fetch_hybrid_key(
|
||||
&mut self,
|
||||
params: node_service::FetchHybridKeyParams,
|
||||
results: node_service::FetchHybridKeyResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_fetch_hybrid_key(params, results)
|
||||
}
|
||||
|
||||
fn opaque_login_start(
|
||||
&mut self,
|
||||
params: node_service::OpaqueLoginStartParams,
|
||||
results: node_service::OpaqueLoginStartResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_opaque_login_start(params, results)
|
||||
}
|
||||
|
||||
fn opaque_register_start(
|
||||
&mut self,
|
||||
params: node_service::OpaqueRegisterStartParams,
|
||||
results: node_service::OpaqueRegisterStartResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_opaque_register_start(params, results)
|
||||
}
|
||||
|
||||
fn opaque_login_finish(
|
||||
&mut self,
|
||||
params: node_service::OpaqueLoginFinishParams,
|
||||
results: node_service::OpaqueLoginFinishResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_opaque_login_finish(params, results)
|
||||
}
|
||||
|
||||
fn opaque_register_finish(
|
||||
&mut self,
|
||||
params: node_service::OpaqueRegisterFinishParams,
|
||||
results: node_service::OpaqueRegisterFinishResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_opaque_register_finish(params, results)
|
||||
}
|
||||
|
||||
fn publish_endpoint(
|
||||
&mut self,
|
||||
params: node_service::PublishEndpointParams,
|
||||
results: node_service::PublishEndpointResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_publish_endpoint(params, results)
|
||||
}
|
||||
|
||||
fn resolve_endpoint(
|
||||
&mut self,
|
||||
params: node_service::ResolveEndpointParams,
|
||||
results: node_service::ResolveEndpointResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
self.handle_resolve_endpoint(params, results)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CURRENT_WIRE_VERSION: u16 = 1;
|
||||
|
||||
pub struct NodeServiceImpl {
|
||||
pub store: Arc<dyn Store>,
|
||||
pub waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
|
||||
pub auth_cfg: Arc<AuthConfig>,
|
||||
pub opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
|
||||
pub pending_logins: Arc<DashMap<String, PendingLogin>>,
|
||||
pub sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
|
||||
pub rate_limits: Arc<DashMap<Vec<u8>, RateEntry>>,
|
||||
/// When true, enqueue does not require identity-bound session (Sealed Sender).
|
||||
pub sealed_sender: bool,
|
||||
}
|
||||
|
||||
impl NodeServiceImpl {
|
||||
pub fn new(
|
||||
store: Arc<dyn Store>,
|
||||
waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
|
||||
auth_cfg: Arc<AuthConfig>,
|
||||
opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
|
||||
pending_logins: Arc<DashMap<String, PendingLogin>>,
|
||||
sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
|
||||
rate_limits: Arc<DashMap<Vec<u8>, RateEntry>>,
|
||||
sealed_sender: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
waiters,
|
||||
auth_cfg,
|
||||
opaque_setup,
|
||||
pending_logins,
|
||||
sessions,
|
||||
rate_limits,
|
||||
sealed_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_node_connection(
|
||||
connecting: quinn::Connecting,
|
||||
store: Arc<dyn Store>,
|
||||
waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
|
||||
auth_cfg: Arc<AuthConfig>,
|
||||
opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
|
||||
pending_logins: Arc<DashMap<String, PendingLogin>>,
|
||||
sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
|
||||
rate_limits: Arc<DashMap<Vec<u8>, RateEntry>>,
|
||||
sealed_sender: bool,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let connection = connecting.await?;
|
||||
|
||||
tracing::info!(peer = %connection.remote_address(), "QUIC connected");
|
||||
|
||||
let (send, recv) = connection
|
||||
.accept_bi()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to accept bi stream: {e}"))?;
|
||||
let (reader, writer) = (recv.compat(), send.compat_write());
|
||||
|
||||
let network = capnp_rpc::twoparty::VatNetwork::new(
|
||||
reader,
|
||||
writer,
|
||||
capnp_rpc::rpc_twoparty_capnp::Side::Server,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
let service: node_service::Client = capnp_rpc::new_client(NodeServiceImpl::new(
|
||||
store,
|
||||
waiters,
|
||||
auth_cfg,
|
||||
opaque_setup,
|
||||
pending_logins,
|
||||
sessions,
|
||||
rate_limits,
|
||||
sealed_sender,
|
||||
));
|
||||
|
||||
RpcSystem::new(Box::new(network), Some(service.client))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("NodeService RPC error: {e}"))
|
||||
}
|
||||
|
||||
const MESSAGE_TTL_SECS: u64 = 7 * 24 * 60 * 60; // 7 days
|
||||
|
||||
pub fn spawn_cleanup_task(
|
||||
sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
|
||||
pending_logins: Arc<DashMap<String, PendingLogin>>,
|
||||
rate_limits: Arc<DashMap<Vec<u8>, RateEntry>>,
|
||||
store: Arc<dyn Store>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let now = current_timestamp();
|
||||
|
||||
sessions.retain(|_, info| info.expires_at > now);
|
||||
pending_logins.retain(|_, pl| now - pl.created_at < PENDING_LOGIN_TTL_SECS);
|
||||
rate_limits.retain(|_, entry| now - entry.window_start < RATE_LIMIT_WINDOW_SECS * 2);
|
||||
|
||||
match store.gc_expired_messages(MESSAGE_TTL_SECS) {
|
||||
Ok(n) if n > 0 => {
|
||||
tracing::debug!(expired = n, "garbage collected expired messages")
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "message GC failed"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user