diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb9b3ea..7b6c7f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,13 @@
### Added
- Add `--channel-open-timeout` CLI flag.
+- Add `--disable-tcp-keepalive` CLI flag.
- Add `--tcp-keepalive-time` CLI flag.
- Add `--tcp-keepalive-interval` CLI flag.
### Fixed
-- Fix dropped handlers not being removed from HTTP pools.
+- Remove dropped handlers from HTTP pools.
## 0.10.0 (2026-06-09)
diff --git a/book/src/cli.md b/book/src/cli.md
index 92e0444..4e035b4 100644
--- a/book/src/cli.md
+++ b/book/src/cli.md
@@ -296,7 +296,7 @@ Expose HTTP/SSH/TCP services through SSH port forwarding.
- default: Default (compact, ANSI-formatted, single-line)
- json: JSON format
- duper: Duper format
-
+
[default: default]
--ip-allowlist <CIDR>
@@ -426,20 +426,25 @@ Expose HTTP/SSH/TCP services through SSH port forwarding.
By default, these connections are not terminated by Sandhole.
+ --disable-tcp-keepalive
+ Disable TCP keepalive on SSH, HTTP, HTTPS and TCP connections.
+ By default, it is enabled.
+
+ Disabling TCP keepalive may lead to "connection reset by peer"
+ errors on socket reuse.
+
--tcp-keepalive-time <DURATION>
- If set, enables TCP keepalive on accepted client connections,
- sending the first probe after the set idle time.
+ Time to send the first TCP keepalive probe after the set idle time.
- Set this option to avoid "connection reset by peer" on
- socket reuse.
+ Only applies when `--disable-tcp-keepalive` is unset.
- By default, keepalive is disabled.
+ [default: 20s]
--tcp-keepalive-interval <DURATION>
Interval between TCP keepalive probes once `--tcp-keepalive-time`
has elapsed.
- Only applies when `--tcp-keepalive-time` is set.
+ Only applies when `--disable-tcp-keepalive` is unset.
[default: 10s]
diff --git a/src/config.rs b/src/config.rs
index 77d24c4..e99f5a0 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -540,18 +540,26 @@ pub struct ApplicationConfig {
#[arg(long, value_parser = validate_duration, value_name = "DURATION")]
pub tcp_connection_timeout: Option,
- /// If set, enables TCP keepalive on accepted client connections,
- /// sending the first probe after the set idle time.
+ /// Disable TCP keepalive on SSH, HTTP, HTTPS and TCP connections. By default, it is enabled.
///
- /// Set this option to avoid "connection reset by peer" on socket reuse.
+ /// Disabling TCP keepalive may lead to "connection reset by peer" errors on socket reuse.
+ #[arg(long, default_value_t = false)]
+ pub disable_tcp_keepalive: bool,
+
+ /// Time to send the first TCP keepalive probe after the set idle time.
///
- /// By default, keepalive is disabled.
- #[arg(long, value_parser = validate_duration, value_name = "DURATION")]
- pub tcp_keepalive_time: Option,
+ /// Only applies when `--disable-tcp-keepalive` is unset.
+ #[arg(
+ long,
+ default_value = "20s",
+ value_parser = validate_duration,
+ value_name = "DURATION"
+ )]
+ pub tcp_keepalive_time: Duration,
/// Interval between TCP keepalive probes once `--tcp-keepalive-time` has elapsed.
///
- /// Only applies when `--tcp-keepalive-time` is set.
+ /// Only applies when `--disable-tcp-keepalive` is unset.
#[arg(
long,
default_value = "10s",
@@ -690,7 +698,8 @@ mod application_config_tests {
authentication_request_timeout: Duration::from_secs(5),
http_request_timeout: None,
tcp_connection_timeout: None,
- tcp_keepalive_time: None,
+ disable_tcp_keepalive: false,
+ tcp_keepalive_time: Duration::from_secs(20),
tcp_keepalive_interval: Duration::from_secs(10),
udp_timeout: Duration::from_secs(60),
}
@@ -763,6 +772,7 @@ mod application_config_tests {
"--authentication-request-timeout=6s",
"--http-request-timeout=15s",
"--tcp-connection-timeout=30s",
+ "--disable-tcp-keepalive",
"--tcp-keepalive-time=15s",
"--tcp-keepalive-interval=5s",
"--udp-timeout=30s",
@@ -839,7 +849,8 @@ mod application_config_tests {
authentication_request_timeout: Duration::from_secs(6),
http_request_timeout: Some(Duration::from_secs(15)),
tcp_connection_timeout: Some(Duration::from_secs(30)),
- tcp_keepalive_time: Some(Duration::from_secs(15)),
+ disable_tcp_keepalive: true,
+ tcp_keepalive_time: Duration::from_secs(15),
tcp_keepalive_interval: Duration::from_secs(5),
udp_timeout: Duration::from_secs(30),
}
diff --git a/src/entrypoint.rs b/src/entrypoint.rs
index c4cf7f4..73ebd7e 100644
--- a/src/entrypoint.rs
+++ b/src/entrypoint.rs
@@ -265,11 +265,15 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> {
Some(max_quota) => Arc::new(Box::new(Arc::new(QuotaMap::new(max_quota.into())))),
None => Arc::new(Box::new(DummyQuotaHandler)),
};
- let tcp_keepalive = config.tcp_keepalive_time.map(|tcp_keepalive_time| {
- TcpKeepalive::new()
- .with_time(tcp_keepalive_time)
- .with_interval(config.tcp_keepalive_interval)
- });
+ let tcp_keepalive = if config.disable_tcp_keepalive {
+ None
+ } else {
+ Some(
+ TcpKeepalive::new()
+ .with_time(config.tcp_keepalive_time)
+ .with_interval(config.tcp_keepalive_interval),
+ )
+ };
let http_connections = Arc::new(
ConnectionMap::builder()
.strategy(config.load_balancing)