# mitmux A terminal-based intercepting proxy for manual pentest work - a daily-driver alternative to Burp Suite, Caido, or OWASP ZAP that runs entirely in your terminal, with vi-style modal editing for raw requests. mitmux is two binaries: `mitmuxd`, a headless daemon that owns the proxy listener and the SQLite history database, and `mitmux`, a Bubble Tea TUI that talks to it over a Unix socket. The daemon keeps running (and keeps capturing traffic) independently of the TUI - close the UI, reopen it later, the proxy never stopped. For the build history, the architectural decisions behind it, and a list of what's deliberately not implemented (and why), see [`PLAN.md`](PLAN.md). ## Features - **Intercepting proxy**: plaintext HTTP passthrough and TLS interception (per-host leaf certificates signed by a locally generated CA), with HTTP/1.1 and HTTP/2 handled natively and independently on the client and upstream legs - a client that only speaks HTTP/1.1 and an origin that prefers HTTP/2 both work correctly in the same request. - **History**: every request/response captured to SQLite. Raw wire bytes are preserved byte-for-byte on HTTP/1.1 legs (what request smuggling and parser-differential analysis actually needs); HTTP/2 legs - which have no single "raw bytes" representation, being multiplexed HPACK-compressed framing - are reconstructed and marked as such, never silently presented as exact. - **Search**: full-text search (FTS5) across headers and bodies, plain text just works (`example.com`, `x-forwarded-for`, `192.168.1.1` - no quoting needed), plus structured filters: `status:404`, `status:4xx`, `status:>=400`, `source:repeater`, `flagged:true`, column filters like `host:example.com`, and `AND`/`OR`/`NOT`. - **Repeater**: edit and resend a raw request. What you type is what goes on the wire - no normalization, no auto-fixed `Content-Length`, no "helpful" reformatting. That's the point of a Repeater. Multiple tabs: sending an entry to Repeater opens a new tab rather than replacing whatever's already there, so you can iterate on several requests side by side. - **Intruder** (Sniper only): mark positions in a request template with `§markers§`, supply a payload list, fuzz one position at a time against a shared payload set. Results land in the same history table as everything else, searchable the same way. Payload processing (optional case and encode rules, applied to every payload before it's sent) and grep-match/grep-extract (flag or pull text out of each result's response with a regexp) are both configurable before starting an attack - see [Intruder](#intruder) below. - **Match-and-replace**: header rewrite rules (add, remove, or modify) for requests and/or responses, applied live as traffic passes through. History still shows what was actually sent/received on each side - match-and-replace transforms the wire, it doesn't rewrite the audit trail. - **Flagging**: mark an entry to revisit later (★), filterable via `flagged:true`. - **Comparer**: mark one entry (`c`), then `c` on a different entry to see a colored unified diff of either side's request or response. - **Decoder**: standalone URL/Base64/Hex/HTML encode and decode (`d`), output updates live as you type or switch transforms. - **Vi-modal editing**: the raw request editors (Repeater, Intruder) are real modal editors - normal mode by default, `i`/`a`/`o`/etc. to insert, `hjkl`, `dd`/`yy`/`p`, word motions, `gg`/`G`. See [Vi bindings](#vi-bindings) below. Everywhere else (history table, read-only response views), standard vi navigation (`j`/`k`, `g`/`G`, `ctrl+u`/`ctrl+d`) already works - that's the underlying TUI library's default, not something layered on top. ## Install / build Requires Go 1.26.5+ (see `go.mod`). ```sh git clone cd mitmux go build -o bin/mitmuxd ./cmd/mitmuxd go build -o bin/mitmux ./cmd/mitmux ``` ## Quick start 1. **Start the daemon.** By default it listens on `127.0.0.1:8080` and stores its CA and history database under `~/.config/mitmux` (XDG config dir): ```sh ./bin/mitmuxd ``` On first run it generates a root CA and prints where the cert landed, e.g. `~/.config/mitmux/ca.pem`. 2. **Trust the CA.** To intercept HTTPS without constant certificate warnings, import `ca.pem` into whatever's making the requests - your browser's certificate store, `curl --cacert`, a mobile device's trusted-certificate settings, etc. For copy-pasteable, OS-specific steps (Linux: whichever of `trust`/`update-ca-trust`/ `update-ca-certificates` is actually on your system, plus Firefox's own NSS store; macOS: Keychain; Windows: `certutil`/PowerShell), run: ```sh ./bin/mitmuxd -install-ca ``` This only prints commands - it never runs anything against your trust store itself. Installing a root CA is a system-wide trust change, so you run the printed command yourself. 3. **Point a client at the proxy.** e.g.: ```sh curl -x http://127.0.0.1:8080 --cacert ~/.config/mitmux/ca.pem https://example.com/ ``` Or configure your browser's proxy settings to `127.0.0.1:8080`. 4. **Open the TUI** (in another terminal - the daemon keeps running independently): ```sh ./bin/mitmux ``` Both binaries take flags for non-default setups - `-listen`, `-socket`, `-ca-dir`, `-db`, `-upstream-proxy` on `mitmuxd`; `-socket` on `mitmux`. Run either with `-h` for the full list. `-listen` takes a comma-separated list to bind more than one address (`-listen "127.0.0.1:8080,127.0.0.1:8081"`) - one logical proxy on several ports/interfaces, sharing the same history, CA and rules. `-upstream-proxy host:port` chains every outbound connection through another HTTP CONNECT proxy (Burp, a corporate proxy, anything that speaks CONNECT) instead of dialing origins directly. Chaining into another *intercepting* proxy needs that proxy's own CA trusted too - it terminates and re-signs the connection with its own CA, which mitmux's outbound TLS client has no reason to trust otherwise; you'll see a clear certificate-verification error in history rather than a silent failure. SOCKS5 upstreams aren't implemented. ## Usage Press `?` from any screen in the TUI for the full, current keybinding reference - it's generated from the same source as this document, so it never drifts out of date the way a static list can. The summary below is enough to get going. ### History (the default view) | Key | Action | |---|---| | `↑`/`↓` or `j`/`k` | navigate (also `g`/`G` top/bottom, `ctrl+u`/`ctrl+d` half-page) | | `enter` | view request/response detail | | `r` | open in Repeater | | `i` | open in Intruder | | `f` | toggle flag | | `c` | mark for comparison - press `c` on another entry to diff | | `x` | delete the selected entry (asks `y`/`n` to confirm) | | `X` | clear ALL history, not just the current search filter (asks `y`/`n` to confirm) | | `E` | export the current view (respects an active search filter) - `.har` or `.csv` | | `d` | Decoder | | `/` | search | | `m` | match-and-replace rules | | `s` | target scope (what gets recorded) | | `q` | quit | ### Detail view `tab` switches request/response, `p` toggles pretty-printed JSON on the response (display-only - never touches the stored or resent bytes), `c` mark/compare (same as the history list), `r`/`i` jump straight to Repeater/Intruder seeded from this entry, `e` exports the entry (request and response, raw bytes, plain text - type a path and press enter), `esc` back. ### Comparer Reachable by pressing `c` on two different history entries (from either the list or detail view). Shows a colored unified diff - `diff -u` style, `+`/`-` lines - of the two entries' requests or responses, `tab` to switch between them. CRLF is normalized before diffing so an exact HTTP/1.1 capture doesn't show every line as changed purely from the invisible `\r`. ### Decoder Reachable with `d` from the history list - a standalone tool, not seeded from any entry. `i` to type or paste text; the output pane updates live as you type. `tab`/`shift+tab` cycles through URL, Base64, and Hex encode/decode and HTML entity encode/decode. Base64 decode tries the standard, URL-safe, padded, and unpadded variants in turn rather than requiring you to know which one you're looking at. Single-transform only - not chained/pipelined the way Burp's Decoder supports. ### Export Two independent export paths, both a modal path-prompt (`enter` writes and confirms, `esc` cancels). Format is picked by the extension you type, the same convention any "save as" dialog uses - no separate format-selection control: - `e` from Detail view exports the single selected entry. - `.txt` (default) - request and response raw bytes, plain text, exactly what Detail view already shows. Each side is annotated when it isn't wire-exact (truncated or reconstructed), matching Detail view's own labels. - `.sh` / `.curl` - the request as a runnable `curl` command line (Burp/DevTools' own "copy as curl"), for handing to someone else or re-running standalone without mitmux. Every value is shell-quoted (a captured or edited request can contain arbitrary bytes). - `E` from the history list exports the current view - the visible, filtered set if a search is active, everything otherwise. - `.har` (default) - one [HAR](https://en.wikipedia.org/wiki/HAR_(file_format)) 1.2 file, for importing into Chrome/Firefox DevTools, Burp, Postman, or anything else that reads HAR. A binary body (an image, say) is base64- encoded rather than corrupted as text. An entry that fails to fetch or parse is skipped rather than aborting the whole export; the status line reports how many, if any. - `.csv` - a summary table (id, method, host, path, status, sizes, timing, flag, source) for a report or spreadsheet - lighter and faster than HAR since it needs no per-entry fetch from the daemon. Any field that could be interpreted as a spreadsheet formula (starts with `=`, `+`, `-`, `@`, tab, or CR - method/host/path/error all ultimately trace back to a request line or Host header, exactly the kind of content this tool exists to inspect from hostile traffic) is neutralized with a leading quote before writing, the standard CSV-injection mitigation. There's no import yet (see `PLAN.md`). ### Search syntax Plain text searches headers and bodies on both sides of the exchange. A handful of characters that are FTS5 syntax rather than literal text (`.`, `-`, `/`, `@`, and more) are handled transparently - you don't need to quote a domain name or an IP address for it to work. - `example.com`, `x-forwarded-for`, `192.168.1.1` - literal text, just works - `host:example.com`, `AND`, `OR`, `NOT` - FTS5 syntax for column filters and boolean queries - `status:404` - exact status code - `status:4xx` (also `2xx`/`3xx`/`5xx`) - status range shorthand - `status:>=400`, `status:!=200` - status comparison operators - `source:proxy` / `source:repeater` / `source:intruder` - where the request came from - `flagged:true` / `flagged:false` - Combine freely: `admin status:200 source:repeater` ### Repeater / Intruder - vi bindings The request editors start in **normal mode**, not insert mode - like real vi. Press `i` (or `a`/`I`/`A`/`o`/`O`) to start typing, `esc` to go back to normal mode. The mode indicator (`-- NORMAL --` / `-- INSERT --`) is always visible while one of these editors is focused. | Normal-mode key | Action | |---|---| | `h` `j` `k` `l` | left / down / up / right | | `0` / `$` | line start / end | | `w` / `b` | word forward / back | | `x` | delete character | | `i` `a` `I` `A` | insert: before cursor / after cursor / line start / line end | | `o` / `O` | open a line below / above and insert | | `dd` / `yy` | delete / yank the current line | | `p` / `P` | paste below / above | | `dw` `d$` `d0` | delete word / to end of line / to start of line | | `gg` / `G` | top / bottom of the buffer | | `esc` | (in insert mode) back to normal mode - never leaves the view | This is a genuine, if intentionally scoped, modal editor: it translates these commands into the underlying text widget's own editing primitives rather than reimplementing cursor and line manipulation. Not implemented: registers beyond a single yank slot, visual mode, ex commands, macros, and counts (`3dd`, `5j`). There's no undo, because the underlying text widget doesn't have one either. `ctrl+r` sends (Repeater) or starts the attack (Intruder). `tab` switches panes. In Intruder's template pane specifically, `ctrl+g` inserts a `§` marker at the cursor if typing the character directly isn't convenient on your keyboard/terminal. Repeater supports multiple concurrent tabs - each open request/response pair is independent. `]`/`[` switch to the next/previous tab, `ctrl+w` closes the active one. All three only fire in normal mode, so they don't interfere with typing (`[`/`]` show up in JSON bodies constantly, and `ctrl+w` is the editor's own delete-word-backward while composing). ### Intruder Beyond marking `§positions§` and supplying payloads, two more things are configurable before `ctrl+r` starts the attack - both normal-mode-only shortcuts, available from any pane: - `c` / `e` cycle **payload processing**: an optional case rule (off/upper/lower) and an optional encode rule (off/URL/Base64/Hex/ HTML), shown in the status line above the results table. Applied to every payload, case first then encode, right before it's substituted into the request - case-folding an already-encoded value would corrupt it (e.g. uppercasing Base64 padding), so case always runs on the original text first. - `m` / `v` edit **grep-match** / **grep-extract**, each a Go regexp evaluated against every result's actual response bytes (same `enter` confirms / `esc` cancels pattern as the history list's `/` search - an invalid regexp is rejected with an error rather than silently accepted). Grep-match flags a result (a `Match` column) if the pattern is found anywhere in the response; grep-extract captures the first submatch - or the whole match, if the pattern has no capturing group - into an `Extract` column. Both are optional and independent; leave either blank to skip that check. Both settings apply for the attack you're about to start - changing them mid-run doesn't retroactively re-evaluate requests already sent, matching Burp's own behavior. ### Match-and-replace rules Press `m` from the history view. Rules match request or response headers (`a` add, `enter`/`e` edit, `d` delete, `space` toggle enabled). Matching is literal-substring by default, or regex if the rule's Regex toggle is on. Rules operate on the raw header *block* as text, not per-value substitution, so a rule can add or remove a header entirely, not just rewrite an existing one. Currently headers only - see `PLAN.md` for why body rules are a separate, harder problem. ### Scope Press `s` from the history view to manage what gets **recorded** to history - not what gets proxied. Out-of-scope traffic still reaches its destination and the response still reaches the client completely normally; it's just not stored, so unrelated CDN/analytics/tracker noise doesn't pollute history and search on a real engagement. No rules (or none enabled) means everything is recorded, same as before scope existed. `a` adds a rule (a pattern, and `tab` to toggle regex matching - same Match-text-or-regex model as match-and-replace rules), `space` toggles one on/off, `d` deletes the selected one. A non-regex pattern matches by case-insensitive substring against the host - `example.com` matches `example.com`, `www.example.com`, and `api.example.com` alike, covering "this domain and its subdomains" without a separate wildcard syntax. Repeater and Intruder always record regardless of scope - a request you deliberately resend or fuzz is something you clearly want to see the result of, not noise scope exists to cut. ## Architecture `mitmuxd` owns the proxy listener and the SQLite database; `mitmux` is a thin client that only ever talks to the daemon over a Unix socket (`internal/ipc`). This split is deliberate: a web UI, a CLI scanner, or any other client could be bolted on later without touching the proxy engine, and the TUI restarting (or crashing) never interrupts capture. Capture fidelity is the other core design constraint: for HTTP/1.1 traffic, request and response bytes stored in history are exactly what was read off the wire - captured via a `net.Conn` wrapper that records bytes as `net/http`'s own (memory-safe, battle-tested) parser consumes them, rather than re-serializing a parsed representation. For HTTP/2, which has no meaningful single "raw bytes" form, the stored representation is a reconstruction, and every stored entry says which kind it is (`request_exact`/`response_exact` in the database, "exact" vs. "reconstructed" in the UI). Repeater and Intruder both write raw bytes straight to the wire for the same reason - the whole point of a Repeater is that a deliberately malformed request reaches the target unmodified. Package layout: ``` cmd/mitmuxd/ daemon entrypoint cmd/mitmux/ TUI entrypoint internal/ca/ root CA + per-host leaf certificate generation internal/proxy/ proxy engine: HTTP/CONNECT handling, capture, Repeater, Intruder internal/rules/ match-and-replace engine internal/scope/ target scope (what gets recorded) internal/store/ SQLite storage, FTS5 search internal/ipc/ daemon <-> client protocol (JSON over a Unix socket) ``` ## Development ```sh go build ./... go vet ./... gofmt -l . # should print nothing go test ./... ``` There's no mock traffic layer - the test suite covers pure logic (marker parsing, search-query parsing) that's worth locking down with real tests rather than trusting by inspection. Everything that touches the network, the daemon, or the TUI has been verified by actually running it against real traffic during development; see commit messages for what was checked and how. ## Known limitations Deliberate scope decisions, not oversights - see `PLAN.md` for the reasoning behind each: - Match-and-replace: headers only, no body rules yet - Intruder: Sniper attack only (no battering ram / pitchfork / cluster bomb), sequential sending, capped at 1000 requests per attack - `mitmuxd -install-ca` prints per-OS trust-store install steps; it never runs them for you (see Quick start above for why) - No WebSocket interception - No client (mutual-TLS) certificate support - Upstream proxy chaining (`-upstream-proxy`) is HTTP CONNECT only, no SOCKS5 - No active or passive vulnerability scanning, no plugin system - this is a manual-testing tool, not a scanner