Skip to content
Merged
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 129 additions & 3 deletions quinn-udp/src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#[cfg(not(any(apple, target_os = "openbsd", solarish)))]
use std::ptr;
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::sync::OnceLock;
Comment thread
inetic marked this conversation as resolved.
Outdated
use std::{
io::{self, IoSliceMut},
mem::{self, MaybeUninit},
Expand Down Expand Up @@ -794,6 +796,91 @@ pub(crate) const BATCH_SIZE: usize = 32;
#[cfg(apple_slow)]
pub(crate) const BATCH_SIZE: usize = 1;

#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: there's no point in making a separate module for this, just move the contents of this into the existing Linux gso module. Try to keep the additions in top-down order, so callers come before callees.

pub(crate) fn kernel_version_string() -> Result<String, libc::c_int> {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
if r != 0 {
return Err(r);
}
Comment on lines +878 to +881

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand libc's uname correctly, the return value is either 0 or -1. On -1 the actual error code can be retrieved via errno:

On success, zero is returned. On error, -1 is returned, and errno
is set to indicate the error.

https://man7.org/linux/man-pages/man2/uname.2.html

Would it make sense to retrieve the actual error code here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok(unsafe {
std::ffi::CStr::from_ptr(n.release[..].as_ptr())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: import CStr at the top of the module, and mem too.

.to_string_lossy()
.into_owned()
})
}

// https://www.linfo.org/kernel_version_numbering.html
#[derive(Eq, PartialEq, Ord, PartialOrd, Debug)]
pub(crate) struct KernelVersion {
version: u8,
major_revision: u8,
}

impl KernelVersion {
pub(crate) const fn new(version: u8, major_revision: u8) -> Self {
Self {
version,
major_revision,
}
}

pub(crate) fn from_str(release: &str) -> Result<Self, String> {
let mut split = release
.split_once('-')
.map(|pair| pair.0)
.unwrap_or(release)
.split('.');
Comment on lines +898 to +902

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... then this can be release.split_once('-')?.0.split('.').

@inetic inetic May 22, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap_or(release) part is there to avoid failing on strings which don't have "-", like "4.18" for example. I don't know whether such release strings exist in real life, but might be good to keep it just in case?

On the other hand if we go with a "let's not worry about what we don't know" approach, then we could get rid of the .split_once('-') part all together because the formats that I've seen so far were always "X.Y.Z-...", for which let mut split = release.split('.'); would suffice.


let Some(version) = split.next().and_then(|s| s.parse().ok()) else {
return Err(format!("Failed to parse kernel version from {release:?}"));
};
let Some(major_revision) = split.next().and_then(|s| s.parse().ok()) else {
Comment thread
inetic marked this conversation as resolved.
Outdated
return Err(format!(
"Failed to parse kernel major revision from {release:?}"
));
};

Ok(Self {
version,
major_revision,
})
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn parse_current_kernel_version_release_string() {
let release = kernel_version_string().unwrap();
KernelVersion::from_str(&release).unwrap();
}

#[test]
fn parse_kernel_version_release_string() {
assert_eq!(
KernelVersion::from_str("4.14"),
Ok(KernelVersion::new(4, 14))
);
assert_eq!(
KernelVersion::from_str("4.18"),
Ok(KernelVersion::new(4, 18))
);
assert_eq!(
KernelVersion::from_str("4.14.186-27095505"),
Ok(KernelVersion::new(4, 14))
);
assert_eq!(
KernelVersion::from_str("6.8.0-59-generic"),
Ok(KernelVersion::new(6, 8))
);
}
}
}

#[cfg(any(target_os = "linux", target_os = "android"))]
mod gso {
use super::*;
Expand All @@ -803,12 +890,21 @@ mod gso {
#[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
// Support for UDP GSO has been added to linux kernel in version 4.18
// https://github.com/torvalds/linux/commit/cb586c63e3fc5b227c51fd8c4cb40b34d3750645
const SUPPORTED_SINCE: linux::KernelVersion = linux::KernelVersion::new(4, 18);
// Avoid calling `supported_by_current_kernel` for each socket by using `OnceLock`.
static SUPPORTED_BY_CURRENT_KERNEL: OnceLock<bool> = OnceLock::new();

/// Checks whether GSO support is available by checking the kernel version followed by setting
/// the UDP_SEGMENT option on a socket
pub(crate) fn max_gso_segments() -> usize {
const GSO_SIZE: libc::c_int = 1500;

if !SUPPORTED_BY_CURRENT_KERNEL.get_or_init(supported_by_current_kernel) {
return 1;
}

let socket = match std::net::UdpSocket::bind("[::]:0")
.or_else(|_| std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)))
{
Expand All @@ -830,6 +926,36 @@ mod gso {
}
}

fn supported_by_current_kernel() -> bool {
let kernel_version_string = match linux::kernel_version_string() {
Ok(kernel_version_string) => kernel_version_string,
Err(_errno) => {
crate::log::warn!(
"Failed to retrieve kernel version string, GSO not enabled ({_errno})"
Comment thread
inetic marked this conversation as resolved.
Outdated
);
return false;
}
};

let kernel_version = match linux::KernelVersion::from_str(&kernel_version_string) {
Ok(kernel_version) => kernel_version,
Err(_reason) => {
crate::log::warn!("GSO not enabled: {}", _reason);
Comment thread
inetic marked this conversation as resolved.
Outdated
return false;
}
};

if kernel_version < SUPPORTED_SINCE {
crate::log::info!(
"GSO supported on Linux kernels 4.18+, current is {:?}",
Comment thread
inetic marked this conversation as resolved.
Outdated
kernel_version_string
);
false
Comment thread
inetic marked this conversation as resolved.
Outdated
} else {
true
}
}

pub(crate) fn set_segment_size(encoder: &mut cmsg::Encoder<libc::msghdr>, segment_size: u16) {
encoder.push(libc::SOL_UDP, UDP_SEGMENT, segment_size);
}
Expand Down