feat: DM epoch fix, federation relay, and mDNS mesh discovery

- schema: createChannel returns wasNew :Bool to elect the MLS initiator
  unambiguously; prevents duplicate group creation on concurrent /dm calls
- core: group helpers for epoch tracking and key-package lifecycle
- server: federation subsystem — mTLS QUIC server-to-server relay with
  Cap'n Proto RPC; enqueue/batchEnqueue relay unknown recipients to their
  home domain via FederationClient
- server: mDNS _quicproquo._udp.local. service announcement on startup
- server: storage + sql_store — identity_exists, peek/ack, federation
  home-server lookup helpers
- client: /mesh peers REPL command (mDNS discovery, feature = "mesh")
- client: MeshDiscovery — background mDNS browse with ServiceDaemon
- client: was_new=false path in cmd_dm waits for peer Welcome instead of
  creating a duplicate initiator group
- p2p: fix ALPN from quicnprotochat/p2p/1 → quicproquo/p2p/1
- workspace: re-include quicproquo-p2p in members
This commit is contained in:
2026-03-03 14:41:56 +01:00
parent e24497bf90
commit c8398d6cb7
27 changed files with 3375 additions and 303 deletions

View File

@@ -7,7 +7,7 @@ use opaque_ke::{
};
use quicproquo_core::{
generate_key_package, hybrid_decrypt, hybrid_encrypt, opaque_auth::OpaqueSuite,
GroupMember, HybridKeypair, IdentityKeypair,
GroupMember, HybridKeypair, IdentityKeypair, ReceivedMessage,
};
use super::{
@@ -376,7 +376,7 @@ pub(crate) async fn opaque_register(
/// Perform OPAQUE login and return the raw session token bytes.
/// Does NOT require init_auth() — OPAQUE RPCs are unauthenticated.
pub(crate) async fn opaque_login(
pub async fn opaque_login(
client: &quicproquo_proto::node_capnp::node_service::Client,
username: &str,
password: &str,
@@ -725,9 +725,10 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) ->
.context("joiner: missing ciphertext from DS")?;
let inner_creator_joiner =
hybrid_decrypt(&joiner_hybrid, raw_creator_joiner, b"", b"").context("hybrid decrypt failed")?;
let plaintext_creator_joiner = joiner
.receive_message(&inner_creator_joiner)?
.context("expected application message")?;
let plaintext_creator_joiner = match joiner.receive_message(&inner_creator_joiner)? {
ReceivedMessage::Application(pt) => pt,
other => anyhow::bail!("expected application message, got {other:?}"),
};
println!(
"creator -> joiner plaintext: {}",
String::from_utf8_lossy(&plaintext_creator_joiner)
@@ -749,9 +750,10 @@ pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) ->
.context("creator: missing ciphertext from DS")?;
let inner_joiner_creator =
hybrid_decrypt(&creator_hybrid, raw_joiner_creator, b"", b"").context("hybrid decrypt failed")?;
let plaintext_joiner_creator = creator
.receive_message(&inner_joiner_creator)?
.context("expected application message")?;
let plaintext_joiner_creator = match creator.receive_message(&inner_joiner_creator)? {
ReceivedMessage::Application(pt) => pt,
other => anyhow::bail!("expected application message, got {other:?}"),
};
println!(
"joiner -> creator plaintext: {}",
String::from_utf8_lossy(&plaintext_joiner_creator)
@@ -1013,8 +1015,8 @@ pub async fn cmd_recv(
}
};
match member.receive_message(&mls_payload) {
Ok(Some(pt)) => println!("[{idx}] plaintext: {}", String::from_utf8_lossy(&pt)),
Ok(None) => println!("[{idx}] commit applied"),
Ok(ReceivedMessage::Application(pt)) => println!("[{idx}] plaintext: {}", String::from_utf8_lossy(&pt)),
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => println!("[{idx}] commit applied"),
Err(_) => pending.push((idx, mls_payload)),
}
}
@@ -1023,11 +1025,11 @@ pub async fn cmd_recv(
let before = pending.len();
pending.retain(|(idx, mls_payload)| {
match member.receive_message(mls_payload) {
Ok(Some(pt)) => {
Ok(ReceivedMessage::Application(pt)) => {
println!("[{idx}/retry] plaintext: {}", String::from_utf8_lossy(&pt));
false
}
Ok(None) => {
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {
println!("[{idx}/retry] commit applied");
false
}
@@ -1078,8 +1080,8 @@ pub async fn receive_pending_plaintexts(
Err(_) => continue,
};
match member.receive_message(&mls_payload) {
Ok(Some(pt)) => plaintexts.push(pt),
Ok(None) => {}
Ok(ReceivedMessage::Application(pt)) => plaintexts.push(pt),
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {}
Err(_) => pending.push(mls_payload),
}
}
@@ -1088,11 +1090,11 @@ pub async fn receive_pending_plaintexts(
let before = pending.len();
pending.retain(|mls_payload| {
match member.receive_message(mls_payload) {
Ok(Some(pt)) => {
Ok(ReceivedMessage::Application(pt)) => {
plaintexts.push(pt);
false
}
Ok(None) => false,
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => false,
Err(_) => true,
}
});
@@ -1250,12 +1252,12 @@ pub async fn cmd_chat(
Err(_) => continue,
};
match member.receive_message(&mls_payload) {
Ok(Some(pt)) => {
Ok(ReceivedMessage::Application(pt)) => {
let s = String::from_utf8_lossy(&pt);
println!("\r\n[peer] {s}\n> ");
std::io::stdout().flush().context("flush stdout")?;
}
Ok(None) => {}
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {}
Err(_) => retry_payloads.push(mls_payload),
}
}
@@ -1264,13 +1266,13 @@ pub async fn cmd_chat(
let before = retry_payloads.len();
retry_payloads.retain(|mls_payload| {
match member.receive_message(mls_payload) {
Ok(Some(pt)) => {
Ok(ReceivedMessage::Application(pt)) => {
let s = String::from_utf8_lossy(&pt);
println!("\r\n[peer] {s}\n> ");
let _ = std::io::stdout().flush();
false
}
Ok(None) => false,
Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => false,
Err(_) => true,
}
});