lemon-tls 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -41
- package/index.cjs +9 -7
- package/index.d.ts +626 -30
- package/package.json +45 -7
- package/src/ca_store.js +31 -0
- package/src/compat.js +76 -116
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1284 -390
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +355 -191
- package/src/secure_context.js +97 -39
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2221 -902
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
package/src/dtls_socket.js
CHANGED
|
@@ -18,14 +18,56 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import dgram from 'node:dgram';
|
|
21
|
+
import dns from 'node:dns';
|
|
22
|
+
import net from 'node:net';
|
|
23
|
+
import crypto from 'node:crypto';
|
|
21
24
|
import { EventEmitter } from 'node:events';
|
|
22
|
-
import DTLSSession from './dtls_session.js';
|
|
25
|
+
import DTLSSession, { SERVER_CID_LEN } from './dtls_session.js';
|
|
26
|
+
import { datagramCid } from './record.js';
|
|
27
|
+
import { localLists, versionsInRange, pskOptions } from './node_options.js';
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
// ============================================================
|
|
26
31
|
// DTLSSocket — single DTLS connection over UDP
|
|
27
32
|
// ============================================================
|
|
28
33
|
|
|
34
|
+
// connectDTLS's own UDP socket (not exported): the DTLSSocket may replace it
|
|
35
|
+
// by one of the family its host name resolves to, and closes it when done.
|
|
36
|
+
const kOwnsTransport = Symbol('ownsTransport');
|
|
37
|
+
const kTransport = Symbol('transport');
|
|
38
|
+
// The server's: told when one of its connections takes a new address
|
|
39
|
+
// (setRemoteAddress), to find it there.
|
|
40
|
+
const kAddressMoved = Symbol('addressMoved');
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// The UDP sockets this module binds itself (createDTLSServer, connectDTLS):
|
|
44
|
+
// 4 MB receive and send buffers, or socketBufferSize (bytes; 0 or false, the
|
|
45
|
+
// OS default). The default (208 KB on Linux) holds some 180 datagrams, and a
|
|
46
|
+
// burst bigger than one event-loop turn drains is dropped in the kernel,
|
|
47
|
+
// silently: a retransmission, and a cut in the peer's sending rate.
|
|
48
|
+
// The kernel caps the size at net.core.rmem_max / wmem_max. A socket handed
|
|
49
|
+
// in (new DTLSSocket(udp, ...)) is the caller's, and keeps its own.
|
|
50
|
+
const DEFAULT_SOCKET_BUFFER_SIZE = 4 << 20;
|
|
51
|
+
function checkedSocketBufferSize(v) {
|
|
52
|
+
if (v === undefined || v === null) return DEFAULT_SOCKET_BUFFER_SIZE;
|
|
53
|
+
if (v === 0 || v === false) return 0;
|
|
54
|
+
if (!Number.isInteger(v) || v < 0 || v > 0x7fffffff) throw new RangeError('socketBufferSize must be a positive integer (bytes), 0 or false, got ' + v);
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
function createUdpSocket(base, size) {
|
|
58
|
+
return dgram.createSocket(size > 0 ? Object.assign({}, base, { recvBufferSize: size, sendBufferSize: size }) : base);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// What a DTLSSocket — or the server, for each of its connections — sets
|
|
62
|
+
// itself. Every other option goes on to the DTLSSession as the caller gave
|
|
63
|
+
// it: these were hand-copied lists, and what one left out did not exist —
|
|
64
|
+
// createDTLSServer dropped ca, rejectUnauthorized, checkServerIdentity and
|
|
65
|
+
// cipherSuites, so a server with requestCert and a ca took ANY client
|
|
66
|
+
// certificate (a self-signed one included).
|
|
67
|
+
function sessionOptions(options, own) {
|
|
68
|
+
return Object.assign({}, options, own);
|
|
69
|
+
}
|
|
70
|
+
|
|
29
71
|
function DTLSSocket(udpSocket, options) {
|
|
30
72
|
if (!(this instanceof DTLSSocket)) return new DTLSSocket(udpSocket, options);
|
|
31
73
|
options = options || {};
|
|
@@ -35,33 +77,70 @@ function DTLSSocket(udpSocket, options) {
|
|
|
35
77
|
|
|
36
78
|
let remoteAddress = options.remoteAddress || null;
|
|
37
79
|
let remotePort = options.remotePort || null;
|
|
80
|
+
let ownsTransport = options[kOwnsTransport] === true;
|
|
81
|
+
|
|
82
|
+
// A client given a host NAME: resolved once, here. It was used as the
|
|
83
|
+
// address as it was — every datagram sent to it paid a lookup of its own
|
|
84
|
+
// (dgram resolves a name per send), and every datagram back was dropped,
|
|
85
|
+
// its rinfo.address (an IP) never equal to the name: connectDTLS to
|
|
86
|
+
// 'localhost' never connected. What the session sends meanwhile — its
|
|
87
|
+
// ClientHello — waits for the address; what arrives meanwhile is no one's.
|
|
88
|
+
// A socket of our own (connectDTLS) resolves to IPv4 or IPv6, whichever the
|
|
89
|
+
// name has first, and gets a UDP socket of that family; a caller's socket
|
|
90
|
+
// keeps its family and the name must resolve to it.
|
|
91
|
+
let resolving = null;
|
|
92
|
+
if (!options.isServer && remoteAddress && net.isIP(remoteAddress) === 0) {
|
|
93
|
+
let name = remoteAddress;
|
|
94
|
+
remoteAddress = null;
|
|
95
|
+
resolving = [];
|
|
96
|
+
let family = ownsTransport ? 0 : (udpSocket && udpSocket.type === 'udp6' ? 6 : 4);
|
|
97
|
+
dns.lookup(name, { family: family }, function(err, address, fam) {
|
|
98
|
+
let held = resolving;
|
|
99
|
+
resolving = null;
|
|
100
|
+
// Closed meanwhile: the UDP socket may be gone, and nothing is owed.
|
|
101
|
+
if (session.state === 'closed') return;
|
|
102
|
+
if (err) { ev.emit('error', err); session.close(); return; }
|
|
103
|
+
if (ownsTransport && (fam === 6) !== (udpSocket.type === 'udp6')) {
|
|
104
|
+
detachTransport(udpSocket);
|
|
105
|
+
try { udpSocket.close(); } catch (e) {}
|
|
106
|
+
udpSocket = createUdpSocket({ type: fam === 6 ? 'udp6' : 'udp4' }, checkedSocketBufferSize(options.socketBufferSize));
|
|
107
|
+
attachTransport(udpSocket);
|
|
108
|
+
}
|
|
109
|
+
remoteAddress = address;
|
|
110
|
+
for (let i = 0; i < held.length; i++) transmit(held[i]);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
38
113
|
|
|
39
|
-
//
|
|
40
|
-
let session = new DTLSSession({
|
|
41
|
-
isServer: !!options.isServer,
|
|
42
|
-
servername: options.servername,
|
|
43
|
-
SNICallback: options.SNICallback,
|
|
44
|
-
cert: options.cert,
|
|
45
|
-
key: options.key,
|
|
46
|
-
rejectUnauthorized: options.rejectUnauthorized,
|
|
47
|
-
checkServerIdentity: options.checkServerIdentity,
|
|
48
|
-
requestCert: options.requestCert,
|
|
49
|
-
ca: options.ca,
|
|
50
|
-
alpnProtocols: options.alpnProtocols,
|
|
51
|
-
minVersion: options.minVersion,
|
|
52
|
-
maxVersion: options.maxVersion,
|
|
53
|
-
mtu: options.mtu,
|
|
54
|
-
groups: options.groups,
|
|
55
|
-
ticketKeys: options.ticketKeys,
|
|
56
|
-
useCookies: options.useCookies,
|
|
57
|
-
srtpProfiles: options.srtpProfiles,
|
|
58
|
-
});
|
|
114
|
+
// External PSK as node:tls takes it, as TLSSocket does (pskOptions).
|
|
115
|
+
let session = new DTLSSession(sessionOptions(options, Object.assign({ isServer: !!options.isServer }, pskOptions(options, self))));
|
|
59
116
|
|
|
60
117
|
// Wire session → UDP
|
|
118
|
+
// Datagrams handed to the UDP socket and not yet sent: dgram's send() is
|
|
119
|
+
// asynchronous, and a socket closed with one queued drops it. The last
|
|
120
|
+
// datagram of a failed connection is its alert — closed at once after it,
|
|
121
|
+
// the peer never learned why (the client's illegal_parameter, BoGo
|
|
122
|
+
// FragmentMessage*Mismatch). whenFlushed() waits for them.
|
|
123
|
+
// One callback for every send, not a closure per datagram. A send error
|
|
124
|
+
// comes to it (not as the UDP socket's 'error' event): this connection's,
|
|
125
|
+
// reported on it — on a server's shared socket, not the whole server's.
|
|
126
|
+
let pendingSends = 0, flushWaiters = [];
|
|
127
|
+
function onSent(err) {
|
|
128
|
+
if (err) ev.emit('error', err);
|
|
129
|
+
if (--pendingSends === 0 && flushWaiters.length > 0) {
|
|
130
|
+
let w = flushWaiters; flushWaiters = [];
|
|
131
|
+
for (let i = 0; i < w.length; i++) w[i]();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function transmit(data) {
|
|
135
|
+
udpSocket.send(data, 0, data.length, remotePort, remoteAddress, onSent);
|
|
136
|
+
pendingSends++; // counted once queued: a send that throws queued nothing
|
|
137
|
+
}
|
|
61
138
|
session.on('packet', function(data) {
|
|
139
|
+
if (resolving) { resolving.push(data); return; }
|
|
62
140
|
if (!remoteAddress || !remotePort) return;
|
|
63
|
-
|
|
141
|
+
transmit(data);
|
|
64
142
|
});
|
|
143
|
+
self.whenFlushed = function(fn) { if (pendingSends === 0) fn(); else flushWaiters.push(fn); };
|
|
65
144
|
|
|
66
145
|
session.on('connect', function() {
|
|
67
146
|
ev.emit('connect');
|
|
@@ -91,9 +170,22 @@ function DTLSSocket(udpSocket, options) {
|
|
|
91
170
|
ev.emit('clienthello', data, message);
|
|
92
171
|
});
|
|
93
172
|
|
|
173
|
+
// An alert received (the peer's warnings among them), and the Connection
|
|
174
|
+
// IDs agreed or moved: as DTLSSession reports them.
|
|
175
|
+
session.on('alert', function(a) { ev.emit('alert', a); });
|
|
176
|
+
session.on('connectionId', function(ids) { ev.emit('connectionId', ids); });
|
|
177
|
+
|
|
94
178
|
/**
|
|
95
179
|
* Feed an incoming UDP datagram (filtered by remote address/port).
|
|
96
180
|
* Called by the UDP socket's message handler or by DTLSServer.
|
|
181
|
+
*
|
|
182
|
+
* With Connection IDs agreed, a datagram of this connection may come from
|
|
183
|
+
* another address (a NAT that rebound the peer's port; the server finds
|
|
184
|
+
* the connection by its CID). It is taken, and when it holds the newest
|
|
185
|
+
* authenticated record yet the new address is reported:
|
|
186
|
+
* 'remoteAddressChange' { address, port }. It is not taken over by itself
|
|
187
|
+
* — RFC 9146 §6 (and RFC 9147 §9) forbid it without a way of knowing the
|
|
188
|
+
* peer is there; an application that has one calls setRemoteAddress.
|
|
97
189
|
*/
|
|
98
190
|
function feedDatagram(data, rinfo) {
|
|
99
191
|
// Update remote address on first datagram (server side)
|
|
@@ -102,22 +194,48 @@ function DTLSSocket(udpSocket, options) {
|
|
|
102
194
|
remotePort = rinfo.port;
|
|
103
195
|
}
|
|
104
196
|
|
|
105
|
-
session.feedDatagram(data);
|
|
197
|
+
let newest = session.feedDatagram(data);
|
|
198
|
+
if (newest && rinfo && (rinfo.address !== remoteAddress || rinfo.port !== remotePort) && session.getConnectionIds()) {
|
|
199
|
+
ev.emit('remoteAddressChange', { address: rinfo.address, port: rinfo.port });
|
|
200
|
+
}
|
|
106
201
|
}
|
|
107
202
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
203
|
+
/** Send to another address from now on — the peer's new one, as the
|
|
204
|
+
* application knows it (see feedDatagram). */
|
|
205
|
+
function setRemoteAddress(address, port) {
|
|
206
|
+
if (net.isIP(address) === 0 || !Number.isInteger(port) || port <= 0 || port > 65535) throw new TypeError('setRemoteAddress: an IP address and a port');
|
|
207
|
+
let previous = { address: remoteAddress, port: remotePort };
|
|
208
|
+
remoteAddress = address;
|
|
209
|
+
remotePort = port;
|
|
210
|
+
if (typeof options[kAddressMoved] === 'function') options[kAddressMoved](previous, { address: address, port: port });
|
|
114
211
|
}
|
|
115
212
|
|
|
213
|
+
// A client reads its UDP socket itself (a server's connections are fed by
|
|
214
|
+
// the server). A socket of our own also reports its errors here — it had
|
|
215
|
+
// no 'error' listener, and one (a failed bind) took the process down.
|
|
216
|
+
function onMessage(msg, rinfo) {
|
|
217
|
+
if (resolving) return;
|
|
218
|
+
if (remoteAddress && (rinfo.address !== remoteAddress || rinfo.port !== remotePort)) return;
|
|
219
|
+
feedDatagram(msg, rinfo);
|
|
220
|
+
}
|
|
221
|
+
function onTransportError(err) { ev.emit('error', err); session.close(); }
|
|
222
|
+
function attachTransport(u) {
|
|
223
|
+
u.on('message', onMessage);
|
|
224
|
+
if (ownsTransport) u.on('error', onTransportError);
|
|
225
|
+
}
|
|
226
|
+
function detachTransport(u) {
|
|
227
|
+
u.removeListener('message', onMessage);
|
|
228
|
+
u.removeListener('error', onTransportError);
|
|
229
|
+
}
|
|
230
|
+
if (!options.isServer && udpSocket) attachTransport(udpSocket);
|
|
231
|
+
self[kTransport] = function() { return udpSocket; };
|
|
232
|
+
|
|
116
233
|
// ---- Public API ----
|
|
117
234
|
|
|
118
235
|
self.send = function(data) { session.send(data); };
|
|
119
236
|
self.close = function() { session.close(); };
|
|
120
237
|
self.feedDatagram = feedDatagram;
|
|
238
|
+
self.setRemoteAddress = setRemoteAddress;
|
|
121
239
|
|
|
122
240
|
self.on = function(name, fn) { ev.on(name, fn); };
|
|
123
241
|
self.off = function(name, fn) { ev.off(name, fn); };
|
|
@@ -130,6 +248,11 @@ function DTLSSocket(udpSocket, options) {
|
|
|
130
248
|
|
|
131
249
|
self.getNegotiationResult = function() { return session.getNegotiationResult(); };
|
|
132
250
|
self.getALPN = function() { return session.getALPN(); };
|
|
251
|
+
self.getEchStatus = function() { return session.getEchStatus(); };
|
|
252
|
+
self.isEchBackend = function() { return session.isEchBackend(); };
|
|
253
|
+
/** External PSK: the identity (a Buffer, as TLSSocket's), or null. */
|
|
254
|
+
self.getPskIdentity = function() { let id = session.getPskIdentity(); return id ? Buffer.from(id) : null; };
|
|
255
|
+
self.getConnectionIds = function() { return session.getConnectionIds(); };
|
|
133
256
|
self.getPeerCertificate = function() { return session.getPeerCertificate(); };
|
|
134
257
|
|
|
135
258
|
/** TLS fingerprints, { hash, raw } or null (see TLSSession): JA4 and JA5
|
|
@@ -154,6 +277,14 @@ function DTLSSocket(udpSocket, options) {
|
|
|
154
277
|
return v == null ? undefined : v;
|
|
155
278
|
};
|
|
156
279
|
|
|
280
|
+
/** The last session ('session' event's Buffer) — pass it as `session` to resume. */
|
|
281
|
+
self.getSession = function() { return session.getSession(); };
|
|
282
|
+
/** The path MTU (largest datagram written); see DTLSSession.setMTU. */
|
|
283
|
+
self.setMTU = function(mtu) { session.setMTU(mtu); };
|
|
284
|
+
self.getMTU = function() { return session.getMTU(); };
|
|
285
|
+
/** Where each flight's retransmission timer starts; see DTLSSession. */
|
|
286
|
+
self.setInitialRetransmitTimeout = function(ms) { session.setInitialRetransmitTimeout(ms); };
|
|
287
|
+
|
|
157
288
|
/** Whether this connection resumed an earlier session. */
|
|
158
289
|
self.isSessionReused = function() {
|
|
159
290
|
return session.isSessionReused ? session.isSessionReused() : false;
|
|
@@ -196,77 +327,170 @@ function createDTLSServer(options, connectionListener) {
|
|
|
196
327
|
options = {};
|
|
197
328
|
}
|
|
198
329
|
options = options || {};
|
|
330
|
+
// Checked here, once, as createServer does: not on each client's hello.
|
|
331
|
+
localLists(options, versionsInRange(options.minVersion, options.maxVersion, true));
|
|
332
|
+
// Connection IDs: one per connection, drawn here — a single one given for
|
|
333
|
+
// the server would be every connection's, and find none of them.
|
|
334
|
+
if (options.connectionId !== undefined && options.connectionId !== null && typeof options.connectionId !== 'boolean') {
|
|
335
|
+
throw new TypeError('createDTLSServer: connectionId is true or false (each connection is given its own)');
|
|
336
|
+
}
|
|
337
|
+
let cidOn = options.connectionId === true;
|
|
338
|
+
let socketBufferSize = checkedSocketBufferSize(options.socketBufferSize);
|
|
339
|
+
let byCid = {}; // CID (hex) → DTLSSocket
|
|
199
340
|
|
|
200
341
|
let ev = new EventEmitter();
|
|
201
342
|
let udpSocket = null;
|
|
202
343
|
let connections = {}; // 'addr:port' → DTLSSocket
|
|
203
344
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
udpSocket.on('message', function(msg, rinfo) {
|
|
210
|
-
let key = rinfo.address + ':' + rinfo.port;
|
|
211
|
-
|
|
212
|
-
if (!(key in connections)) {
|
|
213
|
-
// New client
|
|
214
|
-
let socket = new DTLSSocket(udpSocket, {
|
|
215
|
-
isServer: true,
|
|
216
|
-
remoteAddress: rinfo.address,
|
|
217
|
-
remotePort: rinfo.port,
|
|
218
|
-
cert: options.cert,
|
|
219
|
-
key: options.key,
|
|
220
|
-
requestCert: options.requestCert,
|
|
221
|
-
SNICallback: options.SNICallback,
|
|
222
|
-
alpnProtocols: options.alpnProtocols,
|
|
223
|
-
minVersion: options.minVersion,
|
|
224
|
-
maxVersion: options.maxVersion,
|
|
225
|
-
mtu: options.mtu,
|
|
226
|
-
groups: options.groups,
|
|
227
|
-
ticketKeys: options.ticketKeys,
|
|
228
|
-
useCookies: options.useCookies,
|
|
229
|
-
srtpProfiles: options.srtpProfiles,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
connections[key] = socket;
|
|
345
|
+
// One ticket key for the server, not one per connection: a ticket is
|
|
346
|
+
// opened by the connection that comes back, not the one that issued it.
|
|
347
|
+
// Each DTLSSocket drew its own, so no ticket of this server could ever
|
|
348
|
+
// be used (as TLSServer: tls_socket.js keeps one per server too).
|
|
349
|
+
let ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
233
350
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
351
|
+
// A ClientHello: an unprotected (epoch 0) handshake record whose message
|
|
352
|
+
// is type 1 — the first record of the datagram, as a ClientHello comes.
|
|
353
|
+
function isClientHello(msg) {
|
|
354
|
+
return msg.length > 25 && msg[0] === 22 && msg[3] === 0 && msg[4] === 0 && msg[13] === 1;
|
|
355
|
+
}
|
|
237
356
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
357
|
+
function newConnection(key, rinfo) {
|
|
358
|
+
let cid = null;
|
|
359
|
+
if (cidOn) {
|
|
360
|
+
do { cid = crypto.randomBytes(SERVER_CID_LEN); } while (byCid[cid.toString('hex')]);
|
|
361
|
+
}
|
|
362
|
+
let socket = new DTLSSocket(udpSocket, sessionOptions(options, {
|
|
363
|
+
isServer: true,
|
|
364
|
+
remoteAddress: rinfo.address,
|
|
365
|
+
remotePort: rinfo.port,
|
|
366
|
+
ticketKeys: ticketKeys,
|
|
367
|
+
connectionId: cid || false,
|
|
368
|
+
// Moved (setRemoteAddress): found at its new address from now on —
|
|
369
|
+
// unless another connection holds it; its own records, with the CID,
|
|
370
|
+
// find it anyway (onDatagram), and the other is not ended on the
|
|
371
|
+
// application's word.
|
|
372
|
+
[kAddressMoved]: function(from, to) {
|
|
373
|
+
let next = to.address + ':' + to.port;
|
|
374
|
+
if (connections[key] === socket) delete connections[key];
|
|
375
|
+
if (replacing[key] === socket) delete replacing[key];
|
|
376
|
+
key = next;
|
|
377
|
+
if (!connections[key]) connections[key] = socket;
|
|
378
|
+
},
|
|
379
|
+
}));
|
|
380
|
+
if (cid) byCid[cid.toString('hex')] = socket;
|
|
381
|
+
|
|
382
|
+
socket.on('close', function() {
|
|
383
|
+
if (connections[key] === socket) delete connections[key];
|
|
384
|
+
if (replacing[key] === socket) delete replacing[key];
|
|
385
|
+
if (cid && byCid[cid.toString('hex')] === socket) delete byCid[cid.toString('hex')];
|
|
386
|
+
});
|
|
242
387
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
388
|
+
socket.on('connect', function() {
|
|
389
|
+
// A new association from the address of an existing one (below): it
|
|
390
|
+
// has completed its handshake — the client is there — and takes the
|
|
391
|
+
// old one's place.
|
|
392
|
+
if (replacing[key] === socket) {
|
|
393
|
+
let old = connections[key];
|
|
394
|
+
connections[key] = socket;
|
|
395
|
+
delete replacing[key];
|
|
396
|
+
if (old) old.close();
|
|
246
397
|
}
|
|
247
|
-
|
|
248
|
-
|
|
398
|
+
if (connectionListener) connectionListener(socket);
|
|
399
|
+
ev.emit('connection', socket);
|
|
249
400
|
});
|
|
250
401
|
|
|
251
|
-
|
|
252
|
-
ev.emit('
|
|
402
|
+
socket.on('error', function(err) {
|
|
403
|
+
ev.emit('clientError', err, socket);
|
|
253
404
|
});
|
|
405
|
+
return socket;
|
|
406
|
+
}
|
|
254
407
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
408
|
+
// RFC 6347 §4.2.8 (and RFC 9147 §5.9 the same): a ClientHello from the
|
|
409
|
+
// address of an established association is a new one — the client
|
|
410
|
+
// restarted, or a NAT gave its port to another — and the server "SHOULD
|
|
411
|
+
// proceed with a new handshake but MUST NOT destroy the existing
|
|
412
|
+
// association until the client has demonstrated reachability", by
|
|
413
|
+
// completing the handshake. It went to the old session, which took it for
|
|
414
|
+
// a stray retransmission: the client was never answered while that entry
|
|
415
|
+
// lived — for good, if its close_notify was lost (a dtls_loss_test
|
|
416
|
+
// handshake in ~150 hung so, with no loss at all).
|
|
417
|
+
// So the new handshake runs beside the old association (replacing), and
|
|
418
|
+
// replaces it when it connects. Meanwhile every other datagram goes to
|
|
419
|
+
// both: a protected record opens only under the keys it was made with,
|
|
420
|
+
// and each session drops what it cannot open — and a spoofed ClientHello
|
|
421
|
+
// never touches the old one.
|
|
422
|
+
let replacing = {}; // 'addr:port' → the DTLSSocket of a new handshake there
|
|
423
|
+
|
|
424
|
+
function onDatagram(msg, rinfo) {
|
|
425
|
+
// A record with one of our CIDs is that connection's, whatever address
|
|
426
|
+
// it comes from — the peer moved, maybe onto an address another
|
|
427
|
+
// connection still holds (a NAT that gave the port on).
|
|
428
|
+
if (cidOn) {
|
|
429
|
+
let c = datagramCid(msg, SERVER_CID_LEN);
|
|
430
|
+
let owner = c ? byCid[Buffer.from(c).toString('hex')] : null;
|
|
431
|
+
if (owner) { owner.feedDatagram(msg, rinfo); return; }
|
|
432
|
+
}
|
|
433
|
+
let key = rinfo.address + ':' + rinfo.port;
|
|
434
|
+
let conn = connections[key];
|
|
435
|
+
if (!conn) {
|
|
436
|
+
conn = connections[key] = newConnection(key, rinfo);
|
|
437
|
+
conn.feedDatagram(msg, rinfo);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (isClientHello(msg) && conn.state === 'connected') {
|
|
441
|
+
if (!replacing[key]) replacing[key] = newConnection(key, rinfo);
|
|
442
|
+
replacing[key].feedDatagram(msg, rinfo);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
conn.feedDatagram(msg, rinfo);
|
|
446
|
+
if (replacing[key]) replacing[key].feedDatagram(msg, rinfo);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// listen(port[, address][, callback]). The address's family decides the
|
|
450
|
+
// socket's: an IPv6 one, an IPv6 socket. No address: '::', dual-stack —
|
|
451
|
+
// IPv4 clients too (as '::ffff:a.b.c.d'), as node:net and node:tls listen
|
|
452
|
+
// — or, on a host without IPv6, 0.0.0.0. It was IPv4 only, whatever the
|
|
453
|
+
// address: an IPv6 client could never reach a DTLS server.
|
|
454
|
+
function listen(port, address, callback) {
|
|
455
|
+
if (typeof address === 'function') { callback = address; address = undefined; }
|
|
456
|
+
let open = function(type, bindAddress, fallback) {
|
|
457
|
+
let u = createUdpSocket({ type: type, ipv6Only: false }, socketBufferSize);
|
|
458
|
+
let onBindError = function(err) {
|
|
459
|
+
u.removeListener('error', onBindError);
|
|
460
|
+
try { u.close(); } catch (e) {}
|
|
461
|
+
if (fallback && (err.code === 'EAFNOSUPPORT' || err.code === 'EADDRNOTAVAIL' || err.code === 'EPROTONOSUPPORT' || err.code === 'EINVAL')) { fallback(); return; }
|
|
462
|
+
ev.emit('error', err);
|
|
463
|
+
};
|
|
464
|
+
u.on('error', onBindError);
|
|
465
|
+
u.bind(port, bindAddress, function() {
|
|
466
|
+
u.removeListener('error', onBindError);
|
|
467
|
+
udpSocket = u;
|
|
468
|
+
u.on('message', onDatagram);
|
|
469
|
+
u.on('error', function(err) { ev.emit('error', err); });
|
|
470
|
+
if (callback) callback();
|
|
471
|
+
ev.emit('listening');
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
if (address !== undefined && address !== null) open(net.isIP(address) === 6 ? 'udp6' : 'udp4', address, null);
|
|
475
|
+
else open('udp6', '::', function() { open('udp4', undefined, null); });
|
|
259
476
|
}
|
|
260
477
|
|
|
478
|
+
// Every connection's close_notify first, then the socket — once they have
|
|
479
|
+
// gone (whenFlushed): closed at once, it dropped them.
|
|
261
480
|
function close(callback) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
481
|
+
let open = Object.keys(connections).map(function(k) { return connections[k]; })
|
|
482
|
+
.concat(Object.keys(replacing).map(function(k) { return replacing[k]; }));
|
|
265
483
|
connections = {};
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
484
|
+
replacing = {};
|
|
485
|
+
byCid = {};
|
|
486
|
+
let sock = udpSocket;
|
|
487
|
+
udpSocket = null;
|
|
488
|
+
for (let i = 0; i < open.length; i++) open[i].close();
|
|
489
|
+
if (!sock) return;
|
|
490
|
+
let waiting = open.length + 1;
|
|
491
|
+
let done = function() { if (--waiting === 0) sock.close(callback); };
|
|
492
|
+
for (let i = 0; i < open.length; i++) open[i].whenFlushed(done);
|
|
493
|
+
done();
|
|
270
494
|
}
|
|
271
495
|
|
|
272
496
|
function address() {
|
|
@@ -292,28 +516,26 @@ function connectDTLS(options, callback) {
|
|
|
292
516
|
let host = options.host || options.hostname || '127.0.0.1';
|
|
293
517
|
let port = options.port || 4433;
|
|
294
518
|
|
|
295
|
-
|
|
519
|
+
// An IPv6 literal needs an IPv6 socket; for a name, the DTLSSocket picks
|
|
520
|
+
// the family it resolves to (kOwnsTransport).
|
|
521
|
+
let udp = createUdpSocket({ type: net.isIP(host) === 6 ? 'udp6' : 'udp4' }, checkedSocketBufferSize(options.socketBufferSize));
|
|
296
522
|
|
|
297
|
-
|
|
523
|
+
// Every option goes on to the session (sessionOptions), as given: the
|
|
524
|
+
// client's certificate, ca, cipherSuites, ... — this list too had dropped
|
|
525
|
+
// whatever it did not name.
|
|
526
|
+
let socket = new DTLSSocket(udp, sessionOptions(options, {
|
|
298
527
|
isServer: false,
|
|
299
528
|
remoteAddress: host,
|
|
300
529
|
remotePort: port,
|
|
301
530
|
servername: options.servername || host,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
requestCert: options.requestCert,
|
|
305
|
-
ca: options.ca,
|
|
306
|
-
alpnProtocols: options.alpnProtocols,
|
|
307
|
-
minVersion: options.minVersion,
|
|
308
|
-
maxVersion: options.maxVersion,
|
|
309
|
-
mtu: options.mtu,
|
|
310
|
-
groups: options.groups,
|
|
311
|
-
});
|
|
531
|
+
[kOwnsTransport]: true,
|
|
532
|
+
}));
|
|
312
533
|
|
|
313
534
|
if (callback) socket.on('connect', callback);
|
|
314
535
|
|
|
536
|
+
// After what is still queued has gone — the closing alert above all.
|
|
315
537
|
socket.on('close', function() {
|
|
316
|
-
try {
|
|
538
|
+
socket.whenFlushed(function() { try { socket[kTransport]().close(); } catch(e) {} });
|
|
317
539
|
});
|
|
318
540
|
|
|
319
541
|
return socket;
|