diff --git a/Cargo.lock b/Cargo.lock index 88e99e9..5214ef0 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 58a3782..65618f4 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 144e62c..9cb5dc1 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 d6e1718..2362642 100644 --- a/crates/slipstream-server/src/server.rs +++ b/crates/slipstream-server/src/server.rs @@ -1,15 +1,16 @@ 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 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_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, }; 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,44 @@ 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 +109,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 +123,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 +249,26 @@ pub async fn run_server(config: &ServerConfig) -> Result { .as_ref() .map(|seed| seed.bytes.as_ptr()) .unwrap_or(std::ptr::null()); + 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_storage { + Some(b) => ( + 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, + ), + ), + b.as_ref() as *const u8 as *mut c_void, + ), + None => (None, std::ptr::null_mut()), + }; let quic = unsafe { picoquic_create( config.max_connections, @@ -216,8 +278,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 cb13bb0..4abba5c 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, 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 diff --git a/docs/picoquic-changes.md b/docs/picoquic-changes.md index b3e444d..4fcc112 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