diff --git a/Cargo.lock b/Cargo.lock index a63df40cea..e7ecd2fcd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2875,8 +2875,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" +source = "git+https://github.com/zakura-core/iroh-quinn?rev=1dcc7a43488fecd199d343d47e93e9ed8319fcaa#1dcc7a43488fecd199d343d47e93e9ed8319fcaa" dependencies = [ "bytes", "cfg_aliases", @@ -2895,8 +2894,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" +source = "git+https://github.com/zakura-core/iroh-quinn?rev=1dcc7a43488fecd199d343d47e93e9ed8319fcaa#1dcc7a43488fecd199d343d47e93e9ed8319fcaa" dependencies = [ "bytes", "getrandom 0.2.17", @@ -2915,8 +2913,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" +source = "git+https://github.com/zakura-core/iroh-quinn?rev=1dcc7a43488fecd199d343d47e93e9ed8319fcaa#1dcc7a43488fecd199d343d47e93e9ed8319fcaa" dependencies = [ "cfg_aliases", "libc", diff --git a/Cargo.toml b/Cargo.toml index e2a6f62523..5fb224f178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,14 @@ keywords = ["zakura", "zcash"] rust-version = "1.97" edition = "2021" +# Backport the loss-probe datagram limit fix to Iroh 0.92's exact QUIC versions. +# Keep the related crates on one revision. Workspace patches are not inherited +# by downstream library users; they must apply the same patch in their workspace. +[patch.crates-io] +iroh-quinn = { git = "https://github.com/zakura-core/iroh-quinn", rev = "1dcc7a43488fecd199d343d47e93e9ed8319fcaa" } +iroh-quinn-proto = { git = "https://github.com/zakura-core/iroh-quinn", rev = "1dcc7a43488fecd199d343d47e93e9ed8319fcaa" } +iroh-quinn-udp = { git = "https://github.com/zakura-core/iroh-quinn", rev = "1dcc7a43488fecd199d343d47e93e9ed8319fcaa" } + [workspace.dependencies] # Zakura's published binary assets: the reviewed Mainnet historical frontier grid, which is too # large to carry in git history at the weekly cadence the release state advances. diff --git a/crates/zakura-network/src/zakura/handler.rs b/crates/zakura-network/src/zakura/handler.rs index fefcf45e87..2541103429 100644 --- a/crates/zakura-network/src/zakura/handler.rs +++ b/crates/zakura-network/src/zakura/handler.rs @@ -5465,6 +5465,7 @@ impl ZakuraHandlerError { #[cfg(test)] mod tests { + mod quic_progress; use super::*; use crate::{ protocol::internal::{InventoryResponse, Response}, diff --git a/crates/zakura-network/src/zakura/handler/tests/quic_progress.rs b/crates/zakura-network/src/zakura/handler/tests/quic_progress.rs new file mode 100644 index 0000000000..cd5b0508ba --- /dev/null +++ b/crates/zakura-network/src/zakura/handler/tests/quic_progress.rs @@ -0,0 +1,83 @@ +//! Regression coverage for the pinned QUIC loss-recovery backport. + +use super::*; +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn bidirectional_transfers_exceed_flow_control_windows() -> Result<(), BoxError> { + let _guard = zakura_test::init(); + const ALPN: &[u8] = b"/zakura/testkit/quic-progress/0"; + // Larger than the default send, stream, and connection windows. Both sides + // must receive acknowledgements and new flow-control credit to finish. + const BYTES: usize = 64 * 1024 * 1024; + let limits = ZakuraLocalLimits::from_config(&Config::default()); + let server = LocalEndpointFactory::with_transport_config(limits.transport_config()) + .endpoint(92341) + .await?; + let client = LocalEndpointFactory::with_transport_config(limits.transport_config()) + .endpoint(92342) + .await?; + let (connection_tx, mut connection_rx) = mpsc::channel(1); + let (stream_tx, mut stream_rx) = mpsc::channel(1); + let router = Router::builder(server) + .accept( + ALPN, + CaptureConnection { + connection_tx, + stream_tx, + }, + ) + .spawn(); + let address = LocalEndpointFactory::node_addr(router.endpoint()).await; + let connection = timeout(Duration::from_secs(10), client.connect(address, ALPN)).await??; + let remote = timeout(Duration::from_secs(5), connection_rx.recv()) + .await? + .unwrap(); + let (mut send_a, recv_a) = connection.open_bi().await?; + // Make the stream visible to accept_bi before starting the bulk transfer. + send_a.write_all(&[42]).await?; + let (mut send_b, recv_b) = timeout(Duration::from_secs(5), stream_rx.recv()) + .await? + .unwrap(); + + let transfer = async { + tokio::try_join!( + async { + send_a.write_all(&vec![42; BYTES]).await?; + send_a.finish()?; + Ok::<_, BoxError>(()) + }, + async { + send_b.write_all(&vec![43; BYTES]).await?; + send_b.finish()?; + Ok::<_, BoxError>(()) + }, + drain_stream(recv_a, BYTES, 43), + drain_stream(recv_b, BYTES + 1, 42), + )?; + Ok::<_, BoxError>(()) + }; + timeout(Duration::from_secs(30), transfer) + .await + .unwrap_or_else(|_| { + panic!( + "QUIC transfer stalled: local={:?}; remote={:?}", + connection.stats(), + remote.stats() + ) + })?; + connection.close(0u32.into(), b"done"); + client.close().await; + router.shutdown().await?; + Ok(()) +} + +async fn drain_stream(mut recv: RecvStream, expected: usize, byte: u8) -> Result<(), BoxError> { + let mut buffer = vec![0; 64 * 1024]; + let mut received = 0; + while let Some(count) = recv.read(&mut buffer).await? { + assert!(buffer[..count].iter().all(|value| *value == byte)); + received += count; + assert!(received <= expected); + } + assert_eq!(received, expected); + Ok(()) +} diff --git a/deny.toml b/deny.toml index b98c213547..d28975ad68 100644 --- a/deny.toml +++ b/deny.toml @@ -210,7 +210,7 @@ unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories -allow-git = [] +allow-git = ["https://github.com/zakura-core/iroh-quinn"] [sources.allow-org] github = [ diff --git a/docs/changelog/unreleased/941.md b/docs/changelog/unreleased/941.md new file mode 100644 index 0000000000..3ab1af39c0 --- /dev/null +++ b/docs/changelog/unreleased/941.md @@ -0,0 +1,5 @@ +## Fixed + +- Fix QUIC connections stalling during loss recovery when the socket cannot + send a batch of datagrams + ([#941](https://github.com/zakura-core/zakura/pull/941)). diff --git a/qa/supply-chain/audits.toml b/qa/supply-chain/audits.toml index 7415a56665..672460e02f 100644 --- a/qa/supply-chain/audits.toml +++ b/qa/supply-chain/audits.toml @@ -4381,3 +4381,24 @@ criteria = "safe-to-deploy" user-id = 169181 # Kris Nuttycombe (nuttycom) start = "2024-12-17" end = "2027-07-10" + +[[audits.iroh-quinn]] +who = "OpenAI Codex " +criteria = "safe-to-deploy" +delta = "0.14.0 -> 0.14.0@git:1dcc7a43488fecd199d343d47e93e9ed8319fcaa" +importable = false +notes = "Compared the 25 packaged files, including the original manifest, with the pinned Git source. They are byte-identical to the already audited registry release. The related protocol crate carries the separately audited loss-recovery fix." + +[[audits.iroh-quinn-proto]] +who = "OpenAI Codex " +criteria = "safe-to-deploy" +delta = "0.13.0 -> 0.13.0@git:1dcc7a43488fecd199d343d47e93e9ed8319fcaa" +importable = false +notes = "Compared all 53 packaged files with the pinned source. Only the transmit-bound condition and its regression test differ. The condition counts datagrams directly instead of inferring their count from bytes, so a short loss probe cannot admit a second datagram when the socket supports only one. The integer conversion is bounded by the existing ten-segment clamp. The regression failed on the original condition; all 254 default-feature protocol tests pass with the fix. No parsing, cryptography, dependency, or unsafe-code changes." + +[[audits.iroh-quinn-udp]] +who = "OpenAI Codex " +criteria = "safe-to-deploy" +delta = "0.5.7 -> 0.5.7@git:1dcc7a43488fecd199d343d47e93e9ed8319fcaa" +importable = false +notes = "Compared the 13 packaged files, including the original manifest and build script, with the pinned Git source. They are byte-identical to the already audited registry release. Socket and platform-specific code is unchanged." diff --git a/qa/supply-chain/config.toml b/qa/supply-chain/config.toml index 62b0022789..c192e21d4c 100644 --- a/qa/supply-chain/config.toml +++ b/qa/supply-chain/config.toml @@ -28,6 +28,16 @@ url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/ [imports.zcashd] url = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +# Keep the existing registry audits and require a reviewed delta for the backport. +[policy.iroh-quinn] +audit-as-crates-io = true + +[policy.iroh-quinn-proto] +audit-as-crates-io = true + +[policy.iroh-quinn-udp] +audit-as-crates-io = true + [policy.zakura] audit-as-crates-io = false