From 4ff7f8749689209c7ac585b10ef0c209ff8e74cf Mon Sep 17 00:00:00 2001 From: Anton Liashkevich Date: Tue, 23 Jun 2026 18:38:28 -0400 Subject: [PATCH 1/2] Add active_terminal_timeout and hard_timeout to TimeoutController Two new timeout axes, both configured via env vars and disabled by default (unbounded): - TUNNEL_CONTROLLER_ACTIVE_TERMINAL_TIMEOUT: caps active SSH/TTY/RD sessions from creation time, regardless of activity. Prevents the proxy idle timeout from killing a session mid-task when the user switches browser tabs. - TUNNEL_CONTROLLER_HARD_TIMEOUT: absolute session lifetime from creation, not reset by reconnects. Closes the loophole where a session persists indefinitely by reconnecting within the idle window. Existing env vars are unchanged: - TUNNEL_CONTROLLER_IDLE_TIMEOUT (default 300s) - TUNNEL_CONTROLLER_AWAKE_INTERVAL (default 30s) --- .../src/tunneling/timeout_controller.rs | 89 ++++++++++++++----- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/wallguard-server/src/tunneling/timeout_controller.rs b/wallguard-server/src/tunneling/timeout_controller.rs index 333afeca..9fec8595 100644 --- a/wallguard-server/src/tunneling/timeout_controller.rs +++ b/wallguard-server/src/tunneling/timeout_controller.rs @@ -5,13 +5,15 @@ use tokio::sync::Mutex; pub struct TimeoutController { idle_timeout: u64, awake_interval: u64, + active_terminal_timeout: Option, + hard_timeout: Option, tunnels: Arc>>, } impl TimeoutController { pub fn new(tunnels: Arc>>) -> Self { - const DEFAULT_IDLE_TIMEOUT: u64 = 300; // 5 minutes - const DEFAULT_AWAKE_INTERVAL: u64 = 30; // 30 seconds + const DEFAULT_IDLE_TIMEOUT: u64 = 300; + const DEFAULT_AWAKE_INTERVAL: u64 = 30; let idle_timeout = std::env::var("TUNNEL_CONTROLLER_IDLE_TIMEOUT") .ok() @@ -23,17 +25,23 @@ impl TimeoutController { .and_then(|v| v.parse::().ok()) .unwrap_or(DEFAULT_AWAKE_INTERVAL); + let active_terminal_timeout = std::env::var("TUNNEL_CONTROLLER_ACTIVE_TERMINAL_TIMEOUT") + .ok() + .and_then(|v| v.parse::().ok()); + + let hard_timeout = std::env::var("TUNNEL_CONTROLLER_HARD_TIMEOUT") + .ok() + .and_then(|v| v.parse::().ok()); + Self { idle_timeout, awake_interval, + active_terminal_timeout, + hard_timeout, tunnels, } } - pub fn idle_timeout_duration(&self) -> Duration { - Duration::from_secs(self.idle_timeout) - } - pub fn awake_interval_duration(&self) -> Duration { Duration::from_secs(self.awake_interval) } @@ -72,7 +80,9 @@ impl TimeoutController { tun.data.created_at }; - if timestamp < Self::cutoff_timestamp(self.idle_timeout_duration()) { + if is_idle_expired(timestamp, self.idle_timeout) + || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + { expired_ids.push(tun.data.tunnel_data.id.clone()); } } @@ -101,9 +111,17 @@ impl TimeoutController { tun.data.created_at }; - if timestamp < Self::cutoff_timestamp(self.idle_timeout_duration()) - && !tun.has_active_terminals() - { + let expired = if tun.has_active_terminals() { + is_lifetime_expired( + tun.data.created_at, + self.active_terminal_timeout, + ) || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + } else { + is_idle_expired(timestamp, self.idle_timeout) + || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + }; + + if expired { expired_ids.push(tun.data.tunnel_data.id.clone()); } } @@ -132,9 +150,17 @@ impl TimeoutController { tun.data.created_at }; - if timestamp < Self::cutoff_timestamp(self.idle_timeout_duration()) - && !tun.has_active_terminals() - { + let expired = if tun.has_active_terminals() { + is_lifetime_expired( + tun.data.created_at, + self.active_terminal_timeout, + ) || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + } else { + is_idle_expired(timestamp, self.idle_timeout) + || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + }; + + if expired { expired_ids.push(tun.data.tunnel_data.id.clone()); } } @@ -163,9 +189,17 @@ impl TimeoutController { tun.data.created_at }; - if timestamp < Self::cutoff_timestamp(self.idle_timeout_duration()) - && !tun.has_active_viewers() - { + let expired = if tun.has_active_viewers() { + is_lifetime_expired( + tun.data.created_at, + self.active_terminal_timeout, + ) || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + } else { + is_idle_expired(timestamp, self.idle_timeout) + || is_lifetime_expired(tun.data.created_at, self.hard_timeout) + }; + + if expired { expired_ids.push(tun.data.tunnel_data.id.clone()); } } @@ -186,16 +220,23 @@ impl TimeoutController { } }); } +} - fn cutoff_timestamp(idle_timeout: Duration) -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; +fn cutoff_timestamp(timeout_secs: u64) -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + now.checked_sub(Duration::from_secs(timeout_secs)) + .unwrap_or(Duration::from_secs(0)) + .as_secs() +} - let cutoff = now - .checked_sub(idle_timeout) - .unwrap_or(Duration::from_secs(0)); +fn is_idle_expired(last_access: u64, idle_timeout_secs: u64) -> bool { + last_access < cutoff_timestamp(idle_timeout_secs) +} - cutoff.as_secs() - } +fn is_lifetime_expired(created_at: u64, timeout_secs: Option) -> bool { + timeout_secs + .map(|t| created_at < cutoff_timestamp(t)) + .unwrap_or(false) } From c143646bbc875104f57664626b41ec94d2fa21ad Mon Sep 17 00:00:00 2001 From: Anton Liashkevich Date: Tue, 23 Jun 2026 18:39:44 -0400 Subject: [PATCH 2/2] Version update --- Cargo.lock | 8 ++++---- wallguard-cli/Cargo.toml | 2 +- wallguard-common/Cargo.toml | 2 +- wallguard-server/Cargo.toml | 2 +- wallguard/Cargo.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 288ffa33..221fc7da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6408,7 +6408,7 @@ dependencies = [ [[package]] name = "wallguard" -version = "1.1.11" +version = "1.3.0" dependencies = [ "async-channel", "chrono", @@ -6452,7 +6452,7 @@ dependencies = [ [[package]] name = "wallguard-cli" -version = "1.1.11" +version = "1.3.0" dependencies = [ "anyhow", "clap", @@ -6467,7 +6467,7 @@ dependencies = [ [[package]] name = "wallguard-common" -version = "1.1.11" +version = "1.3.0" dependencies = [ "bincode", "get_if_addrs", @@ -6484,7 +6484,7 @@ dependencies = [ [[package]] name = "wallguard-server" -version = "1.1.11" +version = "1.3.0" dependencies = [ "actix-cors", "actix-web", diff --git a/wallguard-cli/Cargo.toml b/wallguard-cli/Cargo.toml index fbb801f8..0e47647f 100644 --- a/wallguard-cli/Cargo.toml +++ b/wallguard-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wallguard-cli" -version = "1.1.11" +version = "1.3.0" edition = "2024" license = "AGPL-3.0-only" diff --git a/wallguard-common/Cargo.toml b/wallguard-common/Cargo.toml index ce13098a..4e2653de 100644 --- a/wallguard-common/Cargo.toml +++ b/wallguard-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wallguard-common" -version = "1.1.11" +version = "1.3.0" edition = "2024" [dependencies] diff --git a/wallguard-server/Cargo.toml b/wallguard-server/Cargo.toml index a8bf222f..4d574afb 100644 --- a/wallguard-server/Cargo.toml +++ b/wallguard-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wallguard-server" -version = "1.1.11" +version = "1.3.0" edition = "2024" authors = [ "Giuliano Bellini ", diff --git a/wallguard/Cargo.toml b/wallguard/Cargo.toml index a6efc46e..2ca7f35d 100644 --- a/wallguard/Cargo.toml +++ b/wallguard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wallguard" -version = "1.1.11" +version = "1.3.0" edition = "2024" license = "AGPL-3.0-only"