|
| 1 | +# DTLS |
| 2 | + |
| 3 | +<!-- YAML |
| 4 | +added: REPLACEME |
| 5 | +--> |
| 6 | + |
| 7 | +<!-- introduced_in=REPLACEME --> |
| 8 | + |
| 9 | +> Stability: 1 - Experimental |
| 10 | +
|
| 11 | +<!-- source_link=lib/dtls.js --> |
| 12 | + |
| 13 | +The `node:dtls` module provides an implementation of the Datagram Transport |
| 14 | +Layer Security (DTLS) protocol over UDP. DTLS provides TLS-equivalent |
| 15 | +security guarantees for datagram-based communication, including |
| 16 | +confidentiality, integrity, and authentication. |
| 17 | + |
| 18 | +To use this module, it must be enabled at build time with the |
| 19 | +`--experimental-dtls` configure flag and at runtime with the |
| 20 | +`--experimental-dtls` CLI flag. |
| 21 | + |
| 22 | +```bash |
| 23 | +node --experimental-dtls app.mjs |
| 24 | +``` |
| 25 | + |
| 26 | +```mjs |
| 27 | +import { listen, connect } from 'node:dtls'; |
| 28 | +``` |
| 29 | + |
| 30 | +```cjs |
| 31 | +const { listen, connect } = require('node:dtls'); |
| 32 | +``` |
| 33 | + |
| 34 | +## DTLS vs TLS |
| 35 | + |
| 36 | +DTLS is designed for UDP transport and differs from TLS in several key ways: |
| 37 | + |
| 38 | +* No stream guarantees: Messages may arrive out of order or be lost. |
| 39 | + DTLS preserves datagram semantics. |
| 40 | +* One socket, many peers: A single UDP socket can serve multiple DTLS |
| 41 | + sessions. The `DTLSEndpoint` manages this multiplexing. |
| 42 | +* Cookie exchange: DTLS servers use a stateless cookie mechanism |
| 43 | + (HelloVerifyRequest) to prevent denial-of-service amplification attacks. |
| 44 | +* Retransmission: DTLS handles handshake retransmission internally since |
| 45 | + UDP does not guarantee delivery. |
| 46 | + |
| 47 | +## `dtls.listen(callback, options)` |
| 48 | + |
| 49 | +<!-- YAML |
| 50 | +added: REPLACEME |
| 51 | +--> |
| 52 | + |
| 53 | +* `callback` {Function} Called for each new DTLS session accepted by the |
| 54 | + server. |
| 55 | + * `session` {DTLSSession} The new session. |
| 56 | +* `options` {Object} |
| 57 | + * `cert` {string|Buffer} Server certificate in PEM format. **Required.** |
| 58 | + * `key` {string|Buffer} Server private key in PEM format. **Required.** |
| 59 | + * `port` {number} Port to bind to. **Required.** |
| 60 | + * `host` {string} Address to bind to. **Default:** `'0.0.0.0'`. |
| 61 | + * `ca` {string|Buffer|string\[]|Buffer\[]} CA certificates in PEM format. |
| 62 | + * `ciphers` {string} OpenSSL cipher list string. |
| 63 | + * `alpn` {string\[]|Buffer} ALPN protocol names. |
| 64 | + * `srtp` {string} Colon-separated SRTP protection profile names |
| 65 | + (e.g., `'SRTP_AES128_CM_SHA1_80:SRTP_AEAD_AES_128_GCM'`). |
| 66 | + * `requestCert` {boolean} Request client certificate. **Default:** `false`. |
| 67 | + * `mtu` {number} Maximum transmission unit for DTLS records. |
| 68 | + **Default:** `1200`. |
| 69 | +* Returns: {DTLSEndpoint} |
| 70 | + |
| 71 | +Creates a DTLS server bound to the specified address and port. The server |
| 72 | +uses automatic HMAC-based cookie exchange for DoS protection. |
| 73 | + |
| 74 | +```mjs |
| 75 | +import { listen } from 'node:dtls'; |
| 76 | +import { readFileSync } from 'node:fs'; |
| 77 | + |
| 78 | +const endpoint = listen((session) => { |
| 79 | + session.onmessage = (data) => { |
| 80 | + console.log('Received:', data.toString()); |
| 81 | + session.send('pong'); |
| 82 | + }; |
| 83 | + |
| 84 | + session.onhandshake = (protocol) => { |
| 85 | + console.log('Handshake complete:', protocol); |
| 86 | + }; |
| 87 | +}, { |
| 88 | + cert: readFileSync('server-cert.pem'), |
| 89 | + key: readFileSync('server-key.pem'), |
| 90 | + port: 4433, |
| 91 | +}); |
| 92 | + |
| 93 | +console.log('DTLS server listening on', endpoint.address); |
| 94 | +``` |
| 95 | + |
| 96 | +## `dtls.connect(host, port[, options])` |
| 97 | + |
| 98 | +<!-- YAML |
| 99 | +added: REPLACEME |
| 100 | +--> |
| 101 | + |
| 102 | +* `host` {string} Remote host to connect to. |
| 103 | +* `port` {number} Remote port to connect to. |
| 104 | +* `options` {Object} |
| 105 | + * `ca` {string|Buffer|string\[]|Buffer\[]} CA certificates in PEM format. |
| 106 | + * `cert` {string|Buffer} Client certificate in PEM format. |
| 107 | + * `key` {string|Buffer} Client private key in PEM format. |
| 108 | + * `rejectUnauthorized` {boolean} Reject connections with unverifiable |
| 109 | + certificates. **Default:** `true`. |
| 110 | + * `bindHost` {string} Local bind address. **Default:** `'0.0.0.0'`. |
| 111 | + * `bindPort` {number} Local bind port. **Default:** `0` (ephemeral). |
| 112 | + * `alpn` {string\[]|Buffer} ALPN protocol names. |
| 113 | + * `srtp` {string} SRTP protection profile names. |
| 114 | + * `mtu` {number} Maximum transmission unit. **Default:** `1200`. |
| 115 | +* Returns: {DTLSSession} |
| 116 | + |
| 117 | +Connects to a DTLS server. Returns a `DTLSSession` whose `opened` property |
| 118 | +is a `Promise` that resolves when the handshake completes. |
| 119 | + |
| 120 | +```mjs |
| 121 | +import { connect } from 'node:dtls'; |
| 122 | +import { readFileSync } from 'node:fs'; |
| 123 | + |
| 124 | +const session = connect('localhost', 4433, { |
| 125 | + ca: [readFileSync('ca-cert.pem')], |
| 126 | +}); |
| 127 | + |
| 128 | +await session.opened; |
| 129 | +session.send('hello'); |
| 130 | + |
| 131 | +session.onmessage = (data) => { |
| 132 | + console.log('Received:', data.toString()); |
| 133 | +}; |
| 134 | +``` |
| 135 | + |
| 136 | +## Class: `DTLSEndpoint` |
| 137 | + |
| 138 | +<!-- YAML |
| 139 | +added: REPLACEME |
| 140 | +--> |
| 141 | + |
| 142 | +Manages a UDP socket and multiplexes DTLS sessions. |
| 143 | + |
| 144 | +### `endpoint.address` |
| 145 | + |
| 146 | +* Returns: {Object} `{ address, family, port }` |
| 147 | + |
| 148 | +The local address the endpoint is bound to. |
| 149 | + |
| 150 | +### `endpoint.state` |
| 151 | + |
| 152 | +* Returns: {DTLSEndpointState} |
| 153 | + |
| 154 | +Shared state object with properties: |
| 155 | + |
| 156 | +* `bound` {boolean} |
| 157 | +* `listening` {boolean} |
| 158 | +* `closing` {boolean} |
| 159 | +* `destroyed` {boolean} |
| 160 | +* `sessionCount` {number} |
| 161 | +* `busy` {boolean} |
| 162 | + |
| 163 | +### `endpoint.busy` |
| 164 | + |
| 165 | +* {boolean} |
| 166 | + |
| 167 | +When `true`, the endpoint rejects new incoming connections. Can be set |
| 168 | +to implement backpressure. |
| 169 | + |
| 170 | +### `endpoint.close()` |
| 171 | + |
| 172 | +* Returns: {Promise} Resolves when the endpoint is fully closed. |
| 173 | + |
| 174 | +Gracefully closes the endpoint. All active sessions are closed with |
| 175 | +`close_notify` alerts before the UDP socket is released. |
| 176 | + |
| 177 | +### `endpoint.destroy([error])` |
| 178 | + |
| 179 | +Immediately destroys the endpoint without sending `close_notify` alerts. |
| 180 | + |
| 181 | +### `endpoint.closed` |
| 182 | + |
| 183 | +* {Promise} Resolves when the endpoint has fully closed. |
| 184 | + |
| 185 | +### `endpoint[Symbol.asyncDispose]()` |
| 186 | + |
| 187 | +Equivalent to calling `endpoint.close()`. |
| 188 | + |
| 189 | +## Class: `DTLSSession` |
| 190 | + |
| 191 | +<!-- YAML |
| 192 | +added: REPLACEME |
| 193 | +--> |
| 194 | + |
| 195 | +Represents a DTLS association with a single remote peer. |
| 196 | + |
| 197 | +### `session.send(data)` |
| 198 | + |
| 199 | +* `data` {string|Buffer} The data to send. |
| 200 | +* Returns: {number} The number of bytes written to the DTLS layer. |
| 201 | + |
| 202 | +Send application data to the peer. The data is encrypted by DTLS before |
| 203 | +being sent over UDP. Can only be called after the handshake completes |
| 204 | +(`session.opened` has resolved). |
| 205 | + |
| 206 | +### `session.close()` |
| 207 | + |
| 208 | +* Returns: {Promise} Resolves when the session is closed. |
| 209 | + |
| 210 | +Initiates a graceful DTLS shutdown by sending a `close_notify` alert. |
| 211 | + |
| 212 | +### `session.destroy([error])` |
| 213 | + |
| 214 | +Immediately destroys the session without sending `close_notify`. |
| 215 | + |
| 216 | +### `session.opened` |
| 217 | + |
| 218 | +* {Promise} Resolves with `{ protocol }` when the DTLS handshake completes. |
| 219 | + |
| 220 | +### `session.closed` |
| 221 | + |
| 222 | +* {Promise} Resolves when the session is fully closed. |
| 223 | + |
| 224 | +### `session.remoteAddress` |
| 225 | + |
| 226 | +* Returns: {Object} `{ address, family, port }` |
| 227 | + |
| 228 | +### `session.protocol` |
| 229 | + |
| 230 | +* Returns: {string} The negotiated DTLS protocol version |
| 231 | + (e.g., `'DTLSv1.2'`). |
| 232 | + |
| 233 | +### `session.cipher` |
| 234 | + |
| 235 | +* Returns: {Object} `{ name, standardName, version }` |
| 236 | + |
| 237 | +### `session.peerCertificate` |
| 238 | + |
| 239 | +* Returns: {string|undefined} The peer's certificate in PEM format. |
| 240 | + |
| 241 | +### `session.alpnProtocol` |
| 242 | + |
| 243 | +* Returns: {string|undefined} The negotiated ALPN protocol. |
| 244 | + |
| 245 | +### `session.srtpProfile` |
| 246 | + |
| 247 | +* Returns: {string|undefined} The negotiated SRTP protection profile name. |
| 248 | + |
| 249 | +### `session.exportKeyingMaterial(length, label[, context])` |
| 250 | + |
| 251 | +* `length` {number} Number of bytes to export. |
| 252 | +* `label` {string} The label for the exported keying material. |
| 253 | +* `context` {Buffer} Optional context value. |
| 254 | +* Returns: {Buffer} |
| 255 | + |
| 256 | +Exports keying material from the DTLS session, as defined in |
| 257 | +[RFC 5705][]. This is commonly used with DTLS-SRTP to derive |
| 258 | +encryption keys for media streams. |
| 259 | + |
| 260 | +### Callback properties |
| 261 | + |
| 262 | +#### `session.onmessage` |
| 263 | + |
| 264 | +* {Function} |
| 265 | + * `data` {Buffer} |
| 266 | + |
| 267 | +Set to receive application data from the peer. |
| 268 | + |
| 269 | +#### `session.onerror` |
| 270 | + |
| 271 | +* {Function} |
| 272 | + * `error` {Error} |
| 273 | + |
| 274 | +Set to receive error notifications. |
| 275 | + |
| 276 | +#### `session.onhandshake` |
| 277 | + |
| 278 | +* {Function} |
| 279 | + * `protocol` {string} |
| 280 | + |
| 281 | +Set to receive handshake completion notifications. |
| 282 | + |
| 283 | +#### `session.onkeylog` |
| 284 | + |
| 285 | +* {Function} |
| 286 | + * `line` {string} |
| 287 | + |
| 288 | +Set to receive TLS key log lines (for debugging with Wireshark). |
| 289 | + |
| 290 | +### `session[Symbol.asyncDispose]()` |
| 291 | + |
| 292 | +Equivalent to calling `session.close()`. |
| 293 | + |
| 294 | +## DTLS-SRTP example |
| 295 | + |
| 296 | +DTLS-SRTP is used by WebRTC for media encryption. The DTLS handshake |
| 297 | +negotiates the SRTP protection profile and provides keying material. |
| 298 | + |
| 299 | +```mjs |
| 300 | +import { listen, connect } from 'node:dtls'; |
| 301 | +import { readFileSync } from 'node:fs'; |
| 302 | + |
| 303 | +// Server with SRTP |
| 304 | +const server = listen((session) => { |
| 305 | + session.onhandshake = () => { |
| 306 | + console.log('SRTP profile:', session.srtpProfile); |
| 307 | + const keys = session.exportKeyingMaterial( |
| 308 | + 60, |
| 309 | + 'EXTRACTOR-dtls_srtp', |
| 310 | + ); |
| 311 | + console.log('SRTP keying material:', keys); |
| 312 | + }; |
| 313 | +}, { |
| 314 | + cert: readFileSync('server-cert.pem'), |
| 315 | + key: readFileSync('server-key.pem'), |
| 316 | + port: 5004, |
| 317 | + srtp: 'SRTP_AES128_CM_SHA1_80:SRTP_AEAD_AES_128_GCM', |
| 318 | +}); |
| 319 | + |
| 320 | +// Client with SRTP |
| 321 | +const session = connect('localhost', 5004, { |
| 322 | + rejectUnauthorized: false, |
| 323 | + srtp: 'SRTP_AEAD_AES_128_GCM:SRTP_AES128_CM_SHA1_80', |
| 324 | +}); |
| 325 | + |
| 326 | +await session.opened; |
| 327 | +console.log('Negotiated SRTP:', session.srtpProfile); |
| 328 | +const keys = session.exportKeyingMaterial(60, 'EXTRACTOR-dtls_srtp'); |
| 329 | +``` |
| 330 | + |
| 331 | +## MTU considerations |
| 332 | + |
| 333 | +Since libuv does not currently support path MTU discovery, the DTLS module |
| 334 | +uses a conservative default MTU of 1200 bytes. This value works across |
| 335 | +virtually all network paths but may be suboptimal for local networks. |
| 336 | + |
| 337 | +The MTU can be configured via the `mtu` option: |
| 338 | + |
| 339 | +```mjs |
| 340 | +// For a local network where you know the path MTU |
| 341 | +const endpoint = listen(callback, { |
| 342 | + // ... |
| 343 | + mtu: 1400, |
| 344 | +}); |
| 345 | +``` |
| 346 | + |
| 347 | +The minimum allowed MTU is 256 bytes. The maximum is 65535. |
| 348 | + |
| 349 | +[RFC 5705]: https://www.rfc-editor.org/rfc/rfc5705 |
0 commit comments