Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e5a022e
feat(s2n-quic-platform): enable cbpf for packet filtering
boquan-fang Feb 11, 2026
8d35102
add gates for platforms
boquan-fang Feb 11, 2026
0ff2606
address PR comments:
boquan-fang Feb 11, 2026
c7c0920
address PR comments:
boquan-fang Feb 11, 2026
42b7173
address PR comments:
boquan-fang Feb 11, 2026
cca40b8
Revert "address PR comments:"
boquan-fang Feb 12, 2026
49efdb0
fix socket bind with actual address in Server::bind
boquan-fang Feb 12, 2026
f377c97
add checks in bind_udp to make sure that reuse_port and port==0 can not
boquan-fang Feb 12, 2026
e7b4827
address PR comments:
boquan-fang Feb 12, 2026
e550dfe
address PR comments:
boquan-fang Feb 12, 2026
0291087
adding per socket event in progress
boquan-fang Feb 13, 2026
8718163
add a integration test to verify that sockets are properly routed
boquan-fang Feb 13, 2026
5827514
address PR comments:
boquan-fang Feb 13, 2026
aa18563
rebase with main to unblock the CI
boquan-fang Feb 13, 2026
6ca75b3
rebase with main to include clippy fixes
boquan-fang Feb 16, 2026
745bc4d
skip client_hello_routed_test in ASAN and fix some comments
boquan-fang Feb 16, 2026
7b64184
Merge with main to include newly introduced dcQUIC changes
boquan-fang Feb 17, 2026
b65ec51
Merge branch 'main' into boquan-fang/packet-filtering
boquan-fang Feb 18, 2026
c34bda8
implement a load test
boquan-fang Feb 19, 2026
b379631
modify the load test to let client connect, then flood the server
boquan-fang Feb 20, 2026
fb4278c
add subscriber to track the number of packet sent and lost
boquan-fang Feb 20, 2026
4083781
track non-initial packets received by the server
boquan-fang Feb 24, 2026
6d9c4a8
use existing packets for router test
boquan-fang Mar 4, 2026
31c6f30
remove unstable test
boquan-fang Mar 4, 2026
0142a5a
revert to socket 0 for client hello and 1 for others
boquan-fang Mar 10, 2026
d016566
modify the load test so that it will work after the CH is received
boquan-fang Mar 10, 2026
2f58079
add socket poll prioritization
boquan-fang Mar 11, 2026
5f8eac9
simplify the rx logic
boquan-fang Mar 11, 2026
5554957
attempt to add back client_hello_routed_test to ASAN
boquan-fang Mar 11, 2026
4feaef7
Merge branch 'main' into packet-filtering and necessary rebase changes
boquan-fang Mar 11, 2026
298e815
add an integration test for socket prioritization
boquan-fang Mar 11, 2026
096aa9e
use one thread to interleavingly send packets to both sockets in test
boquan-fang Mar 12, 2026
f10457a
Merge remote-tracking branch 'upstream/main' into boquan-fang/packet-…
boquan-fang Mar 12, 2026
93d0a75
send 10 packets to socket 1 and 1 to socket 0
boquan-fang Mar 12, 2026
fd3cf04
change test duration to 5 seconds so that socket 1 can take in more
boquan-fang Mar 12, 2026
6c8cc93
minor changes before making this PR ready for review again
boquan-fang Mar 12, 2026
8683ce8
unwrap the send_to method
boquan-fang Mar 13, 2026
79d9e56
address PR comments:
boquan-fang Mar 16, 2026
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
195 changes: 194 additions & 1 deletion dc/s2n-quic-dc/src/psk/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use s2n_quic::{
server::Name,
};
use s2n_quic_core::inet::SocketAddress;
#[cfg(target_os = "linux")]
use s2n_quic_platform::bpf::cbpf::{abs, and, jeq, ldb, ret, Program};
use s2n_quic_platform::syscall;
use std::{
hash::BuildHasher,
io,
Expand All @@ -38,6 +41,25 @@ const DEFAULT_INITIAL_RTT: Duration = Duration::from_millis(1);

const BUFFER_SIZE: usize = 16 * 1024;

/// cBPF program to route QUIC packets across multiple sockets.
/// Routes Initial packets with DCID length = 8 to socket 0, all other packets to socket 1.
#[cfg(target_os = "linux")]
static ROUTER: Program = Program::new(&[
Comment thread
boquan-fang marked this conversation as resolved.
Outdated
// Load byte 0 and check if it's an Initial packet (first 4 bits = 1100)
ldb(abs(0)),
and(0b1111_0000), // Mask the last four bits of the first byte. The first four bits can confirm if the packet is a INITIAL packet.
// If Initial packet, continue; else jump to ret(1)
jeq(0b1100_0000, 0, 3), // First four bits of INITIAL packet should be 1100.
// Load byte 5 (DCID length) and check if it equals 8
Comment thread
boquan-fang marked this conversation as resolved.
Outdated
ldb(abs(5)),
// If DCID len = 8, continue to ret(0); else jump to ret(1)
jeq(0x08, 0, 1),
// Return 0: socket 0 handles Initial packets with DCID length = 8
ret(0),
// Return 1: socket 1 handles all other packets
ret(1),
]);

pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;

pub type Result<T = (), E = Error> = core::result::Result<T, E>;
Expand All @@ -58,8 +80,30 @@ impl Server {
subscriber: Subscriber,
builder: server::Builder<Event>,
) -> Result<Self, Error> {
let socket_for_client_hello_packets = syscall::bind_udp(addr, false, false, false)?;

// Acquire the bound address with a port assigned
let bound_addr = socket_for_client_hello_packets
.local_addr()?
.as_socket()
.unwrap();

socket_for_client_hello_packets
.set_reuse_port(true)
.unwrap();

let socket_for_other_packets = syscall::bind_udp(bound_addr, false, true, false)?;

// Attach ROUTER to both sockets for packet filtering
#[cfg(target_os = "linux")]
{
ROUTER.attach(&socket_for_client_hello_packets)?;
ROUTER.attach(&socket_for_other_packets)?;
}

let io = s2n_quic::provider::io::default::Builder::default()
.with_receive_address(addr)?
.with_rx_socket(socket_for_client_hello_packets.into())?
.with_rx_socket(socket_for_other_packets.into())?
.with_base_mtu(DEFAULT_BASE_MTU.min(builder.mtu))?
.with_initial_mtu(builder.mtu)?
.with_max_mtu(builder.mtu)?
Expand Down Expand Up @@ -573,6 +617,7 @@ mod tests {
};
use s2n_quic_core::time::StdClock;
use std::time::Instant;
use tokio::net::UdpSocket;
use tokio_util::sync::DropGuard;

/// A test limiter that closes all incoming connections immediately
Expand Down Expand Up @@ -739,4 +784,152 @@ mod tests {
duration
);
}

// Tests that the ROUTER cBPF filter correctly routes packets to the appropriate socket.
#[cfg(target_os = "linux")]
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn router_cbpf_packet_filtering_test() -> io::Result<()> {
static IPV4_LOCALHOST: &str = "127.0.0.1:0";

// Create two rx sockets bound to same port with SO_REUSEPORT
let rx_socket_0 = syscall::bind_udp(IPV4_LOCALHOST, false, false, false)?;
rx_socket_0.set_nonblocking(true)?;
let port = rx_socket_0.local_addr()?.as_socket().unwrap().port();
rx_socket_0.set_reuse_port(true)?;

let rx_socket_1 = syscall::bind_udp(("127.0.0.1", port), false, true, false)?;
rx_socket_1.set_nonblocking(true)?;

// Attach ROUTER to both sockets
ROUTER.attach(&rx_socket_0)?;
ROUTER.attach(&rx_socket_1)?;

// Convert to tokio sockets for async recv
let rx_socket_0 = UdpSocket::from_std(rx_socket_0.into())?;
let rx_socket_1 = UdpSocket::from_std(rx_socket_1.into())?;

// Create sender socket
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let target_addr: std::net::SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();

// Test packet A: Initial packet with DCID length = 8
// Should route to socket 0
// Format: [header byte, version (4 bytes), dcid_len, ...]
let packet_a = {
let mut p = vec![0u8; 32];
p[0] = 0xC0; // Initial packet (first 4 bits = 1100)
p[1..5].copy_from_slice(&[0x00, 0x00, 0x00, 0x01]); // version
p[5] = 0x08; // DCID length = 8
p
};

// Test packet B: Handshake packet
// Should route to socket 1
let packet_b = {
let mut p = vec![0u8; 32];
p[0] = 0xE0; // Handshake packet first four bits are 1110
p
};

// Test packet C: Initial packet but DCID length != 8
// Should route to socket 1
let packet_c = {
let mut p = vec![0u8; 32];
p[0] = 0xC0; // Initial packet (first 4 bits = 1100)
p[1..5].copy_from_slice(&[0x00, 0x00, 0x00, 0x01]); // version
p[5] = 0x10; // DCID length = 16
p
};

// Send packets
sender.send_to(&packet_a, target_addr).await?;
sender.send_to(&packet_b, target_addr).await?;
sender.send_to(&packet_c, target_addr).await?;

// Receive and verify routing
let mut buf_socket0 = [0u8; 1024];
let mut buf_packet1_socket1 = [0u8; 1024];
let mut buf_packet2_socket1 = [0u8; 1024];

// Socket 0 should receive packet_a (Initial with DCID len = 8)
let recv_result = tokio::time::timeout(
Duration::from_millis(500),
rx_socket_0.recv_from(&mut buf_socket0),
)
.await;
assert!(
recv_result.is_ok(),
"Socket 0 should receive packet_a (Initial with DCID len=8)"
);
let (len, _) = recv_result.unwrap()?;
assert_eq!(
buf_socket0[0], 0xC0,
"Socket 0 should receive Initial packet"
);
assert_eq!(
buf_socket0[5], 0x08,
"Socket 0 should receive packet with DCID len=8"
);
assert_eq!(len, 32);
Comment thread
boquan-fang marked this conversation as resolved.
Outdated

// Socket 1 should receive packet_b and packet_c
let recv_result = tokio::time::timeout(
Duration::from_millis(500),
rx_socket_1.recv_from(&mut buf_packet1_socket1),
)
.await;
assert!(
recv_result.is_ok(),
"Socket 1 should receive packet_b or packet_c"
);
let (len1, _) = recv_result.unwrap()?;
assert_eq!(len1, 32);

let recv_result = tokio::time::timeout(
Duration::from_millis(500),
rx_socket_1.recv_from(&mut buf_packet2_socket1),
)
.await;
assert!(
recv_result.is_ok(),
"Socket 1 should receive packet_b or packet_c"
);
let (len2, _) = recv_result.unwrap()?;
assert_eq!(len2, 32);

// Verify that socket 1 received exactly packet_b and packet_c in either order
let received_packets = [&buf_packet1_socket1[..32], &buf_packet2_socket1[..32]];

// Check that one packet matches packet_b (Handshake: header = 0xE0)
let has_packet_b = received_packets
.iter()
.any(|p| p[0] == 0xE0 && p[..] == packet_b[..]);
assert!(
has_packet_b,
"Socket 1 should receive packet_b (Handshake packet with header 0xE0)"
);

// Check that one packet matches packet_c (Initial with DCID len = 16)
let has_packet_c = received_packets
.iter()
.any(|p| p[0] == 0xC0 && p[5] == 0x10 && p[..] == packet_c[..]);
assert!(
has_packet_c,
"Socket 1 should receive packet_c (Initial packet with DCID len=16)"
);

// Socket 0 should not have any more packets
let recv_result = tokio::time::timeout(
Duration::from_millis(100),
rx_socket_0.recv_from(&mut buf_socket0),
)
.await;
assert!(
recv_result.is_err(),
"Socket 0 should not receive any more packets"
);

Ok(())
}
}
72 changes: 38 additions & 34 deletions quic/s2n-quic-platform/src/io/tokio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl Io {
) -> io::Result<(tokio::task::JoinHandle<()>, SocketAddress)> {
let Builder {
handle,
rx_socket,
rx_sockets,
tx_socket,
recv_addr,
send_addr,
Expand Down Expand Up @@ -89,35 +89,51 @@ impl Io {

let guard = handle.enter();

let rx_socket = if let Some(rx_socket) = rx_socket {
rx_socket
// Build the list of rx sockets - either from provided sockets or create from recv_addr
let rx_socket_list = if !rx_sockets.is_empty() {
rx_sockets
} else if let Some(recv_addr) = recv_addr {
syscall::bind_udp(recv_addr, reuse_address, reuse_port, only_v6)?
// Check env var for number of sockets to create (unstable feature)
let rx_socket_count: usize =
parse_env("S2N_QUIC_UNSTABLE_RX_SOCKET_COUNT").unwrap_or(1);
let mut sockets = Vec::with_capacity(rx_socket_count);
for _ in 0..rx_socket_count {
sockets.push(syscall::bind_udp(
recv_addr,
reuse_address,
reuse_port,
only_v6,
)?);
}
sockets
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"missing bind address",
));
};

let rx_addr = convert_addr_to_std(rx_socket.local_addr()?)?;
// Get the address from the first socket
let rx_addr = convert_addr_to_std(rx_socket_list[0].local_addr()?)?;

let tx_socket = if let Some(tx_socket) = tx_socket {
tx_socket
} else if let Some(send_addr) = send_addr {
syscall::bind_udp(send_addr, reuse_address, reuse_port, only_v6)?
} else {
// No tx_socket or send address was specified, so the tx socket
// will be a handle to the rx socket.
rx_socket.try_clone()?
// will be a handle to the first rx socket.
rx_socket_list[0].try_clone()?
};

if let Some(size) = socket_send_buffer_size {
tx_socket.set_send_buffer_size(size)?;
}

if let Some(size) = socket_recv_buffer_size {
rx_socket.set_recv_buffer_size(size)?;
for socket in &rx_socket_list {
socket.set_recv_buffer_size(size)?;
}
}

let mut mtu_config = mtu_config_builder
Expand Down Expand Up @@ -150,19 +166,20 @@ impl Io {
});

// Configure the socket with GRO
let gro_enabled = gro_enabled.unwrap_or(true) && syscall::configure_gro(&rx_socket);
let gro_enabled = gro_enabled.unwrap_or(true) && syscall::configure_gro(&rx_socket_list[0]);
Comment thread
boquan-fang marked this conversation as resolved.
Outdated

publisher.on_platform_feature_configured(event::builder::PlatformFeatureConfigured {
configuration: event::builder::PlatformFeatureConfiguration::Gro {
enabled: gro_enabled,
},
});

// Configure packet info CMSG
syscall::configure_pktinfo(&rx_socket);

// Configure TOS/ECN
let tos_enabled = syscall::configure_tos(&rx_socket);
// Configure packet info CMSG and TOS/ECN for all rx sockets
let mut tos_enabled = false;
for socket in &rx_socket_list {
syscall::configure_pktinfo(socket);
tos_enabled &= syscall::configure_tos(socket);
}

publisher.on_platform_feature_configured(event::builder::PlatformFeatureConfigured {
configuration: event::builder::PlatformFeatureConfiguration::Ecn {
Expand Down Expand Up @@ -193,34 +210,21 @@ impl Io {

let mut consumers = vec![];

let rx_socket_count = parse_env("S2N_QUIC_UNSTABLE_RX_SOCKET_COUNT").unwrap_or(1);

// configure the number of self-wakes before "cooling down" and waiting for epoll to
// complete
let rx_cooldown = cooldown("RX");

for idx in 0usize..rx_socket_count {
for socket in rx_socket_list {
let (producer, consumer) = socket::ring::pair(entries, payload_len);
consumers.push(consumer);

// spawn a task that actually reads from the socket into the ring buffer
if idx + 1 == rx_socket_count {
handle.spawn(task::rx(
rx_socket,
producer,
rx_cooldown,
stats_sender.clone(),
));
break;
} else {
let rx_socket = rx_socket.try_clone()?;
handle.spawn(task::rx(
rx_socket,
producer,
rx_cooldown.clone(),
stats_sender.clone(),
));
}
handle.spawn(task::rx(
Comment thread
boquan-fang marked this conversation as resolved.
socket,
producer,
rx_cooldown.clone(),
stats_sender.clone(),
));
}

// construct the RX side for the endpoint event loop
Expand Down
Loading
Loading