diff --git a/frameworks/vanilla-io_uring/Dockerfile b/frameworks/vanilla-io_uring/Dockerfile index 980002d7b..a4cf3c8b6 100644 --- a/frameworks/vanilla-io_uring/Dockerfile +++ b/frameworks/vanilla-io_uring/Dockerfile @@ -2,7 +2,8 @@ FROM debian:stable-slim AS build RUN apt-get -qq update && \ apt-get -qy install --no-install-recommends \ - ca-certificates build-essential git libpq-dev liburing-dev && \ + ca-certificates build-essential git libpq-dev liburing-dev \ + cmake curl bzip2 python3 && \ rm -rf /var/lib/apt/lists/* # Pinned, reproducible V at master commit 02015a7c (built from source). Master is @@ -24,25 +25,57 @@ RUN git clone https://github.com/vlang/v /opt/v && \ # main moved, but that hit api.github.com every build and a transient 504/rate-limit # failed the whole build (MDA2AV/HttpArena#895). Bump this commit to pick up lib fixes. # -# vanilla main @15bd57e picks up the allocation fixes from a lib-wide audit: -# enghitalo/vanilla#72 (pg_async: read wire ints at offset, drop per-read slices), -# #73 (static_assets: zero-copy conditional-GET/Range parsing) and #74 (TLS: pool the -# per-connection read/response buffers). pg_async#72 is the one that touches this -# backend's async-DB row-decode hot path. +# Pinned to vanilla main @b189036 — the lib-wide alloc audit (#72 pg_async, #73 +# static_assets, #74 TLS buffer pooling) PLUS kTLS record offload (enghitalo/vanilla#79, +# ancestor 5137a9a). The io_uring backend has no TLS, so the json-tls listener runs on +# the lib's epoll backend on :8081 (see main.v); kTLS makes its steady-state read/write +# plain recv/send with the kernel doing AES-128-GCM. pg_async#72 touches this backend's +# async-DB row-decode hot path. RUN git clone https://github.com/enghitalo/vanilla /root/.vmodules/vanilla && \ - git -C /root/.vmodules/vanilla checkout 15bd57e5ae8cf1383bd386826e48e08a10f6d4b4 + git -C /root/.vmodules/vanilla checkout b189036212e4283ef2cffe42b318b556f8a3d1bc + +# Mbed TLS 4 for the json-tls profile (the epoll TLS listener on :8081). The vanilla +# `tls` C shim (built with -d vanilla_tls) targets the Mbed TLS 4.x API; Debian apt +# ships only 2.28 (ABI-incompatible), so build 4.x from the version-pinned release +# tarball into /usr/local. THREAD-SAFETY: enable MBEDTLS_THREADING_C + +# MBEDTLS_THREADING_PTHREAD — the TLS workers share Mbed TLS's global PSA key store, +# which races (heap-use-after-free) under concurrent handshakes without the mutex. +ARG MBEDTLS_VERSION=4.1.0 +RUN curl -fsSL -o /tmp/mbedtls.tar.bz2 \ + "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MBEDTLS_VERSION}/mbedtls-${MBEDTLS_VERSION}.tar.bz2" && \ + tar -xf /tmp/mbedtls.tar.bz2 -C /tmp && \ + cd "/tmp/mbedtls-${MBEDTLS_VERSION}" && \ + python3 tf-psa-crypto/scripts/config.py set MBEDTLS_THREADING_C && \ + python3 tf-psa-crypto/scripts/config.py set MBEDTLS_THREADING_PTHREAD && \ + cmake -S "/tmp/mbedtls-${MBEDTLS_VERSION}" -B /tmp/mbedtls-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DUSE_SHARED_MBEDTLS_LIBRARY=On -DUSE_STATIC_MBEDTLS_LIBRARY=Off \ + -DENABLE_TESTING=Off -DENABLE_PROGRAMS=Off \ + -DCMAKE_INSTALL_PREFIX=/usr/local && \ + cmake --build /tmp/mbedtls-build -j"$(nproc)" && \ + cmake --install /tmp/mbedtls-build && ldconfig && \ + rm -rf /tmp/mbedtls.tar.bz2 "/tmp/mbedtls-${MBEDTLS_VERSION}" /tmp/mbedtls-build WORKDIR /app COPY . . # Default GC (NOT -gc none, NOT -prealloc): this runs on the vanilla HTTP server's own -# manual buffer management, not the stdlib veb/fasthttp per-request arena. -RUN v -prod . -o server +# manual buffer management. -d vanilla_tls compiles the real Mbed TLS backend for the +# json-tls listener (without it the tls module is a stub and mbedtls isn't linked). +RUN v -prod -d vanilla_tls . -o server FROM debian:stable-slim RUN apt-get -qq update && \ apt-get -qy install --no-install-recommends libpq5 liburing2 && \ rm -rf /var/lib/apt/lists/* +# Mbed TLS 4 shared libs for the json-tls (epoll) listener (libmbedcrypto pulls in +# libtfpsacrypto transitively). ldconfig so the loader resolves them with no +# LD_LIBRARY_PATH. +COPY --from=build /usr/local/lib/libmbedcrypto.so* /usr/local/lib/libmbedx509.so* /usr/local/lib/libmbedtls.so* /usr/local/lib/libtfpsacrypto.so* /usr/local/lib/ +RUN ldconfig COPY --from=build /app/server /server -EXPOSE 8080 +# 8080 = io_uring HTTP (non-TLS profiles); 8081 = json-tls (HTTP/1.1 over TLS, epoll +# backend). The harness bind-mounts the cert/key at /certs (override via +# TLS_CERT/TLS_KEY); with no cert mounted the server self-signs. +EXPOSE 8080 8081 CMD ["/server"] diff --git a/frameworks/vanilla-io_uring/main.v b/frameworks/vanilla-io_uring/main.v index 0ca0010b9..b2655372a 100644 --- a/frameworks/vanilla-io_uring/main.v +++ b/frameworks/vanilla-io_uring/main.v @@ -2,6 +2,7 @@ module main import vanilla.http_server import vanilla.http_server.http1_1.request_parser +import vanilla.http_server.tls import db.pg import json import os @@ -704,6 +705,29 @@ fn parse_db_url(u string) pg.Config { } } +// load_tls_config builds the json-tls server's TLS config. It reads the cert/key +// the HttpArena harness bind-mounts at /certs (overridable via TLS_CERT/TLS_KEY). +// If NO cert is mounted (local dev), it falls back to a fresh self-signed cert — +// the benchmark/validate clients use `curl -k` / wrk, which never verify it. If a +// cert IS present but the key is missing/unreadable, it FAILS LOUDLY rather than +// silently self-signing. TLS 1.3 + ALPN http/1.1 are fixed by the tls shim. +fn load_tls_config() &tls.Config { + cert_path := os.getenv_opt('TLS_CERT') or { '/certs/server.crt' } + key_path := os.getenv_opt('TLS_KEY') or { '/certs/server.key' } + cert := os.read_bytes(cert_path) or { + eprintln('vanilla-io_uring: no TLS cert at ${cert_path} (${err}); using ephemeral self-signed') + return tls.new_self_signed() or { + panic('vanilla-io_uring: self-signed TLS bring-up failed: ${err}') + } + } + key := os.read_bytes(key_path) or { + panic('vanilla-io_uring: TLS cert present at ${cert_path} but key unreadable at ${key_path}: ${err}') + } + return tls.new_from_pem(cert, key) or { + panic('vanilla-io_uring: TLS cert/key parse failed: ${err}') + } +} + fn main() { url := os.getenv_opt('DATABASE_URL') or { 'postgres://bench:bench@localhost:5432/benchmark' } mut size := (os.getenv_opt('DATABASE_MAX_CONN') or { '64' }).int() @@ -758,6 +782,59 @@ fn main() { gz_mu: sync.new_rwmutex() } + // ── json-tls profile: /json over HTTPS on :8081 via the epoll + kTLS backend ── + // The lib's io_uring backend has no TLS, so the json-tls listener runs on the + // epoll backend (TLS 1.3 via Mbed TLS; after the handshake the kernel does record + // AES-128-GCM via kTLS where the `tls` module is present, else userspace fallback). + // It serves ONLY /json (404 elsewhere) — minimal TLS surface — reusing the same + // allocation-free write_json_response (read-only: dataset + prefixes). A STATELESS + // request_handler captures `sh`; it never touches the DB/caches, so it runs safely + // alongside the io_uring workers. The io_uring server below keeps the non-TLS + // profiles on :8080. + tls_handler := fn [sh] (req_buffer []u8, fd int, mut out []u8) ! { + mut req := request_parser.HttpRequest{ + buffer: req_buffer + } + if !request_parser.decode_into(mut req) { + wb(mut out, bad_request) + return + } + target := unsafe { tos(&req.buffer[req.path.start], req.path.len) } + qpos := target.index_u8(`?`) + route := if qpos < 0 { target } else { unsafe { tos(target.str, qpos) } } + if route.starts_with('/json/') { + count := clamp_count(parse_u_at(route, 6), sh.dataset.len) + mut m := qint(req, qk_m) + if m == 0 { + m = 1 + } + sh.write_json_response(mut out, count, m) + return + } + wb(mut out, not_found) + } + // Port is fixed to 8081 by the HttpArena harness; TLS_PORT lets local runs pick a + // free port. run() blocks, so the TLS server runs on its own thread (value-mut + // receiver → spawn via a closure with a local mut copy; the two servers are + // independent — own socket, workers and backend). + mut tls_port := (os.getenv_opt('TLS_PORT') or { '8081' }).int() + if tls_port <= 0 { + tls_port = 8081 + } + tls_server := http_server.new_server(http_server.ServerConfig{ + port: tls_port + io_multiplexing: .epoll + limits: http_server.Limits{ + max_request_bytes: 64 * 1024 + } + request_handler: tls_handler + tls_config: load_tls_config() + })! + spawn fn [tls_server] () { + mut s := tls_server + s.run() + }() + mut server := http_server.new_server(http_server.ServerConfig{ port: 8080 io_multiplexing: .io_uring diff --git a/frameworks/vanilla-io_uring/meta.json b/frameworks/vanilla-io_uring/meta.json index 1209ba6d0..d05e363c3 100644 --- a/frameworks/vanilla-io_uring/meta.json +++ b/frameworks/vanilla-io_uring/meta.json @@ -3,7 +3,7 @@ "language": "V", "type": "engine", "engine": "io_uring", - "description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg pooled Go-style DB (db.exec_param_many over a connection pool). Built with the default GC on pinned V master c0624b274 (built from source). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.", + "description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; json-tls serves /json over HTTP/1.1 + TLS 1.3 on :8081 via the lib's epoll backend (the io_uring backend has no TLS) with kTLS record offload where the kernel `tls` module is present; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg pooled Go-style DB (db.exec_param_many over a connection pool). Built with the default GC on pinned V master c0624b274 (built from source). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.", "repo": "https://github.com/enghitalo/vanilla", "enabled": true, "tests": [ @@ -12,6 +12,7 @@ "limited-conn", "json", "json-comp", + "json-tls", "upload", "static", "async-db",