Server draining state ends early
Summary
The earlier static source excerpt was unrelated, but the RFC 9000 closing/draining issue is real. For a server endpoint that keeps accepting new connections, s2n-quic enters Draining on a remote close or stateless reset, then immediately finalizes the connection with no close/drain timeout.
Standard Requirement
Official reference: RFC 9000 Section 10.2, "Immediate Close" and Section 10.2.2, "Draining Connection State": https://www.rfc-editor.org/rfc/rfc9000#section-10.2
Key normative text:
Servers that retain an open socket for accepting new connections SHOULD NOT end the closing or draining state early.
Relevant standard meaning:
- Closing/draining exists to discard delayed or reordered packets.
- These states should normally last at least
3 * PTO.
- Early ending is allowed only when late packets cannot induce a response, for example after closing the UDP socket.
- In
Draining, the endpoint must not send packets.
Interpretation: a server that keeps its UDP socket open for new connections may stop transmitting in Draining, but should not immediately discard the old connection state before the close/drain period expires.
Relevant Source Code
Remote close and stateless reset enter Draining:
// quic/s2n-quic-transport/src/connection/connection_impl.rs:135-149
connection::Error::Closed { .. }
| connection::Error::Transport { .. }
| connection::Error::Application { .. } => ConnectionState::Draining,
connection::Error::StatelessReset { .. } => ConnectionState::Draining,
Only the local close path arms the 3-PTO retention timer:
// quic/s2n-quic-transport/src/connection/connection_impl.rs:913-920
let timeout = 3 * self.current_pto();
self.close_sender.close(packet, timeout, timestamp);
Draining is immediately marked final and receives no timeout:
// quic/s2n-quic-transport/src/connection/connection_impl.rs:2193-2209
ConnectionState::Draining | ConnectionState::Finished => {
interests.transmission = false;
interests.finalization = true;
}
if interests.finalization {
interests = ConnectionInterests { finalization: true, ..Default::default() };
} else {
interests.timeout = self.next_expiration();
}
Finalized connections are removed from the endpoint container:
// quic/s2n-quic-transport/src/connection/connection_container.rs:957-959
for connection in self.interest_lists.done_connections.take() {
self.remove_node(&connection);
}
The public server remains able to accept more connections:
// quic/s2n-quic/src/server.rs:91-94
// Poll::Ready(Some(connection)) once a new connection has been established.
// This function can be called again to try and accept new connections.
Implementation Behavior
Local immediate close follows the RFC retention guidance by creating a CloseSender timeout of 3 * current_pto(). Remote-initiated close and stateless reset do not create that timer. They move to ConnectionState::Draining, set transmission = false, then set finalization = true, which causes container removal.
Inconsistency Reason
The implementation satisfies the Draining no-transmission requirement, but it ends Draining immediately. That conflicts with RFC 9000 Section 10.2 for servers that retain an open socket for accepting new connections, because those servers should not use the early-ending exception.
Runtime Evidence
Temporary focused test added, run, then removed:
connection.close(connection::Error::closed(endpoint::Location::Remote), ...);
let interests = connection.interests();
assert_eq!(ConnectionState::Draining, connection.state);
assert!(interests.finalization);
assert!(interests.timeout.is_none());
Command run from implementions/s2n-quic:
cargo test -p s2n-quic-transport connection::connection_impl::tests::remote_draining_immediately_finalizes_without_timeout -- --exact --nocapture
Result:
running 1 test
test connection::connection_impl::tests::remote_draining_immediately_finalizes_without_timeout ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 354 filtered out
Saved logs:
reports/runtime/0172-remote-draining-finalization.stdout.log
reports/runtime/0172-remote-draining-finalization.stderr.log
reports/runtime/0172-remote-draining-finalization.exitcode.txt
Impact
This is a SHOULD NOT-level conformance issue. A late packet for the old connection can arrive after s2n-quic has already removed the draining connection state, while the server endpoint remains open for new connections.
Fix Direction
Retain remote Draining state until a close/drain deadline, normally 3 * current_pto(), without enabling packet transmission. This could be a separate draining timer or a reuse of the existing close finalization timer in a no-transmit mode.
Server draining state ends early
Summary
The earlier static source excerpt was unrelated, but the RFC 9000 closing/draining issue is real. For a server endpoint that keeps accepting new connections, s2n-quic enters
Drainingon a remote close or stateless reset, then immediately finalizes the connection with no close/drain timeout.Standard Requirement
Official reference: RFC 9000 Section 10.2, "Immediate Close" and Section 10.2.2, "Draining Connection State": https://www.rfc-editor.org/rfc/rfc9000#section-10.2
Key normative text:
Relevant standard meaning:
3 * PTO.Draining, the endpoint must not send packets.Interpretation: a server that keeps its UDP socket open for new connections may stop transmitting in
Draining, but should not immediately discard the old connection state before the close/drain period expires.Relevant Source Code
Remote close and stateless reset enter
Draining:Only the local close path arms the 3-PTO retention timer:
Drainingis immediately marked final and receives no timeout:Finalized connections are removed from the endpoint container:
The public server remains able to accept more connections:
Implementation Behavior
Local immediate close follows the RFC retention guidance by creating a
CloseSendertimeout of3 * current_pto(). Remote-initiated close and stateless reset do not create that timer. They move toConnectionState::Draining, settransmission = false, then setfinalization = true, which causes container removal.Inconsistency Reason
The implementation satisfies the
Drainingno-transmission requirement, but it endsDrainingimmediately. That conflicts with RFC 9000 Section 10.2 for servers that retain an open socket for accepting new connections, because those servers should not use the early-ending exception.Runtime Evidence
Temporary focused test added, run, then removed:
Command run from
implementions/s2n-quic:Result:
Saved logs:
reports/runtime/0172-remote-draining-finalization.stdout.logreports/runtime/0172-remote-draining-finalization.stderr.logreports/runtime/0172-remote-draining-finalization.exitcode.txtImpact
This is a
SHOULD NOT-level conformance issue. A late packet for the old connection can arrive after s2n-quic has already removed the draining connection state, while the server endpoint remains open for new connections.Fix Direction
Retain remote
Drainingstate until a close/drain deadline, normally3 * current_pto(), without enabling packet transmission. This could be a separate draining timer or a reuse of the existing close finalization timer in a no-transmit mode.