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:
23
crates/quicproquo-mobile/Cargo.toml
Normal file
23
crates/quicproquo-mobile/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "quicproquo-mobile"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "C FFI layer for quicproquo, proving QUIC connection migration."
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Async
|
||||
tokio = { workspace = true }
|
||||
|
||||
# QUIC
|
||||
quinn = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen = { workspace = true }
|
||||
331
crates/quicproquo-mobile/src/lib.rs
Normal file
331
crates/quicproquo-mobile/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user