|
7 | 7 | const { |
8 | 8 | ArrayIsArray, |
9 | 9 | FunctionPrototypeBind, |
| 10 | + PromisePrototypeThen, |
10 | 11 | PromiseWithResolvers, |
11 | 12 | SafeSet, |
12 | 13 | SymbolAsyncDispose, |
@@ -41,6 +42,10 @@ const { |
41 | 42 | Buffer, |
42 | 43 | } = require('buffer'); |
43 | 44 |
|
| 45 | +const { |
| 46 | + isIP, |
| 47 | +} = require('internal/net'); |
| 48 | + |
44 | 49 | const { |
45 | 50 | DTLSEndpointState, |
46 | 51 | DTLSSessionState, |
@@ -102,6 +107,12 @@ class DTLSSession { |
102 | 107 | kPrivateConstructor, handle.getStats()); |
103 | 108 | this.#pendingOpen = PromiseWithResolvers(); |
104 | 109 | this.#pendingClose = PromiseWithResolvers(); |
| 110 | + // opened/closed may reject (handshake error, destroy(error)). Attach a |
| 111 | + // no-op rejection handler so a caller that uses the callback API and never |
| 112 | + // awaits them does not trigger an unhandled rejection; an explicit |
| 113 | + // await/then/catch on opened/closed still observes the rejection. |
| 114 | + PromisePrototypeThen(this.#pendingOpen.promise, undefined, () => {}); |
| 115 | + PromisePrototypeThen(this.#pendingClose.promise, undefined, () => {}); |
105 | 116 | } |
106 | 117 |
|
107 | 118 | // --- Callback setters --- |
@@ -261,6 +272,22 @@ class DTLSSession { |
261 | 272 | this.#onerror(error); |
262 | 273 | } |
263 | 274 | this.#pendingOpen.reject(error); |
| 275 | + |
| 276 | + // The session has failed and cannot continue. Tear it down so it does not |
| 277 | + // linger in the endpoint's table, and -- for a client session that owns |
| 278 | + // its internal endpoint -- close the endpoint too so the event loop can |
| 279 | + // drain. destroy() removes the session from the C++ table first, so the |
| 280 | + // endpoint.close() below won't try to re-close it. Reentrant destroy from |
| 281 | + // within the error emit is safe: Cycle()/the timer hold a strong ref. |
| 282 | + const endpoint = this.#endpoint; |
| 283 | + const ownsEndpoint = this.#ownsEndpoint; |
| 284 | + this.destroy(); |
| 285 | + if (endpoint) { |
| 286 | + endpoint.sessions.delete(this); |
| 287 | + if (ownsEndpoint) { |
| 288 | + endpoint.close(); |
| 289 | + } |
| 290 | + } |
264 | 291 | } |
265 | 292 |
|
266 | 293 | [kSessionClose]() { |
@@ -314,6 +341,9 @@ class DTLSEndpoint { |
314 | 341 | this.#stats = new DTLSEndpointStats( |
315 | 342 | kPrivateConstructor, this.#handle.getStats()); |
316 | 343 | this.#pendingClose = PromiseWithResolvers(); |
| 344 | + // See DTLSSession: keep an unobserved closed rejection from surfacing as an |
| 345 | + // unhandled rejection. |
| 346 | + PromisePrototypeThen(this.#pendingClose.promise, undefined, () => {}); |
317 | 347 |
|
318 | 348 | if (options.mtu !== undefined) { |
319 | 349 | validateInteger(options.mtu, 'options.mtu', 256, 65535); |
@@ -359,10 +389,24 @@ class DTLSEndpoint { |
359 | 389 | // --- Client mode --- |
360 | 390 |
|
361 | 391 | connect(context, host, port, servername) { |
362 | | - const sessionHandle = this.#handle.connect(context, host, port); |
363 | | - if (servername) { |
364 | | - sessionHandle.setServername(servername); |
| 392 | + // Resolve SNI and the expected peer identity here so that every caller of |
| 393 | + // the endpoint API -- not only the top-level dtls.connect() -- gets safe |
| 394 | + // defaults. The identity is always bound to the requested servername (or, |
| 395 | + // failing that, the host). OpenSSL only *enforces* it when the context is |
| 396 | + // in a verifying mode, so binding it is a no-op for non-verifying |
| 397 | + // (rejectUnauthorized: false) contexts. |
| 398 | + // |
| 399 | + // These are applied to the client SSL inside the binding, before the |
| 400 | + // handshake's ClientHello is emitted; they cannot be set afterwards. |
| 401 | + let sni = servername !== undefined ? (servername || undefined) : host; |
| 402 | + if (sni !== undefined && isIP(sni) !== 0) { |
| 403 | + sni = undefined; // SNI is never sent for IP literals (matching TLS). |
365 | 404 | } |
| 405 | + const verifyHost = servername || host; |
| 406 | + const verifyIsIp = isIP(verifyHost) !== 0; |
| 407 | + |
| 408 | + const sessionHandle = this.#handle.connect( |
| 409 | + context, host, port, sni, verifyHost, verifyIsIp); |
366 | 410 | const session = new DTLSSession( |
367 | 411 | kPrivateConstructor, sessionHandle, this); |
368 | 412 | this.#sessions.add(session); |
@@ -604,7 +648,12 @@ function listen(onsession, options = kEmptyObject) { |
604 | 648 | * @param {string|Buffer|Array} [options.ca] CA certificates (PEM). |
605 | 649 | * @param {string|Buffer} [options.cert] Client certificate (PEM). |
606 | 650 | * @param {string|Buffer} [options.key] Client private key (PEM). |
607 | | - * @param {boolean} [options.rejectUnauthorized] Reject unauthorized. |
| 651 | + * @param {boolean} [options.rejectUnauthorized] When true (default), verify |
| 652 | + * the server certificate against the trusted CAs and check its identity |
| 653 | + * against servername (or host); aborts the handshake on failure. |
| 654 | + * @param {string} [options.servername] Server name for the SNI extension and |
| 655 | + * the identity checked during certificate verification. Defaults to host; |
| 656 | + * set to '' to disable SNI. Never sent for IP address literals. |
608 | 657 | * @param {string} [options.bindHost] Local bind address. |
609 | 658 | * @param {number} [options.bindPort] Local bind port (0 = ephemeral). |
610 | 659 | * @param {number} [options.mtu] MTU for DTLS records. |
@@ -632,13 +681,11 @@ function connect(host, port, options = kEmptyObject) { |
632 | 681 |
|
633 | 682 | endpoint.bind(bindHost, bindPort); |
634 | 683 |
|
635 | | - // Default SNI servername to the host argument (matching Node.js TLS). |
636 | | - // Can be overridden with options.servername, or disabled with '' or false. |
637 | | - const servername = options.servername !== undefined ? |
638 | | - (options.servername || undefined) : |
639 | | - host; |
640 | | - |
641 | | - const session = endpoint.connect(context, host, port, servername); |
| 684 | + // SNI and peer-identity verification are resolved inside |
| 685 | + // DTLSEndpoint.connect(), which defaults both to the host argument (matching |
| 686 | + // Node.js TLS). The identity is enforced whenever the context verifies, i.e. |
| 687 | + // unless rejectUnauthorized is false. |
| 688 | + const session = endpoint.connect(context, host, port, options.servername); |
642 | 689 | // Mark that this session owns the endpoint so it gets closed |
643 | 690 | // automatically when the session closes, allowing process exit. |
644 | 691 | session.ownsEndpoint = true; |
|
0 commit comments