Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

* Make public errors structured and fatal-only (breaking) #134

# 0.6.2

* Split local DTLS invalid-state errors from peer-input errors #126
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ references into your provided buffer:
- `ApplicationData(&[u8])`: plaintext received from peer
- `CloseNotify`: peer sent a `close_notify` alert (graceful shutdown)

## Error handling

Every `Error` returned by the public API
([`handle_packet`][handle_packet], [`handle_timeout`][handle_timeout],
`send_application_data`, and `close`) is **fatal**: the connection is no
longer usable and must be thrown away. The engine has no recoverable
error states, so the correct response is always to drop the `Dtls`
instance — and start a fresh handshake if you still need a connection.

Transient, non‑fatal conditions inherent to running over an unreliable
transport — malformed datagrams, replayed or out‑of‑window records, and
other parser noise — are handled internally and never surface as an
`Error`. Such packets are discarded (logged at `debug!`) while the
connection keeps running. You therefore never need to distinguish
"retry" from "give up": a returned `Error` always means give up on this
connection.

## Example (Sans‑IO loop)

```rust
Expand Down Expand Up @@ -110,8 +127,8 @@ fn example_event_loop(mut dtls: Dtls) -> Result<(), dimpl::Error> {
// Deliver plaintext to application
}
Output::CloseNotify => {
// Peer initiated graceful shutdown
break;
// Peer initiated graceful shutdown — leave the event loop
return Ok(());
}
_ => {}
}
Expand Down
15 changes: 6 additions & 9 deletions src/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::dtls13::message::SignatureAlgorithmsExtension;
use crate::dtls13::message::SupportedGroupsExtension;
use crate::dtls13::message::UseSrtpExtension;
use crate::types::NamedGroup;
use crate::{Config, DtlsCertificate, Error, Output, SeededRng};
use crate::{Config, CryptoError, DtlsCertificate, Error, Output, SeededRng, TimeoutError};
// Extension type constants
const EXT_SUPPORTED_GROUPS: u16 = 0x000A;
const EXT_EC_POINT_FORMATS: u16 = 0x000B;
Expand Down Expand Up @@ -80,14 +80,11 @@ impl HybridClientHello {
let random = Random::new(&mut rng);

// Start ECDHE key exchange with the first supported group (filtered)
let group = config
.kx_groups()
.next()
.ok_or_else(|| Error::CryptoError("No supported key exchange groups".into()))?;
let group = config.kx_groups().next().ok_or(Error::CryptoError(
CryptoError::NoSupportedKeyExchangeGroups,
))?;
let kx_buf = Buf::new();
let key_exchange = group
.start_exchange(kx_buf)
.map_err(|e| Error::CryptoError(format!("Failed to start key exchange: {}", e)))?;
let key_exchange = group.start_exchange(kx_buf).map_err(Error::CryptoError)?;

// ---- Build the ClientHello body ----
let mut ch_body = Buf::new();
Expand Down Expand Up @@ -304,7 +301,7 @@ impl ClientPending {
if let Some(deadline) = self.retransmit_at {
if now >= deadline {
if self.retransmit_count >= self.config.flight_retries() {
return Err(Error::Timeout("hybrid ClientHello"));
return Err(Error::Timeout(TimeoutError::HybridClientHello));
}
self.retransmit_count += 1;
self.needs_send = true;
Expand Down
76 changes: 25 additions & 51 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use std::panic::{RefUnwindSafe, UnwindSafe};
use std::sync::Arc;
use std::time::Duration;

use crate::Error;
use crate::crypto::{CryptoProvider, SupportedDtls12CipherSuite};
use crate::crypto::{SupportedDtls13CipherSuite, SupportedKxGroup};
use crate::dtls12::message::Dtls12CipherSuite;
use crate::types::{Dtls13CipherSuite, NamedGroup};
use crate::{ConfigError, Error};

/// Callback for resolving PSK identities to shared secrets.
///
Expand Down Expand Up @@ -507,17 +507,15 @@ impl ConfigBuilder {

// Validate MTU: must be large enough for DTLS record + handshake headers
if self.mtu < 64 {
return Err(Error::ConfigError(format!(
"MTU {} is too small (minimum 64)",
self.mtu
)));
return Err(Error::ConfigError(ConfigError::MtuTooSmall {
mtu: self.mtu as u16,
minimum: 64,
}));
}

// Validate aead_encryption_limit: must be at least 1
if self.aead_encryption_limit == 0 {
return Err(Error::ConfigError(
"aead_encryption_limit must be at least 1".to_string(),
));
return Err(Error::ConfigError(ConfigError::AeadEncryptionLimitTooSmall));
}

// Validate cipher suite filters: at least one version must have suites.
Expand All @@ -544,9 +542,7 @@ impl ConfigBuilder {
};
if dtls12_count + dtls13_count == 0 {
return Err(Error::ConfigError(
"No cipher suites remain after filtering. \
At least one DTLS 1.2 or DTLS 1.3 cipher suite must be available."
.to_string(),
ConfigError::NoCipherSuitesAfterFiltering,
));
}

Expand All @@ -557,10 +553,7 @@ impl ConfigBuilder {
// by CryptoContext::is_cipher_suite_compatible.
if has_psk && !dtls12_suites.iter().any(|cs| cs.suite().is_psk()) {
return Err(Error::ConfigError(
"PSK is configured but no PSK cipher suite remains after filtering \
DTLS 1.2 suites. Include at least one PSK suite in \
dtls12_cipher_suites."
.to_string(),
ConfigError::PskConfiguredWithoutPskCipherSuite,
));
}

Expand All @@ -585,9 +578,7 @@ impl ConfigBuilder {
.count();
if dtls12_kx_count == 0 {
return Err(Error::ConfigError(
"DTLS 1.2 cipher suites are enabled but no compatible key exchange \
groups remain after filtering."
.to_string(),
ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering,
));
}
}
Expand All @@ -598,9 +589,7 @@ impl ConfigBuilder {
.count();
if kx_count == 0 {
return Err(Error::ConfigError(
"DTLS 1.3 cipher suites are enabled but no key exchange groups \
remain after filtering."
.to_string(),
ConfigError::NoDtls13KeyExchangeGroupsAfterFiltering,
));
}
}
Expand Down Expand Up @@ -707,8 +696,9 @@ mod tests {
#[test]
fn rejects_zero_mtu() {
match Config::builder().mtu(0).build() {
Err(Error::ConfigError(msg)) => {
assert!(msg.contains("MTU"), "error should mention MTU: {msg}")
Err(Error::ConfigError(ConfigError::MtuTooSmall { mtu, minimum })) => {
assert_eq!(mtu, 0);
assert_eq!(minimum, 64);
}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error for MTU=0"),
Expand All @@ -718,8 +708,9 @@ mod tests {
#[test]
fn rejects_small_mtu() {
match Config::builder().mtu(32).build() {
Err(Error::ConfigError(msg)) => {
assert!(msg.contains("MTU"), "error should mention MTU: {msg}")
Err(Error::ConfigError(ConfigError::MtuTooSmall { mtu, minimum })) => {
assert_eq!(mtu, 32);
assert_eq!(minimum, 64);
}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error for MTU=32"),
Expand All @@ -737,10 +728,7 @@ mod tests {
#[test]
fn rejects_zero_aead_limit() {
match Config::builder().aead_encryption_limit(0).build() {
Err(Error::ConfigError(msg)) => assert!(
msg.contains("aead_encryption_limit"),
"error should mention aead_encryption_limit: {msg}"
),
Err(Error::ConfigError(ConfigError::AeadEncryptionLimitTooSmall)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error for aead_encryption_limit=0"),
}
Expand Down Expand Up @@ -811,12 +799,7 @@ mod tests {
.dtls13_cipher_suites(&[])
.build()
{
Err(Error::ConfigError(msg)) => {
assert!(
msg.contains("No cipher suites"),
"error should mention cipher suites: {msg}"
)
}
Err(Error::ConfigError(ConfigError::NoCipherSuitesAfterFiltering)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error when both versions are empty"),
}
Expand All @@ -825,12 +808,10 @@ mod tests {
#[test]
fn empty_kx_groups_filter_rejected() {
match Config::builder().kx_groups(&[]).build() {
Err(Error::ConfigError(msg)) => {
assert!(
msg.contains("key exchange"),
"error should mention key exchange: {msg}"
)
}
Err(Error::ConfigError(
ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering
| ConfigError::NoDtls13KeyExchangeGroupsAfterFiltering,
)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error for empty kx groups"),
}
Expand Down Expand Up @@ -928,9 +909,7 @@ mod tests {
.dtls13_cipher_suites(&[])
.build();
match result {
Err(Error::ConfigError(msg)) => {
assert!(msg.contains("PSK"), "error should mention PSK: {msg}")
}
Err(Error::ConfigError(ConfigError::PskConfiguredWithoutPskCipherSuite)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!("expected error for PSK config with only non-PSK suites"),
}
Expand All @@ -954,9 +933,7 @@ mod tests {
.dtls12_cipher_suites(&[Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256])
.build();
match result {
Err(Error::ConfigError(msg)) => {
assert!(msg.contains("PSK"), "error should mention PSK: {msg}")
}
Err(Error::ConfigError(ConfigError::PskConfiguredWithoutPskCipherSuite)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!(
"expected error for PSK config with only non-PSK DTLS 1.2 suites, \
Expand Down Expand Up @@ -988,10 +965,7 @@ mod tests {
.kx_groups(&[])
.build();
match result {
Err(Error::ConfigError(msg)) => assert!(
msg.contains("key exchange"),
"error should mention key exchange groups: {msg}"
),
Err(Error::ConfigError(ConfigError::NoDtls12KeyExchangeGroupsAfterFiltering)) => {}
Err(other) => panic!("expected ConfigError, got: {other:?}"),
Ok(_) => panic!(
"expected error when a cert-based DTLS 1.2 suite is enabled \
Expand Down
Loading
Loading