Skip to content

feat: Implement graceful shutdown#91

Open
HMBSbige wants to merge 2 commits intoalgesten:mainfrom
HMBSbige:feature/close-notify
Open

feat: Implement graceful shutdown#91
HMBSbige wants to merge 2 commits intoalgesten:mainfrom
HMBSbige:feature/close-notify

Conversation

@HMBSbige
Copy link
Contributor

@HMBSbige HMBSbige commented Mar 10, 2026

Closes #72

Code cleanup (commit 1)

  • Remove unnecessary &mut on OsRng in rust_crypto kx_group
  • Replace if let Err(_) with idiomatic .is_err() in auto cross_matrix tests
  • Inline self.last_now and remove extra blank lines in Server::poll_output (dtls12)
  • Move helper functions before #[cfg(test)] per file ordering convention (dtls12 server)

close_notify implementation (commit 2)

  • Add Dtls::close() API for graceful connection shutdown via close_notify alert
  • Add Output::CloseNotify variant to signal peer-initiated closure to the application
  • Add Error::ConnectionClosed for send attempts on closed connections
  • DTLS 1.2: receiver auto-replies with reciprocal close_notify per RFC 5246 §7.2.1
  • DTLS 1.3: sender enters HalfClosedLocal (write-closed, read-open) per RFC 8446 §6.1; receiver filters records by sequence number per RFC 9147 §5.10
  • Engine handles record-layer concerns (detecting alerts, filtering records); client/server handles connection state and Output::CloseNotify emission
  • Extract test helpers (complete_dtls12_handshake, setup_connected_12_pair, complete_dtls13_handshake, setup_connected_13_pair) from edge.rs into common.rs and consolidate imports
  • Update README with Output::CloseNotify documentation and example

@HMBSbige HMBSbige marked this pull request as draft March 10, 2026 10:16
@HMBSbige HMBSbige force-pushed the feature/close-notify branch from 7d7f3e8 to 77bf722 Compare March 10, 2026 10:45
@HMBSbige HMBSbige marked this pull request as ready for review March 10, 2026 10:45
Copy link
Owner

@algesten algesten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking at this!

A couple of comments and questions. I also see we have some unrelated code cleanup, like moving code around and removing white space, and a change to &mut OsRng. We can keep that in PR, but you need to force push and make "clean" commits I can rebase at the end.

@HMBSbige HMBSbige force-pushed the feature/close-notify branch from 77bf722 to 9820a7a Compare March 11, 2026 03:03
- Remove unnecessary `&mut` on `OsRng` (rust_crypto kx_group)
- Replace `if let Err(_)` with idiomatic `.is_err()` (auto cross_matrix)
- Inline `self.last_now` and remove extra blank lines in Server::poll_output
- Move helper functions before `#[cfg(test)]` per file ordering convention
@HMBSbige HMBSbige force-pushed the feature/close-notify branch from 9820a7a to ebb9d22 Compare March 11, 2026 03:09
Copy link
Owner

@algesten algesten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Much improved with the client/server having the state logic.

This read through I got stuck on insert_incoming and the amount of logic being done there. I see dtls13 has set some pattern that I don't quite feel is right.

// Handle Alert records individually; collect the rest for queuing.
// A single UDP datagram can contain mixed record types, so we process
// each record individually without discarding siblings.
let mut remaining = ArrayVec::new();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal throughout dimpl is to avoid extra buffering like this if we can. I.e. here we "pre-process" all the incoming records into a new temporary field.

I'm also in two minds whether this really belongs in a function labelled "insert_incoming" (because it's more like "preprocess").

The code almost feels more related to the decryption pass we do in incoming.rs via the RecordDecrypt trait.

What do you think? Would it sit more naturally in incoming.rs?

Some(Incoming {
records: Box::new(Records { records }),
})
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this doesn't feel great, recreating the incoming like this. Related to previous comment.

// each record individually without discarding siblings.
let mut non_ack_records = ArrayVec::new();
for record in incoming.into_records() {
// Per RFC 9147 §5.10: discard any record whose epoch/sequence
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now where this into_records/from_records pattern comes from. I wasn't aware we had this situation. Feels like we should maybe tidy this up as well and not do this in "insert incoming"

@HMBSbige HMBSbige force-pushed the feature/close-notify branch from ebb9d22 to 82fbb62 Compare March 13, 2026 02:06
Add close_notify support for both DTLS 1.2 and DTLS 1.3, implementing
graceful connection shutdown per RFC 5246 §7.2.1 and RFC 9147 §5.10.

DTLS 1.2: close() sends close_notify and transitions to Closed state.
Receiving close_notify triggers a reciprocal alert and discards pending
writes. No half-close support (full close only).

DTLS 1.3: close() sends close_notify and enters HalfClosedLocal state
where the read half remains open. Receiving close_notify while
half-closed transitions to Closed. Incoming KeyUpdate messages are
still processed (recv keys updated) but no outgoing records are sent.

Engine tracks close_notify at the record layer (filtering app data
after the alert sequence), while client/server handle connection state
and Output::CloseNotify emission.

Error::ConnectionClosed replaces SecurityError("connection closed") for
send_application_data on closed connections.
@HMBSbige HMBSbige force-pushed the feature/close-notify branch from 82fbb62 to 563ed6a Compare March 13, 2026 02:08
Copy link
Contributor Author

@HMBSbige HMBSbige left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted the per-record processing out of insert_incoming into dedicated methods:

  • dtls12: extract_alerts() — epoch-aware alert handling + post-close_notify filtering
  • dtls13: extract_control_records() — ACK/Alert/CCS + close_notify sequence filtering

insert_incoming is back to capacity-check + dispatch in both versions.

The into_records/from_records decompose-recompose pattern is still there though. I considered moving it into incoming.rs alongside RecordDecrypt, but alert processing needs engine state (peer_encryption_enabled, buffers_free, close_notify_received/close_notify_sequence) which doesn't fit the stateless parsing layer.

If you have a different direction in mind for eliminating the pattern, I'm happy to follow your lead.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add close_notify graceful shutdown API

2 participants