DTLS#

Stability: 1 - Experimental

The node:dtls module provides an implementation of the Datagram Transport Layer Security (DTLS) protocol over UDP. DTLS provides TLS-equivalent security guarantees for datagram-based communication, including confidentiality, integrity, and authentication.

To use this module, it must be enabled at build time with the --experimental-dtls configure flag and at runtime with the --experimental-dtls CLI flag.

import { listen, connect } from 'node:dtls';
const { listen, connect } = require('node:dtls');
javascript

Permission model#

When using the Permission Model, the --allow-net flag must be passed to allow DTLS network operations. Without it, calling dtls.connect() or dtls.listen() will throw an ERR_ACCESS_DENIED error.

node --permission --allow-fs-read=* --experimental-dtls index.mjs
Error: Access to this API has been restricted. Use --allow-net to manage permissions.
  code: 'ERR_ACCESS_DENIED',
  permission: 'Net',
}
console

Creating a DTLSEndpoint instance without connecting or listening is permitted even without --allow-net, since no network I/O occurs until dtls.connect() or dtls.listen() is called.

DTLS vs TLS#

DTLS is designed for UDP transport and differs from TLS in several key ways:

  • No stream guarantees: Messages may arrive out of order or be lost. DTLS preserves datagram semantics.
  • One socket, many peers: A single UDP socket can serve multiple DTLS sessions. The DTLSEndpoint manages this multiplexing.
  • Cookie exchange: DTLS servers use a stateless cookie mechanism (HelloVerifyRequest) to prevent denial-of-service amplification attacks.
  • Retransmission: DTLS handles handshake retransmission internally since UDP does not guarantee delivery.

dtls.listen(callback, options)#

  • callback <Function> Called for each new DTLS session accepted by the server.
    • session {DTLSSession} The new session.
  • options <Object>
    • cert <string> | <Buffer> Server certificate in PEM format. Required.
    • key <string> | <Buffer> Server private key in PEM format. Required.
    • port <number> Port to bind to. Required.
    • host <string> Address to bind to. Default: '0.0.0.0'.
    • ca <string> | <Buffer> | <string[]> | <Buffer[]> CA certificates in PEM format.
    • ciphers <string> OpenSSL cipher list string.
    • alpn <string[]> | <Buffer> ALPN protocol names.
    • srtp <string> Colon-separated SRTP protection profile names (e.g., 'SRTP_AES128_CM_SHA1_80:SRTP_AEAD_AES_128_GCM').
    • requestCert <boolean> Request client certificate. Default: false.
    • mtu <number> Maximum transmission unit for DTLS records. Default: 1200.
  • Returns: {DTLSEndpoint}

Creates a DTLS server bound to the specified address and port. The server uses automatic HMAC-based cookie exchange for DoS protection.

import { listen } from 'node:dtls';
import { readFileSync } from 'node:fs';

const endpoint = listen((session) => {
  session.onmessage = (data) => {
    console.log('Received:', data.toString());
    session.send('pong');
  };

  session.onhandshake = (protocol) => {
    console.log('Handshake complete:', protocol);
  };
}, {
  cert: readFileSync('server-cert.pem'),
  key: readFileSync('server-key.pem'),
  port: 4433,
});

console.log('DTLS server listening on', endpoint.address);
mjs

dtls.connect(host, port[, options])#

Connects to a DTLS server. Returns a DTLSSession whose opened property is a Promise that resolves when the handshake completes.

import { connect } from 'node:dtls';
import { readFileSync } from 'node:fs';

const session = connect('localhost', 4433, {
  ca: [readFileSync('ca-cert.pem')],
});

await session.opened;
session.send('hello');

session.onmessage = (data) => {
  console.log('Received:', data.toString());
};
mjs

Class: DTLSEndpoint#

Manages a UDP socket and multiplexes DTLS sessions.

endpoint.address#

  • Returns: <Object> { address, family, port }

The local address the endpoint is bound to.

endpoint.state#

  • Returns: {DTLSEndpointState}

Shared state object with properties:

endpoint.stats#

The statistics collected for this endpoint. Read only. The stats object is live and updated by the C++ internals as data flows through the endpoint.

endpoint.busy#

When true, the endpoint rejects new incoming connections. Can be set to implement backpressure.

endpoint.close()#

  • Returns: <Promise> Resolves when the endpoint is fully closed.

Gracefully closes the endpoint. All active sessions are closed with close_notify alerts before the UDP socket is released.

endpoint.destroy([error])#

Immediately destroys the endpoint without sending close_notify alerts.

endpoint.closed#

  • <Promise> Resolves when the endpoint has fully closed.

endpoint[Symbol.asyncDispose]()#

Equivalent to calling endpoint.close().

Class: DTLSEndpoint.Stats#

A view of the collected statistics for an endpoint.

endpointStats.createdAt#

  • Type: <bigint> A timestamp indicating when the endpoint was created. Read only.

endpointStats.destroyedAt#

  • Type: <bigint> A timestamp indicating when the endpoint was destroyed. Read only.

endpointStats.bytesReceived#

  • Type: <bigint> The total number of bytes received by this endpoint. Read only.

endpointStats.bytesSent#

  • Type: <bigint> The total number of bytes sent by this endpoint. Read only.

endpointStats.packetsReceived#

  • Type: <bigint> The total number of UDP packets received by this endpoint. Read only.

endpointStats.packetsSent#

  • Type: <bigint> The total number of UDP packets sent by this endpoint. Read only.

endpointStats.serverSessions#

  • Type: <bigint> The total number of peer-initiated sessions accepted by this endpoint. Read only.

endpointStats.clientSessions#

  • Type: <bigint> The total number of sessions initiated by this endpoint. Read only.

endpointStats.serverBusyCount#

  • Type: <bigint> The total number of incoming connections rejected because the endpoint was marked busy. Read only.

endpointStats.isConnected#

true if the stats object is still connected to the underlying endpoint. Once the endpoint is destroyed, the stats become a stale snapshot.

Class: DTLSSession#

Represents a DTLS association with a single remote peer.

session.send(data)#

Send application data to the peer. The data is encrypted by DTLS before being sent over UDP. Can only be called after the handshake completes (session.opened has resolved).

session.close()#

  • Returns: <Promise> Resolves when the session is closed.

Initiates a graceful DTLS shutdown by sending a close_notify alert.

session.destroy([error])#

Immediately destroys the session without sending close_notify.

session.opened#

  • <Promise> Resolves with { protocol } when the DTLS handshake completes.

session.closed#

  • <Promise> Resolves when the session is fully closed.

session.remoteAddress#

  • Returns: <Object> { address, family, port }

session.protocol#

  • Returns: <string> The negotiated DTLS protocol version (e.g., 'DTLSv1.2').

session.cipher#

  • Returns: <Object> { name, standardName, version }

session.peerCertificate#

session.alpnProtocol#

session.srtpProfile#

session.stats#

The statistics collected for this session. Read only. The stats object is live and updated as data flows through the session.

session.exportKeyingMaterial(length, label[, context])#

  • length <number> Number of bytes to export.
  • label <string> The label for the exported keying material.
  • context <Buffer> Optional context value.
  • Returns: <Buffer>

Exports keying material from the DTLS session, as defined in RFC 5705. This is commonly used with DTLS-SRTP to derive encryption keys for media streams.

Class: DTLSSession.Stats#

A view of the collected statistics for a session.

sessionStats.createdAt#

  • Type: <bigint> A timestamp indicating when the session was created. Read only.

sessionStats.destroyedAt#

  • Type: <bigint> A timestamp indicating when the session was destroyed. Read only.

sessionStats.closingAt#

  • Type: <bigint> A timestamp indicating when close() was called. Read only.

sessionStats.handshakeCompletedAt#

  • Type: <bigint> A timestamp indicating when the DTLS handshake completed. Read only.

sessionStats.bytesReceived#

  • Type: <bigint> The total number of application data bytes received. Read only.

sessionStats.bytesSent#

  • Type: <bigint> The total number of application data bytes sent. Read only.

sessionStats.messagesReceived#

  • Type: <bigint> The total number of application messages received. Read only.

sessionStats.messagesSent#

  • Type: <bigint> The total number of application messages sent. Read only.

sessionStats.retransmitCount#

  • Type: <bigint> The total number of DTLS handshake retransmissions. Read only.

sessionStats.isConnected#

true if the stats object is still connected to the underlying session. Once the session is destroyed, the stats become a stale snapshot.

Callback properties#

session.onmessage#

Set to receive application data from the peer.

session.onerror#

Set to receive error notifications.

session.onhandshake#

Set to receive handshake completion notifications.

session.onkeylog#

Set to receive TLS key log lines (for debugging with Wireshark).

session[Symbol.asyncDispose]()#

Equivalent to calling session.close().

DTLS-SRTP example#

DTLS-SRTP is used by WebRTC for media encryption. The DTLS handshake negotiates the SRTP protection profile and provides keying material.

import { listen, connect } from 'node:dtls';
import { readFileSync } from 'node:fs';

// Server with SRTP
const server = listen((session) => {
  session.onhandshake = () => {
    console.log('SRTP profile:', session.srtpProfile);
    const keys = session.exportKeyingMaterial(
      60,
      'EXTRACTOR-dtls_srtp',
    );
    console.log('SRTP keying material:', keys);
  };
}, {
  cert: readFileSync('server-cert.pem'),
  key: readFileSync('server-key.pem'),
  port: 5004,
  srtp: 'SRTP_AES128_CM_SHA1_80:SRTP_AEAD_AES_128_GCM',
});

// Client with SRTP
const session = connect('localhost', 5004, {
  rejectUnauthorized: false,
  srtp: 'SRTP_AEAD_AES_128_GCM:SRTP_AES128_CM_SHA1_80',
});

await session.opened;
console.log('Negotiated SRTP:', session.srtpProfile);
const keys = session.exportKeyingMaterial(60, 'EXTRACTOR-dtls_srtp');
mjs

MTU considerations#

Since libuv does not currently support path MTU discovery, the DTLS module uses a conservative default MTU of 1200 bytes. This value works across virtually all network paths but may be suboptimal for local networks.

The MTU can be configured via the mtu option:

// For a local network where you know the path MTU
const endpoint = listen(callback, {
  // ...
  mtu: 1400,
});
mjs

The minimum allowed MTU is 256 bytes. The maximum is 65535.