Skip to content

Commit 5ab44a0

Browse files
danlapidclaude
andcommitted
Rust HTTP backend: kj's HTTP interfaces on hyper, rustls and tungstenite
Under --//:io_backend=rust, workerd's HTTP/1.1 client and server, WebSockets, CONNECT tunnels and TLS run in Rust on hyper, hyper-util, rustls and tungstenite's frame codec. workerd is still written against kj's HTTP interfaces (kj::HttpClient, HttpService, HttpServer, WebSocket, SecureNetworkWrapper); only their implementations change, by symbol override, as the I/O layer does. Product code is unchanged, and the default C++ build is untouched. Layout: - kj-hyper (src/rust/cxx/kj-hyper, ~4.3k lines): hyper's pooling and single-stream clients, hyper's server fused into one kj event per connection, the WebSocket frame loop with permessage-deflate (flate2/zlib-rs), rustls TLS, and the kj <-> tokio stream bridges, with a thin C++ seam. - kj-http-tokio.c++: the kj:: HTTP entry points over kj-hyper. - tls-network: config::TlsOptions as a kj::SecureNetworkWrapper for both engines, so server.c++ has no per-backend #if. - src/rust/kj: the kj crate links kj-http-types only, and gains header iteration and a batch add for kj-hyper. - capnproto (companion change): kj-http-types holds everything that is not kj's wire implementation -- header codec, permessage-deflate negotiation, WebSocket pump and pipe, client/service adapters -- so this backend reuses kj's own code for those and links without kj-http-impl. Behavior follows kj's contracts where workerd relies on them: - Wire fidelity: header spellings (hyper HeaderCaseMap patch), reason phrases, kj's Content-Length and Connection: close rules, drain finishing a partially received request. - Errors: HttpServerErrorHandler/WebSocketErrorHandler run as in kj's server; kj exceptions survive the trip through hyper. - WebSocket: text passes through unvalidated, the application owns Close, maxSize is checked from the frame header, pongs queue behind a send in flight, wire byte counters. The server refuses bad handshakes with 400, or 426 with Sec-WebSocket-Version for an unsupported version (RFC 6455 4.2.2); the client validates Sec-WebSocket-Accept and takes its key from the configured EntropySource. - Disconnects: whenWriteDisconnected() and whenAborted() resolve on peer hangup, and a peer that disconnects cancels its connection's service calls. - Server timeouts: headerTimeout applies to each request head. - TLS: trustBrowserCas uses the platform verifier, otherwise only trustedCertificates are trusted (also directly, as OpenSSL); minVersion TLS 1.3 and cipherList's TLS 1.2 suites are honored. Crate patches: http/httparse lenient header values, hyper's public header case map, graceful shutdown finishing a partial request, lazily read client response bodies. Tests: kj-http-test (48) and tls-network-test (14) check kj's HTTP and TLS entry points against the wire and the TLS policy, and run unchanged on both backends. kj-hyper_test covers the deflate codec (RFC 7692 vectors) and the TLS option mapping. server-test drives its loop with a TokioEventPort, gains a WebSocket compression case, and TestServer::wait() lets events due at its final instant settle. bazel test //src/... passes under the rust backend; the cxx build's server-test, kj-http-test and tls-network-test pass. Requires the capnproto kj-http split (kj-http-types). Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent e7b231c commit 5ab44a0

48 files changed

Lines changed: 7718 additions & 176 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BUILD.bazel

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,18 @@ bool_flag(
131131
# =============================================================================
132132
# I/O backend selection: a build/compile/link-time hermeticity guarantee.
133133
#
134-
# --//:io_backend=rust (default) -- the rust I/O layer: the process event loop is tokio
135-
# (kj-rs-tokio) and every socket/stream is tokio-backed (kj-rs-io); the
136-
# C++ layers above the streams (kj-http, kj-tls, capnp-rpc) are unchanged
137-
# and run over those tokio streams. In this config kj's own OS-I/O
138-
# layer (kj-async-os) must be ABSENT from the link: checked on the
139-
# dependency graph by build/rust_io_graph_check.sh and on the linked
134+
# --//:io_backend=rust (default) -- the rust I/O and HTTP/TLS layer: the process event loop is
135+
# tokio (kj-rs-tokio), every socket/stream is tokio-backed (kj-rs-io),
136+
# and kj's HTTP and TLS interfaces are implemented on hyper and rustls
137+
# (kj-hyper). capnp-rpc remains the unchanged C++ implementation, running
138+
# over tokio-backed streams. In this config kj's own OS-I/O layer
139+
# (kj-async-os), kj's HTTP implementation (kj-http-impl) and kj-tls must
140+
# be ABSENT from the link: checked on the dependency graph by
141+
# build/rust_io_graph_check.sh, and for kj-async-os also on the linked
140142
# binary by //src/workerd/server:rust-io-link-check.
141-
# --//:io_backend=cxx -- workerd's I/O is the concrete C++ stack end to end, including the
142-
# kj OS event loop and sockets. Byte-identical to the pre-migration
143-
# build.
143+
# --//:io_backend=cxx -- workerd's I/O is the concrete C++ stack end to end: kj-http, kj-tls
144+
# and the kj OS event loop and sockets. Byte-identical to the
145+
# pre-migration build.
144146
#
145147
# Code that must differ per backend keys off the WORKERD_RUST_IO_BACKEND_RUST define (see
146148
# //src/workerd/util:setup-async-io) or off the config_settings below in a select().

build/deps/rust.MODULE.bazel

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,42 @@ crate.annotation(
5757
crate = "capnpc",
5858
gen_binaries = ["capnpc-rust"],
5959
)
60+
61+
# Header-value leniency patches for the Rust HTTP path (kj-hyper / hyper): KJ, following the
62+
# browsers' lead, forbids only NUL, CR, and LF in header values, while the `http` crate's
63+
# HeaderValue constructors and `httparse`'s wire parser additionally reject the other C0
64+
# control bytes and DEL. workerd must round-trip such values byte-for-byte in both directions
65+
# (WPT fetch/api headers/header-values*.any.js asserts exactly this), so both validators are
66+
# relaxed to KJ's rule. CR/LF stay forbidden, so header injection/splitting stays impossible.
67+
crate.annotation(
68+
crate = "http",
69+
patch_args = ["-p1"],
70+
patches = ["//:patches/rust/http-kj-lenient-header-values.patch"],
71+
)
72+
crate.annotation(
73+
crate = "httparse",
74+
patch_args = ["-p1"],
75+
patches = ["//:patches/rust/httparse-kj-lenient-header-values.patch"],
76+
)
77+
78+
# Header-name case fidelity: kj-http writes header names in exactly the case the application
79+
# supplied, and workerd's tests (and some peers) assert the raw bytes. hyper's encoder already
80+
# honors a per-message HeaderCaseMap extension (falling back to title-casing), but upstream
81+
# only populates it for forwarded messages; this patch makes the type constructible so the
82+
# kj<->hyper translation layer can populate it for locally-built requests/responses.
83+
#
84+
# Graceful shutdown keeps a connection with a partially received request open until that request
85+
# is answered, as kj's HttpServer::drain() does; upstream closes it as idle. And a client keeps a
86+
# connection out of the pool until the response body is consumed, as kj's HttpClient does.
87+
crate.annotation(
88+
crate = "hyper",
89+
patch_args = ["-p1"],
90+
patches = [
91+
"//:patches/rust/hyper-public-header-case-map.patch",
92+
"//:patches/rust/hyper-graceful-shutdown-partial-request.patch",
93+
"//:patches/rust/hyper-client-lazy-response-body.patch",
94+
],
95+
)
6096
crate.from_cargo(
6197
# TODO(cleanup): This name is a bit misleading now that we don't vendor. We can clean it up as a
6298
# followup.

build/rust_io_backend.bzl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
33
The dependency side of the backend switch lives in //src/rust/cxx/kj-rs-io:active-backend (one
44
select over the per-backend deps); this file carries the compile-time side: the define read by
5-
the two translation units that have a per-backend arm.
5+
the translation units that have a per-backend arm.
66
7-
Hermeticity of the rust config (kj-async-os, kj's own event loop and sockets, must be ABSENT from
8-
the workerd link, or its kj::setupAsyncIo() / kj::UnixEventPort definitions collide with the tokio
9-
shim's and the linker silently keeps whichever archive it meets first) is checked in two places:
10-
* build/rust_io_graph_check.sh -- one `bazel cquery somepath(...)` over the dependency graph
7+
Hermeticity of the rust config is checked in two places. kj-async-os (kj's own event loop and
8+
sockets) must be ABSENT from the workerd link, or its kj::setupAsyncIo() / kj::UnixEventPort
9+
definitions collide with the tokio shim's and the linker silently keeps whichever archive it meets
10+
first; likewise kj-http-impl (kj's HTTP/1.1 implementation, whose kj:: symbols
11+
//src/workerd/util:kj-http defines over hyper) and kj-tls (replaced by rustls).
12+
* build/rust_io_graph_check.sh -- `bazel cquery somepath(...)` over the dependency graph
1113
(`just check-io-backend-graph`; the lint CI lane runs it);
1214
* //src/workerd/server:rust-io-link-check -- the linked binary's symbol names.
1315
"""

build/rust_io_graph_check.sh

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
#!/bin/bash
22
# Dependency-graph check for the Rust I/O backend: under --//:io_backend=rust, the workerd binary
3-
# must not reach kj-async-os (kj's own event loop and sockets), directly or through the
4-
# @capnp-cpp//src/kj:kj-async umbrella. If it does, kj::setupAsyncIo() and kj::UnixEventPort's
5-
# members are defined twice -- by kj and by the tokio shim (//src/workerd/util:setup-async-io) --
6-
# and with static archives the linker keeps whichever it meets first, silently.
3+
# must not reach
4+
# * kj-async-os (kj's own event loop and sockets), directly or through the
5+
# @capnp-cpp//src/kj:kj-async umbrella: kj::setupAsyncIo() and kj::UnixEventPort's members
6+
# would be defined twice -- by kj and by the tokio shim (//src/workerd/util:setup-async-io);
7+
# * kj-http-impl (kj's HTTP/1.1 implementation), directly or through the kj-http umbrella: its
8+
# kj:: symbols are defined over hyper by //src/workerd/util:kj-http;
9+
# * kj-tls (OpenSSL), replaced by rustls.
10+
# With static archives the linker keeps whichever definition it meets first, silently.
711
#
8-
# One query, empty output means clean; otherwise it prints one offending dependency path. Extra
12+
# One query per forbidden target; empty output means clean, otherwise it prints one offending
13+
# dependency path. Extra
914
# arguments are passed to bazel (CI passes its --config flags). The linked binary's symbols are
1015
# checked separately by //src/workerd/server:rust-io-link-check.
1116
set -euo pipefail
1217
cd "$(dirname "$0")/.."
1318

1419
TARGET=//src/workerd/server:workerd
15-
FORBIDDEN=@capnp-cpp//src/kj:kj-async-os
20+
FORBIDDEN=(
21+
"@capnp-cpp//src/kj:kj-async-os|Retarget the offending edge from the @capnp-cpp//src/kj:kj-async umbrella to :kj-async-core / :kj-async-io."
22+
"@capnp-cpp//src/kj/compat:kj-http-impl|Depend on //src/workerd/util:kj-http (or :kj-http-types) instead of @capnp-cpp//src/kj/compat:kj-http."
23+
"@capnp-cpp//src/kj/compat:kj-tls|Use //src/workerd/server:tls-network instead of kj-tls."
24+
)
1625

1726
errlog=$(mktemp)
1827
trap 'rm -f "$errlog"' EXIT
19-
if ! paths=$(bazel cquery "$@" --//:io_backend=rust "somepath($TARGET, $FORBIDDEN)" 2>"$errlog"); then
20-
cat "$errlog" >&2
21-
exit 1
22-
fi
23-
if [ -n "$paths" ]; then
24-
echo "FAIL: $TARGET reaches $FORBIDDEN under --//:io_backend=rust:"
25-
echo "$paths"
26-
echo "Retarget the offending edge from the @capnp-cpp//src/kj:kj-async umbrella to :kj-async-core / :kj-async-io."
27-
exit 1
28-
fi
29-
echo "ok: $TARGET does not reach $FORBIDDEN under --//:io_backend=rust"
28+
status=0
29+
for entry in "${FORBIDDEN[@]}"; do
30+
forbidden=${entry%%|*}
31+
hint=${entry#*|}
32+
if ! paths=$(bazel cquery "$@" --//:io_backend=rust "somepath($TARGET, $forbidden)" 2>"$errlog"); then
33+
cat "$errlog" >&2
34+
exit 1
35+
fi
36+
if [ -n "$paths" ]; then
37+
echo "FAIL: $TARGET reaches $forbidden under --//:io_backend=rust:"
38+
echo "$paths"
39+
echo "$hint"
40+
status=1
41+
else
42+
echo "ok: $TARGET does not reach $forbidden under --//:io_backend=rust"
43+
fi
44+
done
45+
exit $status

deps/rust/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ cc_library(
1111
linkopts = select({
1212
"@platforms//os:windows": [
1313
"ntdll.lib",
14+
# System certificate store APIs behind rustls-native-certs/rustls-platform-verifier.
15+
"crypt32.lib",
16+
],
17+
"@platforms//os:macos": [
18+
"-framework",
19+
"Security",
20+
"-framework",
21+
"CoreFoundation",
1422
],
1523
"//conditions:default": [],
1624
}),

0 commit comments

Comments
 (0)