# mitmux - Intercepting Proxy TUI (Burp/Caido replacement) ## Overview Daily-driver intercepting proxy for manual pentest work, terminal-based. Prior art to read before writing code: Cruster (Rust, built on hudsucker) - same problem, worth studying even though this build is Go. ## Stack - Language: Go - memory safety on hostile input matters here more than in the other projects, since this parses attacker-adjacent traffic - TLS interception: Go's own `crypto/tls` + a CA cert generator (analogous to `rcgen`) for per-domain leaf certs - Proxy core: `net/http` + manual `CONNECT` handling, or a MITM proxy library if one fits without fighting Go's aggressive header normalization. Upstream requests are round-tripped manually (write the request, read the response off the same connection) rather than through `http.Transport` - Transport's automatic HTTP/2 dispatch keys off a literal `*tls.Conn` type assertion on the dialed connection, which a raw-byte-capturing wrapper around that connection defeats (found by testing: it silently parsed HTTP/2 framing as HTTP/1.1). - Storage: SQLite in WAL mode - blob columns for raw request/response bytes, FTS5 index for search across bodies - UI: Bubble Tea + Lipgloss (TUI), same family as the packet analyzer's Go sibling if that ever gets built ## Architecture sketch (important - don't skip this) - Split proxy engine from TUI. Headless daemon owns the listening socket and the DB; TUI is a client over a Unix socket. The proxy keeps running when the UI restarts, and a web UI or CLI scanner can be bolted on later without touching the engine. - Store raw bytes as the source of truth. Parse into a display view, never re-serialize for storage - request smuggling, header injection, and parser-differential bugs depend on the original malformed framing surviving. For Repeater specifically, write requests as raw bytes over the socket rather than through a normalizing HTTP client. ## Build order 1. Proxy + CA cert generation + plaintext HTTP passthrough 2. TLS interception (per-host cert generation, install CA) 3. History view (SQLite storage, raw bytes preserved) in the TUI 4. Repeater (raw-byte send/resend, the feature used daily) 5. Search/filter (FTS5) 6. Match-and-replace rules 7. Intruder-equivalent (last, optional) ## Open questions - HTTP/2: handle natively (decided) - full fidelity over MITM'd connections rather than downgrading to HTTP/1.1. Adds complexity to CONNECT handling, stream framing, and step 3 storage (multiplexed streams over one connection need per-stream request/response boundaries, not just per-connection ones). - CA install UX per OS (Linux/macOS/Windows trust stores) - Whether WebSocket interception is v1 or a later addition - Step 6 match-and-replace shipped headers-only. Body rules are a separate, harder problem: request-body capture currently relies on streaming the body straight from the client connection to the upstream write (that's what makes it exact, byte for byte); a body rule needs to materialize, transform, and re-send it instead, which means deciding what "exact" even means for a rule-modified request before touching that path again. Also still single-line-text-field limited in the TUI (bubbles/textinput can't hold a literal CRLF), so even with header rules, injecting a brand-new header line via the form isn't possible yet - only rewriting/removing existing ones. The underlying engine (rules.ApplyHeaders) already supports arbitrary text-block edits; it's specifically the form UI that's constrained. - Step 7 (Intruder-equivalent) shipped Sniper only: one payload set, one §marked§ position fuzzed at a time, every other marked position held at its base value - the mode that covers most real Intruder usage. Battering ram / pitchfork / cluster bomb aren't implemented. Sequential sending only (no concurrency), capped at 1000 generated requests as a fixed safety limit against an accidental huge wordlist combined with several positions. Reuses the Repeater send primitive (proxy.Server.sendRaw) directly - an attack is just that primitive run in a loop with generated bytes - and results land in the same history table tagged source="intruder", same as Repeater's source="repeater", rather than a separate results store. ## Post-build-order: Burp/ZAP/Caido parity pass Build order 1-7 is done. Researched what those three actually offer (features and basic UI/UX) and triaged the gap into "should build soon" / "worth considering" / "skip" - see commit history for the full list; tracking what's shipped vs. deferred here. Shipped: vi-modal editing for the raw request textareas (table and viewport already had vi nav by default - this was specifically about textarea/textinput, which don't); a persistent status bar and a '?' keybinding reference; display-only response JSON pretty-printing; structured search filters (status:, source:, flagged:) alongside the existing FTS5 text search; a flagged marker (★) for "revisit this" - deliberately simpler than full free-text notes/comments, which would need their own text-input overlay for comparatively modest extra value over a boolean; noted as a real follow-up, not dropped silently; a Comparer tool - mark an entry with 'c' (from history list or detail view), 'c' again on a different entry opens a unified diff (git-diff style, colored) of either side's request or response. Unified rather than Burp's side-by-side: a two-column layout fights terminal width for anything but a narrow window, and unified reuses the same scrollable- viewport pattern already used everywhere else in the TUI. CRLF is normalized to LF before diffing (display-only, same reasoning as the JSON pretty-printer) so an HTTP/1.1 exact capture doesn't show every line as changed from an invisible trailing \r; a standalone Decoder tool ('d') - URL/Base64/Hex/HTML encode and decode, live output as you type, tab to cycle transforms. Deliberately single-transform, not chained/pipelined like Burp's Decoder - v1 scope, and pipeline-building UI is real added complexity for a feature that's already useful without it. Base64 decode tries standard/URL-safe/padded/unpadded variants in turn rather than making the user pick, since real pasted data is as likely to be one as the other. URL encode/decode uses strict RFC 3986 percent-encoding (space <-> %20), not Go's url.QueryEscape's form- encoding behavior (space <-> '+'), since "URL encode" for a pentester almost always means the former. Shipped since: a standalone Decoder tool ('d') - see above; multiple concurrent Repeater tabs - 'r' from the history list or detail view now opens a NEW tab rather than overwriting whatever was already open, `]`/`[` switch tabs, `ctrl+w` closes the active one (all three gated to normal mode, so they're inert while typing - `[`/`]` are common JSON body characters and `ctrl+w` is a textarea binding for delete-word- backward that must still work while composing a request). Each tab owns its own request buffer, response view, and send-in-flight state; a slow send whose result lands after the user has switched away still updates the correct tab (send results carry the tab index they belong to), and the status line/response pane it's shown in only updates live if that tab is still the one on screen. Shipped since: Intruder payload processing and grep-match/grep-extract. Payload processing - an optional case rule (upper/lower) and an optional encode rule (URL/Base64/Hex/HTML), cycled with `c`/`e` - is applied client-side to each payload line before it ever crosses the IPC socket, case first then encode (encoding an already-case-folded value is safe; the reverse would corrupt e.g. Base64 padding), since it's a pure string transform with no proxy-side state involved and reuses the Decoder's own `urlEncodeAll`. Grep-match/grep-extract are optional Go regexps (`m`/`v` to edit, both gated to normal mode and both revert-on-esc / validate-on-enter the same way the history list's `/` search box works), evaluated server-side in `internal/ipc/server.go`'s "intrude" handler against each result's actual response bytes - chosen over a client-side implementation because the daemon already has `entry. ResponseRaw` in hand right where the result is built, and Burp's own grep options work the same way (matched against the real response, not a client-refetched copy). Grep-match flags a result (shown as a Match column); grep-extract captures the first submatch (or the whole match if the pattern has no capturing group) into an Extract column. Both are configured once before `ctrl+r` starts an attack and apply for that run only - matching Burp, which doesn't retroactively re-grep already-fired requests if you change the options mid-attack. Shipped since: CA install UX per OS (`mitmuxd -install-ca`) - generates the CA if needed, prints copy-pasteable install steps for the detected platform, and exits without starting the proxy. Deliberately instructions-only, never auto-executing: trust-store tooling varies enough across Linux distros that guessing wrong and running the wrong command unattended is worse than asking, and installing a root CA is a system-wide trust change that affects every TLS connection on the machine, not just mitmux's own traffic - the user running the printed command themselves keeps them in control of that. On Linux, detects `trust` (p11-kit - Arch, and Fedora also ships it)/`update-ca-trust` (RHEL/Fedora/CentOS)/`update-ca-certificates` (Debian/Ubuntu/Gentoo) via PATH lookup and picks whichever is actually present, plus separate `certutil` (NSS) instructions for Firefox/Chrome's own certificate store, which doesn't always follow the system trust store on Linux. macOS (`security add-trusted-cert`) and Windows (`certutil -addstore` / `Import-Certificate`) instructions are implemented but, unlike the Linux path, not verified live - no macOS/Windows machine was available to test against; only the command text itself (sourced from each platform's standard, documented tooling) is confirmed correct by inspection. Shipped since: multiple proxy listeners and upstream proxy chaining - the last two items from the original "worth considering" list. Multiple listeners: `-listen` takes a comma-separated address list (`-listen "127.0.0.1:8080,127.0.0.1:8081"`); all bound addresses share the same handler, history store, CA and rules - one logical proxy reachable on more than one address/port, not several independent proxies in one process. Every address is bound up front before any of them start serving, so a bad address fails startup immediately instead of leaving the daemon partially listening. Upstream proxy chaining: `-upstream-proxy host:port` (optional `http://` prefix, stripped) routes every outbound connection through another HTTP CONNECT proxy instead of dialing origins directly - chaining mitmux into Burp, a corporate proxy, or any other CONNECT-speaking proxy. SOCKS5 upstreams aren't implemented. For the CONNECT/HTTPS path, chaining is transparent below the tunnel: once the CONNECT handshake to the upstream proxy succeeds, TLS and the request/response on top of it are identical to a direct connection, so no other code needed to change. The plain-HTTP path is different: an absolute-form request line ("GET http://host/path HTTP/1.1") has to be sent to the upstream proxy instead of origin-form, so `roundTripH1` gained a `proxyForm` parameter and `forward()` selects it based on whether the request is plain HTTP and an upstream proxy is configured. Chaining into another intercepting/MITM proxy (including another mitmuxd instance) will fail TLS verification unless that proxy's own CA is separately trusted - expected, not a mitmux-specific gap: the upstream MITM terminates and re-signs the connection with its own CA, which mitmux's outbound TLS client (verifying against the system root store) has no reason to trust. Confirmed live: chaining through a genuine passthrough CONNECT proxy (tunnels raw bytes, no MITM) works correctly for both plain HTTP and HTTPS; chaining through a second mitmuxd instance correctly fails with a clear "certificate signed by unknown authority" error recorded in history, rather than hanging or crashing. This closes every item from the original "worth considering" list. Skipped deliberately (from the research, matches this tool's stated scope): active/passive vulnerability scanning, plugin marketplace, Collaborator/OAST, team collaboration, CI integration, client TLS (mutual-TLS) certs, invisible/non-proxy-aware proxying.