Merge pull request #847 from AndreyOsipuk/feat/client-mss-relay

feat(server): client_mss_bulk — fragment only the handshake, restore MSS for bulk data (cuts pps)
This commit is contained in:
Alexey
2026-06-20 22:10:04 +03:00
committed by GitHub
7 changed files with 86 additions and 1 deletions
+1
View File
@@ -300,6 +300,7 @@ const SERVER_CONFIG_KEYS: &[&str] = &[
"listen_unix_sock_perm",
"listen_tcp",
"client_mss",
"client_mss_bulk",
"proxy_protocol",
"proxy_protocol_header_timeout_ms",
"proxy_protocol_trusted_cidrs",
+15
View File
@@ -1527,6 +1527,15 @@ pub struct ServerConfig {
#[serde(default)]
pub client_mss: Option<String>,
/// Client-facing TCP MSS to switch to AFTER the TLS handshake (ServerHello)
/// is sent. Lets `client_mss` fragment ONLY the handshake (the DPI-inspected
/// part) while the bulk transfer uses normal-size packets — avoids the ~10x
/// packets-per-second blowup that triggers anti-DDoS abuse blocks on
/// pps-policing hosts. Empty/omitted = keep the handshake MSS for the whole
/// connection (previous behavior). Same preset/int grammar as `client_mss`.
#[serde(default)]
pub client_mss_bulk: Option<String>,
/// Accept HAProxy PROXY protocol headers on incoming connections.
/// When enabled, real client IPs are extracted from PROXY v1/v2 headers.
#[serde(default)]
@@ -1594,6 +1603,7 @@ impl Default for ServerConfig {
listen_unix_sock_perm: None,
listen_tcp: None,
client_mss: None,
client_mss_bulk: None,
proxy_protocol: false,
proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(),
proxy_protocol_trusted_cidrs: default_proxy_protocol_trusted_cidrs(),
@@ -2218,6 +2228,11 @@ impl ServerConfig {
pub fn client_mss_value(&self) -> std::result::Result<Option<u16>, String> {
parse_client_mss(self.client_mss.as_deref())
}
/// Resolves the post-handshake (bulk transfer) client MSS, if configured.
pub fn client_mss_bulk_value(&self) -> std::result::Result<Option<u16>, String> {
parse_client_mss(self.client_mss_bulk.as_deref())
}
}
impl ListenerConfig {
+14
View File
@@ -1105,6 +1105,12 @@ impl RunningClientHandler {
#[cfg(unix)]
let raw_fd = self.raw_fd;
let rst_on_close = self.rst_on_close;
// MSS for the bulk data phase: once the handshake (incl. ServerHello) is
// sent, restore a normal MSS so only the handshake stays fragmented by the
// low listener `client_mss`. Cuts pps ~10x (anti-DDoS abuse on pps-policing
// hosts like FastVPS). None = keep handshake MSS for the whole connection.
#[cfg(unix)]
let bulk_mss: Option<u16> = self.config.server.client_mss_bulk_value().ok().flatten();
let outcome = match self.do_handshake().await? {
Some(outcome) => outcome,
@@ -1118,6 +1124,14 @@ impl RunningClientHandler {
if matches!(rst_on_close, crate::config::RstOnCloseMode::Errors) {
let _ = crate::transport::socket::clear_linger_fd(raw_fd);
}
// Handshake (ServerHello) done — raise MSS for bulk transfer.
#[cfg(unix)]
if let Some(mss) = bulk_mss {
if let Err(e) = crate::transport::socket::set_tcp_mss_fd(raw_fd, u32::from(mss))
{
debug!(error = %e, "Failed to raise bulk MSS; keeping handshake MSS");
}
}
fut.await
}
HandshakeOutcome::NeedsMasking(fut) => fut.await,
+33
View File
@@ -125,6 +125,39 @@ pub fn clear_linger_fd(fd: std::os::unix::io::RawFd) -> Result<()> {
Ok(())
}
/// Raise the TCP MSS on an already-accepted connection's fd. Used to fragment
/// ONLY the TLS handshake (via a low listener MSS) and then restore a normal MSS
/// for the bulk (post-handshake) data phase — cuts packets-per-second ~10x without losing the
/// DPI evasion that the fragmented ServerHello provides. No-op safe: errors are
/// returned to the caller, which logs and continues with the handshake MSS.
#[cfg(target_os = "linux")]
pub fn set_tcp_mss_fd(fd: std::os::unix::io::RawFd, mss: u32) -> Result<()> {
use std::io::Error;
let mss = i32::try_from(mss)
.map_err(|_| Error::new(std::io::ErrorKind::InvalidInput, "bulk MSS out of range"))?;
// Direct setsockopt(TCP_MAXSEG) — same pattern as the TCP_USER_TIMEOUT call
// above; avoids socket2 method-name drift across versions.
let rc = unsafe {
libc::setsockopt(
fd,
libc::IPPROTO_TCP,
libc::TCP_MAXSEG,
&mss as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(Error::last_os_error());
}
Ok(())
}
/// Non-Linux stub: MSS shaping only on Linux (TCP_MAXSEG).
#[cfg(all(unix, not(target_os = "linux")))]
pub fn set_tcp_mss_fd(_fd: std::os::unix::io::RawFd, _mss: u32) -> Result<()> {
Ok(())
}
/// Create a new TCP socket for outgoing connections
#[allow(dead_code)]
pub fn create_outgoing_socket(addr: SocketAddr) -> Result<Socket> {