feat(quic): fix QUIC connection teardown + add FS QUIC integration tests
- Add QuicConfig::insecure() with NoopVerifier for subprocess integration tests where server/client generate independent self-signed certs - Fix QUIC endpoint lifetime bug: store _endpoint in QuicConnection so quinn's endpoint outlives its connection (previously dropped on return) - Add install_crypto_provider() to transport lib; call it in main() to satisfy rustls 0.23's explicit install_default() requirement - Add QuicConnection::wait_for_peer_close(): server passively awaits client close instead of racing to send CONNECTION_CLOSE before in-flight streams (FsDirComplete, FsFileAck) reach the client - Add SyncPeer::quic_conn_clone() + update shutdown/drain to use close_and_drain (client) vs wait_for_peer_close (server) - Add 3 QUIC FS sync integration tests: cold-copy, warm-noop, incremental Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ac8969fb5d
commit
0ed2f5054f
@@ -21,3 +21,12 @@ pub use peer::{PipeReadHalf, PipeWriteHalf, SyncPeer};
|
||||
pub use protocol::SyncMessage;
|
||||
pub use quic::{QuicConfig, QuicConnection, QuicServer, quic_connect};
|
||||
pub use tcp::{TcpConnection, TcpServer};
|
||||
|
||||
/// Install the default rustls CryptoProvider (ring) for the current process.
|
||||
///
|
||||
/// Must be called once before any QUIC/TLS code runs. Safe to call multiple
|
||||
/// times — subsequent calls are no-ops. This is required by rustls 0.23+
|
||||
/// regardless of whether the `ring` feature is enabled at compile time.
|
||||
pub fn install_crypto_provider() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
@@ -62,13 +62,26 @@ impl SyncPeer {
|
||||
match self {
|
||||
SyncPeer::Tcp(mut c) => c.shutdown().await,
|
||||
SyncPeer::Quic(c) => {
|
||||
c.close();
|
||||
c.close_and_drain().await;
|
||||
Ok(())
|
||||
}
|
||||
SyncPeer::Mmap { .. } => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a cloned `Arc` if this is a QUIC connection, otherwise `None`.
|
||||
///
|
||||
/// This is used by servers to keep the QUIC connection alive after the
|
||||
/// `FsSyncServer` or other handler has been dropped (which drops the
|
||||
/// server's copy of the `Arc`), so that `close_and_drain()` can be
|
||||
/// called to flush pending stream data before the connection is closed.
|
||||
pub fn quic_conn_clone(&self) -> Option<Arc<QuicConnection>> {
|
||||
match self {
|
||||
SyncPeer::Quic(c) => Some(c.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Split into independent read/write halves for concurrent pipelined I/O.
|
||||
///
|
||||
/// TCP: yields `OwnedReadHalf` / `OwnedWriteHalf` via `into_split()`.
|
||||
@@ -112,7 +125,7 @@ impl PipeWriteHalf {
|
||||
match self {
|
||||
PipeWriteHalf::Tcp(mut h) => h.shutdown().await,
|
||||
PipeWriteHalf::Quic(c) => {
|
||||
c.close();
|
||||
c.close_and_drain().await;
|
||||
Ok(())
|
||||
}
|
||||
PipeWriteHalf::Mmap(_) => Ok(()),
|
||||
|
||||
@@ -19,7 +19,9 @@ use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use quinn::{ClientConfig, Connection, Endpoint, ServerConfig};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
|
||||
use rustls::{DigitallySignedStruct, Error as TlsError, SignatureScheme};
|
||||
|
||||
use crate::error::TransportError;
|
||||
use crate::protocol::SyncMessage;
|
||||
@@ -65,6 +67,94 @@ impl QuicConfig {
|
||||
client_config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a server-only config with a self-signed cert whose **client side
|
||||
/// skips all certificate verification**.
|
||||
///
|
||||
/// For use in integration tests where server and client run as separate
|
||||
/// processes and cannot share a certificate at build time.
|
||||
///
|
||||
/// **Never use in production.**
|
||||
pub fn insecure() -> Result<Self, TransportError> {
|
||||
let cert = rcgen::generate_simple_self_signed(vec![
|
||||
"localhost".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
])
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
|
||||
let cert_der = CertificateDer::from(cert.cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::try_from(cert.key_pair.serialize_der())
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
|
||||
let server_config = ServerConfig::with_single_cert(vec![cert_der], key_der)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
|
||||
// Client config: accept any server certificate (testing only).
|
||||
let tls_client = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoopVerifier))
|
||||
.with_no_client_auth();
|
||||
let client_config = ClientConfig::new(Arc::new(
|
||||
quinn::crypto::rustls::QuicClientConfig::try_from(tls_client)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?,
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
server_config,
|
||||
client_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A TLS certificate verifier that accepts any certificate.
|
||||
/// Only for use in local integration tests.
|
||||
#[derive(Debug)]
|
||||
struct NoopVerifier;
|
||||
|
||||
impl ServerCertVerifier for NoopVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -72,13 +162,29 @@ impl QuicConfig {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A QUIC connection capable of sending/receiving `SyncMessage`s.
|
||||
///
|
||||
/// The `Endpoint` is kept alive here because quinn requires the endpoint to
|
||||
/// outlive all connections it manages. Server-accepted connections pass
|
||||
/// `None` since the server's `QuicServer` holds the endpoint.
|
||||
pub struct QuicConnection {
|
||||
conn: Connection,
|
||||
/// Client-owned endpoint; `None` for server-accepted connections.
|
||||
_endpoint: Option<Endpoint>,
|
||||
}
|
||||
|
||||
impl QuicConnection {
|
||||
pub fn new(conn: Connection) -> Self {
|
||||
Self { conn }
|
||||
Self {
|
||||
conn,
|
||||
_endpoint: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_endpoint(conn: Connection, endpoint: Endpoint) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
_endpoint: Some(endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a `SyncMessage` on a new unidirectional QUIC stream.
|
||||
@@ -127,10 +233,33 @@ impl QuicConnection {
|
||||
SyncMessage::from_bytes(&body).map_err(TransportError::Deserialization)
|
||||
}
|
||||
|
||||
/// Close the QUIC connection with a normal code.
|
||||
/// Close the QUIC connection with a normal code (non-blocking).
|
||||
pub fn close(&self) {
|
||||
self.conn.close(0u32.into(), b"done");
|
||||
}
|
||||
|
||||
/// Close the QUIC connection and wait until all pending stream data has
|
||||
/// been delivered to the peer.
|
||||
///
|
||||
/// Use this on the **client** side: the client initiates close after it
|
||||
/// has read all data, then waits for the server to acknowledge.
|
||||
pub async fn close_and_drain(&self) {
|
||||
self.conn.close(0u32.into(), b"done");
|
||||
// `closed()` resolves once the peer has also closed or the drain
|
||||
// period (≈ 3× PTO) has elapsed. Ignore the result — errors here
|
||||
// just mean the peer already closed first, which is fine.
|
||||
let _ = self.conn.closed().await;
|
||||
}
|
||||
|
||||
/// Wait for the peer to close the connection without initiating close ourselves.
|
||||
///
|
||||
/// Use this on the **server** side after sending the final message. The
|
||||
/// client will call `close_and_drain()` once it has read all data, which
|
||||
/// resolves this future. This avoids the race where `close_and_drain()`
|
||||
/// sends `CONNECTION_CLOSE` before in-flight streams reach the client.
|
||||
pub async fn wait_for_peer_close(&self) {
|
||||
let _ = self.conn.closed().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -188,7 +317,9 @@ pub async fn quic_connect(
|
||||
.await
|
||||
.map_err(|e| TransportError::Quic(e.to_string()))?;
|
||||
|
||||
Ok(QuicConnection::new(conn))
|
||||
// Keep the endpoint alive for the duration of this connection.
|
||||
// quinn requires the Endpoint to outlive all connections it manages.
|
||||
Ok(QuicConnection::with_endpoint(conn, endpoint))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user