zlodev is a local reverse proxy with TLS termination, custom DNS, and a terminal UI. It sits between the browser and a local dev server, providing HTTPS with auto-generated certificates at https://dev.lo, and a TUI for inspecting/intercepting/replaying HTTP traffic. The domain is hardcoded to dev.lo — no custom domains or TLDs.
Written in Zig 0.15.1, uses BoringSSL (compiled from source) for TLS and libvaxis for the terminal UI.
zig build # build (output: zig-out/bin/zlodev)
zig build -Doptimize=ReleaseSafe # release build
zig test src/dns.zig # run DNS unit tests
zig test src/har.zig # run HAR + requests tests
zig test src/proxy.zig # run proxy tests
zig test src/intercept.zig # run intercept tests
zig test src/requests.zig # run requests testsThere is no zig build test step — tests are run per-file with zig test src/<file>.zig.
main.zig CLI parsing, config file loading, command dispatch, thread orchestration
proxy.zig HTTPS reverse proxy (TLS termination, upstream forwarding, replay, route resolution)
tui.zig Terminal UI (vaxis-based, list/detail/edit views, split-pane logs, route colors)
subprocess.zig Dev server launcher & log capture (process spawn, pipe reading, ANSI strip, log ring buffer)
dns.zig UDP DNS server (resolves *.lo → 127.0.0.1)
http_server.zig HTTP server on port 80 (CA cert download page, HTTPS redirect)
cert.zig Certificate generation and system trust store management (BoringSSL)
requests.zig Thread-safe ring buffer for captured request/response entries
intercept.zig Request interception (pattern matching, hold/accept/drop with thread sync, dropAll/getPendingCount)
shutdown.zig Global atomic shutdown flag + signal handlers (SIGINT/SIGTERM)
log.zig Structured logging (stderr or file when TUI is active, mutex-protected writes)
search.zig Entry search/filter logic
clipboard.zig Copy-as-curl and case-insensitive string helpers
har.zig HAR (HTTP Archive) export
sys.zig System command helpers (sudo, tmp files, dir checks)
compat.zig Cross-platform compatibility (Windows socket I/O, networking init)
- Thread model: proxy uses a 64-thread pool (256KB stacks), HTTP server uses 8 threads, DNS is single-threaded, subprocess uses 2 reader threads (stdout/stderr). All loops use
poll()with 1s timeout +shutdown.isRunning()check. - Ring buffers:
requests.zig: stores HTTP entries in a fixed-size ring (max_entries, default 500). Entries are ~69KB each (fixed-size arrays for headers/body). Pinned entries (intercepted, WebSocket, starred) are skipped during overwrite.copyEntry()provides mutex-protected entry copies for TUI replay.lock()/unlock()expose the mutex for external callers (e.g. TUI edits).clearAll()callsintercept.dropAll()and waits for pending entries to drain.subprocess.zig: stores dev server log lines in a fixed-size ring (max_log_lines, default 5000).LogLineentries (~4KB each) are heap-allocated only when--commandis used. Oldest lines are unconditionally evicted on overflow. Protected by mutex;copyRange()provides thread-safe window reads for TUI display.
- TLS: BoringSSL via
@cImport(API-compatible with OpenSSL).SSL_set_fdusesBIO_NOCLOSE— the caller must close the socket afterSSL_free. - Entry lifecycle: Normal requests use
push(). Intercepted requests usepushAndPin()→finishEntry()(which unpins). The TUI can edit pinned entries in-place before accepting. Starred entries (*key) setpinned=trueviatoggleStar()to survive ring buffer overflow;starredis a separate bool frompinnedso unstarring doesn't interfere with intercept pins. - Replay: Connects to the proxy's own TLS endpoint (127.0.0.1:443) so the request goes through the full proxy path and gets captured naturally.
- Chunked encoding: A state machine parser (
chunkedStep) decodes chunks for body capture while forwarding raw chunked data to the client. Invalid hex digits transition to.parse_errorstate, and all forwarding loops check for this alongside.done. - Routing:
--route=api=3001(subdomain) and--route=/api=3001(path prefix).resolveRoute()in proxy.zig matches Host header for subdomains, longest prefix for paths, falls back to default port. Each entry storesroute_indexfor TUI color-coding. Routes can target external hosts (--route=api=staging.example.com:443) — theRoute.hostnamefield is set, and the proxy connects via outbound TLS with SNI, rewritesHostheader to upstream, and rewritesSet-Cookie Domain=to the proxy domain. - Config file:
.zlodevin project directory, parsed byreadConfigFile()in main.zig. One option per line (same keys as CLI). CLI args override config values. Only read forstartcommand. Supportsintercept=PATTERNfor default intercept pattern andcommand=SHELL_CMDfor dev server integration. - Intercept patterns:
intercept.zigstores a pattern (pattern_buf/pattern_lenwithpattern_mutex) and aPhase(.both,.request,.response).shouldInterceptRequest/shouldInterceptResponsedo case-insensitive substring match against method, path, or combined "METHOD PATH". Empty pattern matches all. Prefixreq:intercepts only requests,resp:only responses, no prefix = both. TUI prompts for pattern onikey. Config file can set a default pattern. - Response intercept: When
shouldInterceptResponsematches, the proxy buffers the entire response body (instead of streaming), stores it in the entry withresp_intercepted=true, pins, and waits. The TUI shows "RESP" in the status column.eopens the response editor (status, headers, body). On accept,forwardResponseFromEntrysends the (possibly edited) response with correctedContent-Length.finishResponseInterceptunpins without overwriting response data. - Windows:
compat.SocketStreamwraps Winsockrecv/send(std.net.Stream uses ReadFile which doesn't work with sockets on Windows).socketToFd/fdToSockethandle SOCKET↔c_int conversion. TUI skipsqueryTerminalon Windows to avoid spurious key events. Subprocess uses job objects for process-group cleanup; closing the job handle atomically terminates all child processes with no graceful signal. - Dev server integration:
subprocess.zigspawns a shell command (sh -con Unix,cmd /con Windows), captures stdout/stderr into the log ring, strips ANSI escape sequences on ingest, and splits bytes on\ninto fixed-length lines. Reader threads checkshutdown.isRunning()to participate in graceful shutdown. TUI split-pane (60% requests, 40% logs) is toggled withlkey; focus switches withTab; autoscroll is per-pane viaskey. Restart withRkey sends SIGTERM (3s grace) then SIGKILL on Unix, or closes job handle on Windows.
- Structured log format:
component=X op=Y field=value - Error handling: functions return
!voidor!T, errors are logged with context before propagating - BoringSSL interop via
@cImport— C types/functions accessed throughssl_c.*(proxy) orc.*(cert) - TUI renders at 50ms intervals via
std.Thread.sleep, not event-driven - All string comparisons for HTTP headers use
startsWithIgnoreCase(defined locally in proxy.zig and http_server.zig) isChunkedEncodingsplits Transfer-Encoding by comma and checks each token (handlesgzip, chunked)- HTTP server sets
SO_RCVTIMEO(5s) on accepted connections to prevent slow-client thread exhaustion - HTTP server rejects paths containing
\ror\nto prevent CRLF injection in redirects - DNS server sets the AA (Authoritative Answer) flag and rejects compression pointers in queries
cert.ziguses atomic write (temp file + rename) for git CA bundle modificationclipboard.zigdetects truncation during curl command building and skips clipboard copy if truncated
Entryis ~69KB. Never pass by value to threads — heap-allocate withpage_allocator.create()and let the calleedestroy().sudoCmduses a switch onTermvariants — the process may exit via signal, not just exit code.- Log is muted before spawning server threads in TUI mode to prevent stderr leaking through the alt screen buffer.
- The
max_body_len(32KB) andmax_header_len(2KB) are compile-time constants inrequests.zig. - CLI flag
-cis--config, not--cert.-dis removed — use--dnsand--cert(no short forms). - Subdomain routes are blocked in local mode (
-l) since mDNS doesn't support arbitrary subdomains. start --dnscannot be combined with other start options (port, bind, routes, etc.).--commandcannot be combined with--dns(no TUI to display logs) or--no-tui(no pane for output).- Error messages use full flag names (
--dnsand--cert, not-dand-c). - Proxy rejects intercepted requests with truncated bodies (413 status) rather than forwarding partial data.
- After
sslSendError, alwaysreturn(nevercontinuein keep-alive loop) since the TLS state may be corrupted. forwardResponseFromEntryalways emitsContent-Length, even for zero-length bodies.- TUI split-pane layout uses
win.child()for clipping; requests pane is rendered into the child window withdraw_footer=falseto avoid footer-drawing in the middle of the split.