/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#[repr(C)] pubstruct NeqoHttp3Conn {
conn: Http3Client,
local_addr: SocketAddr,
refcnt: AtomicRefcnt, /// Socket to use for IO. /// /// When [`None`], NSPR is used for IO. // // Use a `BorrowedSocket` instead of e.g. `std::net::UdpSocket`. The latter // would close the file descriptor on `Drop`. The lifetime of the underlying // OS socket is managed not by `neqo_glue` but `NSPR`.
socket: Option<neqo_udp::Socket<BorrowedSocket>>, /// Buffered outbound datagram from previous send that failed with /// WouldBlock. To be sent once UDP socket has write-availability again.
buffered_outbound_datagram: Option<datagram::Batch>,
unsafe { let family = i32::from(moz_netaddr_get_family(arg)); if family == AF_INET { let port = u16::from_be(moz_netaddr_get_network_order_port(arg)); let ipv4 = Ipv4Addr::from(u32::from_be(moz_netaddr_get_network_order_ip(arg))); return Ok(SocketAddr::new(IpAddr::V4(ipv4), port));
}
if family == AF_INET6 { let port = u16::from_be(moz_netaddr_get_network_order_port(arg)); let ipv6_slice: [u8; 16] = slice::from_raw_parts(moz_netaddr_get_ipv6(arg), 16)
.try_into()
.expect("slice with incorrect length"); let ipv6 = Ipv6Addr::from(ipv6_slice); return Ok(SocketAddr::new(IpAddr::V6(ipv6), port));
}
}
type SetTimerFunc = extern"C"fn(context: *mut c_void, timeout: u64);
#[cfg(unix)] type BorrowedSocket = std::os::fd::BorrowedFd<'static>; #[cfg(windows)] type BorrowedSocket = std::os::windows::io::BorrowedSocket<'static>;
impl NeqoHttp3Conn { /// Create a new [`NeqoHttp3Conn`]. /// /// Note that [`NeqoHttp3Conn`] works under the assumption that the UDP /// socket of the connection, i.e. the one provided to /// [`NeqoHttp3Conn::new`], does not change throughout the lifetime of /// [`NeqoHttp3Conn`]. #[expect(
clippy::too_many_arguments,
clippy::too_many_lines,
reason = "Nothing to be done about it."
)] fn new(
origin: &nsACString,
alpn: &nsACString,
local_addr: *const NetAddr,
remote_addr: *const NetAddr,
max_table_size: u64,
max_blocked_streams: u16,
max_data: u64,
max_stream_data: u64,
version_negotiation: bool,
webtransport: bool,
qlog_dir: &nsACString,
idle_timeout: u32,
fast_pto: u32,
pmtud_enabled: bool,
socket: Option<i64>,
) -> Result<RefPtr<Self>, nsresult> { // Nss init.
init().map_err(|_| NS_ERROR_UNEXPECTED)?;
let socket = socket
.map(|socket| { #[cfg(unix)] let borrowed = { use std::os::fd::{BorrowedFd, RawFd}; if socket == -1 {
qerror!("got invalid socked {}", socket); return Err(NS_ERROR_INVALID_ARG);
} let raw: RawFd = socket.try_into().map_err(|e| {
qerror!("got invalid socked {}: {}", socket, e);
NS_ERROR_INVALID_ARG
})?; unsafe { BorrowedFd::borrow_raw(raw) }
}; #[cfg(windows)] let borrowed = { use std::os::windows::io::{BorrowedSocket, RawSocket}; if socket as usize == winapi::um::winsock2::INVALID_SOCKET {
qerror!("got invalid socked {}", socket); return Err(NS_ERROR_INVALID_ARG);
} let raw: RawSocket = socket.try_into().map_err(|e| {
qerror!("got invalid socked {}: {}", socket, e);
NS_ERROR_INVALID_ARG
})?; unsafe { BorrowedSocket::borrow_raw(raw) }
}; let s = neqo_udp::Socket::new(borrowed).map_err(|e| {
qerror!("failed to initialize socket {}: {}", socket, e);
into_nsresult(&e)
})?; // Called after Socket::new (which sets up IP_RECVTOS etc.) since // enable_apple_fast_path only sets the fast-path flag on the // already-constructed UdpSocketState. #[cfg(target_vendor = "apple")] if APPLE_FAST_PATH.load(Ordering::Relaxed)
&& static_prefs::pref!("network.http.http3.apple_fast_datapath")
{ // SAFETY: The probe has verified the APIs work on this OS version. unsafe { s.enable_apple_fast_path() }
}
Ok(s)
})
.transpose()?;
let origin_conv = str::from_utf8(origin).map_err(|_| NS_ERROR_INVALID_ARG)?;
let alpn_conv = str::from_utf8(alpn).map_err(|_| NS_ERROR_INVALID_ARG)?;
let local: SocketAddr = netaddr_to_socket_addr(local_addr)?;
let remote: SocketAddr = netaddr_to_socket_addr(remote_addr)?;
let quic_version = match alpn_conv { "h3" => Version::Version1,
_ => return Err(NS_ERROR_INVALID_ARG),
};
let version_list = if version_negotiation {
Version::all()
} else {
vec![quic_version]
};
let cc_algorithm = match static_prefs::pref!("network.http.http3.cc_algorithm") { 0 => CongestionControl::NewReno, 1 => CongestionControl::Cubic,
_ => { // Unknown preferences; default to Cubic
CongestionControl::Cubic
}
};
let slow_start = match static_prefs::pref!("network.http.http3.slow_start_algorithm") { 0 => SlowStart::Classic, 1 => SlowStart::HyStart, 2 => SlowStart::Search,
_ => { // Unknown preferences; default to Classic
debug!("Unknown http3.slow_start_algorithm pref, defaulting to SlowStart::Classic");
SlowStart::Classic
}
};
let pmtud_enabled = // Check if PMTUD is explicitly enabled,
pmtud_enabled // or enabled via pref,
|| static_prefs::pref!("network.http.http3.pmtud") // but disable PMTUD if NSPR is used (socket == None) or // transmitted UDP datagrams might get fragmented by the IP layer.
&& socket.as_ref().map_or(false, |s| !s.may_fragment());
let spurious_recovery = static_prefs::pref!("network.http.http3.spurious_recovery");
let css_baseline = if static_prefs::pref!("network.http.http3.hystart_alternative_css_baseline") {
HyStartCssBaseline::EntryThreshold
} else {
HyStartCssBaseline::CurrentRoundMinRtt
};
letmut params = ConnectionParameters::default()
.versions(quic_version, version_list)
.congestion_control(cc_algorithm)
.slow_start(slow_start)
.max_data(max_data)
.max_stream_data(StreamType::BiDi, false, max_stream_data)
.grease(static_prefs::pref!("security.tls.grease_http3_enable"))
.sni_slicing(static_prefs::pref!("network.http.http3.sni-slicing"))
.idle_timeout(Duration::from_secs(idle_timeout.into())) // Disabled on OpenBSD. See <https://bugzilla.mozilla.org/show_bug.cgi?id=1952304>.
.pmtud_iface_mtu(cfg!(not(target_os = "openbsd"))) // MLKEM support is configured further below. By default, disable it.
.mlkem(false)
.pmtud(pmtud_enabled)
.spurious_recovery(spurious_recovery)
.hystart_css_baseline(css_baseline);
// Set a short timeout when fuzzing. #[cfg(feature = "fuzzing")] if static_prefs::pref!("fuzzing.necko.http3") {
params = params.idle_timeout(Duration::from_millis(10));
}
if !qlog_dir.is_empty() { let qlog_dir_conv = str::from_utf8(qlog_dir).map_err(|_| NS_ERROR_INVALID_ARG)?; let qlog_path = PathBuf::from(qlog_dir_conv);
match Qlog::enabled_with_file(
qlog_path.clone(),
Role::Client,
Some("Firefox Client qlog".to_string()),
Some("Firefox Client qlog".to_string()),
format!("{}_{}.qlog", origin, Uuid::new_v4()),
Instant::now(),
) {
Ok(qlog) => conn.set_qlog(qlog),
Err(e) => { // Emit warnings but to not return an error if qlog initialization // fails.
qwarn!("failed to create Qlog at {}: {}", qlog_path.display(), e);
}
}
}
fn record_stats_in_glean(&self) { use firefox_on_glean::metrics::networking as glean; use neqo_common::Ecn; use neqo_transport::{ecn, SlowStartExitReason}; use std::cmp::Ordering;
/// The biggest initial congestion window that can be set in neqo. Needs to be kept in sync with neqo. const MAX_INITIAL_CWND: usize = 12520; // Metric values must be recorded as integers. Glean does not support // floating point distributions. In order to represent values <1, they // are multiplied by `PRECISION_FACTOR`. A `PRECISION_FACTOR` of // `10_000` allows one to represent fractions down to 0.0001. const PRECISION_FACTOR: u64 = 10_000; #[allow(clippy::cast_possible_truncation, reason = "see check below")] const PRECISION_FACTOR_USIZE: usize = PRECISION_FACTOR as usize;
static_assertions::const_assert_eq!(PRECISION_FACTOR_USIZE as u64, PRECISION_FACTOR);
let stats = self.conn.transport_stats();
if stats.packets_tx == 0 { return;
}
for (s, postfix) in [(&stats.frame_tx, "_tx"), (&stats.frame_rx, "_rx")] { let add = |label: &str, value: usize| {
glean::http_3_quic_frame_count
.get(&(label.to_string() + postfix))
.add(value.try_into().unwrap_or(i32::MAX));
};
// Only record the metrics below for connections that established. Not // gating on `stats.packets_rx` as it counts garbage too, so it would // still admit a network that replies to every UDP packet with junk // without ever speaking QUIC. if stats.frame_rx.handshake_done == 0 { return;
}
if !static_prefs::pref!("network.http.http3.use_nspr_for_io")
&& static_prefs::pref!("network.http.http3.ecn_report")
{ let rx_ect0_sum: u64 = stats.ecn_rx.into_values().map(|v| v[Ecn::Ect0]).sum(); let rx_ce_sum: u64 = stats.ecn_rx.into_values().map(|v| v[Ecn::Ce]).sum(); if rx_ect0_sum > 0 { iflet Ok(ratio) = i64::try_from((rx_ce_sum * PRECISION_FACTOR) / rx_ect0_sum) {
glean::http_3_ecn_ce_ect0_ratio_received.accumulate_single_sample_signed(ratio);
} else { let msg = "Failed to convert ratio to i64 for use with glean";
qwarn!("{msg}");
debug_assert!(false, "{msg}");
}
}
}
if !static_prefs::pref!("network.http.http3.use_nspr_for_io")
&& static_prefs::pref!("network.http.http3.ecn_mark")
{ let tx_ect0_sum: u64 = stats.ecn_tx_acked.into_values().map(|v| v[Ecn::Ect0]).sum(); let tx_ce_sum: u64 = stats.ecn_tx_acked.into_values().map(|v| v[Ecn::Ce]).sum(); if tx_ect0_sum > 0 { iflet Ok(ratio) = i64::try_from((tx_ce_sum * PRECISION_FACTOR) / tx_ect0_sum) {
glean::http_3_ecn_ce_ect0_ratio_sent.accumulate_single_sample_signed(ratio);
} else { let msg = "Failed to convert ratio to i64 for use with glean";
qwarn!("{msg}");
debug_assert!(false, "{msg}");
}
} for (outcome, value) in stats.ecn_path_validation.into_iter() { let Ok(value) = i32::try_from(value) else { let msg = format!("Failed to convert {value} to i32 for use with glean");
qwarn!("{msg}");
debug_assert!(false, "{msg}"); continue;
}; match outcome {
ecn::ValidationOutcome::Capable => {
glean::http_3_ecn_path_capability.get("capable").add(value);
}
ecn::ValidationOutcome::NotCapable(ecn::ValidationError::BlackHole) => {
glean::http_3_ecn_path_capability
.get("black-hole")
.add(value);
}
ecn::ValidationOutcome::NotCapable(ecn::ValidationError::Bleaching) => {
glean::http_3_ecn_path_capability
.get("bleaching")
.add(value);
}
ecn::ValidationOutcome::NotCapable(
ecn::ValidationError::ReceivedUnsentECT1,
) => {
glean::http_3_ecn_path_capability
.get("received-unsent-ect-1")
.add(value);
}
}
}
}
// Calculate and collect packet loss ratio. The value is used later to also record the filtered loss ratio for connections that used the congestion controller. let loss_ratio = match i64::try_from((stats.lost * PRECISION_FACTOR_USIZE) / stats.packets_tx) {
Ok(v) => {
glean::http_3_loss_ratio.accumulate_single_sample_signed(v);
Some(v)
}
Err(e) => {
qwarn!("Failed to convert ratio to i64 for use with glean: {e}");
debug_assert!( false, "Failed to convert ratio to i64 for use with glean: {e}"
);
None
}
}; // Records the unfiltered (old) slow start exit ratio if stats.cc.slow_start_exit_cwnd.is_some() {
glean::http_3_slow_start_exited.get("exited").add(1);
} else {
glean::http_3_slow_start_exited.get("not_exited").add(1);
}
let cwnd_that_grew = stats.cc.cwnd.filter(|&c| c > MAX_INITIAL_CWND); let growth_label = match (cwnd_that_grew, stats.cc.slow_start_exit_cwnd) {
(Some(_), Some(exit_cwnd)) if exit_cwnd < MAX_INITIAL_CWND => { "no_growth_then_exit_then_growth"
}
(Some(_), _) => "had_growth",
(None, Some(_)) => "no_growth_but_exit",
(None, None) => "no_growth",
};
glean::http_3_congestion_window_growth
.get(growth_label)
.add(1); // Filtered: only record CC metrics for connections that grew past the initial window. iflet Some(final_cwnd) = cwnd_that_grew {
glean::http_3_final_cwnd.accumulate(final_cwnd as u64); iflet Some(loss) = loss_ratio {
glean::http_3_loss_ratio_filtered.accumulate_single_sample_signed(loss);
} // Record metrics concerning the slow start exit point below this filter.
debug_assert_eq!(
stats.cc.slow_start_exit_cwnd.is_some(),
stats.cc.slow_start_exit_reason.is_some(), "slow_start_exit_cwnd and slow_start_exit_reason must always be set together"
); letmut hystart_label = "not_exited"; letmut search_label = "not_exited"; iflet (Some(exit_cwnd), Some(reason)) = (
stats.cc.slow_start_exit_cwnd,
stats.cc.slow_start_exit_reason,
) {
glean::http_3_slow_start_exit_cwnd.accumulate(exit_cwnd as u64);
glean::http_3_slow_start_exited_filtered
.get("exited")
.add(1); let accuracy_cwnd =
((exit_cwnd.abs_diff(final_cwnd) as f64) / final_cwnd as f64) * 100.0; let accuracy_w_max = iflet Some(final_w_max) = stats.cc.w_max {
assert!(final_w_max > 0.0, "w_max can never be non-positive");
glean::http_3_final_w_max.accumulate(final_w_max as u64);
Some(((exit_cwnd as f64 - final_w_max).abs() / final_w_max) * 100.0)
} else {
None
}; let direction_label = match exit_cwnd.cmp(&final_cwnd) {
Ordering::Greater => "overshoot",
Ordering::Less => "undershoot",
Ordering::Equal => "exact",
}; let (reason_label, accuracy_label) = match reason {
SlowStartExitReason::CongestionEvent => {
glean::http_3_slow_start_exit_direction_loss
.get(direction_label)
.add(1);
hystart_label = "exited_ce";
search_label = "exited_ce";
("ce", "ce_exit")
}
SlowStartExitReason::Heuristic => {
glean::http_3_slow_start_exit_direction_heuristic
.get(direction_label)
.add(1);
hystart_label = "exited_hystart";
search_label = "exited_search";
("heuristic", "heuristic_exit")
}
};
glean::http_3_slow_start_exit_reason
.get(reason_label)
.add(1);
glean::http_3_slow_start_exit_accuracy
.get(accuracy_label)
.accumulate_single_sample_signed(accuracy_cwnd as i64); iflet Some(accuracy_w_max) = accuracy_w_max {
glean::http_3_slow_start_exit_accuracy_w_max
.get(accuracy_label)
.accumulate_single_sample_signed(accuracy_w_max as i64);
}
} else {
glean::http_3_slow_start_exited_filtered
.get("not_exited")
.add(1);
} // Only record HyStart metrics when HyStart is enabled (1 == HyStart, see constructor). if static_prefs::pref!("network.http.http3.slow_start_algorithm") == 1 {
glean::http_3_hystart_css_rounds_finished
.get(hystart_label)
.accumulate_single_sample_signed(stats.cc.hystart_css_rounds_finished as i64);
glean::http_3_hystart_css_entries
.get(hystart_label)
.accumulate_single_sample_signed(stats.cc.hystart_css_entries as i64);
}
// Only record SEARCH metrics when SEARCH is enabled (2 == SEARCH, see constructor). if static_prefs::pref!("network.http.http3.slow_start_algorithm") == 2 { // Metrics for drain phase evaluation iflet Some(empty_buffer_bdp) = stats.cc.search_empty_buffer_target {
glean::http_3_search_empty_buffer_bdp_estimate.accumulate(empty_buffer_bdp);
} iflet Some(full_buffer_bdp) = stats.cc.search_full_buffer_target {
glean::http_3_search_full_buffer_bdp_estimate.accumulate(full_buffer_bdp);
} // Metrics to tune EXTRA_BINS iflet Some(lookback_bins) = stats.cc.search_lookback_bins_needed {
glean::http_3_search_lookback_bins
.accumulate_single_sample_signed(lookback_bins as i64);
glean::http_3_search_rtt_inflated.get("inflated").add(1);
} else {
glean::http_3_search_rtt_inflated
.get("never_inflated")
.add(1);
} // Metrics to tune THRESH iflet Some(max_norm_diff) = stats.cc.search_max_norm_diff {
glean::http_3_search_max_norm_diff
.get(search_label)
.accumulate_single_sample_signed(max_norm_diff as i64);
} // Metrics to calibrate reset mechanism
glean::http_3_search_reset_count
.get(search_label)
.accumulate_single_sample_signed(stats.cc.search_reset.count as i64); iflet Some(max_passed_bins) = stats.cc.search_reset.max_passed_bins {
glean::http_3_search_max_passed_bins
.accumulate_single_sample_signed(max_passed_bins as i64);
} // Metrics to gain insights into app-limited behavior during SEARCH slow start
glean::http_3_search_zero_bytes_sent
.get(search_label)
.accumulate_single_sample_signed(stats.cc.search_zero_sent_bytes as i64);
// Metrics to evaluate whether the first RTT used to initialize SEARCH is inflated iflet Some(first_rtt) = stats.cc.search_first_rtt { let first_us = u64::try_from(first_rtt.as_micros()).unwrap_or(u64::MAX); let min_us = u64::try_from(stats.min_rtt.as_micros()).unwrap_or(u64::MAX); if min_us > 0 {
glean::http_3_search_first_rtt_vs_min_rtt
.accumulate_single_sample_signed((first_us * 100 / min_us) as i64);
} // And whether using `min(first, second)` would be a viable fix iflet Some(second_rtt) = stats.cc.search_second_rtt { let second_us = u64::try_from(second_rtt.as_micros()).unwrap_or(u64::MAX); if second_us > 0 {
glean::http_3_search_first_rtt_vs_second_rtt
.accumulate_single_sample_signed(
(first_us * 100 / second_us) as i64,
);
}
}
}
}
}
glean::http_3_congestion_event_count.accumulate_single_sample_signed(
(stats.cc.congestion_events.ecn + stats.cc.congestion_events.loss)
.saturating_sub(stats.cc.congestion_events.spurious) as i64,
);
// Ignore connections that never had loss induced congestion events (and prevent dividing by zero). if stats.cc.congestion_events.loss != 0 { iflet Ok(spurious) = i64::try_from(
(stats.cc.congestion_events.spurious * PRECISION_FACTOR_USIZE)
/ stats.cc.congestion_events.loss,
) {
glean::http_3_spurious_congestion_event_ratio
.accumulate_single_sample_signed(spurious);
} else { let msg = "Failed to convert ratio to i64 for use with glean";
qwarn!("{msg}");
debug_assert!(false, "{msg}");
}
}
// Collect congestion event reason metric iflet Ok(ce_loss) = i32::try_from(stats.cc.congestion_events.loss) {
glean::http_3_congestion_event_reason
.get("loss")
.add(ce_loss);
} else { let msg = "Failed to convert to i32 for use with glean";
qwarn!("{msg}");
debug_assert!(false, "{msg}");
} iflet Ok(ce_ecn) = i32::try_from(stats.cc.congestion_events.ecn) {
glean::http_3_congestion_event_reason
.get("ecn-ce")
.add(ce_ecn);
} else { let msg = "Failed to convert to i32 for use with glean";
qwarn!("{msg}");
debug_assert!(false, "{msg}");
}
}
/// # Safety /// /// Manually drops a pointer without consuming pointee. The caller needs to /// ensure no other referenecs remain. In addition safety conditions of /// [`AtomicRefcnt::dec`] apply. #[no_mangle] pubunsafeextern"C"fn neqo_http3conn_release(conn: &NeqoHttp3Conn) { let rc = conn.refcnt.dec(); if rc == 0 {
drop(Box::from_raw(ptr::from_ref(conn).cast_mut()));
}
}
// xpcom::RefPtr support unsafeimpl RefCounted for NeqoHttp3Conn { unsafefn addref(&self) {
neqo_http3conn_addref(self);
} unsafefn release(&self) {
neqo_http3conn_release(self);
}
}
/// Process input, reading incoming datagrams from the socket and passing them /// to the Neqo state machine. /// /// # Safety /// /// Marked as unsafe given exposition via FFI i.e. `extern "C"`. #[no_mangle] pubunsafeextern"C"fn neqo_http3conn_process_input(
conn: &mut NeqoHttp3Conn,
) -> ProcessInputResult { letmut bytes_read = 0;
// Attach metric instrumentation to `dgrams` iterator. letmut sum = 0; letmut segment_count = 0; let datagram_segment_size_received = &mut conn.datagram_segment_size_received; let dgrams = dgrams.inspect(|d| {
datagram_segment_size_received.accumulate(d.len() as u64);
sum += d.len();
segment_count += 1;
});
// Override `dgrams` ECN marks according to prefs. let ecn_enabled = static_prefs::pref!("network.http.http3.ecn_report"); let dgrams = dgrams.map(|mut d| { if !ecn_enabled {
d.set_tos(Tos::default());
}
d
});
/// Process output, retrieving outgoing datagrams from the Neqo state machine /// and writing them to the socket. #[no_mangle] pubextern"C"fn neqo_http3conn_process_output_and_send(
conn: &mut NeqoHttp3Conn,
context: *mut c_void,
set_timer_func: SetTimerFunc,
) -> ProcessOutputAndSendResult { letmut bytes_written: usize = 0; loop { let Ok(max_gso_segments) = min(
static_prefs::pref!("network.http.http3.max_gso_segments")
.try_into()
.expect("u32 fit usize"),
conn.socket
.as_mut()
.expect("non NSPR IO")
.max_gso_segments(),
)
.try_into() else {
qerror!("Socket return GSO size of 0"); return ProcessOutputAndSendResult {
result: NS_ERROR_UNEXPECTED,
bytes_written: 0,
};
};
let output = conn
.buffered_outbound_datagram
.take()
.map(OutputBatch::DatagramBatch)
.unwrap_or_else(|| {
conn.conn
.process_multiple_output(Instant::now(), max_gso_segments)
}); match output {
OutputBatch::DatagramBatch(mut dg) => { if !static_prefs::pref!("network.http.http3.ecn_mark") {
dg.set_tos(Tos::default());
}
if static_prefs::pref!("network.http.http3.block_loopback_ipv6_addr")
&& matches!(dg.destination(), SocketAddr::V6(addr) if addr.ip().is_loopback())
{
qdebug!("network.http.http3.block_loopback_ipv6_addr is set, returning NS_ERROR_CONNECTION_REFUSED for localhost IPv6"); return ProcessOutputAndSendResult {
result: NS_ERROR_CONNECTION_REFUSED,
bytes_written: 0,
};
}
match conn.socket.as_mut().expect("non NSPR IO").send(&dg) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
conn.increment_would_block_tx(); if static_prefs::pref!("network.http.http3.pr_poll_write") {
qdebug!("Buffer outbound datagram to be sent once UDP socket has write-availability.");
conn.buffered_outbound_datagram = Some(dg); return ProcessOutputAndSendResult { // Propagate WouldBlock error, thus indicating that // the UDP socket should be polled for // write-availability.
result: NS_BASE_STREAM_WOULD_BLOCK,
bytes_written: bytes_written.try_into().unwrap_or(u32::MAX),
};
} else {
qwarn!("dropping datagram as socket would block"); break;
}
}
Err(e) if e.raw_os_error() == Some(libc::EIO) && dg.num_datagrams() > 1 => { // See following resources for details: // - <https://github.com/quinn-rs/quinn/blob/93b6d01605147b9763ee1b1b381a6feb9fcd454e/quinn-udp/src/unix.rs#L345-L349> // - <https://bugzilla.mozilla.org/show_bug.cgi?id=1989895> // // Ideally one would retry at the quinn-udp layer, see <https://github.com/quinn-rs/quinn/issues/2399>.
qdebug!("Failed to send datagram batch size {} with error {e}. Missing GSO support? Socket will set max_gso_segments to 1. QUIC layer will retry.", dg.num_datagrams());
}
Err(e) => {
qwarn!("failed to send datagram: {}", e); return ProcessOutputAndSendResult {
result: into_nsresult(&e),
bytes_written: 0,
};
}
}
bytes_written += dg.data().len();
// Glean metrics
conn.datagram_size_sent.accumulate(dg.data().len() as u64);
conn.datagram_segments_sent
.accumulate(dg.num_datagrams() as u64); for _ in0..(dg.data().len() / dg.datagram_size()) {
conn.datagram_segment_size_sent
.accumulate(dg.datagram_size().get() as u64);
}
conn.datagram_segment_size_sent.accumulate(
dg.data()
.len()
.checked_rem(dg.datagram_size().get())
.expect("datagram_size is a NonZeroUsize") as u64,
);
}
OutputBatch::Callback(to) => { let timeout = if to.is_zero() {
Duration::from_millis(1)
} else {
to
}; let Ok(timeout) = u64::try_from(timeout.as_millis()) else { return ProcessOutputAndSendResult {
result: NS_ERROR_UNEXPECTED,
bytes_written: 0,
};
};
set_timer_func(context, timeout); break;
}
OutputBatch::None => {
set_timer_func(context, u64::MAX); break;
}
}
}
fn parse_headers(headers: &nsACString) -> Result<Vec<Header>, nsresult> { letmut hdrs = Vec::new(); // this is only used for headers built by Firefox. // Firefox supplies all headers already prepared for sending over http1. // They need to be split into (name, value) pairs where name is a String // and value is a Vec<u8>.
let headers_bytes: &[u8] = headers;
// Split on either \r or \n. When splitting "\r\n" sequences, this produces // an empty element between them which is filtered out by the is_empty check. // This also handles malformed inputs with bare \r or \n. for elem in headers_bytes.split(|&b| b == b'\r' || b == b'\n').skip(1) { if elem.is_empty() { continue;
} if elem.starts_with(b":") { // colon headers are for http/2 and 3 and this is http/1 // input, so that is probably a smuggling attack of some // kind. continue;
}
let colon_pos = match elem.iter().position(|&b| b == b':') {
Some(pos) => pos,
None => continue, // No colon, skip this line
};
let name_bytes = &elem[..colon_pos]; // Safe: if colon is at the end, this yields an empty slice let value_bytes = &elem[colon_pos + 1..];
// Header names must be valid UTF-8 let name = match str::from_utf8(name_bytes) {
Ok(n) => n.trim().to_lowercase(),
Err(_) => return Err(NS_ERROR_DOM_INVALID_HEADER_NAME),
};
if is_excluded_header(&name) { continue;
}
// Trim leading and trailing optional whitespace (OWS) from value. // Per RFC 9110, OWS is defined as *( SP / HTAB ), i.e., space and tab only. let value = value_bytes
.iter()
.position(|&b| b != b' ' && b != b'\t')
.map_or(&value_bytes[0..0], |start| { let end = value_bytes
.iter()
.rposition(|&b| b != b' ' && b != b'\t')
.map_or(value_bytes.len(), |pos| pos + 1);
&value_bytes[start..end]
})
.to_vec();
// This is only used for telemetry. Therefore we only return error code // numbers and do not label them. Recording telemetry is easier with a // number. #[repr(C)] pubenum CloseError {
TransportInternalError,
TransportInternalErrorOther(u16),
TransportError(u64),
CryptoError(u64),
CryptoAlert(u8),
PeerAppError(u64),
PeerError(u64),
AppError(u64),
EchRetry,
}
// Close sending side of a stream with stream_id #[no_mangle] pubextern"C"fn neqo_http3conn_close_stream(
conn: &mut NeqoHttp3Conn,
stream_id: u64,
) -> nsresult { match conn
.conn
.stream_close_send(StreamId::from(stream_id), Instant::now())
{
Ok(()) => NS_OK,
Err(_) => NS_ERROR_INVALID_ARG,
}
}
// WebTransport streams can be unidirectional and bidirectional. // It is mapped to and from neqo's StreamType enum. #[repr(C)] pubenum WebTransportStreamType {
BiDi,
UniDi,
}
impl From<StreamType> for WebTransportStreamType { fn from(t: StreamType) -> Self { match t {
StreamType::BiDi => Self::BiDi,
StreamType::UniDi => Self::UniDi,
}
}
}
impl From<WebTransportStreamType> for StreamType { fn from(t: WebTransportStreamType) -> Self { match t {
WebTransportStreamType::BiDi => Self::BiDi,
WebTransportStreamType::UniDi => Self::UniDi,
}
}
}
#[repr(C)] #[derive(Default)] pubstruct Http3Stats { /// Total packets received, including all the bad ones. pub packets_rx: usize, /// Duplicate packets received. pub dups_rx: usize, /// Dropped packets or dropped garbage. pub dropped_rx: usize, /// The number of packet that were saved for later processing. pub saved_datagrams: usize, /// Total packets sent. pub packets_tx: usize, /// Total number of packets that are declared lost. pub lost: usize, /// Late acknowledgments, for packets that were declared lost already. pub late_ack: usize, /// Acknowledgments for packets that contained data that was marked /// for retransmission when the PTO timer popped. pub pto_ack: usize, /// Count PTOs. Single PTOs, 2 PTOs in a row, 3 PTOs in row, etc. are counted /// separately. pub pto_counts: [usize; 16], /// The count of WouldBlock errors encountered during receive operations on the UDP socket. pub would_block_rx: usize, /// The count of WouldBlock errors encountered during transmit operations on the UDP socket. pub would_block_tx: usize,
}
/// # Safety /// /// Use of raw (i.e. unsafe) pointers as arguments. #[no_mangle] pubunsafeextern"C"fn neqo_http3conn_webtransport_set_sendorder(
conn: &mut NeqoHttp3Conn,
stream_id: u64,
sendorder: *const i64,
) -> nsresult { match conn
.conn
.webtransport_set_sendorder(StreamId::from(stream_id), sendorder.as_ref().copied())
{
Ok(()) => NS_OK,
Err(_) => NS_ERROR_UNEXPECTED,
}
}
/// Convert a [`std::io::Error`] into a [`nsresult`]. /// /// Note that this conversion is specific to `neqo_glue`, i.e. does not aim to /// implement a general-purpose conversion. /// Treat NS_ERROR_NET_RESET as a generic retryable error for the upper layer. /// /// Modeled after /// [`ErrorAccordingToNSPR`](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/netwerk/base/nsSocketTransport2.cpp#164-168). // // TODO: Use `non_exhaustive_omitted_patterns_lint` [once stablized](https://github.com/rust-lang/rust/issues/89554). fn into_nsresult(e: &io::Error) -> nsresult { #[expect(clippy::match_same_arms, reason = "It's cleaner this way.")] match e.kind() {
io::ErrorKind::ConnectionRefused => NS_ERROR_CONNECTION_REFUSED,
io::ErrorKind::ConnectionReset => NS_ERROR_NET_RESET,
// > We lump the following NSPR codes in with PR_CONNECT_REFUSED_ERROR. We // > could get better diagnostics by adding distinct XPCOM error codes for // > each of these, but there are a lot of places in Gecko that check // > specifically for NS_ERROR_CONNECTION_REFUSED, all of which would need to // > be checked. // // <https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/netwerk/base/nsSocketTransport2.cpp#164-168> // // TODO: `HostUnreachable` and `NetworkUnreachable` available since Rust // v1.83.0 only <https://doc.rust-lang.org/std/io/enum.ErrorKind.html>. // io::ErrorKind::HostUnreachable | io::ErrorKind::NetworkUnreachable |
io::ErrorKind::AddrNotAvailable => NS_ERROR_CONNECTION_REFUSED,
// The errors below are either not relevant for `neqo_glue`, or not // defined as `nsresult`.
io::ErrorKind::NotFound
| io::ErrorKind::PermissionDenied
| io::ErrorKind::BrokenPipe
| io::ErrorKind::InvalidData
| io::ErrorKind::WriteZero
| io::ErrorKind::Unsupported
| io::ErrorKind::Other => NS_ERROR_NET_RESET,
#[no_mangle] pubunsafeextern"C"fn neqo_decode(
decoder: &mut NeqoDecoder,
n: u32,
buf: *mut *const u8,
read: &mut u32,
) -> bool {
let decoder = decoder.decoder.as_mut().unwrap(); if let Some(data) = decoder.decode(n as usize) {
*buf = data.as_ptr();
*read = data.len() as u32; return true;
} false
}
#[no_mangle]
pub unsafe extern"C" fn neqo_decode_remainder(
decoder: &mut NeqoDecoder,
buf: *mut *const u8,
read: &mut u32,
) {
let decoder = decoder.decoder.as_mut().unwrap();
let data = decoder.decode_remainder();
*buf = data.as_ptr();
*read = data.len() as u32;
}
#[no_mangle]
pub unsafe extern"C" fn neqo_decoder_remaining(decoder: &mut NeqoDecoder) -> u64 {
let decoder = decoder.decoder.as_mut().unwrap();
decoder.remaining() as u64
}
#[no_mangle]
pub unsafe extern"C" fn neqo_decoder_offset(decoder: &mut NeqoDecoder) -> u64 {
let decoder = decoder.decoder.as_mut().unwrap();
decoder.offset() as u64
}
/// Enables the Apple fast datapath (`sendmsg_x`/`recvmsg_x`) for all /// subsequently created QUIC sockets. Must only be called after the caller /// has verified that these private APIs are available and functional. #[cfg(target_vendor = "apple")] #[no_mangle]
pub extern"C" fn neqo_glue_enable_apple_fast_path() {
APPLE_FAST_PATH.store(true, Ordering::Relaxed);
}
/// Inner implementation for [`neqo_glue_probe_apple_fast_path`]. #[cfg(target_vendor = "apple")]
fn probe_apple_fast_path_inner(send_fd: c_int, recv_fd: c_int) -> io::Result<()> {
use std::os::fd::BorrowedFd;
use neqo_common::Ecn;
use rustix::{
fs::{fcntl_getfl, fcntl_setfl, OFlags},
net::{getsockname, sockopt::{set_socket_timeout, Timeout}},
};
// Wrap a raw fd in neqo_udp::Socket, enable the fast path, restore blocking // mode (UdpSocketState::new sets non-blocking), and return the socket's // local address.
let make_socket = |fd: c_int| -> io::Result<(neqo_udp::Socket<BorrowedFd<'static>>, SocketAddr)> {
let bfd = unsafe { BorrowedFd::borrow_raw(fd) };
let socket = neqo_udp::Socket::new(bfd)?; // SAFETY: The C++ caller has verified via dlsym that the APIs are present.
unsafe { socket.enable_apple_fast_path() };
fcntl_setfl(bfd, fcntl_getfl(bfd)? & !OFlags::NONBLOCK)?;
set_socket_timeout(bfd, Timeout::Recv, Some(Duration::from_secs(1)))?;
let addr: SocketAddr = getsockname(bfd)?
.try_into()
.map_err(|e: rustix::io::Errno| io::Error::from_raw_os_error(e.raw_os_error()))?;
Ok((socket, addr))
};
let (sender, send_addr) = make_socket(send_fd)?;
let (receiver, recv_addr) = make_socket(recv_fd)?;
if sender.max_gso_segments() <= 1 { return Err(io::Error::other("max_gso_segments not increased"));
}
// Send two datagrams with distinct single-byte payloads and ECN codepoints, // then receive them across one or more recvmsg_x calls, in any order.
let mut remaining: Vec<(u8, Ecn)> = vec![(0, Ecn::Ect0), (1, Ecn::Ect1)]; for &(byte, ecn) in &remaining {
sender.send(&Datagram::new(send_addr, recv_addr, Tos::from(ecn), vec![byte]).into())?;
}
let mut recv_buf = neqo_udp::RecvBuf::default(); while !remaining.is_empty() { for d in receiver.recv(recv_addr, &mut recv_buf)? {
let &byte = d
.as_ref()
.first()
.ok_or_else(|| io::Error::other("empty datagram"))?;
let idx = remaining
.iter()
.position(|&(b, _)| b == byte)
.ok_or_else(|| io::Error::other("unexpected datagram payload"))?;
let (_, expected_ecn) = remaining.swap_remove(idx); if Ecn::from(d.tos()) != expected_ecn { return Err(io::Error::other("ECN mismatch"));
} if d.source() != send_addr { return Err(io::Error::other("source address mismatch"));
}
}
}
Ok(())
}
/// Tests the Apple fast UDP datapath end-to-end using the same neqo-udp code /// path used in production. Called during socket process initialisation /// with two pre-created, loopback-bound UDP sockets. Returns `true` only if a /// datagram with ECN bits set survives the send/receive round-trip through the /// `sendmsg_x`/`recvmsg_x` APIs. #[cfg(target_vendor = "apple")] #[no_mangle]
pub extern"C" fn neqo_glue_probe_apple_fast_path(send_fd: c_int, recv_fd: c_int) -> bool {
probe_apple_fast_path_inner(send_fd, recv_fd).is_ok()
}
// Test function called from C++ gtest // Callback signature: fn(user_data, name_ptr, name_len, value_ptr, value_len)
type HeaderCallback = extern"C" fn(*mut c_void, *const u8, usize, *const u8, usize);
#[no_mangle]
pub extern"C" fn neqo_glue_test_parse_headers(
headers_input: &nsACString,
callback: HeaderCallback,
user_data: *mut c_void,
) -> bool {
match parse_headers(headers_input) {
Ok(headers) => { for header in headers {
let name_bytes = header.name().as_bytes();
let value_bytes = header.value();
callback(
user_data,
name_bytes.as_ptr(),
name_bytes.len(),
value_bytes.as_ptr(),
value_bytes.len(),
);
}
true
}
Err(_) => false,
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.57 Sekunden
(vorverarbeitet am 2026-08-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.