Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
81 changes: 79 additions & 2 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ static int us_ssl_pending_keylog_idx = -1;
* received NewSessionTicket, so SSL_get_session() alone gives an unresumable
* snapshot; node:tls's getSession()/getTLSTicket() read from here instead. */
static int us_ssl_new_session_ref_idx = -1;
/* Optional per-SSL sink for resumable sessions: an owner pointer plus a
* callback that receives each SSL_SESSION_up_ref'd session. Checked before the
* us_ssl_is_socket_ex_idx opt-in so consumers that don't surface a JS
* 'session' event (fetch) can still cache without paying the serialized
* pending-session queue. */
static int us_ssl_session_sink_idx = -1;
#ifdef _WIN32
static INIT_ONCE us_ex_idx_once = INIT_ONCE_STATIC_INIT;
#else
Expand Down Expand Up @@ -282,6 +288,20 @@ static void us_ssl_new_session_ref_free(void *parent, void *ptr, CRYPTO_EX_DATA
(void)parent; (void)ad; (void)index; (void)argl; (void)argp;
if (ptr) SSL_SESSION_free((SSL_SESSION *)ptr);
}

struct us_ssl_session_sink_t {
void *owner;
void (*on_new_session)(void *owner, SSL_SESSION *session);
void (*on_free)(void *owner);
};
static void us_ssl_session_sink_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
int index, long argl, void *argp) {
(void)parent; (void)ad; (void)index; (void)argl; (void)argp;
if (!ptr) return;
struct us_ssl_session_sink_t *sink = ptr;
if (sink->on_free) sink->on_free(sink->owner);
us_free(sink);
}
/* NSS key-log lines are produced from inside SSL_do_handshake/SSL_read, so
* they are parked on the SSL the same way new sessions are and delivered once
* the read unwinds. The stored bytes already carry the trailing newline Node
Expand Down Expand Up @@ -334,12 +354,21 @@ static void ssl_flush_pending_keylog(struct us_socket_t *s) {
}

static int us_ssl_new_session_cb(SSL *ssl, SSL_SESSION *session) {
/* The session sink is the cheap path: hand the session to an owner-provided
* callback (one up_ref, no i2d serialize, no queue). Used by the HTTP
* client's per-origin session cache. */
struct us_ssl_session_sink_t *sink = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
if (sink && sink->on_new_session) {
SSL_SESSION_up_ref(session);
sink->on_new_session(sink->owner, session);
return 0;
}
/* Park only for consumers that will drain the queue: SSLs attached to a
* real us_socket_t (flushed into us_dispatch_session once the read unwinds)
* and SSLs whose owner opted in via us_ssl_enable_pending_events (the
* Rust SSLWrapper behind TLS-over-duplex / named pipes, which polls
* us_ssl_pop_pending_session after its reads). Everything else (fetch,
* WebSocket tunnels) has no consumer - don't queue. */
* us_ssl_pop_pending_session after its reads). Everything else (WebSocket
* tunnels) has no consumer - don't queue. */
if (!SSL_get_ex_data(ssl, us_ssl_is_socket_ex_idx)) {
return 0;
}
Expand Down Expand Up @@ -421,6 +450,7 @@ static void us_ex_idx_init(void) {
us_ssl_pending_session_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_pending_session_free);
us_ssl_pending_keylog_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_pending_session_free);
us_ssl_new_session_ref_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_new_session_ref_free);
us_ssl_session_sink_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_session_sink_free);
}

#ifdef _WIN32
Expand All @@ -444,6 +474,53 @@ static inline int us_ssl_ctx_ex_idx(void) {
return us_ctx_ex_idx;
}

/* Install a session sink on `ssl`: each resumable session reaching the
* new-session callback is SSL_SESSION_up_ref'd and passed to `on_new_session`
* (which takes ownership of that reference). `on_free(owner)` runs once on
* SSL_free. Replacing an existing sink first frees the old one. */
void us_ssl_set_session_sink(SSL *ssl, void *owner,
void (*on_new_session)(void *, SSL_SESSION *),
void (*on_free)(void *)) {
us_ex_idx_ensure();
struct us_ssl_session_sink_t *sink = us_malloc(sizeof(*sink));
if (!sink) {
if (on_free) on_free(owner);
return;
}
sink->owner = owner;
sink->on_new_session = on_new_session;
sink->on_free = on_free;
struct us_ssl_session_sink_t *old = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
SSL_set_ex_data(ssl, us_ssl_session_sink_idx, sink);
if (old) {
if (old->on_free) old->on_free(old->owner);
us_free(old);
}
}

/* The `owner` pointer installed via us_ssl_set_session_sink, or NULL. */
void *us_ssl_get_session_sink_owner(SSL *ssl) {
if (us_ssl_session_sink_idx < 0) return NULL;
struct us_ssl_session_sink_t *sink = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
return sink ? sink->owner : NULL;
}

/* Free the session sink on every socket in `group`. Called at process shutdown
* so LeakSanitizer doesn't report sinks on SSLs that survive until exit (the
* HTTP thread's keep-alive pool is never drained, and its TLS-rooted owner is
* not scanned as an LSAN root). */
void us_socket_group_clear_session_sinks(struct us_socket_group_t *group) {
if (!group || us_ssl_session_sink_idx < 0) return;
for (struct us_socket_t *s = group->head_sockets; s; s = s->next) {
if (!s->ssl) continue;
struct us_ssl_session_sink_t *sink = SSL_get_ex_data(s_ssl(s), us_ssl_session_sink_idx);
if (!sink) continue;
SSL_set_ex_data(s_ssl(s), us_ssl_session_sink_idx, NULL);
if (sink->on_free) sink->on_free(sink->owner);
us_free(sink);
}
}

/* TLS-over-duplex / named-pipe owners (the Rust SSLWrapper): opt this SSL
* into the parked session/keylog queues so us_ssl_new_session_cb /
* us_ssl_keylog_cb collect them. There is no us_socket_t to flush into
Expand Down
8 changes: 8 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,14 @@ int us_ssl_pop_pending_keylog(struct ssl_st *ssl, unsigned char *out, int out_ca
/* The resumable session most recently delivered via the new-session callback,
* or NULL if none. Borrowed; valid until the next NewSessionTicket or SSL_free. */
struct ssl_session_st *us_ssl_get_new_session(struct ssl_st *ssl);
/* Per-SSL session sink: each resumable session reaching the new-session
* callback is SSL_SESSION_up_ref'd and handed to on_new_session (which takes
* ownership of that reference). on_free(owner) runs once on SSL_free. */
void us_ssl_set_session_sink(struct ssl_st *ssl, void *owner,
void (*on_new_session)(void *, struct ssl_session_st *),
void (*on_free)(void *));
void *us_ssl_get_session_sink_owner(struct ssl_st *ssl);
void us_socket_group_clear_session_sinks(struct us_socket_group_t *group);

/* Public interfaces for loops */

Expand Down
1 change: 1 addition & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ pub mod feature_flag {
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, "BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE, "BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE, "BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO", {});
// Force the event loop to use epoll_pwait(2) instead of epoll_pwait2(2).
// Escape hatch for seccomp policies that block syscall 441 without
Expand Down
10 changes: 9 additions & 1 deletion src/http/HTTPContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use bun_uws as uws;
bun_core::declare_scope!(HTTPContext, hidden);

const POOL_SIZE: usize = 64;
const MAX_KEEPALIVE_HOSTNAME: usize = 128;
pub(crate) const MAX_KEEPALIVE_HOSTNAME: usize = 128;

/// The const-generic `SSL` is load-bearing for monomorphization (gates hot
/// inner-loop branches); do not demote to a runtime bool.
Expand Down Expand Up @@ -55,6 +55,8 @@ pub struct HTTPContext<const SSL: bool> {
// into the box interior; unboxing would dangle it on `Vec` realloc.
#[expect(clippy::vec_box)]
pub(crate) pending_h2_connects: Vec<Box<h2::PendingConnect>>,
/// Client-side TLS session cache; populated only when `SSL`.
pub(crate) session_cache: crate::session_cache::SessionCache,
}

// Intrusive refcount:
Expand Down Expand Up @@ -1177,6 +1179,12 @@ impl<const SSL: bool> Handler<SSL> {
// `client` again.
return;
}
// Peer chain + hostname verified: let the session sink
// flush its pending TLS 1.2 ticket (parked before this
// dispatch) and cache later TLS 1.3 tickets directly.
// SAFETY: `ssl` is the live handle for this socket on the
// HTTP thread.
unsafe { crate::session_cache::arm(ssl) };
}

return client.first_call::<SSL>(socket);
Expand Down
7 changes: 7 additions & 0 deletions src/http/HTTPThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
},
https_context: NewHttpContext::<true> {
ref_count: Cell::new(1),
Expand All @@ -170,6 +171,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
},
lazy_https_init: None,
queued_tasks: Queue::new(),
Expand Down Expand Up @@ -530,6 +532,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
}));
if let Err(err) = custom_context.init_with_client_config(client) {
// `init_with_client_config` fails before `group.init()` runs.
Expand Down Expand Up @@ -1066,6 +1069,10 @@ impl HttpThread {
);
}
}
crate::session_cache::drain_for_exit(&mut self.https_context);
for entry in custom_ssl_context_map().values_mut() {
crate::session_cache::drain_for_exit(entry.ctx_mut());
}
}

pub(crate) fn wakeup(&self) {
Expand Down
24 changes: 24 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub mod lshpack;
pub mod proxy_tunnel;
#[path = "SendFile.rs"]
pub mod send_file;
#[path = "session_cache.rs"]
pub mod session_cache;
#[path = "Signals.rs"]
pub mod signals;
#[path = "ThreadSafeStreamBuffer.rs"]
Expand Down Expand Up @@ -1851,6 +1853,28 @@ impl<'a> HTTPClient<'a> {
host_z,
self.alpn_offer(),
);

if crate::session_cache::eligible(self) {
let want_tunnel = self.http_proxy.is_some() && self.url.is_https();
// SAFETY: `ssl_ptr` is live and pre-handshake (guarded by
// `SSL_is_init_finished == 0` above); `get_ssl_ctx` returns
// the static `https_context` or the heap context this
// client holds a strong ref on, both of which outlive
// every SSL attached to their socket group.
unsafe {
crate::session_cache::install(
ssl_ptr,
self.get_ssl_ctx::<true>(),
self.connected_url.hostname,
self.connected_url.get_port_auto(),
if want_tunnel || self.http_proxy.is_none() {
self.proxy_auth_hash()
} else {
0
},
);
}
}
}
} else {
self.first_call::<IS_SSL>(socket);
Expand Down
Loading
Loading