Skip to content

Commit 8ecdb17

Browse files
committed
src,lib: implement experimental DTLS API
Decided to take a short break from the work on QUIC to implement a DTLS API. Very experimental at this point but the basic API is there (inspired by the QUIC API work). The implementation is based on OpenSSL's built-in DTLS support and no other dependencies are required. DTLS is a datagram-based version of TLS that is used for things like WebRTC and CoAP. It provides similar security guarantees as TLS but is designed to work over UDP instead of TCP. This shouldn't be considered ready for production but it is a good starting point for experimentation and feedback. ```bash ./configure --experimental-dtls make -j{nproc} ./node --experimental-dtls my-dtls-app.js ``` Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6
1 parent 1fd4949 commit 8ecdb17

34 files changed

Lines changed: 4101 additions & 1 deletion

configure.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,12 @@
10651065
default=None,
10661066
help='build with experimental QUIC support')
10671067

1068+
parser.add_argument('--experimental-dtls',
1069+
action='store_true',
1070+
dest='experimental_dtls',
1071+
default=None,
1072+
help='build with experimental DTLS support')
1073+
10681074
parser.add_argument('--ninja',
10691075
action='store_true',
10701076
dest='use_ninja',
@@ -2350,6 +2356,10 @@ def configure_quic(o):
23502356
o['variables']['node_use_quic'] = b(options.experimental_quic and
23512357
not options.without_ssl)
23522358

2359+
def configure_dtls(o):
2360+
o['variables']['node_use_dtls'] = b(options.experimental_dtls and
2361+
not options.without_ssl)
2362+
23532363
def configure_static(o):
23542364
if options.fully_static or options.partly_static:
23552365
if flavor == 'mac':
@@ -2808,6 +2818,7 @@ def make_bin_override():
28082818
configure_v8(output, configurations)
28092819
configure_openssl(output)
28102820
configure_quic(output)
2821+
configure_dtls(output)
28112822
configure_intl(output)
28122823
configure_static(output)
28132824
configure_inspector(output)

doc/api/dtls.md

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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

Comments
 (0)