Skip to content

feat(NODE-7546): add HTTP Proxy support for QE & CSFLE - #5007

Open
tadjik1 wants to merge 9 commits into
mainfrom
NODE-7546
Open

feat(NODE-7546): add HTTP Proxy support for QE & CSFLE#5007
tadjik1 wants to merge 9 commits into
mainfrom
NODE-7546

Conversation

@tadjik1

@tadjik1 tadjik1 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Description

Summary of Changes

Adds kmsConnectCallback to AutoEncryptionOptions and ClientEncryptionOptions. When provided, the driver invokes the callback to establish the TCP connection to a KMS host instead of opening the socket itself, then wraps the returned socket with TLS using the KMS provider's configured TLS options. The primary use case is routing KMS requests through an HTTP proxy via the HTTP CONNECT method, which the existing SOCKS5 proxyOptions does not cover.

Spec: mongodb/specifications#1956

Release Highlight

HTTP proxy support for KMS requests in CSFLE and Queryable Encryption

In-use encryption can now route KMS requests through an HTTP proxy. Set kmsConnectCallback on your ClientEncryption or auto-encryption options to control how the driver connects to a KMS host. The callback receives the target host and port and returns a connected socket (for example, a tunnel
opened with HTTP CONNECT); the driver then performs the KMS TLS handshake over that socket using the provider's configured TLS options. This unblocks CSFLE and Queryable Encryption in environments that require an HTTP forward proxy for outbound KMS traffic, which the existing SOCKS5 proxyOptions does not cover.

const clientEncryption = new ClientEncryption(keyVaultClient, {
  keyVaultNamespace,
  kmsProviders,
  // Establish the KMS connection through your HTTP proxy; the driver adds TLS.
  kmsConnectCallback: ({ host, port }) => connectThroughHttpProxy(host, port)
});

Double check the following

  • Lint is passing (npm run check:lint)
  • Self-review completed using the steps outlined here
  • PR title follows the correct format: type(NODE-xxxx)[!]: description
    • Example: feat(NODE-1234)!: rewriting everything in coffeescript
  • Changes are covered by tests
  • New TODOs have a related JIRA ticket

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for customizing how KMS sockets are established for CSFLE/Queryable Encryption by introducing a kmsConnectCallback API (enabling HTTP CONNECT proxy tunneling) and wiring it through the CSFLE state machine, with accompanying unit + prose integration coverage.

Changes:

  • Introduces KMSConnectCallback (and related TLS/socket option types) in a new kms_options module and exports them from the public index.
  • Wires kmsConnectCallback through AutoEncrypter/ClientEncryption into the StateMachine KMS request path (including CSOT-aware behavior) and enforces mutual exclusivity with proxyOptions.
  • Adds unit tests and a new prose integration test (Prose test 28) validating callback invocation, proxy tunneling behavior, and timeout/error propagation.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/unit/client-side-encryption/state_machine.test.ts Adds unit coverage for CSOT behavior and new kmsConnectCallback socket establishment + timeout semantics.
test/unit/client-side-encryption/client_encryption.test.ts Adds unit coverage for proxyOptions vs kmsConnectCallback conflict validation in ClientEncryption.
test/unit/client-side-encryption/auto_encrypter.test.ts Adds unit coverage for proxyOptions vs kmsConnectCallback conflict validation in AutoEncrypter.
test/mongodb_all.ts Re-exports new kms_options types for tests.
test/integration/client-side-encryption/client_side_encryption.prose.28.kms_connect_callback.test.ts Adds prose integration coverage for KMS connect callback through HTTP/HTTPS proxy.
src/index.ts Publicly exports kms_options types and removes those type exports from state_machine.
src/client-side-encryption/state_machine.ts Implements kmsConnectCallback support and revises CSOT timeout handling for KMS requests.
src/client-side-encryption/kms_options.ts New module defining public KMS TLS/socket option types and KMSConnectCallback contract.
src/client-side-encryption/client_encryption.ts Stores/validates kmsConnectCallback and passes it into StateMachine.
src/client-side-encryption/auto_encrypter.ts Stores/validates kmsConnectCallback and passes it into StateMachine.

Comment thread src/client-side-encryption/state_machine.ts Outdated
const timeoutMS = Number.isFinite(remainingTimeMS) ? remainingTimeMS : undefined;
kmsRequestTimeout = timeoutMS ? Timeout.expires(timeoutMS) : undefined;
await (kmsRequestTimeout
? Promise.race([willResolveKmsRequest, kmsRequestTimeout])

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: .race() instead of .all() since the timeout is throwing on expire. This is fix for the pre-existing bug (test coverage also added).

}
}

/**

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is small refactoring, instead of adding one more Options type into StateMachine code I have extracted it into separate module kms_options.ts.

@tadjik1
tadjik1 marked this pull request as ready for review July 17, 2026 14:28
@tadjik1
tadjik1 requested a review from a team as a code owner July 17, 2026 14:28
@tadjik1
tadjik1 requested a review from nbbeeken July 17, 2026 14:28
Comment thread src/client-side-encryption/kms_options.ts Outdated
@dariakp dariakp added the Blocked Blocked on other work label Jul 17, 2026
nbbeeken
nbbeeken previously approved these changes Jul 17, 2026

@nbbeeken nbbeeken left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies for the piecemeal review, noticed this on the second read. LGTM either way

Comment thread src/client-side-encryption/state_machine.ts

@dariakp dariakp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this PR must wait for mongodb/specifications#1956 to merge before it can be fully reviewed and merged. (Will resolve this comment once this PR is unblocked).

@dariakp
dariakp marked this pull request as draft July 20, 2026 14:54
@dariakp
dariakp requested a review from PavelSafronov July 20, 2026 14:55
host: socketOptions.host,
port: socketOptions.port,
timeoutMS,
signal: controller.signal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the signal also include options.signal? That's an optional signal that is honored elsewhere (line 450), but we're ignore it here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, addressed!

const timeoutError = new MongoOperationTimeoutError('KMS request timed out');
// Abort with the timeout error as the reason so the callback sees it on `signal.reason`.
controller.abort(timeoutError);
throw timeoutError;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If connectPromise eventually (after timeout) returns a socket, that socket will never be closed. Consider something like to close the socket:

connectPromise.then(
  sock => { if (controller.signal.aborted) sock.destroy(); },
  () => {} // nothing to cleanup
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THanks! Added.

@tadjik1 tadjik1 removed the Blocked Blocked on other work label Jul 28, 2026
@tadjik1
tadjik1 marked this pull request as ready for review July 28, 2026 11:17
nbbeeken
nbbeeken previously approved these changes Jul 28, 2026
PavelSafronov
PavelSafronov previously approved these changes Jul 28, 2026

@PavelSafronov PavelSafronov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New changes look good, approving and moving to team review.

@PavelSafronov PavelSafronov self-assigned this Jul 28, 2026
@PavelSafronov PavelSafronov added the Team Review Needs review from team label Jul 28, 2026
const timeoutMS = Number.isFinite(remainingTimeMS) ? remainingTimeMS : undefined;
const controller = new AbortController();
// Forward the operation's own abort signal so aborting the operation also aborts the callback.
const forwardAbort = addAbortListener(options?.signal, function () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a guard here to protect against already-aborted signals? E.g: something like:
options?.signal?.throwIfAborted() before this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, great catch. I updated all paths to respect already expired timeout (test coverage added as well).

if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) {
if (this.options.kmsConnectCallback) {
const remainingTimeMS = options?.timeoutContext?.csotEnabled()
? options.timeoutContext.getRemainingTimeMSOrThrow('KMS request timed out')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we include the timeoutMS in the throw reason? The default message includes it, so technically this is less informative.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added.

const timeout = timeoutMS ? Timeout.expires(timeoutMS) : undefined;
try {
socketOptions.socket = timeout
? await Promise.race([connectPromise, timeout])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: I'm noticing here we're exclusively relying on the user to unblock (either with a supplied CSOT timeout or by resolving from kmsConnectCallback()). The timeout here can be very long (or infinite, with a timeoutMS of 0). This makes me worry about open handles (http requests) dangling beyond the lifetime of parent objects (e.g: a client closure). Probably a separate PR, but I think it's worth considering.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call, I will request spec owners to update spec, I guess the legacy timeout might fit here just fine.

@tadjik1
tadjik1 dismissed stale reviews from PavelSafronov and nbbeeken via 6f3140c July 29, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team Review Needs review from team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants