diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 7fa7bcfdd4..83da040fd2 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -16,7 +16,7 @@ jobs: - uses: taiki-e/install-action@cargo-llvm-cov - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: lcov.info diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e9af883275..edc0ce4865 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,6 +60,28 @@ jobs: cargo build --all-targets && cargo test && cargo test --manifest-path fuzz/Cargo.toml && cargo test -p quinn-udp --benches + test-solaris: + name: test on solaris + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: test on Solaris + uses: vmactions/solaris-vm@v1 + with: + release: "11.4-gcc" + usesh: true + mem: 4096 + copyback: false + prepare: | + source <(curl -s https://raw.githubusercontent.com/psumbera/solaris-rust/refs/heads/main/sh.rust-web-install) + echo "~~~~ rustc --version ~~~~" + rustc --version + echo "~~~~ Solaris-version ~~~~" + uname -a + run: | + export PATH=$HOME/.rust_solaris/bin:$PATH + cargo build --all-targets && cargo test && cargo test --manifest-path fuzz/Cargo.toml && cargo test -p quinn-udp --benches + test: strategy: matrix: @@ -121,11 +143,6 @@ jobs: - name: Install cargo binstall uses: cargo-bins/cargo-binstall@main - - # We need to downgrade cc to version 1.1.31 for ring Wasm compilation to work. - # See the upstream issue https://github.com/rust-lang/cc-rs/issues/1275 - - name: Use working `cc` version 1.1.31 - run: cargo update -p cc --precise 1.1.31 - name: build wasm32 tests (quinn-proto) run: cargo test -p quinn-proto --target wasm32-unknown-unknown --no-run @@ -146,6 +163,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # Note that we must also update the README when changing the MSRV - uses: dtolnay/rust-toolchain@1.70.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --lib --all-features -p quinn-udp -p quinn-proto -p quinn diff --git a/README.md b/README.md index b82f9c4fd4..d93c872838 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The project was founded by [Dirkjan Ochtman](https://github.com/djc) and [rustls][rustls] and [*ring*][ring] - Application-layer datagrams for small, unreliable messages - Future-based async API -- Minimum supported Rust version of 1.66 +- Minimum supported Rust version of 1.70 ## Overview diff --git a/deny.toml b/deny.toml index 8998d62442..1fb8356f20 100644 --- a/deny.toml +++ b/deny.toml @@ -7,7 +7,7 @@ allow = [ "MIT", "MPL-2.0", "OpenSSL", - "Unicode-DFS-2016", + "Unicode-3.0", ] private = { ignore = true } diff --git a/perf/src/bin/perf_client.rs b/perf/src/bin/perf_client.rs index d081ae1799..a8ceeceec8 100644 --- a/perf/src/bin/perf_client.rs +++ b/perf/src/bin/perf_client.rs @@ -308,7 +308,7 @@ async fn request( let send_stream_stats = stream_stats.new_sender(&send, upload); - const DATA: [u8; 1024 * 1024] = [42; 1024 * 1024]; + static DATA: [u8; 1024 * 1024] = [42; 1024 * 1024]; while upload > 0 { let chunk_len = upload.min(DATA.len() as u64); send.write_chunk(Bytes::from_static(&DATA[..chunk_len as usize])) diff --git a/perf/src/bin/perf_server.rs b/perf/src/bin/perf_server.rs index 5d29701dbd..74b527acab 100644 --- a/perf/src/bin/perf_server.rs +++ b/perf/src/bin/perf_server.rs @@ -215,7 +215,7 @@ async fn drain_stream(mut stream: quinn::RecvStream) -> Result<()> { } async fn respond(mut bytes: u64, mut stream: quinn::SendStream) -> Result<()> { - const DATA: [u8; 1024 * 1024] = [42; 1024 * 1024]; + static DATA: [u8; 1024 * 1024] = [42; 1024 * 1024]; while bytes > 0 { let chunk_len = bytes.min(DATA.len() as u64); diff --git a/quinn-proto/Cargo.toml b/quinn-proto/Cargo.toml index 617c41081c..8a91c14dff 100644 --- a/quinn-proto/Cargo.toml +++ b/quinn-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quinn-proto" -version = "0.11.9" +version = "0.11.10" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -10,9 +10,6 @@ keywords.workspace = true categories.workspace = true workspace = ".." -[package.metadata.docs.rs] -all-features = true - [features] default = ["rustls-ring", "log"] aws-lc-rs = ["dep:aws-lc-rs", "aws-lc-rs?/aws-lc-sys", "aws-lc-rs?/prebuilt-nasm"] @@ -66,3 +63,7 @@ wasm-bindgen-test = { workspace = true } [lints.rust] # https://rust-fuzz.github.io/book/cargo-fuzz/guide.html#cfgfuzzing unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } + +[package.metadata.docs.rs] +# all non-default features except fips (cannot build on docs.rs environment) +features = ["rustls-aws-lc-rs", "rustls-ring", "platform-verifier", "log", "rustls-log"] diff --git a/quinn-proto/src/connection/ack_frequency.rs b/quinn-proto/src/connection/ack_frequency.rs index 94b6805fbc..ab06e45ac8 100644 --- a/quinn-proto/src/connection/ack_frequency.rs +++ b/quinn-proto/src/connection/ack_frequency.rs @@ -121,9 +121,7 @@ impl AckFrequencyState { ) -> Result { if self .last_ack_frequency_frame - .map_or(false, |highest_sequence_nr| { - frame.sequence.into_inner() <= highest_sequence_nr - }) + .is_some_and(|highest_sequence_nr| frame.sequence.into_inner() <= highest_sequence_nr) { return Ok(false); } diff --git a/quinn-proto/src/connection/mod.rs b/quinn-proto/src/connection/mod.rs index aa1521b568..a29d25932f 100644 --- a/quinn-proto/src/connection/mod.rs +++ b/quinn-proto/src/connection/mod.rs @@ -32,7 +32,7 @@ use crate::{ token::ResetToken, transport_parameters::TransportParameters, Dir, Duration, EndpointConfig, Frame, Instant, Side, StreamId, Transmit, TransportError, - TransportErrorCode, VarInt, MAX_CID_SIZE, MAX_STREAM_COUNT, MIN_INITIAL_SIZE, + TransportErrorCode, VarInt, INITIAL_MTU, MAX_CID_SIZE, MAX_STREAM_COUNT, MIN_INITIAL_SIZE, TIMER_GRANULARITY, }; @@ -700,13 +700,21 @@ impl Connection { // waste large amounts of bandwidth. The exact threshold is a bit arbitrary // and might benefit from further tuning, though there's no universally // optimal value. + // + // Additionally, if this datagram is a loss probe and `segment_size` is + // larger than `INITIAL_MTU`, then padding it to `segment_size` to continue + // the GSO batch would risk failure to recover from a reduction in path + // MTU. Loss probes are the only packets for which we might grow + // `buf_capacity` by less than `segment_size`. const MAX_PADDING: usize = 16; let packet_len_unpadded = cmp::max(builder.min_size, buf.len()) - datagram_start + builder.tag_len; - if packet_len_unpadded + MAX_PADDING < segment_size { + if packet_len_unpadded + MAX_PADDING < segment_size + || datagram_start + segment_size > buf_capacity + { trace!( - "GSO truncated by demand for {} padding bytes", + "GSO truncated by demand for {} padding bytes or loss probe", segment_size - packet_len_unpadded ); builder_storage = Some(builder); @@ -749,7 +757,17 @@ impl Connection { } // Allocate space for another datagram - buf_capacity += segment_size; + let next_datagram_size_limit = match self.spaces[space_id].loss_probes { + 0 => segment_size, + _ => { + self.spaces[space_id].loss_probes -= 1; + // Clamp the datagram to at most the minimum MTU to ensure that loss probes + // can get through and enable recovery even if the path MTU has shrank + // unexpectedly. + usize::from(INITIAL_MTU) + } + }; + buf_capacity += next_datagram_size_limit; if buf.capacity() < buf_capacity { // We reserve the maximum space for sending `max_datagrams` upfront // to avoid any reallocations if more datagrams have to be appended later on. @@ -963,14 +981,10 @@ impl Connection { // Send MTU probe if necessary if buf.is_empty() && self.state.is_established() { let space_id = SpaceId::Data; - let probe_size = match self + let probe_size = self .path .mtud - .poll_transmit(now, self.packet_number_filter.peek(&self.spaces[space_id])) - { - Some(next_probe_size) => next_probe_size, - None => return None, - }; + .poll_transmit(now, self.packet_number_filter.peek(&self.spaces[space_id]))?; let buf_capacity = probe_size as usize; buf.reserve(buf_capacity); @@ -1645,7 +1659,7 @@ impl Connection { None if self .path .first_packet_after_rtt_sample - .map_or(false, |x| x < (pn_space, packet)) => + .is_some_and(|x| x < (pn_space, packet)) => { persistent_congestion_start = Some(info.time_sent); } @@ -2220,7 +2234,7 @@ impl Connection { let _guard = span.enter(); let is_duplicate = |n| self.spaces[packet.header.space()].dedup.insert(n); - if number.map_or(false, is_duplicate) { + if number.is_some_and(is_duplicate) { debug!("discarding possible duplicate packet"); return; } else if self.state.is_handshake() && packet.header.is_short() { @@ -3640,13 +3654,13 @@ impl Connection { || self .prev_path .as_ref() - .map_or(false, |(_, x)| x.challenge_pending) + .is_some_and(|(_, x)| x.challenge_pending) || !self.path_responses.is_empty() || self .datagrams .outgoing .front() - .map_or(false, |x| x.size(true) <= max_size) + .is_some_and(|x| x.size(true) <= max_size) } /// Update counters to account for a packet becoming acknowledged, lost, or abandoned diff --git a/quinn-proto/src/connection/mtud.rs b/quinn-proto/src/connection/mtud.rs index c221ab48dc..be1fe7eef1 100644 --- a/quinn-proto/src/connection/mtud.rs +++ b/quinn-proto/src/connection/mtud.rs @@ -417,7 +417,7 @@ impl BlackHoleDetector { let end_last_burst = self .current_loss_burst .as_ref() - .map_or(false, |current| pn - current.latest_non_probe != 1); + .is_some_and(|current| pn - current.latest_non_probe != 1); if end_last_burst { self.finish_loss_burst(); diff --git a/quinn-proto/src/connection/packet_builder.rs b/quinn-proto/src/connection/packet_builder.rs index bc1cd7ffd3..868a8c7ca8 100644 --- a/quinn-proto/src/connection/packet_builder.rs +++ b/quinn-proto/src/connection/packet_builder.rs @@ -1,5 +1,3 @@ -use std::cmp; - use bytes::Bytes; use rand::Rng; use tracing::{trace, trace_span}; @@ -8,7 +6,7 @@ use super::{spaces::SentPacket, Connection, SentFrames}; use crate::{ frame::{self, Close}, packet::{Header, InitialHeader, LongType, PacketNumber, PartialEncode, SpaceId, FIXED_BIT}, - ConnectionId, Instant, TransportError, TransportErrorCode, INITIAL_MTU, + ConnectionId, Instant, TransportError, TransportErrorCode, }; pub(super) struct PacketBuilder { @@ -38,7 +36,7 @@ impl PacketBuilder { space_id: SpaceId, dst_cid: ConnectionId, buffer: &mut Vec, - mut buffer_capacity: usize, + buffer_capacity: usize, datagram_start: usize, ack_eliciting: bool, conn: &mut Connection, @@ -79,13 +77,6 @@ impl PacketBuilder { } let space = &mut conn.spaces[space_id]; - - if space.loss_probes != 0 { - space.loss_probes -= 1; - // Clamp the packet size to at most the minimum MTU to ensure that loss probes can get - // through and enable recovery even if the path MTU has shrank unexpectedly. - buffer_capacity = cmp::min(buffer_capacity, datagram_start + usize::from(INITIAL_MTU)); - } let exact_number = match space_id { SpaceId::Data => conn.packet_number_filter.allocate(&mut conn.rng, space), _ => space.get_tx_number(), diff --git a/quinn-proto/src/connection/packet_crypto.rs b/quinn-proto/src/connection/packet_crypto.rs index ffb98ea8ad..0d3063aa91 100644 --- a/quinn-proto/src/connection/packet_crypto.rs +++ b/quinn-proto/src/connection/packet_crypto.rs @@ -131,7 +131,7 @@ pub(super) fn decrypt_packet_body( if crypto_update { // Validate incoming key update - if number <= rx_packet || prev_crypto.map_or(false, |x| x.update_unacked) { + if number <= rx_packet || prev_crypto.is_some_and(|x| x.update_unacked) { return Err(Some(TransportError::KEY_UPDATE_ERROR(""))); } } diff --git a/quinn-proto/src/connection/spaces.rs b/quinn-proto/src/connection/spaces.rs index 3832582fb9..0d0edad68d 100644 --- a/quinn-proto/src/connection/spaces.rs +++ b/quinn-proto/src/connection/spaces.rs @@ -104,7 +104,7 @@ impl PacketSpace { /// Queue data for a tail loss probe (or anti-amplification deadlock prevention) packet /// - /// Probes are sent similarly to normal packets when an expect ACK has not arrived. We never + /// Probes are sent similarly to normal packets when an expected ACK has not arrived. We never /// deem a packet lost until we receive an ACK that should have included it, but if a trailing /// run of packets (or their ACKs) are lost, this might not happen in a timely fashion. We send /// probe packets to force an ACK, and exempt them from congestion control to prevent a deadlock @@ -856,7 +856,7 @@ impl PacketNumberFilter { if space_id == SpaceId::Data && self .prev_skipped_packet_number - .map_or(false, |x| range.contains(&x)) + .is_some_and(|x| range.contains(&x)) { return Err(TransportError::PROTOCOL_VIOLATION("unsent packet acked")); } diff --git a/quinn-proto/src/connection/streams/state.rs b/quinn-proto/src/connection/streams/state.rs index a3f14b3b5f..90b853b7be 100644 --- a/quinn-proto/src/connection/streams/state.rs +++ b/quinn-proto/src/connection/streams/state.rs @@ -396,7 +396,7 @@ impl StreamsState { self.send .get(&stream.id) .and_then(|s| s.as_ref()) - .map_or(false, |s| !s.is_reset()) + .is_some_and(|s| !s.is_reset()) }) } @@ -406,7 +406,7 @@ impl StreamsState { .get(&id) .and_then(|s| s.as_ref()) .and_then(|s| s.as_open_recv()) - .map_or(false, |s| s.can_send_flow_control()) + .is_some_and(|s| s.can_send_flow_control()) } pub(in crate::connection) fn write_control_frames( diff --git a/quinn-proto/src/connection/timer.rs b/quinn-proto/src/connection/timer.rs index 1bf67e0232..566652d0da 100644 --- a/quinn-proto/src/connection/timer.rs +++ b/quinn-proto/src/connection/timer.rs @@ -60,6 +60,6 @@ impl TimerTable { } pub(super) fn is_expired(&self, timer: Timer, after: Instant) -> bool { - self.data[timer as usize].map_or(false, |x| x <= after) + self.data[timer as usize].is_some_and(|x| x <= after) } } diff --git a/quinn-proto/src/endpoint.rs b/quinn-proto/src/endpoint.rs index 6210aafe91..fc652a8bc1 100644 --- a/quinn-proto/src/endpoint.rs +++ b/quinn-proto/src/endpoint.rs @@ -31,8 +31,8 @@ use crate::{ }, token::TokenDecodeError, transport_parameters::{PreferredAddress, TransportParameters}, - Instant, ResetToken, RetryToken, Side, SystemTime, Transmit, TransportConfig, TransportError, - INITIAL_MTU, MAX_CID_SIZE, MIN_INITIAL_SIZE, RESET_TOKEN_SIZE, + Duration, Instant, ResetToken, RetryToken, Side, SystemTime, Transmit, TransportConfig, + TransportError, INITIAL_MTU, MAX_CID_SIZE, MIN_INITIAL_SIZE, RESET_TOKEN_SIZE, }; /// The main entry point to the library @@ -215,11 +215,11 @@ impl Endpoint { if incoming_buffer .total_bytes .checked_add(datagram_len as u64) - .map_or(false, |n| n <= config.incoming_buffer_size) + .is_some_and(|n| n <= config.incoming_buffer_size) && self .all_incoming_buffers_total_bytes .checked_add(datagram_len as u64) - .map_or(false, |n| n <= config.incoming_buffer_size_total) + .is_some_and(|n| n <= config.incoming_buffer_size_total) { incoming_buffer.datagrams.push(event); incoming_buffer.total_bytes += datagram_len as u64; @@ -284,7 +284,7 @@ impl Endpoint { return match first_decode.finish(Some(&*crypto.header.remote)) { Ok(packet) => { - self.handle_first_packet(addresses, ecn, packet, remaining, crypto, buf) + self.handle_first_packet(addresses, ecn, packet, remaining, crypto, buf, now) } Err(e) => { trace!("unable to decode initial packet: {}", e); @@ -334,7 +334,7 @@ impl Endpoint { ) -> Option { if self .last_stateless_reset - .map_or(false, |last| last + self.config.min_reset_interval > now) + .is_some_and(|last| last + self.config.min_reset_interval > now) { debug!("ignoring unexpected packet within minimum stateless reset interval"); return None; @@ -482,6 +482,7 @@ impl Endpoint { rest: Option, crypto: Keys, buf: &mut Vec, + now: Instant, ) -> Option { if !packet.reserved_bits_valid() { debug!("dropping connection attempt with invalid reserved bits"); @@ -533,6 +534,7 @@ impl Endpoint { .insert_initial_incoming(header.dst_cid, incoming_idx); Some(DatagramEvent::NewConnection(Incoming { + received_at: now, addresses, ecn, packet: InitialPacket { @@ -569,6 +571,23 @@ impl Endpoint { version, .. } = incoming.packet.header; + let server_config = + server_config.unwrap_or_else(|| self.server_config.as_ref().unwrap().clone()); + + if server_config + .transport + .max_idle_timeout + .is_some_and(|timeout| { + incoming.received_at + Duration::from_millis(timeout.into()) <= now + }) + { + debug!("abandoning accept of stale initial"); + self.index.remove_initial(dst_cid); + return Err(AcceptError { + cause: ConnectionError::TimedOut, + response: None, + }); + } if self.cids_exhausted() { debug!("refusing connection"); @@ -586,9 +605,6 @@ impl Endpoint { }); } - let server_config = - server_config.unwrap_or_else(|| self.server_config.as_ref().unwrap().clone()); - if incoming .crypto .packet @@ -644,7 +660,7 @@ impl Endpoint { src_cid, pref_addr_cid, incoming.addresses, - now, + incoming.received_at, tls, Some(server_config), transport_config, @@ -653,7 +669,7 @@ impl Endpoint { self.index.insert_initial(dst_cid, ch); match conn.handle_first_packet( - now, + incoming.received_at, incoming.addresses.remote, incoming.ecn, packet_number, @@ -1178,6 +1194,7 @@ pub enum DatagramEvent { /// An incoming connection for which the server has not yet begun its part of the handshake. pub struct Incoming { + received_at: Instant, addresses: FourTuple, ecn: Option, packet: InitialPacket, diff --git a/quinn-proto/src/packet.rs b/quinn-proto/src/packet.rs index fba0b40db2..1970b4679e 100644 --- a/quinn-proto/src/packet.rs +++ b/quinn-proto/src/packet.rs @@ -769,7 +769,7 @@ impl PacketNumber { // The following code calculates a candidate value and makes sure it's within the packet // number window. let candidate = (expected & !mask) | truncated; - if expected.checked_sub(hwin).map_or(false, |x| candidate <= x) { + if expected.checked_sub(hwin).is_some_and(|x| candidate <= x) { candidate + win } else if candidate > expected + hwin && candidate > win { candidate - win diff --git a/quinn-proto/src/range_set/btree_range_set.rs b/quinn-proto/src/range_set/btree_range_set.rs index 85a2c538ae..d8eb11de30 100644 --- a/quinn-proto/src/range_set/btree_range_set.rs +++ b/quinn-proto/src/range_set/btree_range_set.rs @@ -18,7 +18,7 @@ impl RangeSet { } pub fn contains(&self, x: u64) -> bool { - self.pred(x).map_or(false, |(_, end)| end > x) + self.pred(x).is_some_and(|(_, end)| end > x) } pub fn insert_one(&mut self, x: u64) -> bool { diff --git a/quinn-proto/src/tests/util.rs b/quinn-proto/src/tests/util.rs index 8cb47d30fe..7e927e2035 100644 --- a/quinn-proto/src/tests/util.rs +++ b/quinn-proto/src/tests/util.rs @@ -356,7 +356,7 @@ impl TestEndpoint { let buffer_size = self.endpoint.config().get_max_udp_payload_size() as usize; let mut buf = Vec::with_capacity(buffer_size); - while self.inbound.front().map_or(false, |x| x.0 <= now) { + while self.inbound.front().is_some_and(|x| x.0 <= now) { let (recv_time, ecn, packet) = self.inbound.pop_front().unwrap(); if let Some(event) = self .endpoint @@ -415,7 +415,7 @@ impl TestEndpoint { loop { let mut endpoint_events: Vec<(ConnectionHandle, EndpointEvent)> = vec![]; for (ch, conn) in self.connections.iter_mut() { - if self.timeout.map_or(false, |x| x <= now) { + if self.timeout.is_some_and(|x| x <= now) { self.timeout = None; conn.handle_timeout(now); } diff --git a/quinn-proto/src/transport_parameters.rs b/quinn-proto/src/transport_parameters.rs index 8d8996ef69..03f8cf3874 100644 --- a/quinn-proto/src/transport_parameters.rs +++ b/quinn-proto/src/transport_parameters.rs @@ -151,7 +151,7 @@ impl TransportParameters { initial_max_stream_data_uni: config.stream_receive_window, max_udp_payload_size: endpoint_config.max_udp_payload_size, max_idle_timeout: config.max_idle_timeout.unwrap_or(VarInt(0)), - disable_active_migration: server_config.map_or(false, |c| !c.migration), + disable_active_migration: server_config.is_some_and(|c| !c.migration), active_connection_id_limit: if cid_gen.cid_len() == 0 { 2 // i.e. default, i.e. unsent } else { @@ -474,7 +474,7 @@ impl TransportParameters { || params.initial_max_streams_bidi.0 > MAX_STREAM_COUNT || params.initial_max_streams_uni.0 > MAX_STREAM_COUNT // https://www.ietf.org/archive/id/draft-ietf-quic-ack-frequency-08.html#section-3-4 - || params.min_ack_delay.map_or(false, |min_ack_delay| { + || params.min_ack_delay.is_some_and(|min_ack_delay| { // min_ack_delay uses microseconds, whereas max_ack_delay uses milliseconds min_ack_delay.0 > params.max_ack_delay.0 * 1_000 }) @@ -486,8 +486,7 @@ impl TransportParameters { || params.stateless_reset_token.is_some())) // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.38.1 || params - .preferred_address - .map_or(false, |x| x.connection_id.is_empty()) + .preferred_address.is_some_and(|x| x.connection_id.is_empty()) { return Err(Error::IllegalValue); } diff --git a/quinn-udp/Cargo.toml b/quinn-udp/Cargo.toml index 795de3ef72..8d5f056ce7 100644 --- a/quinn-udp/Cargo.toml +++ b/quinn-udp/Cargo.toml @@ -10,9 +10,6 @@ keywords.workspace = true categories.workspace = true workspace = ".." -[package.metadata.docs.rs] -all-features = true - [features] default = ["tracing", "log"] # Configure `tracing` to log events via `log` if no `tracing` subscriber exists. @@ -45,3 +42,6 @@ bench = false [[bench]] name = "throughput" harness = false + +[package.metadata.docs.rs] +all-features = true diff --git a/quinn-udp/src/unix.rs b/quinn-udp/src/unix.rs index b8ff6e5929..38b322e244 100644 --- a/quinn-udp/src/unix.rs +++ b/quinn-udp/src/unix.rs @@ -127,8 +127,7 @@ impl UdpSocketState { #[cfg(any(target_os = "linux", target_os = "android"))] { // opportunistically try to enable GRO. See gro::gro_segments(). - #[cfg(target_os = "linux")] - let _ = set_socket_option(&*io, libc::SOL_UDP, libc::UDP_GRO, OPTION_ON); + let _ = set_socket_option(&*io, libc::SOL_UDP, gro::UDP_GRO, OPTION_ON); // Forbid IPv4 fragmentation. Set even for IPv6 to account for IPv6 mapped IPv4 addresses. // Set `may_fragment` to `true` if this option is not supported on the platform. @@ -318,12 +317,14 @@ fn send( // Some network adapters and drivers do not support GSO. Unfortunately, Linux // offers no easy way for us to detect this short of an EIO or sometimes EINVAL // when we try to actually send datagrams using it. - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] if let Some(libc::EIO) | Some(libc::EINVAL) = e.raw_os_error() { // Prevent new transmits from being scheduled using GSO. Existing GSO transmits // may already be in the pipeline, so we need to tolerate additional failures. if state.max_gso_segments() > 1 { - crate::log::error!("got transmit error, halting segmentation offload"); + crate::log::info!( + "`libc::sendmsg` failed with {e}; halting segmentation offload" + ); state .max_gso_segments .store(1, std::sync::atomic::Ordering::Relaxed); @@ -592,7 +593,12 @@ fn prepare_msg( encoder.push(libc::IPPROTO_IPV6, libc::IPV6_TCLASS, ecn); } - if let Some(segment_size) = transmit.segment_size { + // Only set the segment size if it is different from the size of the contents. + // Some network drivers don't like being told to do GSO even if there is effectively only a single segment. + if let Some(segment_size) = transmit + .segment_size + .filter(|segment_size| *segment_size != transmit.contents.len()) + { gso::set_segment_size(&mut encoder, segment_size as u16); } @@ -719,8 +725,8 @@ fn decode_recv( let pktinfo = unsafe { cmsg::decode::(cmsg) }; dst_ip = Some(IpAddr::V6(Ipv6Addr::from(pktinfo.ipi6_addr.s6_addr))); } - #[cfg(target_os = "linux")] - (libc::SOL_UDP, libc::UDP_GRO) => unsafe { + #[cfg(any(target_os = "linux", target_os = "android"))] + (libc::SOL_UDP, gro::UDP_GRO) => unsafe { stride = cmsg::decode::(cmsg) as usize; }, _ => {} @@ -767,10 +773,16 @@ pub(crate) const BATCH_SIZE: usize = 32; #[cfg(apple_slow)] pub(crate) const BATCH_SIZE: usize = 1; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod gso { use super::*; + #[cfg(not(target_os = "android"))] + const UDP_SEGMENT: libc::c_int = libc::UDP_SEGMENT; + #[cfg(target_os = "android")] + // TODO: Add this to libc + const UDP_SEGMENT: libc::c_int = 103; + /// Checks whether GSO support is available by setting the UDP_SEGMENT /// option on a socket pub(crate) fn max_gso_segments() -> usize { @@ -785,26 +797,39 @@ mod gso { // As defined in linux/udp.h // #define UDP_MAX_SEGMENTS (1 << 6UL) - match set_socket_option(&socket, libc::SOL_UDP, libc::UDP_SEGMENT, GSO_SIZE) { + match set_socket_option(&socket, libc::SOL_UDP, UDP_SEGMENT, GSO_SIZE) { Ok(()) => 64, - Err(_) => 1, + Err(_e) => { + crate::log::debug!( + "failed to set `UDP_SEGMENT` socket option ({_e}); setting `max_gso_segments = 1`" + ); + + 1 + } } } pub(crate) fn set_segment_size(encoder: &mut cmsg::Encoder, segment_size: u16) { - encoder.push(libc::SOL_UDP, libc::UDP_SEGMENT, segment_size); + encoder.push(libc::SOL_UDP, UDP_SEGMENT, segment_size); } } // On Apple platforms using the `sendmsg_x` call, UDP datagram segmentation is not // offloaded to the NIC or even the kernel, but instead done here in user space in // [`send`]) and then passed to the OS as individual `iovec`s (up to `BATCH_SIZE`). -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod gso { use super::*; pub(super) fn max_gso_segments() -> usize { - BATCH_SIZE + #[cfg(apple_fast)] + { + BATCH_SIZE + } + #[cfg(not(apple_fast))] + { + 1 + } } pub(super) fn set_segment_size( @@ -815,10 +840,16 @@ mod gso { } } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod gro { use super::*; + #[cfg(not(target_os = "android"))] + pub(crate) const UDP_GRO: libc::c_int = libc::UDP_GRO; + #[cfg(target_os = "android")] + // TODO: Add this to libc + pub(crate) const UDP_GRO: libc::c_int = 104; + pub(crate) fn gro_segments() -> usize { let socket = match std::net::UdpSocket::bind("[::]:0") .or_else(|_| std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))) @@ -834,7 +865,7 @@ mod gro { // (get_max_udp_payload_size() * gro_segments()) is large enough to hold the largest GRO // list the kernel might potentially produce. See // https://github.com/quinn-rs/quinn/pull/1354. - match set_socket_option(&socket, libc::SOL_UDP, libc::UDP_GRO, OPTION_ON) { + match set_socket_option(&socket, libc::SOL_UDP, UDP_GRO, OPTION_ON) { Ok(()) => 64, Err(_) => 1, } @@ -882,7 +913,7 @@ fn set_socket_option( const OPTION_ON: libc::c_int = 1; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "android")))] mod gro { pub(super) fn gro_segments() -> usize { 1 diff --git a/quinn-udp/src/windows.rs b/quinn-udp/src/windows.rs index 886f455117..223ab0fdf6 100644 --- a/quinn-udp/src/windows.rs +++ b/quinn-udp/src/windows.rs @@ -14,7 +14,7 @@ use windows_sys::Win32::Networking::WinSock; use crate::{ cmsg::{self, CMsgHdr}, - log::{debug, error}, + log::debug, log_sendmsg_error, EcnCodepoint, RecvMeta, Transmit, UdpSockRef, IO_ERROR_LOG_INTERVAL, }; @@ -61,9 +61,10 @@ impl UdpSocketState { // We don't support old versions of Windows that do not enable access to `WSARecvMsg()` if WSARECVMSG_PTR.is_none() { - error!("network stack does not support WSARecvMsg function"); - - return Err(io::Error::from(io::ErrorKind::Unsupported)); + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "network stack does not support WSARecvMsg function", + )); } if is_ipv4 { diff --git a/quinn-udp/tests/tests.rs b/quinn-udp/tests/tests.rs index a0dd32fda2..7169ed6ae8 100644 --- a/quinn-udp/tests/tests.rs +++ b/quinn-udp/tests/tests.rs @@ -156,7 +156,10 @@ fn ecn_v4_mapped_v6() { } #[test] -#[cfg_attr(not(any(target_os = "linux", target_os = "windows")), ignore)] +#[cfg_attr( + not(any(target_os = "linux", target_os = "windows", target_os = "android")), + ignore +)] fn gso() { let send = UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)) .or_else(|_| UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))) @@ -190,7 +193,7 @@ fn test_send_recv(send: &Socket, recv: &Socket, transmit: Transmit) { // Reverse non-blocking flag set by `UdpSocketState` to make the test non-racy recv.set_nonblocking(false).unwrap(); - send_state.send(send.into(), &transmit).unwrap(); + send_state.try_send(send.into(), &transmit).unwrap(); let mut buf = [0; u16::MAX as usize]; let mut meta = RecvMeta::default(); diff --git a/quinn/Cargo.toml b/quinn/Cargo.toml index 0ae4d99b18..d60989abcd 100644 --- a/quinn/Cargo.toml +++ b/quinn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quinn" -version = "0.11.6" +version = "0.11.7" license.workspace = true repository.workspace = true description = "Versatile QUIC transport protocol implementation" @@ -11,9 +11,6 @@ workspace = ".." edition.workspace = true rust-version.workspace = true -[package.metadata.docs.rs] -all-features = true - [features] default = ["log", "platform-verifier", "runtime-tokio", "rustls-ring"] # Enables `Endpoint::client` and `Endpoint::server` conveniences @@ -97,3 +94,7 @@ required-features = ["rustls-ring"] name = "bench" harness = false required-features = ["rustls-ring"] + +[package.metadata.docs.rs] +# all non-default features except fips (cannot build on docs.rs environment) +features = ["lock_tracking", "rustls-aws-lc-rs", "rustls-ring", "runtime-tokio", "runtime-async-std", "runtime-smol", "log", "rustls-log"] diff --git a/quinn/examples/server.rs b/quinn/examples/server.rs index ae1e6b3e25..b65d739bee 100644 --- a/quinn/examples/server.rs +++ b/quinn/examples/server.rs @@ -70,7 +70,7 @@ fn main() { async fn run(options: Opt) -> Result<()> { let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { let key = fs::read(key_path).context("failed to read private key")?; - let key = if key_path.extension().map_or(false, |x| x == "der") { + let key = if key_path.extension().is_some_and(|x| x == "der") { PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) } else { rustls_pemfile::private_key(&mut &*key) @@ -78,7 +78,7 @@ async fn run(options: Opt) -> Result<()> { .ok_or_else(|| anyhow::Error::msg("no private keys found"))? }; let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; - let cert_chain = if cert_path.extension().map_or(false, |x| x == "der") { + let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { vec![CertificateDer::from(cert_chain)] } else { rustls_pemfile::certs(&mut &*cert_chain) @@ -143,7 +143,7 @@ async fn run(options: Opt) -> Result<()> { while let Some(conn) = endpoint.accept().await { if options .connection_limit - .map_or(false, |n| endpoint.open_connections() >= n) + .is_some_and(|n| endpoint.open_connections() >= n) { info!("refusing due to open connection limit"); conn.refuse(); diff --git a/quinn/src/tests.rs b/quinn/src/tests.rs index 021a1a650b..ea5a657130 100755 --- a/quinn/src/tests.rs +++ b/quinn/src/tests.rs @@ -387,6 +387,7 @@ async fn zero_rtt() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Fails on Solaris")] fn echo_v6() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), @@ -399,6 +400,7 @@ fn echo_v6() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Sometimes hangs in poll() on Solaris")] fn echo_v4() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), @@ -411,6 +413,7 @@ fn echo_v4() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] fn echo_dualstack() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), @@ -423,6 +426,7 @@ fn echo_dualstack() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] fn stress_receive_window() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), @@ -435,6 +439,7 @@ fn stress_receive_window() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] fn stress_stream_receive_window() { // Note that there is no point in running this with too many streams, // since the window is only active within a stream. @@ -449,6 +454,7 @@ fn stress_stream_receive_window() { } #[test] +#[cfg_attr(target_os = "solaris", ignore = "Hangs in poll() on Solaris")] fn stress_both_windows() { run_echo(EchoArgs { client_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),