Skip to content

Commit 4f54897

Browse files
divybotlittledivy
andauthored
fix(ext/node): implement setKeepAlive on native TCPWrap (#34865)
## Summary The native `TCPWrap` rewrite (`feat(ext/node): rewrite node:http with llhttp and native TCPWrap` #33208, plus `use native LibUvStreamWrap` #33301), which shipped in **2.7.13**, dropped the `setKeepAlive` method from the TCP handle. As a result `socket.setKeepAlive(enable, delay)` became a **silent no-op**: - `uv_tcp_keepalive` in `libs/core/uv_compat/tcp.rs` was a stub that returned `0` without touching the socket, and - `TCPWrap` no longer exposed a `setKeepAlive` method, so net.ts's `ReflectHas(this._handle, "setKeepAlive")` guard short-circuited. Before the rewrite (e.g. Deno 2.5.4) the handle implemented `setKeepAlive` and `SO_KEEPALIVE` was actually set on the socket. ## Fix - Implement `uv_tcp_keepalive` for real: toggle `SO_KEEPALIVE`, and when enabling, set the per-connection idle time via `TCP_KEEPIDLE` (Linux/Android) / `TCP_KEEPALIVE` (macOS/BSD). Windows toggles `SO_KEEPALIVE`. - Record the requested keepalive state on the handle so a `setKeepAlive` issued before the connect future resolves is (re)applied once the socket exists — mirroring how `internal_nodelay` is carried and applied on connect. - Expose `setKeepAlive` on `TCPWrap` (`ext/node/ops/tcp_wrap.rs`); `delay` is the idle time in seconds, matching what net.ts passes (`~~(initialDelay / 1000)`). ## Why this matters for #34729 Found while investigating the tedious/mssql ECONNRESET-over-SSH-tunnel regression (#34729). `tedious` enables TCP keepalive immediately after connecting (`socket.setKeepAlive(true, KEEP_ALIVE_INITIAL_DELAY)`) specifically to keep tunneled/long-lived connections from being reaped by SSH/NAT/firewalls. Since the rewrite that call silently did nothing, removing protection that worked on 2.5.4. I could not fully reproduce the exact `socket hang up` in a sandbox without a real SQL Server — a faithful harness (real `sshd` + `ssh -L`, dual-stack parallel connect like tedious's `connectInParallel`, a fake TDS server completing the full PRELOGIN/LOGIN7 handshake, plus fragmentation/latency) showed no old-vs-new difference for the core read/write/connect path. So this is `Refs` rather than `Closes`: a confirmed regression in the affected code path that restores the pre-2.7.13 behavior tedious depends on. ## Tests - `libs/core/uv_compat/tests.rs`: `tcp_keepalive_sets_so_keepalive_on_connected_socket` connects a real socket, calls `uv_tcp_keepalive`, and asserts via `getsockopt` that `SO_KEEPALIVE` is enabled and `TCP_KEEPIDLE` matches the requested delay (and that disabling clears it). - `tests/unit_node/net_test.ts`: `socket.setKeepAlive()` reaches the handle and returns the socket for chaining. Both new Rust tests pass locally. Refs #34729 Closes denoland/divybot#474 --------- Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 33b909e commit 4f54897

4 files changed

Lines changed: 393 additions & 9 deletions

File tree

ext/node/ops/tcp_wrap.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,22 @@ impl TCPWrap {
554554
unsafe { uv_compat::uv_tcp_nodelay(tcp, enable as i32) }
555555
}
556556

557+
/// Enable/disable `SO_KEEPALIVE`. `delay` is the idle time in seconds
558+
/// before the first keepalive probe (Node passes seconds here). Matches
559+
/// Node's `TCPWrap::SetKeepAlive`, which libraries such as `tedious`
560+
/// rely on to keep tunneled/long-lived connections from being reaped.
561+
#[fast]
562+
#[rename("setKeepAlive")]
563+
fn set_keep_alive(&self, enable: bool, #[smi] delay: i32) -> i32 {
564+
let tcp = self.tcp_ptr();
565+
if tcp.is_null() {
566+
return -1;
567+
}
568+
let delay = delay.max(0) as u32;
569+
// SAFETY: tcp is valid (null-checked above).
570+
unsafe { uv_compat::uv_tcp_keepalive(tcp, enable as i32, delay) }
571+
}
572+
557573
/// Set SO_LINGER to 0 so the next close sends RST instead of FIN.
558574
#[fast]
559575
fn reset(&self) -> i32 {

libs/core/uv_compat/tcp.rs

Lines changed: 199 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ pub struct uv_tcp_t {
112112
pub(crate) internal_listener: Option<tokio::net::TcpListener>,
113113
pub(crate) internal_listener_addr: Option<SocketAddr>,
114114
pub(crate) internal_nodelay: bool,
115+
/// Desired SO_KEEPALIVE state `(enable, delay_secs)`. Stored so the
116+
/// option can be (re)applied once the underlying socket exists — e.g.
117+
/// when `uv_tcp_keepalive` is called before the connect future
118+
/// resolves, the option is applied from `poll_tcp_handle` after the
119+
/// stream is created. Mirrors how `internal_nodelay` is carried.
120+
pub(crate) internal_keepalive: Option<(bool, c_uint)>,
115121
pub(crate) internal_alloc_cb: Option<uv_alloc_cb>,
116122
pub(crate) internal_read_cb: Option<uv_read_cb>,
117123
pub(crate) internal_reading: bool,
@@ -399,6 +405,7 @@ pub unsafe fn uv_tcp_init(loop_: *mut uv_loop_t, tcp: *mut uv_tcp_t) -> c_int {
399405
write(addr_of_mut!((*tcp).internal_listener), None);
400406
write(addr_of_mut!((*tcp).internal_listener_addr), None);
401407
write(addr_of_mut!((*tcp).internal_nodelay), false);
408+
write(addr_of_mut!((*tcp).internal_keepalive), None);
402409
write(addr_of_mut!((*tcp).internal_alloc_cb), None);
403410
write(addr_of_mut!((*tcp).internal_read_cb), None);
404411
write(addr_of_mut!((*tcp).internal_reading), false);
@@ -883,17 +890,198 @@ pub unsafe fn uv_tcp_getsockname(
883890
}
884891
}
885892

893+
/// Apply `SO_KEEPALIVE` (and, when enabling, the idle delay) to a raw
894+
/// socket descriptor. Mirrors libuv's `uv__tcp_keepalive`: toggle
895+
/// `SO_KEEPALIVE`, then set the per-connection idle time via the
896+
/// platform's TCP keepalive-idle option. Returns 0 on success or a
897+
/// negative uv error code.
898+
///
886899
/// ### Safety
887-
/// `_tcp` must be a valid pointer to a `uv_tcp_t` initialized by `uv_tcp_init`.
900+
/// `fd` must be a valid, open socket descriptor.
901+
#[cfg(unix)]
902+
unsafe fn apply_keepalive_fd(
903+
fd: std::os::unix::io::RawFd,
904+
enable: bool,
905+
delay: c_uint,
906+
) -> c_int {
907+
// SAFETY: fd is a valid socket per the caller contract.
908+
unsafe {
909+
let on: c_int = if enable { 1 } else { 0 };
910+
if libc::setsockopt(
911+
fd,
912+
libc::SOL_SOCKET,
913+
libc::SO_KEEPALIVE,
914+
&on as *const c_int as *const c_void,
915+
std::mem::size_of::<c_int>() as libc::socklen_t,
916+
) != 0
917+
{
918+
return io_error_to_uv(&std::io::Error::last_os_error());
919+
}
920+
921+
// The idle delay is only meaningful when keepalive is enabled.
922+
// Linux/Android use TCP_KEEPIDLE; the BSDs/macOS use TCP_KEEPALIVE.
923+
// Both take the idle time in seconds. On any other platform we leave
924+
// SO_KEEPALIVE on with the system default idle time (still correct).
925+
#[cfg(any(target_os = "linux", target_os = "android"))]
926+
let idle_opt: Option<c_int> = Some(libc::TCP_KEEPIDLE);
927+
#[cfg(any(
928+
target_os = "macos",
929+
target_os = "ios",
930+
target_os = "freebsd",
931+
target_os = "netbsd",
932+
target_os = "openbsd",
933+
target_os = "dragonfly"
934+
))]
935+
let idle_opt: Option<c_int> = Some(libc::TCP_KEEPALIVE);
936+
#[cfg(not(any(
937+
target_os = "linux",
938+
target_os = "android",
939+
target_os = "macos",
940+
target_os = "ios",
941+
target_os = "freebsd",
942+
target_os = "netbsd",
943+
target_os = "openbsd",
944+
target_os = "dragonfly"
945+
)))]
946+
let idle_opt: Option<c_int> = None;
947+
948+
if enable
949+
&& delay > 0
950+
&& let Some(idle_opt) = idle_opt
951+
{
952+
let secs = delay as c_int;
953+
if libc::setsockopt(
954+
fd,
955+
libc::IPPROTO_TCP,
956+
idle_opt,
957+
&secs as *const c_int as *const c_void,
958+
std::mem::size_of::<c_int>() as libc::socklen_t,
959+
) != 0
960+
{
961+
return io_error_to_uv(&std::io::Error::last_os_error());
962+
}
963+
}
964+
}
965+
0
966+
}
967+
968+
/// Windows variant: toggle `SO_KEEPALIVE`. The idle interval can only be
969+
/// configured via `WSAIoctl(SIO_KEEPALIVE_VALS)`, which libuv uses; we
970+
/// keep to the on/off toggle (matching the system default idle time),
971+
/// which is what consumers like tedious rely on to keep tunneled
972+
/// connections from being reaped by intermediaries.
973+
///
974+
/// ### Safety
975+
/// `sock` must be a valid socket handle.
976+
#[cfg(windows)]
977+
unsafe fn apply_keepalive_socket(
978+
sock: usize,
979+
enable: bool,
980+
_delay: c_uint,
981+
) -> c_int {
982+
unsafe extern "system" {
983+
fn setsockopt(
984+
s: usize,
985+
level: c_int,
986+
optname: c_int,
987+
optval: *const c_void,
988+
optlen: c_int,
989+
) -> c_int;
990+
}
991+
const SOL_SOCKET: c_int = 0xffff;
992+
const SO_KEEPALIVE: c_int = 0x0008;
993+
let on: c_int = if enable { 1 } else { 0 };
994+
// SAFETY: sock is a valid socket per the caller contract.
995+
let rc = unsafe {
996+
setsockopt(
997+
sock,
998+
SOL_SOCKET,
999+
SO_KEEPALIVE,
1000+
&on as *const c_int as *const c_void,
1001+
std::mem::size_of::<c_int>() as c_int,
1002+
)
1003+
};
1004+
if rc != 0 {
1005+
return io_error_to_uv(&std::io::Error::last_os_error());
1006+
}
1007+
0
1008+
}
1009+
1010+
/// Apply the stored keepalive option to a connected stream's socket.
1011+
/// Called from `poll_tcp_handle` once the connect future resolves so a
1012+
/// `uv_tcp_keepalive` issued before connect completion still takes
1013+
/// effect. No-op when no keepalive was requested.
1014+
///
1015+
/// ### Safety
1016+
/// `tcp` must be a valid pointer to an initialized `uv_tcp_t`.
1017+
pub(crate) unsafe fn apply_pending_keepalive(tcp: *mut uv_tcp_t) {
1018+
// SAFETY: tcp is valid and initialized per the caller contract.
1019+
unsafe {
1020+
let Some((enable, delay)) = (*tcp).internal_keepalive else {
1021+
return;
1022+
};
1023+
#[cfg(unix)]
1024+
if let Some(ref stream) = (*tcp).internal_stream {
1025+
use std::os::unix::io::AsRawFd;
1026+
apply_keepalive_fd(stream.as_raw_fd(), enable, delay);
1027+
}
1028+
#[cfg(windows)]
1029+
if let Some(ref stream) = (*tcp).internal_stream {
1030+
use std::os::windows::io::AsRawSocket;
1031+
apply_keepalive_socket(stream.as_raw_socket() as usize, enable, delay);
1032+
}
1033+
}
1034+
}
1035+
1036+
/// Enable or disable TCP keepalive on the handle, applying it to the
1037+
/// underlying socket when one already exists. Mirrors libuv's
1038+
/// `uv_tcp_keepalive`. The requested state is also stored so it is
1039+
/// applied if/when the socket is created later (e.g. a keepalive set
1040+
/// while the connect is still pending). `delay` is the idle time in
1041+
/// seconds before the first keepalive probe.
1042+
///
1043+
/// ### Safety
1044+
/// `tcp` must be a valid pointer to a `uv_tcp_t` initialized by `uv_tcp_init`.
8881045
#[cfg_attr(feature = "uv_compat_export", unsafe(no_mangle))]
8891046
pub unsafe extern "C" fn uv_tcp_keepalive(
890-
_tcp: *mut uv_tcp_t,
891-
_enable: c_int,
892-
_delay: c_uint,
1047+
tcp: *mut uv_tcp_t,
1048+
enable: c_int,
1049+
delay: c_uint,
8931050
) -> c_int {
894-
// Keepalive is a no-op: tokio's TcpStream doesn't expose SO_KEEPALIVE
895-
// configuration in a cross-platform way, and nghttp2 only uses this
896-
// as a best-effort hint.
1051+
// SAFETY: tcp is valid and initialized per the caller contract.
1052+
unsafe {
1053+
let enabled = enable != 0;
1054+
(*tcp).internal_keepalive = Some((enabled, delay));
1055+
1056+
#[cfg(unix)]
1057+
{
1058+
use std::os::unix::io::AsRawFd;
1059+
// Prefer the live stream's fd; fall back to the pre-connect socket
1060+
// fd recorded by bind so an option set between bind and connect is
1061+
// preserved on the same descriptor (matching uv__stream_open).
1062+
let fd = if let Some(ref stream) = (*tcp).internal_stream {
1063+
Some(stream.as_raw_fd())
1064+
} else {
1065+
(*tcp).internal_fd
1066+
};
1067+
if let Some(fd) = fd {
1068+
return apply_keepalive_fd(fd, enabled, delay);
1069+
}
1070+
}
1071+
#[cfg(windows)]
1072+
{
1073+
use std::os::windows::io::AsRawSocket;
1074+
let sock = if let Some(ref stream) = (*tcp).internal_stream {
1075+
Some(stream.as_raw_socket() as usize)
1076+
} else {
1077+
(*tcp).internal_fd.map(|s| s as usize)
1078+
};
1079+
if let Some(sock) = sock {
1080+
return apply_keepalive_socket(sock, enabled, delay);
1081+
}
1082+
}
1083+
}
1084+
// No socket yet: state stored, will be applied on connect.
8971085
0
8981086
}
8991087

@@ -1056,6 +1244,7 @@ pub fn new_tcp() -> uv_tcp_t {
10561244
internal_listener: None,
10571245
internal_listener_addr: None,
10581246
internal_nodelay: false,
1247+
internal_keepalive: None,
10591248
internal_alloc_cb: None,
10601249
internal_read_cb: None,
10611250
internal_reading: false,
@@ -1108,6 +1297,9 @@ pub(crate) unsafe fn poll_tcp_handle(
11081297
stream.set_nodelay(true).ok();
11091298
}
11101299
(*tcp_ptr).internal_stream = Some(stream);
1300+
// Apply a keepalive option requested before the connect
1301+
// resolved, now that the socket exists.
1302+
apply_pending_keepalive(tcp_ptr);
11111303
0
11121304
}
11131305
Err(ref e) => io_error_to_uv(e),

0 commit comments

Comments
 (0)