Last updated

🔗DTLS

net/dtls.nim adds DTLS 1.2 (RFC 6347) on top of the UDP backend. One bound socket multiplexes every peer; each peer's handshake state lives in a DtlsSession keyed by its network address. Handshakes run over memory BIOs (the same driver the io_uring TLS path uses), so the module is transport-agnostic: it works unchanged on kqueue/epoll/IOCP readiness loops and on the io_uring backend.

🔗Server

import powpow

let loop = newLoop()
let ctx = newServerDtlsContext("cert.pem", "key.pem")   # PEM pair, DTLS 1.2

let srv = newDtlsServer(loop, "0.0.0.0", 4433, ctx) do (sess: DtlsSession, data: openArray[byte]):
  discard sess.send(data)          # echo

loop.run()

Callbacks are also available as named parameters:

let srv = newDtlsServer(loop, "0.0.0.0", 4433, ctx,
  onData          = proc(sess: DtlsSession; data: openArray[byte]) =
    echo "got ", data.len, " plaintext bytes"
  ,
  onHandshakeDone = proc(sess: DtlsSession) =
    echo "peer up: ", sess.peerAddrStr()
  ,
  onClose         = proc(sess: DtlsSession) =
    discard
  ,
  onError         = proc(sess: DtlsSession; msg: string) =
    echo "session failed: ", msg)

onError fires for fatal session errors — handshake failures, TLS alerts, read/write errors — followed by onClose. User-initiated sess.close()produces only onClose.

A session is created only when an inbound datagram looks like a DTLS ClientHello — random garbage never allocates OpenSSL state.

🔗Client

let ctx = newClientDtlsContext(verifyPeer = false)   # self-signed test servers
let cli = connectDtls(loop, "127.0.0.1", 4433, ctx,
  onData          = proc(sess: DtlsSession; data: openArray[byte]) =
    echo "reply: ", cast[string](@data)
  ,
  onHandshakeDone = proc(sess: DtlsSession) =
    discard sess.send("hello"))

discard cli.send("later message")

connectDtls sends the ClientHello immediately; send() before the handshake completes returns -1.

Servers require a HelloVerifyRequest cookie by default before any handshake crypto is committed. The cookie is HMAC-SHA256 over the peer's address bytes using a per-context random secret (RAND_bytes) — nothing is stored, so spoofed-address floods cannot consume memory or CPU beyond one short datagram per attempt.

Disable it for closed networks where every client is known:

let ctx = newServerDtlsContext("cert.pem", "key.pem", cookieExchange = false)

OpenSSL clients handle the extra round trip transparently.

🔗Resource caps

All knobs live in DtlsConfig; pass it to newDtlsServer(..., config = cfg):

FieldDefaultMeaning
maxSessions1024Hard cap on concurrent sessions; further ClientHellos are ignored
handshakeTimeoutMs10_000Sweeper reaps sessions stuck mid-handshake
idleTimeoutMs120_000Sweeper reaps active sessions idle this long
mtu1400Link MTU hint used for record sizing
cookieExchangetrueRequire the stateless cookie round
sweepIntervalMs5_000Sweeper period (0 disables sweeping)

UDP has no FIN: without the sweeper, vanished peers would leak sessions forever. Keep idleTimeoutMs finite for anything internet-facing.

🔗Sending data — what to expect

  • sess.send(data) accepts at most ~1.35 KiB per DTLS message internally; larger payloads are split into MTU-sized messages automatically and the peer reassembles them across successive onData callbacks (ordered, stream-like). This avoids OpenSSL's 16 KiB single-message cap and keeps datagrams inside typical path MTUs so they are not truncated by receivers.
  • Datagrams are atomic but not acknowledged: if the network drops one, that slice of application data is lost. Reliability belongs above DTLS (CoAP-style retransmits, QUIC, ...) — this matches how DTLS is used in the wild.
  • While the socket send buffer is full, records stay queued and flush on a short internal retry timer; send then reports only the accepted prefix (or 0) rather than blocking.

🔗Closure semantics

sess.close() emits a best-effort close_notify for both roles while the socket is still open, so peers learn about the closure immediately instead of waiting for their idle/handshake sweeper. A peer that stops responding mid-handshake is reaped by OpenSSL's retransmission budget: when the flight retransmissions are exhausted, the session fails with onError (if wired) followed by onClose — it never lingers forever. UDP has no FIN, so the sweeper remains the backstop for silently vanished active sessions.

🔗Platform support

POSIX only (macOS, Linux, BSD) — same policy as TLS. On Windows every call raises DtlsError. Requires OpenSSL ≥ 1.1.0 linked at compile time (-lssl -lcrypto), identical to net/tls.nim.