Skip to content
39 changes: 15 additions & 24 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,30 +492,21 @@ function onClientHandshake(self, socket, success, verifyError) {
}
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1081-L1090
if (self.destroyed) return;
// https://github.com/nodejs/node/blob/v26.3.0/src/crypto/crypto_tls.cc#L1203-L1213
if (!success && verifyError == null && self.writableFinished) {
self.secureConnecting = false;
return;
}
// The second argument is "authorized" (handshake + verification +
// hostname), matching the public Bun.connect handshake callback. node:tls
// decides what to do with verification results in JS via the
// rejectUnauthorized / checkServerIdentity handling below, so a
// verification-class result (an X509 code such as
// UNABLE_TO_VERIFY_LEAF_SIGNATURE, or the native hostname verdict) still
// means the TLS session itself was established. Only a fatal TLS protocol
// failure tears the socket down here: those arrive as EPROTO carrying the
// OpenSSL "error:...:SSL routines:..." reason (or an already decomposed
// ERR_SSL_* / ERR_OSSL_* code).
const isProtocolFailure =
!success &&
verifyError?.code != null &&
(verifyError.code === "EPROTO" || /^ERR_(SSL|OSSL)_/.test(verifyError.code));
if (isProtocolFailure) {
// Surface the OpenSSL reason instead of letting the close path report a
// generic disconnect.
self.destroy(tlsHandshakeError(verifyError));
return;
if (!success) {
const code = verifyError?.code;
if (code === "EPROTO" || /^ERR_(SSL|OSSL)_/.test(code)) {
// Surface the OpenSSL reason instead of letting the close path report a
// generic disconnect.
self.destroy(tlsHandshakeError(verifyError));
return;
}
// Only a chain that this client refuses goes on: it gets its X509 error below.
if (code == null || !self._rejectUnauthorized) {
// https://github.com/nodejs/node/blob/v26.3.0/src/crypto/crypto_tls.cc#L1203-L1213
if (self.writableFinished) self.secureConnecting = false;
else onConnectEnd.$call(self);
return;
}
}

self._securePending = false;
Expand Down
21 changes: 10 additions & 11 deletions src/runtime/socket/socket_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1793,7 +1793,12 @@ impl<const SSL: bool> NewSocket<SSL> {
&& success == 0
&& ssl_error.error_no == uws::us_bun_verify_error_t::HOSTNAME_MISMATCH;

if SSL && (authorized || rejected_in_handshake) && !this.acts_as_tls_server() {
let flags = this.flags.get();
if SSL
&& (authorized || rejected_in_handshake)
&& !flags.contains(Flags::DEFERS_SERVER_IDENTITY)
&& !this.acts_as_tls_server()
{
if let Some(ssl_ptr) = this.socket.get().ssl() {
let hostname = this.server_identity_hostname();
if rejected_in_handshake
Expand Down Expand Up @@ -1830,9 +1835,6 @@ impl<const SSL: bool> NewSocket<SSL> {
})
});

// node:tls sockets defer the hostname verdict: their JS layer applies
// `checkServerIdentity` (default or user override) itself.
let flags = this.flags.get();
// node:tls closes its own sockets, and nothing marks the ones its servers accept.
let failed_native_client = SSL
&& success == 0
Expand All @@ -1845,9 +1847,7 @@ impl<const SSL: bool> NewSocket<SSL> {
// handshake never has a usable transport, so fail closed on every
// failure flavor).
let reject_unauthorized = flags.contains(Flags::REJECT_UNAUTHORIZED)
&& (verify_failed
|| failed_native_client
|| (hostname_mismatch && !flags.contains(Flags::DEFERS_SERVER_IDENTITY)));
&& (verify_failed || failed_native_client || hostname_mismatch);
// A handshake that failed outright (success == 0 with no policy
// verdict: protocol error, peer alert, EOF mid-handshake) never has a
// usable transport either — node destroys the underlying socket before
Expand Down Expand Up @@ -1925,8 +1925,7 @@ impl<const SSL: bool> NewSocket<SSL> {
} else {
// call handhsake callback with authorized and authorization error if has one
let authorization_error: JSValue = if ssl_error.error_no == 0 || rejected_in_handshake {
// node:tls (DEFERS) builds its own identity error in JS.
if hostname_mismatch && !flags.contains(Flags::DEFERS_SERVER_IDENTITY) {
if hostname_mismatch {
this.stored_verify_error_to_js(&global)
.unwrap_or(JSValue::NULL)
} else {
Expand Down Expand Up @@ -4197,8 +4196,8 @@ bitflags::bitflags! {
const HOSTNAME_MISMATCH = 1 << 11;
const REJECT_UNAUTHORIZED = 1 << 12;
/// Set only by the node:net / node:tls socket constructors: their JS
/// layer owns server-identity policy (`checkServerIdentity`), so a
/// hostname mismatch alone is reported but never enforced natively.
/// layer owns server-identity policy (`checkServerIdentity`), so the
/// native name check does not run for them.
const DEFERS_SERVER_IDENTITY = 1 << 13;
/// `upgradeTLS({ isServer: true })`: the socket acts as the TLS server
/// even though its `Handlers` mode is `Client` (no listener), so the
Expand Down
Loading
Loading