Skip to content

Commit ec0283f

Browse files
committed
Add DTLS close state predicates
1 parent 1825196 commit ec0283f

10 files changed

Lines changed: 273 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
* Add `Dtls::is_closing()` and `Dtls::is_closed()` shutdown predicates #155
4+
35
# 0.7.0
46

57
* Omit empty DTLS 1.3 CertificateRequest certificate authorities #153

src/dtls12/client.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ impl Client {
186186
self.state.name()
187187
}
188188

189+
pub fn is_closing(&self) -> bool {
190+
self.state == State::Closed && !self.is_closed()
191+
}
192+
193+
pub fn is_closed(&self) -> bool {
194+
self.state == State::Closed
195+
&& self.local_events.is_empty()
196+
&& !self.engine.has_pending_close_output()
197+
}
198+
189199
pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
190200
match self
191201
.engine

src/dtls12/engine.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,16 @@ impl Engine {
10181018
self.close_notify_received
10191019
}
10201020

1021+
/// Whether a received close_notify still needs to be surfaced.
1022+
pub fn close_notify_pending(&self) -> bool {
1023+
self.close_notify_received && !self.close_notify_reported
1024+
}
1025+
1026+
/// Whether close-related output remains to be polled.
1027+
pub fn has_pending_close_output(&self) -> bool {
1028+
!self.queue_tx.is_empty() || self.close_notify_pending()
1029+
}
1030+
10211031
/// Discard all pending outgoing data.
10221032
///
10231033
/// RFC 5246 §7.2.1: on receiving close_notify, discard any pending writes.

src/dtls12/server.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ impl Server {
188188
self.state.name()
189189
}
190190

191+
pub fn is_closing(&self) -> bool {
192+
self.state == State::Closed && !self.is_closed()
193+
}
194+
195+
pub fn is_closed(&self) -> bool {
196+
self.state == State::Closed
197+
&& self.local_events.is_empty()
198+
&& !self.engine.has_pending_close_output()
199+
}
200+
191201
pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
192202
match self
193203
.engine

src/dtls13/client.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ impl Client {
218218
self.state.name()
219219
}
220220

221+
pub fn is_closing(&self) -> bool {
222+
(matches!(self.state, State::HalfClosedLocal | State::Closed) && !self.is_closed())
223+
|| self.engine.close_notify_pending()
224+
}
225+
226+
pub fn is_closed(&self) -> bool {
227+
self.state == State::Closed
228+
&& self.local_events.is_empty()
229+
&& !self.engine.has_pending_close_output()
230+
}
231+
221232
pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
222233
match self
223234
.engine

src/dtls13/engine.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,16 @@ impl Engine {
12701270
self.close_notify_sequence.is_some()
12711271
}
12721272

1273+
/// Whether a received close_notify still needs to be surfaced.
1274+
pub fn close_notify_pending(&self) -> bool {
1275+
self.close_notify_sequence.is_some() && !self.close_notify_reported
1276+
}
1277+
1278+
/// Whether close-related output remains to be polled.
1279+
pub fn has_pending_close_output(&self) -> bool {
1280+
!self.queue_tx.is_empty() || self.close_notify_pending()
1281+
}
1282+
12731283
/// Cancel in-flight retransmissions without clearing the transmit queue.
12741284
/// Used by close() to stop retransmitting control records while still
12751285
/// allowing the queued close_notify alert to be sent.

src/dtls13/server.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,17 @@ impl Server {
236236
self.state.name()
237237
}
238238

239+
pub fn is_closing(&self) -> bool {
240+
(matches!(self.state, State::HalfClosedLocal | State::Closed) && !self.is_closed())
241+
|| self.engine.close_notify_pending()
242+
}
243+
244+
pub fn is_closed(&self) -> bool {
245+
self.state == State::Closed
246+
&& self.local_events.is_empty()
247+
&& !self.engine.has_pending_close_output()
248+
}
249+
239250
pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
240251
// In auto-sense mode, buffer raw packets while still waiting for
241252
// the ClientHello so they can be replayed to Server12 on fallback.

src/lib.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,63 @@ impl Dtls {
553553
}
554554
}
555555

556+
/// Return true while DTLS shutdown is in progress.
557+
///
558+
/// This becomes true after local shutdown starts, or while a received
559+
/// `close_notify` still needs to be reported by
560+
/// [`poll_output`](Self::poll_output). Callers should continue polling
561+
/// until this returns false or [`is_closed`](Self::is_closed) returns true.
562+
///
563+
/// Pending auto-sense handshakes are neither closing nor closed.
564+
pub fn is_closing(&self) -> bool {
565+
let Some(inner) = self.inner.as_ref() else {
566+
return false;
567+
};
568+
569+
match inner {
570+
Inner::Client12(client) => client.is_closing(),
571+
Inner::Server12(server) => server.is_closing(),
572+
Inner::Client13(client) => client.is_closing(),
573+
Inner::Server13(server) => {
574+
if server.is_auto_mode() {
575+
false
576+
} else {
577+
server.is_closing()
578+
}
579+
}
580+
Inner::ClientPending(_) => false,
581+
}
582+
}
583+
584+
/// Return true once DTLS shutdown is terminal for the embedder.
585+
///
586+
/// This only becomes true after no application data can be sent and no
587+
/// close-related packets or events remain to be drained through
588+
/// [`poll_output`](Self::poll_output). In particular, DTLS 1.2 can enter
589+
/// its internal closed state before the queued `close_notify` packet has
590+
/// been polled out; this method remains false until that output is drained.
591+
///
592+
/// Pending auto-sense handshakes are neither closing nor closed.
593+
pub fn is_closed(&self) -> bool {
594+
let Some(inner) = self.inner.as_ref() else {
595+
return false;
596+
};
597+
598+
match inner {
599+
Inner::Client12(client) => client.is_closed(),
600+
Inner::Server12(server) => server.is_closed(),
601+
Inner::Client13(client) => client.is_closed(),
602+
Inner::Server13(server) => {
603+
if server.is_auto_mode() {
604+
false
605+
} else {
606+
server.is_closed()
607+
}
608+
}
609+
Inner::ClientPending(_) => false,
610+
}
611+
}
612+
556613
/// Return true if the instance is operating in the client role.
557614
pub fn is_active(&self) -> bool {
558615
matches!(

tests/dtls12/edge.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,19 +916,32 @@ fn dtls12_reciprocal_close_notify_and_no_further_sends() {
916916
let (mut client, mut server, now_hs) = setup_connected_12_pair(now);
917917
now = now_hs;
918918

919+
assert!(!client.is_closing());
920+
assert!(!client.is_closed());
921+
assert!(!server.is_closing());
922+
assert!(!server.is_closed());
923+
919924
// Client sends close_notify
920925
client.close().unwrap();
926+
assert!(client.is_closing());
927+
assert!(!client.is_closed());
928+
921929
now += Duration::from_millis(10);
922930
client.handle_timeout(now).expect("client timeout");
923931
let client_out = drain_outputs(&mut client);
924932
assert!(
925933
!client_out.packets.is_empty(),
926934
"Client should emit close_notify alert"
927935
);
936+
assert!(!client.is_closing());
937+
assert!(client.is_closed());
928938

929939
// Deliver to server
930940
deliver_packets(&client_out.packets, &mut server);
931941
server.handle_timeout(now).expect("server timeout");
942+
assert!(server.is_closing());
943+
assert!(!server.is_closed());
944+
932945
let server_out = drain_outputs(&mut server);
933946

934947
// Server should emit CloseNotify event
@@ -942,17 +955,24 @@ fn dtls12_reciprocal_close_notify_and_no_further_sends() {
942955
!server_out.packets.is_empty(),
943956
"Server should emit a reciprocal close_notify packet"
944957
);
958+
assert!(!server.is_closing());
959+
assert!(server.is_closed());
945960

946961
// Deliver reciprocal back to client and verify it sees CloseNotify.
947962
deliver_packets(&server_out.packets, &mut client);
948963
client
949964
.handle_timeout(now)
950965
.expect("client timeout after reciprocal");
966+
assert!(client.is_closing());
967+
assert!(!client.is_closed());
968+
951969
let client_out2 = drain_outputs(&mut client);
952970
assert!(
953971
client_out2.close_notify,
954972
"Client should emit Output::CloseNotify after receiving reciprocal close_notify"
955973
);
974+
assert!(!client.is_closing());
975+
assert!(client.is_closed());
956976

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

988+
#[test]
989+
#[cfg(feature = "rcgen")]
990+
fn dtls12_close_state_matrix() {
991+
let mut now = Instant::now();
992+
let (mut client, mut server, now_hs) = setup_connected_12_pair(now);
993+
now = now_hs;
994+
995+
assert!(!client.is_closing());
996+
assert!(!client.is_closed());
997+
assert!(!server.is_closing());
998+
assert!(!server.is_closed());
999+
1000+
client.close().unwrap();
1001+
assert!(client.is_closing(), "local close pending");
1002+
assert!(!client.is_closed(), "local close not drained");
1003+
1004+
now += Duration::from_millis(10);
1005+
client.handle_timeout(now).expect("client timeout");
1006+
let client_out = drain_outputs(&mut client);
1007+
assert!(!client_out.packets.is_empty(), "local close_notify packet");
1008+
assert!(!client.is_closing(), "local close drained");
1009+
assert!(client.is_closed(), "local close terminal");
1010+
1011+
deliver_packets(&client_out.packets, &mut server);
1012+
server.handle_timeout(now).expect("server timeout");
1013+
assert!(server.is_closing(), "remote close pending");
1014+
assert!(!server.is_closed(), "remote close not drained");
1015+
1016+
let server_out = drain_outputs(&mut server);
1017+
assert!(server_out.close_notify, "remote CloseNotify event");
1018+
assert!(
1019+
!server_out.packets.is_empty(),
1020+
"remote reciprocal close_notify packet"
1021+
);
1022+
assert!(!server.is_closing(), "remote close drained");
1023+
assert!(server.is_closed(), "remote close terminal");
1024+
}
1025+
9681026
#[test]
9691027
#[cfg(feature = "rcgen")]
9701028
fn dtls12_discard_pending_writes_on_close_notify() {

0 commit comments

Comments
 (0)