Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

* Add `Dtls::is_closing()` and `Dtls::is_closed()` shutdown predicates #155

# 0.7.0

* Omit empty DTLS 1.3 CertificateRequest certificate authorities #153
Expand Down
10 changes: 10 additions & 0 deletions src/dtls12/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ impl Client {
self.state.name()
}

pub fn is_closing(&self) -> bool {
self.state == State::Closed && !self.is_closed()
}

pub fn is_closed(&self) -> bool {
self.state == State::Closed
&& self.local_events.is_empty()
&& !self.engine.has_pending_close_output()
}

pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
match self
.engine
Expand Down
10 changes: 10 additions & 0 deletions src/dtls12/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,16 @@ impl Engine {
self.close_notify_received
}

/// Whether a received close_notify still needs to be surfaced.
pub fn close_notify_pending(&self) -> bool {
self.close_notify_received && !self.close_notify_reported
}

/// Whether close-related output remains to be polled.
pub fn has_pending_close_output(&self) -> bool {
!self.queue_tx.is_empty() || self.close_notify_pending()
}

/// Discard all pending outgoing data.
///
/// RFC 5246 §7.2.1: on receiving close_notify, discard any pending writes.
Expand Down
10 changes: 10 additions & 0 deletions src/dtls12/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ impl Server {
self.state.name()
}

pub fn is_closing(&self) -> bool {
self.state == State::Closed && !self.is_closed()
}

pub fn is_closed(&self) -> bool {
self.state == State::Closed
&& self.local_events.is_empty()
&& !self.engine.has_pending_close_output()
}

pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
match self
.engine
Expand Down
11 changes: 11 additions & 0 deletions src/dtls13/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ impl Client {
self.state.name()
}

pub fn is_closing(&self) -> bool {
(matches!(self.state, State::HalfClosedLocal | State::Closed) && !self.is_closed())
|| self.engine.close_notify_pending()
}

pub fn is_closed(&self) -> bool {
self.state == State::Closed
&& self.local_events.is_empty()
&& !self.engine.has_pending_close_output()
}

pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
match self
.engine
Expand Down
10 changes: 10 additions & 0 deletions src/dtls13/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,16 @@ impl Engine {
self.close_notify_sequence.is_some()
}

/// Whether a received close_notify still needs to be surfaced.
pub fn close_notify_pending(&self) -> bool {
self.close_notify_sequence.is_some() && !self.close_notify_reported
}

/// Whether close-related output remains to be polled.
pub fn has_pending_close_output(&self) -> bool {
!self.queue_tx.is_empty() || self.close_notify_pending()
}

/// Cancel in-flight retransmissions without clearing the transmit queue.
/// Used by close() to stop retransmitting control records while still
/// allowing the queued close_notify alert to be sent.
Expand Down
11 changes: 11 additions & 0 deletions src/dtls13/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ impl Server {
self.state.name()
}

pub fn is_closing(&self) -> bool {
(matches!(self.state, State::HalfClosedLocal | State::Closed) && !self.is_closed())
|| self.engine.close_notify_pending()
}

pub fn is_closed(&self) -> bool {
self.state == State::Closed
&& self.local_events.is_empty()
&& !self.engine.has_pending_close_output()
}

pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
// In auto-sense mode, buffer raw packets while still waiting for
// the ClientHello so they can be replayed to Server12 on fallback.
Expand Down
57 changes: 57 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,63 @@ impl Dtls {
}
}

/// Return true while DTLS shutdown is in progress.
///
/// This becomes true after local shutdown starts, or while a received
/// `close_notify` still needs to be reported by
/// [`poll_output`](Self::poll_output). Callers should continue polling
/// until this returns false or [`is_closed`](Self::is_closed) returns true.
///
/// Pending auto-sense handshakes are neither closing nor closed.
pub fn is_closing(&self) -> bool {
let Some(inner) = self.inner.as_ref() else {
return false;
};

match inner {
Inner::Client12(client) => client.is_closing(),
Inner::Server12(server) => server.is_closing(),
Inner::Client13(client) => client.is_closing(),
Inner::Server13(server) => {
if server.is_auto_mode() {
false
} else {
server.is_closing()
}
}
Inner::ClientPending(_) => false,
}
}

/// Return true once DTLS shutdown is terminal for the embedder.
///
/// This only becomes true after no application data can be sent and no
/// close-related packets or events remain to be drained through
/// [`poll_output`](Self::poll_output). In particular, DTLS 1.2 can enter
/// its internal closed state before the queued `close_notify` packet has
/// been polled out; this method remains false until that output is drained.
///
/// Pending auto-sense handshakes are neither closing nor closed.
pub fn is_closed(&self) -> bool {
let Some(inner) = self.inner.as_ref() else {
return false;
};

match inner {
Inner::Client12(client) => client.is_closed(),
Inner::Server12(server) => server.is_closed(),
Inner::Client13(client) => client.is_closed(),
Inner::Server13(server) => {
if server.is_auto_mode() {
false
} else {
server.is_closed()
}
}
Inner::ClientPending(_) => false,
}
}

/// Return true if the instance is operating in the client role.
pub fn is_active(&self) -> bool {
matches!(
Expand Down
58 changes: 58 additions & 0 deletions tests/dtls12/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,19 +916,32 @@ fn dtls12_reciprocal_close_notify_and_no_further_sends() {
let (mut client, mut server, now_hs) = setup_connected_12_pair(now);
now = now_hs;

assert!(!client.is_closing());
assert!(!client.is_closed());
assert!(!server.is_closing());
assert!(!server.is_closed());

// Client sends close_notify
client.close().unwrap();
assert!(client.is_closing());
assert!(!client.is_closed());

now += Duration::from_millis(10);
client.handle_timeout(now).expect("client timeout");
let client_out = drain_outputs(&mut client);
assert!(
!client_out.packets.is_empty(),
"Client should emit close_notify alert"
);
assert!(!client.is_closing());
assert!(client.is_closed());

// Deliver to server
deliver_packets(&client_out.packets, &mut server);
server.handle_timeout(now).expect("server timeout");
assert!(server.is_closing());
assert!(!server.is_closed());

let server_out = drain_outputs(&mut server);

// Server should emit CloseNotify event
Expand All @@ -942,17 +955,24 @@ fn dtls12_reciprocal_close_notify_and_no_further_sends() {
!server_out.packets.is_empty(),
"Server should emit a reciprocal close_notify packet"
);
assert!(!server.is_closing());
assert!(server.is_closed());

// Deliver reciprocal back to client and verify it sees CloseNotify.
deliver_packets(&server_out.packets, &mut client);
client
.handle_timeout(now)
.expect("client timeout after reciprocal");
assert!(client.is_closing());
assert!(!client.is_closed());

let client_out2 = drain_outputs(&mut client);
assert!(
client_out2.close_notify,
"Client should emit Output::CloseNotify after receiving reciprocal close_notify"
);
assert!(!client.is_closing());
assert!(client.is_closed());

// No half-close in DTLS 1.2: both sides must reject further sends.
assert!(
Expand All @@ -965,6 +985,44 @@ fn dtls12_reciprocal_close_notify_and_no_further_sends() {
);
}

#[test]
#[cfg(feature = "rcgen")]
fn dtls12_close_state_matrix() {
let mut now = Instant::now();
let (mut client, mut server, now_hs) = setup_connected_12_pair(now);
now = now_hs;

assert!(!client.is_closing());
assert!(!client.is_closed());
assert!(!server.is_closing());
assert!(!server.is_closed());

client.close().unwrap();
assert!(client.is_closing(), "local close pending");
assert!(!client.is_closed(), "local close not drained");

now += Duration::from_millis(10);
client.handle_timeout(now).expect("client timeout");
let client_out = drain_outputs(&mut client);
assert!(!client_out.packets.is_empty(), "local close_notify packet");
assert!(!client.is_closing(), "local close drained");
assert!(client.is_closed(), "local close terminal");

deliver_packets(&client_out.packets, &mut server);
server.handle_timeout(now).expect("server timeout");
assert!(server.is_closing(), "remote close pending");
assert!(!server.is_closed(), "remote close not drained");

let server_out = drain_outputs(&mut server);
assert!(server_out.close_notify, "remote CloseNotify event");
assert!(
!server_out.packets.is_empty(),
"remote reciprocal close_notify packet"
);
assert!(!server.is_closing(), "remote close drained");
assert!(server.is_closed(), "remote close terminal");
}

#[test]
#[cfg(feature = "rcgen")]
fn dtls12_discard_pending_writes_on_close_notify() {
Expand Down
Loading
Loading