Skip to content

fix(socket/tcp): do not abort idle connections on user timeout - #1204

Open
Aditya-9-6 wants to merge 1 commit into
smoltcp-rs:mainfrom
Aditya-9-6:fix/tcp-timeout-idle-abort
Open

Aditya-9-6 wants to merge 1 commit into
smoltcp-rs:mainfrom
Aditya-9-6:fix/tcp-timeout-idle-abort

Conversation

@Aditya-9-6

Copy link
Copy Markdown
Contributor

Summary

Fixes #1202.

Per RFC 5482 (TCP_USER_TIMEOUT) and smoltcp's documented set_timeout contract, user timeout bounds how long transmitted data may remain unacknowledged, or how long an unresponsive peer is tolerated during keep-alive probing or handshake/teardown:

A socket with a timeout duration set will abort the connection if either of the following occurs:

  • After a connect call, the remote endpoint does not respond within the specified duration;
  • After establishing a connection, there is data in the transmit buffer and the remote endpoint exceeds the specified duration between any two packets it sends;
  • After enabling keep-alive, the remote endpoint exceeds the specified duration between any two packets it sends.

RFC 5482 Section 2 states:

"The TCP user timeout controls how long transmitted data may remain unacknowledged before a connection is forcefully closed. It does not control how long an idle connection may remain open without traffic."

Root Cause

  1. In poll_at(), timeout_poll_at unconditionally armed PollAt::Time(remote_last_ts + timeout) whenever remote_last_ts and timeout were present, even when the socket was in Established with an empty transmit buffer and no keep-alive.
  2. In timed_out(), expiration was evaluated solely as timestamp >= remote_last_ts + timeout, causing connections to abort with RST after timeout of quiet.
  3. In close(), initiating a FIN on a connection that had been idle for an extended period inherited the stale remote_last_ts, which could trigger an immediate/premature timeout before the remote peer could ACK the FIN.

Solution

  • timeout_active(&self) -> bool: Added helper to determine whether the socket is in a state actively awaiting a remote peer response:
    • SynSent | SynReceived: Connection handshake active.
    • FinWait1 | Closing | LastAck: Connection teardown active (waiting for FIN ACK).
    • Established | CloseWait: Active if !self.tx_buffer.is_empty() (data in flight) OR self.keep_alive.is_some() (keepalive probing active).
    • All other states: Inactive.
  • poll_at: Returns PollAt::Ingress for idle connections, and only queries PollAt::Now for timestamp initialization when timeout_active() is true.
  • close(): Clears self.remote_last_ts = None when transitioning to FinWait1 or LastAck so the FIN timeout begins freshly upon FIN transmission.
  • Tests:
    • Restored test_established_timeout to assert PollAt::Ingress while idle before data is queued.
    • Added test_established_idle_no_timeout verifying extended quiet periods do not abort established connections.
    • Added test_established_idle_close_timeout verifying close() after extended idle periods does not prematurely abort the FIN handshake.

Per RFC 5482 (TCP_USER_TIMEOUT) and smoltcp's documented set_timeout
contract, user timeout bounds how long transmitted data may remain
unacknowledged, or how long an unresponsive peer is tolerated during
keep-alive probing or handshake/teardown. A quiescent, healthy connection
with an empty transmit buffer should not be aborted.

Previously, poll_at() unconditionally armed remote_last_ts + timeout
even when the connection was completely idle, and timed_out() aborted
connections after the timeout window elapsed without traffic. Additionally,
close() from an idle connection retained stale idle timestamps, which
could trigger premature abortion of the in-flight FIN.

This change:
- Introduces timeout_active() to verify whether the socket is in a state
  actively awaiting remote response (SYN/FIN in flight, pending data in
  transmit buffer, or keep-alive enabled).
- Updates timed_out() and poll_at() to return PollAt::Ingress when
  idle rather than scheduling an abort timer.
- Clears remote_last_ts on close() when transitioning to FinWait1 or
  LastAck so FIN timeouts are counted from FIN transmission.
- Restores test_established_timeout to assert PollAt::Ingress while idle
  and adds regression tests for extended idle periods and close timeouts.

Fixes smoltcp-rs#1202.
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.60%. Comparing base (efcba2e) to head (1f45f82).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1204      +/-   ##
==========================================
+ Coverage   82.57%   82.60%   +0.02%     
==========================================
  Files          83       83              
  Lines       26174    26215      +41     
==========================================
+ Hits        21613    21654      +41     
  Misses       4561     4561              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

tcp: set_timeout incorrectly aborts idle connections

1 participant