# 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. Still open from "worth considering": CA install UX per OS, multiple proxy listeners and upstream proxy chaining. Neither is started yet. 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.