Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 343 additions & 17 deletions doc/api/quic.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/internal/blocklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,5 @@ ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype);
module.exports = {
BlockList,
InternalBlockList,
kHandle,
};
126 changes: 118 additions & 8 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ if (!process.features.quic || !getOptionValue('--experimental-quic')) {
}

const { inspect } = require('internal/util/inspect');
const {
BlockList,
kHandle: kBlockListHandle,
} = require('internal/blocklist');

let debug = require('internal/util/debuglog').debuglog('quic', (fn) => {
debug = fn;
Expand Down Expand Up @@ -183,6 +187,7 @@ const {
kGoaway,
kHandshake,
kHandshakeCompleted,
kVerifyPeer,
kHeaders,
kOwner,
kRemoveSession,
Expand Down Expand Up @@ -307,8 +312,18 @@ const endpointRegistry = new SafeSet();
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
* @property {bigint|number} [maxRetries] The maximum number of retries
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
* @property {number} [retryRate] Global rate limit for retry packets (per second)
* @property {number} [retryBurst] Burst capacity for retry rate limiter
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
* @property {net.BlockList} [blockList] Block list for filtering incoming packets by source address
* @property {'deny'|'allow'} [blockListPolicy='deny'] How to interpret the block list
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
Expand Down Expand Up @@ -374,6 +389,7 @@ const endpointRegistry = new SafeSet();
* @property {number} [version] The QUIC version
* @property {number} [minVersion] The minimum acceptable QUIC version
* @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy
* @property {'strict'|'auto'|'manual'} [verifyPeer='auto'] Peer certificate verification policy (client only)
* @property {ApplicationOptions} [application] The application options
* @property {TransportParams} [transportParams] The transport parameters
* @property {string} [servername] The server name identifier (client only)
Expand Down Expand Up @@ -420,6 +436,7 @@ const endpointRegistry = new SafeSet();
* @property {number} [drainingPeriodMultiplier] Multiplier applied to the
* draining period (3 * PTO) used by ngtcp2. Range `3..255`.
* **Default:** `3`.
* @property {bigint|number} [streamIdleTimeout] Time in ms before idle peer-initiated streams are destroyed
* @property {number} [maxDatagramSendAttempts] Maximum number of times a
* datagram is retried before being abandoned. Range `1..255`.
* **Default:** `5`.
Expand Down Expand Up @@ -906,6 +923,17 @@ setCallbacks({
// from QuicError::ToV8Value. Convert to a proper Node.js Error.
if (error !== undefined) {
error = convertQuicError(error);
} else if (this[kOwner] && !this[kOwner].destroyed) {
// The stream is closing cleanly, but it may have been reset by the
// peer (ReceiveStreamReset) or locally (resetStream). The C++ side
// records the reset code in state.resetCode. If set, surface the
// reset as the close error so stream.closed rejects -- the reset
// was an abnormal termination even if the session closed cleanly.
const resetCode = getQuicStreamState(this[kOwner]).resetCode;
if (resetCode !== undefined && resetCode > 0n) {
error = new ERR_QUIC_APPLICATION_ERROR(
resetCode, `stream reset with code ${resetCode}`);
}
}
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
this[kOwner][kFinishClose](error);
Expand Down Expand Up @@ -2620,6 +2648,11 @@ class QuicSession {
onkeylog: undefined,
onqlog: undefined,
pendingQlog: undefined,
// Default to 'manual' (no auto-rejection). Client sessions override
// this via kVerifyPeer in kConnect. Server sessions keep 'manual'
// because server-side cert validation is handled by rejectUnauthorized
// at the C++ level.
verifyPeer: 'manual',
handshakeInfo: undefined,
/** @type {QuicSessionPath|undefined} */
path: undefined,
Expand Down Expand Up @@ -3836,6 +3869,22 @@ class QuicSession {
safeCallbackInvoke(inner.onhandshake, this, info);
}

// In 'auto' mode, reject the connection if peer certificate validation
// failed. In 'manual' mode, resolve regardless and let the application
// decide. In 'strict' mode, the handshake already failed at the C++
// level (SSL_VERIFY_PEER) so we won't reach here.
if (inner.verifyPeer === 'auto' && validationErrorReason !== undefined) {
const err = new ERR_QUIC_TRANSPORT_ERROR(
0, `Peer certificate validation failed: ${validationErrorReason}` +
` [${validationErrorCode}]`);
inner.pendingOpen.reject?.(err);
inner.pendingOpen.resolve = undefined;
inner.pendingOpen.reject = undefined;
inner.handshakeCompleted = true;
this.destroy();
return;
}

inner.pendingOpen.resolve?.(info);
inner.pendingOpen.resolve = undefined;
inner.pendingOpen.reject = undefined;
Expand All @@ -3847,6 +3896,14 @@ class QuicSession {
return this.#inner.handshakeCompleted;
}

get [kVerifyPeer]() {
return this.#inner.verifyPeer;
}

set [kVerifyPeer](value) {
this.#inner.verifyPeer = value;
}

/**
* @param {object} handle
* @param {number} direction
Expand Down Expand Up @@ -3997,10 +4054,20 @@ class QuicEndpoint {
tokenExpiration,
maxConnectionsPerHost = 100,
maxConnectionsTotal = 10_000,
maxStatelessResetsPerHost,
disableStatelessReset,
addressLRUSize,
maxRetries,
retryRate,
retryBurst,
statelessResetRate,
statelessResetBurst,
versionNegotiationRate,
versionNegotiationBurst,
immediateCloseRate,
immediateCloseBurst,
sessionCreationRate,
sessionCreationBurst,
blockList,
blockListPolicy = 'deny',
rxDiagnosticLoss,
txDiagnosticLoss,
udpReceiveBufferSize,
Expand All @@ -4015,6 +4082,16 @@ class QuicEndpoint {
tokenSecret,
} = options;

if (blockList !== undefined) {
if (!BlockList.isBlockList(blockList)) {
throw new ERR_INVALID_ARG_TYPE('options.blockList',
'net.BlockList', blockList);
}
}

validateOneOf(blockListPolicy, 'options.blockListPolicy',
['deny', 'allow']);

// All of the other options will be validated internally by the C++ code
if (address !== undefined && !SocketAddress.isSocketAddress(address)) {
if (typeof address === 'string') {
Expand All @@ -4034,10 +4111,21 @@ class QuicEndpoint {
// Connection limits are set on the state buffer, not passed to C++.
maxConnectionsPerHost,
maxConnectionsTotal,
maxStatelessResetsPerHost,
disableStatelessReset,
addressLRUSize,
maxRetries,
retryRate,
retryBurst,
statelessResetRate,
statelessResetBurst,
versionNegotiationRate,
versionNegotiationBurst,
immediateCloseRate,
immediateCloseBurst,
sessionCreationRate,
sessionCreationBurst,
// Pass the C++ handle, not the JS BlockList wrapper.
blockList: blockList?.[kBlockListHandle],
blockListPolicy,
rxDiagnosticLoss,
txDiagnosticLoss,
udpReceiveBufferSize,
Expand Down Expand Up @@ -4282,6 +4370,10 @@ class QuicEndpoint {
// Set callbacks before any async work to avoid missing events
// that fire during or immediately after the handshake.
applyCallbacks(session, options);
// Store the verifyPeer policy for use in the handshake handler.
if (options.verifyPeer !== undefined) {
session[kVerifyPeer] = options.verifyPeer;
}
return session;
}

Expand Down Expand Up @@ -4919,7 +5011,7 @@ function processSessionOptions(options, config = kEmptyObject) {
reuseEndpoint = true,
version,
minVersion,
preferredAddressPolicy = 'default',
preferredAddressPolicy = 'ignore',
transportParams = kEmptyObject,
qlog = false,
sessionTicket,
Expand All @@ -4935,6 +5027,8 @@ function processSessionOptions(options, config = kEmptyObject) {
datagramDropPolicy = 'drop-oldest',
drainingPeriodMultiplier = 3,
maxDatagramSendAttempts = 5,
streamIdleTimeout,
verifyPeer = 'auto',
// HTTP/3 application-specific options. Nested under `application`
// to separate protocol-specific settings from transport-level ones.
application = kEmptyObject,
Expand Down Expand Up @@ -4981,6 +5075,9 @@ function processSessionOptions(options, config = kEmptyObject) {
validateOneOf(datagramDropPolicy, 'options.datagramDropPolicy',
['drop-oldest', 'drop-newest']);

validateOneOf(verifyPeer, 'options.verifyPeer',
['strict', 'auto', 'manual']);

validateInteger(drainingPeriodMultiplier, 'options.drainingPeriodMultiplier',
3, 255);

Expand Down Expand Up @@ -5030,7 +5127,19 @@ function processSessionOptions(options, config = kEmptyObject) {
preferredAddressIpv4: preferredAddressIpv4?.[kSocketAddressHandle],
preferredAddressIpv6: preferredAddressIpv6?.[kSocketAddressHandle],
},
tls: processTlsOptions(options, forServer),
tls: {
...processTlsOptions(options, forServer),
// Forward strict mode to C++ so SSL_VERIFY_PEER is set on the
// client SSL_CTX. For 'auto' and 'manual' modes, the handshake
// completes regardless and the result is handled in JS.
verifyPeerStrict: verifyPeer === 'strict',
// Enable hostname verification for 'strict' and 'auto' modes.
// SSL_set1_host tells OpenSSL to verify the server certificate's
// SAN/CN matches the servername. Without this, a valid cert for
// any domain would be accepted.
verifyHostname: verifyPeer !== 'manual',
},
verifyPeer,
qlog,
maxPayloadSize,
unacknowledgedPacketThreshold,
Expand All @@ -5045,6 +5154,7 @@ function processSessionOptions(options, config = kEmptyObject) {
datagramDropPolicy,
drainingPeriodMultiplier,
maxDatagramSendAttempts,
streamIdleTimeout,
application,
onerror,
onstream,
Expand Down
Loading
Loading