From a66895ecee89fe6ecc2e9a5adc9ed3c0bc3ee350 Mon Sep 17 00:00:00 2001 From: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:48:03 +0100 Subject: [PATCH 1/3] server: add QUIC-LB plaintext CIDs (--quic-lb-server-id) for stateless LB routing Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> --- Cargo.lock | 12 +++++ crates/slipstream-server/Cargo.toml | 1 + crates/slipstream-server/src/main.rs | 18 +++++++ crates/slipstream-server/src/server.rs | 74 +++++++++++++++++++++++--- docs/config.md | 5 ++ docs/picoquic-changes.md | 8 +++ 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88e99e9e..5214ef0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" @@ -463,6 +474,7 @@ name = "slipstream-server" version = "0.1.0" dependencies = [ "clap", + "getrandom", "libc", "openssl", "slipstream-core", diff --git a/crates/slipstream-server/Cargo.toml b/crates/slipstream-server/Cargo.toml index 58a37828..65618f4f 100644 --- a/crates/slipstream-server/Cargo.toml +++ b/crates/slipstream-server/Cargo.toml @@ -9,6 +9,7 @@ readme = "../../README.md" [dependencies] clap = { workspace = true } +getrandom = "0.2" slipstream-core = { path = "../slipstream-core" } slipstream-dns = { path = "../slipstream-dns" } slipstream-ffi = { path = "../slipstream-ffi" } diff --git a/crates/slipstream-server/src/main.rs b/crates/slipstream-server/src/main.rs index 144e62cf..9cb5dc1d 100644 --- a/crates/slipstream-server/src/main.rs +++ b/crates/slipstream-server/src/main.rs @@ -47,6 +47,9 @@ struct Args { debug_streams: bool, #[arg(long = "debug-commands")] debug_commands: bool, + /// QUIC-LB server ID (0–255) for stateless LB routing. + #[arg(long = "quic-lb-server-id", value_name = "ID", value_parser = parse_quic_lb_server_id)] + quic_lb_server_id: Option, } fn main() { @@ -147,6 +150,13 @@ fn main() { args.max_connections }; + let quic_lb_server_id = if cli_provided(&matches, "quic_lb_server_id") { + args.quic_lb_server_id + } else { + sip003::last_option_value(&sip003_env.plugin_options, "quic_lb_server_id") + .map(|v| unwrap_or_exit(parse_quic_lb_server_id(&v), "SIP003 env error", 2)) + }; + let config = ServerConfig { dns_listen_host, dns_listen_port, @@ -160,6 +170,7 @@ fn main() { idle_timeout_seconds: args.idle_timeout_seconds, debug_streams: args.debug_streams, debug_commands: args.debug_commands, + quic_lb_server_id, }; let runtime = Builder::new_current_thread() @@ -200,6 +211,13 @@ fn parse_max_connections(input: &str) -> Result { Ok(value) } +fn parse_quic_lb_server_id(input: &str) -> Result { + let trimmed = input.trim(); + trimmed + .parse::() + .map_err(|_| format!("quic-lb-server-id must be 0–255, got: {}", trimmed)) +} + fn cli_provided(matches: &clap::ArgMatches, id: &str) -> bool { matches.value_source(id) == Some(ValueSource::CommandLine) } diff --git a/crates/slipstream-server/src/server.rs b/crates/slipstream-server/src/server.rs index d6e1718a..335db14d 100644 --- a/crates/slipstream-server/src/server.rs +++ b/crates/slipstream-server/src/server.rs @@ -5,11 +5,12 @@ use slipstream_core::{ normalize_dual_stack_addr, resolve_host_port, HostPort, }; use slipstream_dns::{encode_response, Question, Rcode, ResponseParams}; +use libc::c_void; use slipstream_ffi::picoquic::{ - picoquic_cnx_t, picoquic_create, picoquic_current_time, picoquic_delete_cnx, - picoquic_get_first_cnx, picoquic_get_next_cnx, picoquic_prepare_packet_ex, picoquic_quic_t, - slipstream_has_ready_stream, slipstream_is_flow_blocked, slipstream_server_cc_algorithm, - PICOQUIC_MAX_PACKET_SIZE, PICOQUIC_PACKET_LOOP_RECV_MAX, + picoquic_connection_id_t, picoquic_cnx_t, picoquic_create, picoquic_current_time, + picoquic_delete_cnx, picoquic_get_first_cnx, picoquic_get_next_cnx, picoquic_prepare_packet_ex, + picoquic_quic_t, slipstream_has_ready_stream, slipstream_is_flow_blocked, + slipstream_server_cc_algorithm, PICOQUIC_MAX_PACKET_SIZE, PICOQUIC_PACKET_LOOP_RECV_MAX, }; use slipstream_ffi::{ configure_quic_with_custom, socket_addr_to_storage, take_crypto_errors, QuicGuard, @@ -19,7 +20,7 @@ use std::ffi::CString; use std::fmt; use std::net::SocketAddr; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::UdpSocket as TokioUdpSocket; @@ -49,6 +50,42 @@ extern "C" fn handle_sigterm(_signum: libc::c_int) { SHOULD_SHUTDOWN.store(true, Ordering::Relaxed); } +/// QUIC-LB plaintext CID callback (config_rotation=0, 1 byte server_id + 6 byte nonce = 8 bytes total). +/// Uses 8 bytes so picoquic's default local_cnxid_length (8) matches without calling picoquic_set_default_connection_id_length. +/// +/// # Safety +/// `cnx_id_cb_data` must point to a valid `u8` (server_id) for the QUIC context lifetime. +unsafe extern "C" fn quic_lb_cnx_id_callback( + _quic: *mut picoquic_quic_t, + _cnx_id_local: picoquic_connection_id_t, + _cnx_id_remote: picoquic_connection_id_t, + cnx_id_cb_data: *mut c_void, + cnx_id_returned: *mut picoquic_connection_id_t, +) { + if cnx_id_cb_data.is_null() || cnx_id_returned.is_null() { + return; + } + let server_id = *cnx_id_cb_data.cast::(); + const CONFIG_ROTATION: u8 = 0; + const LENGTH_AFTER_FIRST: u8 = 7; // 8 bytes total: first octet (1) + server_id (1) + nonce (6) + let first_octet = (CONFIG_ROTATION << 6) | LENGTH_AFTER_FIRST; + (*cnx_id_returned).id[0] = first_octet; + (*cnx_id_returned).id[1] = server_id; + let mut nonce = [0u8; 6]; + if getrandom::getrandom(&mut nonce).is_err() { + static QUIC_LB_NONCE_FALLBACK: AtomicU32 = AtomicU32::new(0); + let c = QUIC_LB_NONCE_FALLBACK.fetch_add(1, Ordering::Relaxed); + nonce[..4].copy_from_slice(&c.to_le_bytes()); + // bytes 4..6 left zero or could add more entropy + } + std::ptr::copy_nonoverlapping( + nonce.as_ptr(), + std::ptr::addr_of_mut!((*cnx_id_returned).id).cast::().add(2), + 6, + ); + (*cnx_id_returned).id_len = 8; +} + #[derive(Debug)] pub struct ServerError { message: String, @@ -70,6 +107,7 @@ impl fmt::Display for ServerError { impl std::error::Error for ServerError {} +#[derive(Debug)] pub struct ServerConfig { pub dns_listen_host: String, pub dns_listen_port: u16, @@ -83,6 +121,8 @@ pub struct ServerConfig { pub idle_timeout_seconds: u64, pub debug_streams: bool, pub debug_commands: bool, + /// QUIC-LB server_id for stateless LB routing (plaintext CIDs). + pub quic_lb_server_id: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -207,6 +247,26 @@ pub async fn run_server(config: &ServerConfig) -> Result { .as_ref() .map(|seed| seed.bytes.as_ptr()) .unwrap_or(std::ptr::null()); + let mut quic_lb_server_id_holder = config.quic_lb_server_id; + if let Some(sid) = config.quic_lb_server_id { + tracing::info!( + "QUIC-LB enabled: server_id={}", + sid + ); + } + let (cnx_id_callback, cnx_id_cb_data) = match quic_lb_server_id_holder.as_mut() { + Some(id) => ( + Some(quic_lb_cnx_id_callback as unsafe extern "C" fn( + *mut picoquic_quic_t, + picoquic_connection_id_t, + picoquic_connection_id_t, + *mut c_void, + *mut picoquic_connection_id_t, + )), + id as *mut u8 as *mut c_void, + ), + None => (None, std::ptr::null_mut()), + }; let quic = unsafe { picoquic_create( config.max_connections, @@ -216,8 +276,8 @@ pub async fn run_server(config: &ServerConfig) -> Result { alpn.as_ptr(), Some(server_callback), state_ptr as *mut _, - None, - std::ptr::null_mut(), + cnx_id_callback, + cnx_id_cb_data, reset_seed_ptr, current_time, std::ptr::null_mut(), diff --git a/docs/config.md b/docs/config.md index cb13bb0b..82a8f3cc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -54,6 +54,11 @@ certificates are not verified. exist, the server generates one and writes it with 0600 permissions. If not provided, the server uses an ephemeral seed and stateless resets will not survive restarts. +- `--quic-lb-server-id` (0–255) + When set, the server generates QUIC-LB plaintext connection IDs (config + rotation 0, 1-byte server_id, 4-byte nonce) so a QUIC-LB-aware load balancer + can route by server_id statelessly. Each backend in a pool should use a + distinct value (e.g. 0, 1, 2). SIP003 plugin option: `quic_lb_server_id=N`. ## picoquic build environment diff --git a/docs/picoquic-changes.md b/docs/picoquic-changes.md index b3e444d9..4fcc1127 100644 --- a/docs/picoquic-changes.md +++ b/docs/picoquic-changes.md @@ -113,6 +113,14 @@ The following use `picoquic_internal.h` and therefore depend on picoquic interna - Why: The authoritative client derives its DNS poll QPS budget from picoquic's pacing rate and uses cwnd as a fallback when pacing is unavailable. +- `picoquic_create(..., cnx_id_callback, cnx_id_callback_data, ...)` and `picoquic_connection_id_cb_fn` + - Declared in `picoquic.h`: connection ID callback and context pointer passed into `picoquic_create`. + - Used in: `crates/slipstream-server/src/server.rs` (QUIC-LB plaintext CID generation when + `--quic-lb-server-id` is set). The callback is invoked from `picoquic_create_local_cnx_id` in + `quicctx.c` so the server can supply custom CIDs (e.g. QUIC-LB format for stateless load balancing). + - Why: Optional QUIC-LB support so a load balancer can decode server_id from the CID and route + statelessly without a sticky table. + ## Notes - Internal usage means the submodule version is coupled to slipstream. Any picoquic update From 804f204a3db6f6f9821b62601ec4daa18528b778 Mon Sep 17 00:00:00 2001 From: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:55:03 +0100 Subject: [PATCH 2/3] fix code format Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> --- crates/slipstream-server/src/server.rs | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/slipstream-server/src/server.rs b/crates/slipstream-server/src/server.rs index 335db14d..efb958b0 100644 --- a/crates/slipstream-server/src/server.rs +++ b/crates/slipstream-server/src/server.rs @@ -1,13 +1,13 @@ use crate::config::{ensure_cert_key, load_or_create_reset_seed, ResetSeed}; use crate::udp_fallback::{handle_packet, FallbackManager, PacketContext, MAX_UDP_PACKET_SIZE}; +use libc::c_void; use slipstream_core::{ net::{bind_first_resolved, bind_udp_socket_addr, is_transient_udp_error}, normalize_dual_stack_addr, resolve_host_port, HostPort, }; use slipstream_dns::{encode_response, Question, Rcode, ResponseParams}; -use libc::c_void; use slipstream_ffi::picoquic::{ - picoquic_connection_id_t, picoquic_cnx_t, picoquic_create, picoquic_current_time, + picoquic_cnx_t, picoquic_connection_id_t, picoquic_create, picoquic_current_time, picoquic_delete_cnx, picoquic_get_first_cnx, picoquic_get_next_cnx, picoquic_prepare_packet_ex, picoquic_quic_t, slipstream_has_ready_stream, slipstream_is_flow_blocked, slipstream_server_cc_algorithm, PICOQUIC_MAX_PACKET_SIZE, PICOQUIC_PACKET_LOOP_RECV_MAX, @@ -80,7 +80,9 @@ unsafe extern "C" fn quic_lb_cnx_id_callback( } std::ptr::copy_nonoverlapping( nonce.as_ptr(), - std::ptr::addr_of_mut!((*cnx_id_returned).id).cast::().add(2), + std::ptr::addr_of_mut!((*cnx_id_returned).id) + .cast::() + .add(2), 6, ); (*cnx_id_returned).id_len = 8; @@ -249,20 +251,20 @@ pub async fn run_server(config: &ServerConfig) -> Result { .unwrap_or(std::ptr::null()); let mut quic_lb_server_id_holder = config.quic_lb_server_id; if let Some(sid) = config.quic_lb_server_id { - tracing::info!( - "QUIC-LB enabled: server_id={}", - sid - ); + tracing::info!("QUIC-LB enabled: server_id={}", sid); } let (cnx_id_callback, cnx_id_cb_data) = match quic_lb_server_id_holder.as_mut() { Some(id) => ( - Some(quic_lb_cnx_id_callback as unsafe extern "C" fn( - *mut picoquic_quic_t, - picoquic_connection_id_t, - picoquic_connection_id_t, - *mut c_void, - *mut picoquic_connection_id_t, - )), + Some( + quic_lb_cnx_id_callback + as unsafe extern "C" fn( + *mut picoquic_quic_t, + picoquic_connection_id_t, + picoquic_connection_id_t, + *mut c_void, + *mut picoquic_connection_id_t, + ), + ), id as *mut u8 as *mut c_void, ), None => (None, std::ptr::null_mut()), From 9df3d4c11358e6c87519ab7ec3528c30efe99bfd Mon Sep 17 00:00:00 2001 From: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:31:07 +0100 Subject: [PATCH 3/3] box QUIC-LB server_id so callback data outlives .await Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> --- crates/slipstream-server/src/server.rs | 12 ++++++------ docs/config.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/slipstream-server/src/server.rs b/crates/slipstream-server/src/server.rs index efb958b0..2362642c 100644 --- a/crates/slipstream-server/src/server.rs +++ b/crates/slipstream-server/src/server.rs @@ -249,12 +249,12 @@ pub async fn run_server(config: &ServerConfig) -> Result { .as_ref() .map(|seed| seed.bytes.as_ptr()) .unwrap_or(std::ptr::null()); - let mut quic_lb_server_id_holder = config.quic_lb_server_id; - if let Some(sid) = config.quic_lb_server_id { - tracing::info!("QUIC-LB enabled: server_id={}", sid); + let quic_lb_server_id_storage: Option> = config.quic_lb_server_id.map(Box::new); + if let Some(ref sid) = quic_lb_server_id_storage { + tracing::info!("QUIC-LB enabled: server_id={}", **sid); } - let (cnx_id_callback, cnx_id_cb_data) = match quic_lb_server_id_holder.as_mut() { - Some(id) => ( + let (cnx_id_callback, cnx_id_cb_data) = match &quic_lb_server_id_storage { + Some(b) => ( Some( quic_lb_cnx_id_callback as unsafe extern "C" fn( @@ -265,7 +265,7 @@ pub async fn run_server(config: &ServerConfig) -> Result { *mut picoquic_connection_id_t, ), ), - id as *mut u8 as *mut c_void, + b.as_ref() as *const u8 as *mut c_void, ), None => (None, std::ptr::null_mut()), }; diff --git a/docs/config.md b/docs/config.md index 82a8f3cc..4abba5c6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -56,9 +56,9 @@ certificates are not verified. survive restarts. - `--quic-lb-server-id` (0–255) When set, the server generates QUIC-LB plaintext connection IDs (config - rotation 0, 1-byte server_id, 4-byte nonce) so a QUIC-LB-aware load balancer - can route by server_id statelessly. Each backend in a pool should use a - distinct value (e.g. 0, 1, 2). SIP003 plugin option: `quic_lb_server_id=N`. + rotation 0, 1-byte server_id, 6-byte nonce; 8 bytes total) so a QUIC-LB-aware + load balancer can route by server_id statelessly. Each backend in a pool + should use a distinct value (e.g. 0, 1, 2). SIP003 plugin option: `quic_lb_server_id=N`. ## picoquic build environment