From 5c2b40ef8f70cde16329281703f8e0c52e53ac45 Mon Sep 17 00:00:00 2001 From: Pavel Lizunov Date: Fri, 12 Jun 2026 18:51:38 +0300 Subject: [PATCH] fix(server): apply --idle-timeout-seconds to the QUIC transport idle timeout The documented `--idle-timeout-seconds` flag currently only gates the application-level connection GC (`maybe_gc_idle_connections`). picoquic's own transport idle timeout is never set on the server, so it stays at the built-in 30s default. A connection idle for more than 30s is therefore torn down by the transport regardless of the configured value, and the next client packet then hits an unknown connection ID and receives a stateless reset (forcing the client to reconnect). This is especially visible when tunnelling over a recursive DNS resolver that intermittently throttles/drops the covert flow: a silence gap longer than 30s evicts the connection even though `--idle-timeout-seconds` is set much higher. Wire the flag into picoquic's transport idle timeout (FFI binding for `picoquic_set_default_idle_timeout` added). The transport timeout is only *raised* above picoquic's built-in default, never lowered below it: the goal is to extend connection lifetime past the default, and a sub-default transport timeout would pre-empt the application-level GC (which the existing idle-gc e2e test relies on). `0` disables the idle timeout, matching the GC's "never GC" semantics. Note: per RFC 9000 the effective idle timeout is `min(client, server)`, so a client advertising a compatible idle timeout is also required for the full window; the bundled client currently relies on picoquic's default. Co-Authored-By: Claude Opus 4.8 --- crates/slipstream-ffi/src/picoquic.rs | 5 +++++ crates/slipstream-server/src/server.rs | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/slipstream-ffi/src/picoquic.rs b/crates/slipstream-ffi/src/picoquic.rs index 081706e5..70226759 100644 --- a/crates/slipstream-ffi/src/picoquic.rs +++ b/crates/slipstream-ffi/src/picoquic.rs @@ -229,6 +229,11 @@ extern "C" { quic: *mut picoquic_quic_t, multipath_option: c_int, ); + /// Set the default transport `max_idle_timeout` (milliseconds) for new + /// connections. `0` disables the idle timeout. picoquic's built-in default is + /// 30s; without calling this the value never reflects the operator's + /// `--idle-timeout-seconds`. + pub fn picoquic_set_default_idle_timeout(quic: *mut picoquic_quic_t, idle_timeout_ms: u64); pub fn picoquic_set_preemptive_repeat_policy(quic: *mut picoquic_quic_t, do_repeat: c_int); pub fn picoquic_disable_port_blocking( quic: *mut picoquic_quic_t, diff --git a/crates/slipstream-server/src/server.rs b/crates/slipstream-server/src/server.rs index 6183a016..c9d002bb 100644 --- a/crates/slipstream-server/src/server.rs +++ b/crates/slipstream-server/src/server.rs @@ -8,8 +8,8 @@ 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_set_default_idle_timeout, 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, @@ -244,6 +244,22 @@ pub async fn run_server(config: &ServerConfig) -> Result { )); } configure_quic_with_custom(quic, slipstream_server_cc_algorithm, QUIC_MTU); + // Honour --idle-timeout-seconds at the QUIC transport layer, not just the + // application-level GC. picoquic's built-in transport idle timeout (~30s) is + // otherwise shorter than a higher configured value, so a connection idle for + // more than ~30s is torn down by the transport before the configured timeout, + // and the next client packet then hits an unknown connection ID and gets a + // stateless reset. Only *raise* the transport timeout above the default, never + // below it: the goal is to extend connection lifetime, and a sub-default + // transport timeout would also pre-empt the app-level GC. 0 disables it. + // + // The effective timeout is min(client, server) per RFC 9000, so a client + // advertising a compatible value is also required for the full window. + let idle_timeout_ms = match config.idle_timeout_seconds { + 0 => 0, + secs => secs.saturating_mul(1000).max(30_000), + }; + picoquic_set_default_idle_timeout(quic, idle_timeout_ms); } let udp = Arc::new(bind_udp_socket(&config.dns_listen_host, config.dns_listen_port).await?);