From f3db71b32e6b9f4eed0f1a3433d8e6f8e653a527 Mon Sep 17 00:00:00 2001 From: Vasiliy Stelmachenok Date: Sun, 8 Mar 2026 17:14:36 +0300 Subject: [PATCH 1/3] Add support for systemd socket activation Co-authored-by: Jason Volk Signed-off-by: Vasiliy Stelmachenok --- src/router/serve.rs | 82 ++++++++++++++++++++++++++++++--------- src/router/serve/plain.rs | 11 ++++-- src/router/serve/tls.rs | 25 ++++++++---- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/src/router/serve.rs b/src/router/serve.rs index 9f4d6981..2bd1240c 100644 --- a/src/router/serve.rs +++ b/src/router/serve.rs @@ -3,7 +3,12 @@ mod plain; mod tls; mod unix; -use std::sync::{Arc, atomic::Ordering}; +#[cfg(all(feature = "systemd", target_os = "linux"))] +use std::os::fd::FromRawFd; +use std::{ + net::{SocketAddr, TcpListener}, + sync::{Arc, atomic::Ordering}, +}; use tokio::task::JoinSet; use tuwunel_core::{Result, debug_info}; @@ -29,24 +34,65 @@ pub(super) async fn serve(services: Arc, handle: ServerHandle) -> Resu .await?; } - let addrs = config.get_bind_addrs(); - if !addrs.is_empty() { - #[cfg_attr(not(feature = "direct_tls"), expect(clippy::redundant_else))] - if config.tls.certs.is_some() { - #[cfg(feature = "direct_tls")] - { - services.globals.init_rustls_provider()?; - tls::serve(server, &app, &handle.handle_ip, &mut join_set, &addrs).await?; - } + let mut listen_addrs: Vec = vec![]; - #[cfg(not(feature = "direct_tls"))] - return tuwunel_core::Err!(Config( - "tls", - "tuwunel was not built with direct TLS support (\"direct_tls\")" - )); - } else { - plain::serve(server, &app, &handle.handle_ip, &mut join_set, &addrs); + #[cfg(all(feature = "systemd", target_os = "linux"))] + let mut listeners: Vec = if let Ok(fds) = sd_notify::listen_fds() { + fds.filter_map(|fd| { + // SAFETY: systemd should already take care of providing + // the correct TCP socket, so we just use it via raw fd + let listener = unsafe { TcpListener::from_raw_fd(fd) }; + listener + .set_nonblocking(true) + .expect("Failed to set non-blocking"); + if let Ok(addr) = listener.local_addr() { + listen_addrs.push(addr); + Some(listener) + } else { + None + } + }) + .collect() + } else { + vec![] + }; + + #[cfg(not(all(feature = "systemd", target_os = "linux")))] + let mut listeners: Vec = vec![]; + + let addrs = config.get_bind_addrs(); + + if !addrs.is_empty() && listeners.is_empty() { + listeners = addrs + .clone() + .into_iter() + .map(|addr| { + let listener = TcpListener::bind(addr).expect("Failed to bind {addr:?}"); + listener + .set_nonblocking(true) + .expect("Failed to set non-blocking"); + listen_addrs.push(addr); + listener + }) + .collect(); + } + + #[cfg_attr(not(feature = "direct_tls"), expect(clippy::redundant_else))] + if config.tls.certs.is_some() { + #[cfg(feature = "direct_tls")] + { + services.globals.init_rustls_provider()?; + tls::serve(server, &app, &handle.handle_ip, &mut join_set, &listen_addrs, &listeners) + .await?; } + + #[cfg(not(feature = "direct_tls"))] + return tuwunel_core::Err!(Config( + "tls", + "tuwunel was not built with direct TLS support (\"direct_tls\")" + )); + } else { + plain::serve(server, &app, &handle.handle_ip, &mut join_set, &listen_addrs, &listeners); } assert!(!join_set.is_empty(), "at least one listener should be installed"); @@ -68,7 +114,7 @@ pub(super) async fn serve(services: Arc, handle: ServerHandle) -> Resu .requests_panic .load(Ordering::Acquire), handle_active, - "Stopped listening on {addrs:?}", + "Stopped listening on {listen_addrs:?}", ); debug_assert_eq!(0, handle_active, "active request handles still pending"); diff --git a/src/router/serve/plain.rs b/src/router/serve/plain.rs index 4ee8fcf1..9826d0f5 100644 --- a/src/router/serve/plain.rs +++ b/src/router/serve/plain.rs @@ -1,4 +1,7 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{ + net::{SocketAddr, TcpListener}, + sync::Arc, +}; use axum::Router; use axum_server::Handle; @@ -11,12 +14,14 @@ pub(super) fn serve( handle: &Handle, join_set: &mut JoinSet>, addrs: &[SocketAddr], + listeners: &Vec, ) { let router = router .clone() .into_make_service_with_connect_info::(); - for addr in addrs { - let acceptor = axum_server::bind(*addr) + for listener in listeners { + let acceptor = axum_server::from_tcp(listener.try_clone().unwrap()) + .unwrap() .handle(handle.clone()) .serve(router.clone()); join_set.spawn_on(acceptor, server.runtime()); diff --git a/src/router/serve/tls.rs b/src/router/serve/tls.rs index 09ecce31..d11e7d5e 100644 --- a/src/router/serve/tls.rs +++ b/src/router/serve/tls.rs @@ -1,4 +1,7 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{ + net::{SocketAddr, TcpListener}, + sync::Arc, +}; use axum::Router; use axum_server::Handle; @@ -12,6 +15,7 @@ pub(super) async fn serve( handle: &Handle, join_set: &mut JoinSet>, addrs: &[SocketAddr], + listeners: &Vec, ) -> Result { let tls = &server.config.tls; let certs = tls.certs.as_ref().unwrap(); @@ -30,12 +34,16 @@ pub(super) async fn serve( .clone() .into_make_service_with_connect_info::(); if tls.dual_protocol { - for addr in addrs { + for listener in listeners { join_set.spawn_on( - axum_server_dual_protocol::bind_dual_protocol(*addr, conf.clone()) - .set_upgrade(false) - .handle(handle.clone()) - .serve(app.clone()), + axum_server_dual_protocol::from_tcp_dual_protocol( + listener.try_clone().unwrap(), + conf.clone(), + ) + .unwrap() + .set_upgrade(false) + .handle(handle.clone()) + .serve(app.clone()), server.runtime(), ); } @@ -45,9 +53,10 @@ pub(super) async fn serve( (HTTP) connections too (insecure!)", ); } else { - for addr in addrs { + for listener in listeners { join_set.spawn_on( - axum_server::bind_rustls(*addr, conf.clone()) + axum_server::from_tcp_rustls(listener.try_clone().unwrap(), conf.clone()) + .unwrap() .handle(handle.clone()) .serve(app.clone()), server.runtime(), From 2a1d34bee104aa440bafac8dff88a44a5636de92 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Tue, 10 Mar 2026 01:14:20 +0000 Subject: [PATCH 2/3] Bump quinn-proto. Signed-off-by: Jason Volk --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e677b1a7..7d742002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2451,9 +2451,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" @@ -3487,9 +3487,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -6442,18 +6442,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", From aa847e484403bbba1572cee70c77854ce976ad5f Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Tue, 10 Mar 2026 01:09:36 +0000 Subject: [PATCH 3/3] Flatten conditional branches; eliminate unwrap(). Signed-off-by: Jason Volk --- src/router/serve.rs | 83 +++++++++++++++++++-------------------- src/router/serve/plain.rs | 13 +++--- src/router/serve/tls.rs | 34 +++++++--------- 3 files changed, 64 insertions(+), 66 deletions(-) diff --git a/src/router/serve.rs b/src/router/serve.rs index 2bd1240c..63b569f0 100644 --- a/src/router/serve.rs +++ b/src/router/serve.rs @@ -3,8 +3,6 @@ mod plain; mod tls; mod unix; -#[cfg(all(feature = "systemd", target_os = "linux"))] -use std::os::fd::FromRawFd; use std::{ net::{SocketAddr, TcpListener}, sync::{Arc, atomic::Ordering}, @@ -34,55 +32,32 @@ pub(super) async fn serve(services: Arc, handle: ServerHandle) -> Resu .await?; } - let mut listen_addrs: Vec = vec![]; + let systemd_listeners: Vec<_> = systemd_listeners().collect(); + let systemd_listeners_is_empty = systemd_listeners.is_empty(); + let (listeners, addrs): (Vec<_>, Vec<_>) = config + .get_bind_addrs() + .into_iter() + .filter(|_| systemd_listeners_is_empty) + .map(|addr| { + let listener = TcpListener::bind(addr) + .expect("Failed to bind configured TcpListener to {addr:?}"); - #[cfg(all(feature = "systemd", target_os = "linux"))] - let mut listeners: Vec = if let Ok(fds) = sd_notify::listen_fds() { - fds.filter_map(|fd| { - // SAFETY: systemd should already take care of providing - // the correct TCP socket, so we just use it via raw fd - let listener = unsafe { TcpListener::from_raw_fd(fd) }; + (listener, addr) + }) + .chain(systemd_listeners) + .inspect(|(listener, _)| { listener .set_nonblocking(true) .expect("Failed to set non-blocking"); - if let Ok(addr) = listener.local_addr() { - listen_addrs.push(addr); - Some(listener) - } else { - None - } }) - .collect() - } else { - vec![] - }; - - #[cfg(not(all(feature = "systemd", target_os = "linux")))] - let mut listeners: Vec = vec![]; - - let addrs = config.get_bind_addrs(); - - if !addrs.is_empty() && listeners.is_empty() { - listeners = addrs - .clone() - .into_iter() - .map(|addr| { - let listener = TcpListener::bind(addr).expect("Failed to bind {addr:?}"); - listener - .set_nonblocking(true) - .expect("Failed to set non-blocking"); - listen_addrs.push(addr); - listener - }) - .collect(); - } + .unzip(); #[cfg_attr(not(feature = "direct_tls"), expect(clippy::redundant_else))] if config.tls.certs.is_some() { #[cfg(feature = "direct_tls")] { services.globals.init_rustls_provider()?; - tls::serve(server, &app, &handle.handle_ip, &mut join_set, &listen_addrs, &listeners) + tls::serve(server, &app, &handle.handle_ip, &mut join_set, &listeners, &addrs) .await?; } @@ -92,7 +67,7 @@ pub(super) async fn serve(services: Arc, handle: ServerHandle) -> Resu "tuwunel was not built with direct TLS support (\"direct_tls\")" )); } else { - plain::serve(server, &app, &handle.handle_ip, &mut join_set, &listen_addrs, &listeners); + plain::serve(server, &app, &handle.handle_ip, &mut join_set, &listeners, &addrs)?; } assert!(!join_set.is_empty(), "at least one listener should be installed"); @@ -114,10 +89,34 @@ pub(super) async fn serve(services: Arc, handle: ServerHandle) -> Resu .requests_panic .load(Ordering::Acquire), handle_active, - "Stopped listening on {listen_addrs:?}", + "Stopped listening on {addrs:?}", ); debug_assert_eq!(0, handle_active, "active request handles still pending"); Ok(()) } + +#[cfg(all(feature = "systemd", target_os = "linux"))] +fn systemd_listeners() -> impl Iterator { + sd_notify::listen_fds() + .into_iter() + .flatten() + .filter_map(|fd| { + use std::os::fd::FromRawFd; + + debug_assert!(fd >= 3, "fdno probably not a listener socket"); + // SAFETY: systemd should already take care of providing + // the correct TCP socket, so we just use it via raw fd + let listener = unsafe { TcpListener::from_raw_fd(fd) }; + + let Ok(addr) = listener.local_addr() else { + return None; + }; + + Some((listener, addr)) + }) +} + +#[cfg(any(not(feature = "systemd"), not(target_os = "linux")))] +fn systemd_listeners() -> impl Iterator { std::iter::empty() } diff --git a/src/router/serve/plain.rs b/src/router/serve/plain.rs index 9826d0f5..91ffc10b 100644 --- a/src/router/serve/plain.rs +++ b/src/router/serve/plain.rs @@ -6,26 +6,29 @@ use std::{ use axum::Router; use axum_server::Handle; use tokio::task::JoinSet; -use tuwunel_core::{Server, info}; +use tuwunel_core::{Result, Server, info}; pub(super) fn serve( server: &Arc, router: &Router, handle: &Handle, join_set: &mut JoinSet>, + listeners: &[TcpListener], addrs: &[SocketAddr], - listeners: &Vec, -) { +) -> Result { let router = router .clone() .into_make_service_with_connect_info::(); + for listener in listeners { - let acceptor = axum_server::from_tcp(listener.try_clone().unwrap()) - .unwrap() + let acceptor = axum_server::from_tcp(listener.try_clone()?)? .handle(handle.clone()) .serve(router.clone()); + join_set.spawn_on(acceptor, server.runtime()); } info!("Listening on {addrs:?}"); + + Ok(()) } diff --git a/src/router/serve/tls.rs b/src/router/serve/tls.rs index d11e7d5e..bd599a0e 100644 --- a/src/router/serve/tls.rs +++ b/src/router/serve/tls.rs @@ -14,8 +14,8 @@ pub(super) async fn serve( app: &Router, handle: &Handle, join_set: &mut JoinSet>, + listeners: &[TcpListener], addrs: &[SocketAddr], - listeners: &Vec, ) -> Result { let tls = &server.config.tls; let certs = tls.certs.as_ref().unwrap(); @@ -35,17 +35,15 @@ pub(super) async fn serve( .into_make_service_with_connect_info::(); if tls.dual_protocol { for listener in listeners { - join_set.spawn_on( - axum_server_dual_protocol::from_tcp_dual_protocol( - listener.try_clone().unwrap(), - conf.clone(), - ) - .unwrap() - .set_upgrade(false) - .handle(handle.clone()) - .serve(app.clone()), - server.runtime(), - ); + let acceptor = axum_server_dual_protocol::from_tcp_dual_protocol( + listener.try_clone()?, + conf.clone(), + )? + .set_upgrade(false) + .handle(handle.clone()) + .serve(app.clone()); + + join_set.spawn_on(acceptor, server.runtime()); } warn!( @@ -54,13 +52,11 @@ pub(super) async fn serve( ); } else { for listener in listeners { - join_set.spawn_on( - axum_server::from_tcp_rustls(listener.try_clone().unwrap(), conf.clone()) - .unwrap() - .handle(handle.clone()) - .serve(app.clone()), - server.runtime(), - ); + let acceptor = axum_server::from_tcp_rustls(listener.try_clone()?, conf.clone())? + .handle(handle.clone()) + .serve(app.clone()); + + join_set.spawn_on(acceptor, server.runtime()); } info!("Listening on {addrs:?} with TLS certificate {certs}");