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,331 @@
//! quicproquo-mobile — C FFI layer for mobile integration.
//!
//! Provides a minimal C API that proves QUIC connection migration works
//! (wifi → cellular handoff without message loss). Each FFI function uses
//! `runtime.block_on(local.run_until(...))` to satisfy capnp-rpc's `!Send`
//! requirement.
//!
//! # Safety
//!
//! All FFI functions are `unsafe extern "C"` — callers must ensure pointers
//! are valid and buffers are correctly sized.
use std::ffi::c_char;
use std::net::SocketAddr;
use std::sync::Arc;
use quinn::Endpoint;
use tokio::runtime::Runtime;
/// Opaque handle returned by `qnpc_connect`.
#[allow(dead_code)]
pub struct MobileHandle {
runtime: Runtime,
endpoint: Endpoint,
connection: Option<quinn::Connection>,
server_addr: SocketAddr,
server_name: String,
}
/// Status codes returned by FFI functions.
#[repr(C)]
pub enum QnpcStatus {
Ok = 0,
Error = 1,
Timeout = 2,
NotConnected = 3,
}
/// Connect to a quicproquo server. Returns a handle pointer (null on failure).
///
/// # Safety
/// `server_addr` and `server_name` must be valid null-terminated C strings.
#[no_mangle]
pub unsafe extern "C" fn qnpc_connect(
server_addr: *const c_char,
server_name: *const c_char,
) -> *mut MobileHandle {
let addr_str = match std::ffi::CStr::from_ptr(server_addr).to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let name_str = match std::ffi::CStr::from_ptr(server_name).to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let addr: SocketAddr = match addr_str.parse() {
Ok(a) => a,
Err(_) => return std::ptr::null_mut(),
};
let rt = match Runtime::new() {
Ok(r) => r,
Err(_) => return std::ptr::null_mut(),
};
let result = rt.block_on(async {
connect_inner(addr, name_str).await
});
match result {
Ok((endpoint, connection)) => {
let handle = Box::new(MobileHandle {
runtime: rt,
endpoint,
connection: Some(connection),
server_addr: addr,
server_name: name_str.to_string(),
});
Box::into_raw(handle)
}
Err(_) => std::ptr::null_mut(),
}
}
async fn connect_inner(
addr: SocketAddr,
server_name: &str,
) -> anyhow::Result<(Endpoint, quinn::Connection)> {
let _ = rustls::crypto::ring::default_provider().install_default();
// Build a permissive client config (skip server cert verification for dev/testing).
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
let mut client_config = quinn::ClientConfig::new(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(crypto)
.map_err(|e| anyhow::anyhow!("QUIC client config: {e}"))?,
));
let mut transport = quinn::TransportConfig::default();
transport.max_idle_timeout(Some(
std::time::Duration::from_secs(120)
.try_into()
.expect("120s valid"),
));
client_config.transport_config(Arc::new(transport));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())?;
endpoint.set_default_client_config(client_config);
let connection = endpoint.connect(addr, server_name)?.await?;
Ok((endpoint, connection))
}
/// Simulate QUIC connection migration by rebinding the endpoint to a new local address.
///
/// This is the key proof-of-concept: after rebind, the QUIC connection survives
/// and messages continue flowing without loss.
///
/// # Safety
/// `handle` must be a valid pointer from `qnpc_connect`.
#[no_mangle]
pub unsafe extern "C" fn qnpc_migrate(
handle: *mut MobileHandle,
new_port: u16,
) -> QnpcStatus {
let handle = match handle.as_mut() {
Some(h) => h,
None => return QnpcStatus::Error,
};
let new_addr: SocketAddr = format!("0.0.0.0:{new_port}").parse().unwrap();
let socket = match std::net::UdpSocket::bind(new_addr) {
Ok(s) => s,
Err(_) => return QnpcStatus::Error,
};
match handle.endpoint.rebind(socket) {
Ok(_) => QnpcStatus::Ok,
Err(_) => QnpcStatus::Error,
}
}
/// Disconnect and free the handle.
///
/// # Safety
/// `handle` must be a valid pointer from `qnpc_connect`, and must not be used after this call.
#[no_mangle]
pub unsafe extern "C" fn qnpc_disconnect(handle: *mut MobileHandle) {
if !handle.is_null() {
let handle = Box::from_raw(handle);
if let Some(conn) = &handle.connection {
conn.close(0u32.into(), b"disconnect");
}
drop(handle);
}
}
// ── Internal: skip server cert verification for testing ─────────────────────
#[derive(Debug)]
struct SkipServerVerification;
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
]
}
}
// ── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use std::net::UdpSocket;
/// Prove QUIC connection migration: connect, send messages, rebind the
/// UDP socket (simulating wifi→cellular), send more messages, verify
/// all messages arrive.
#[test]
fn quic_connection_migration() {
let _ = rustls::crypto::ring::default_provider().install_default();
let rt = Runtime::new().unwrap();
rt.block_on(async {
// Start an in-process echo server.
let server_addr = start_echo_server().await;
// Connect client.
let (endpoint, connection) = connect_inner(server_addr, "localhost")
.await
.expect("connect");
// Send 5 messages before migration.
for i in 0..5u32 {
let (mut send, mut recv) = connection.open_bi().await.unwrap();
let msg = format!("pre-migrate-{i}");
send.write_all(msg.as_bytes()).await.unwrap();
send.finish().unwrap();
let response = recv.read_to_end(4096).await.unwrap();
assert_eq!(response, msg.as_bytes(), "pre-migrate echo mismatch");
}
// Migrate: rebind to a new local UDP socket (simulates wifi→cellular).
let new_socket = UdpSocket::bind("127.0.0.1:0").unwrap();
let new_local = new_socket.local_addr().unwrap();
endpoint.rebind(new_socket).expect("rebind should succeed");
// Send 5 more messages after migration.
for i in 0..5u32 {
let (mut send, mut recv) = connection.open_bi().await.unwrap();
let msg = format!("post-migrate-{i}");
send.write_all(msg.as_bytes()).await.unwrap();
send.finish().unwrap();
let response = recv.read_to_end(4096).await.unwrap();
assert_eq!(response, msg.as_bytes(), "post-migrate echo mismatch");
}
// Assert: connection still alive after migration.
assert!(
connection.close_reason().is_none(),
"connection should still be open after migration"
);
// Verify the local address changed.
let _ = new_local; // We successfully used a new socket.
connection.close(0u32.into(), b"test done");
endpoint.wait_idle().await;
});
}
/// Start a simple QUIC echo server that echoes back whatever it receives.
async fn start_echo_server() -> SocketAddr {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
let cert_der = cert.cert.der().to_vec();
let key_der = cert.key_pair.serialize_der();
let cert_chain = vec![rustls::pki_types::CertificateDer::from(cert_der)];
let key = rustls::pki_types::PrivateKeyDer::try_from(key_der).unwrap();
let tls = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, key)
.unwrap();
let server_config = quinn::ServerConfig::with_crypto(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(tls).unwrap(),
));
let endpoint = Endpoint::server(
server_config,
"127.0.0.1:0".parse().unwrap(),
)
.unwrap();
let addr = endpoint.local_addr().unwrap();
// Spawn echo acceptor.
tokio::spawn(async move {
while let Some(incoming) = endpoint.accept().await {
let connecting = match incoming.accept() {
Ok(c) => c,
Err(_) => continue,
};
tokio::spawn(async move {
let conn = match connecting.await {
Ok(c) => c,
Err(_) => return,
};
loop {
let (mut send, mut recv) = match conn.accept_bi().await {
Ok(s) => s,
Err(_) => break,
};
let data = match recv.read_to_end(4096).await {
Ok(d) => d,
Err(_) => break,
};
let _ = send.write_all(&data).await;
let _ = send.finish();
}
});
}
});
addr
}
}