srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 54c3e3a18587ed30bc03a86c5ae84d48549f24ac (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
# 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). To write a plugin - any language, connects over
the same socket the TUI itself uses - see [`PLUGINS.md`](PLUGINS.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.
- **WebSocket**: `ws://`/`wss://` connections are relayed byte-for-byte
  unmodified with every frame captured for display, not just the
  upgrade handshake - see WebSocket below.
- **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`. Sort
  the loaded page by any column (`o`/`O`).
- **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**: mark positions in a request template with
  `§markers§`, supply payloads, and fuzz them with Sniper, Battering
  ram, Pitchfork, or Cluster bomb - Burp's own four attack modes.
  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 or body 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.
- **Client certificates**: configure a mutual-TLS cert/key per host
  pattern, presented automatically on matching handshakes - for
  proxied traffic and Repeater/Intruder resends alike.
- **Flagging**: mark an entry to revisit later (★), filterable via
  `flagged:true`.
- **Plugins**: any external process, any language, can connect over the
  same socket the TUI uses and tag entries with structured data - a
  JWT decoder, an authorization-bypass checker, anything. Tags show as
  a badge in the history list, searchable via `tag:name`, viewable
  (`T` from detail view) with JSON data syntax-highlighted the same way
  a pretty-printed response is. See [`PLUGINS.md`](PLUGINS.md) and
  `plugins/authcheck`/`plugins/paramminer`/`plugins/jslibscan`/
  `plugins/bpscanner` for real, working ones (an Autorize-style
  authorization checker, a Param Miner-style hidden parameter prober, a
  Retire.js-style scanner for known-vulnerable JS library versions, a
  Backslash Powered Scanner-style generic injection detector).
- **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.
- **Mouse support**: wheel scroll anywhere there's something to
  scroll, right-click for a context menu on the history/rules/scope
  lists. Coexists with tmux's own mouse mode the same way any other
  mouse-aware terminal app does. See [Mouse](#mouse) below.

## Install / build

Requires Go 1.26.5+ (see `go.mod`). No other dependencies - the SQLite
driver ([modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)) is
pure Go, so there's no C toolchain to install and nothing to link
against, on any platform.

```sh
git clone <this repo>
cd mitmux
make build          # -> bin/mitmuxd, bin/mitmux
```

`make build` covers the common case; see the [`Makefile`](Makefile)
itself for the rest - `make install` (via `go install`, respecting
`GOBIN`/`GOPATH` as usual), `make release` (cross-compiles both
binaries for every platform in `PLATFORMS` into `dist/`), `make test`
(the same build/vet/gofmt/test checks expected before every commit -
see `PLAN.md`). Pushing a `v*` tag runs the same `make release` in CI
and attaches the resulting archives (one per platform, `.tar.gz` on
Unix/`.zip` on Windows, `SHA256SUMS` alongside them) to a GitHub
Release, once this repo actually lives on GitHub - see
`.github/workflows/release.yml`. Building without `make` works
identically:

```sh
go build -o bin/mitmuxd ./cmd/mitmuxd
go build -o bin/mitmux  ./cmd/mitmux
```

### Platforms

Linux is the primary target and the only one this has actually run on
during development - every feature in this document was verified live
against a real daemon and real traffic there. macOS, Windows, and
FreeBSD all cross-compile cleanly with `CGO_ENABLED=0` and pass `go
vet` (`make release` builds all of them), and the code itself has
nothing Linux-specific in it - CA/history storage uses Go's own
cross-platform `os.UserConfigDir()`, not a hardcoded XDG path - but
they haven't been run on real hardware, so treat them as "should work,
not yet verified" rather than a tested claim. If you try one and hit
something, that's useful to know about.

One concrete thing worth knowing on macOS specifically: its Unix
domain socket path limit is shorter than Linux's, and the default
control-socket location lives under `~/Library/Application
Support/mitmux/` - a deeper path than Linux's usual
`$XDG_RUNTIME_DIR`. A long username or home directory path could push
past the limit; if `mitmuxd` fails to bind its control socket, pass a
shorter `-socket /tmp/mitmux.sock` (and the matching `-socket` to
`mitmux`) to both binaries.

## Quick start

1. **Start the daemon.** By default it listens on `127.0.0.1:8080` and
   stores its CA and history database under your OS's standard config
   directory (Linux: `~/.config/mitmux`, macOS: `~/Library/Application
   Support/mitmux`, Windows: `%AppData%\mitmux`):

   ```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.

   For a phone, tablet, or any other device where copying a file over
   and importing it manually is awkward - the actual common case for
   mobile testing - point the device's browser at
   **`http://mitmux.cert/`** once it's configured to proxy through
   mitmux (step 3). mitmuxd recognizes that hostname specifically and
   serves its own CA certificate as a download, the same trick
   [mitmproxy's `mitm.it`](https://mitm.it) uses: no DNS lookup, no
   real domain, works the moment traffic is flowing through the proxy
   at all - the browser's install-certificate prompt handles the rest.
   Plain `http://`, not `https://`: fetching it over TLS would need the
   device to already trust mitmux's CA to intercept that very
   connection, which is the exact problem this page solves.

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` -
   directly, or via a proxy-switcher extension like
   [FoxyProxy](https://getfoxyproxy.org/): add a new proxy profile
   pointing at `127.0.0.1:8080` (HTTP, and reused for HTTPS - mitmux
   handles both on the same listener), no mitmux-specific setup needed
   since it's a standard forward proxy speaking the same protocol Burp/
   ZAP/Caido all do. Same story for a phone or tablet: set its Wi-Fi
   proxy to your machine's LAN address and the daemon's port.

   Or skip manual configuration entirely: `mitmux -launch-browser=chrome`
   (or `firefox`, or `auto` to use whichever's installed) opens a fresh,
   throwaway browser profile already pointed at the proxy, landing
   straight on `http://mitmux.cert/` so installing the CA in that one
   profile is a single click. The profile is brand new every time -
   no cookies, extensions, or cached certificate-trust decisions carried
   over from your regular browsing - and never reused, matching the
   "don't disturb your everyday session" spirit of a pentest tool.
   mitmux doesn't ship its own browser - building and maintaining one
   is a different project entirely, and a terminal proxy tool has no
   business trying; this launches your existing one instead.

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`,
`-launch-browser` on `mitmux`.
Both also take `-version` (prints version/commit/date and exits - `dev`
for a plain `go build`; `make build`/`make release` fill it in from
`git describe`) and `-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. `-upstream-proxy socks5://[user:pass@]host:port`
chains through a SOCKS5 proxy instead - Tor, `ssh -D`, or any other
SOCKS5 relay - with optional username/password auth.

## 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` |
| `I` | import a HAR file's entries into history |
| `d` | Decoder |
| `/` | search |
| `o` | cycle sort column (captured/status/size/time taken/method/host/path) |
| `O` | reverse the current sort column's direction |
| `m` | match-and-replace rules |
| `s` | target scope (what gets recorded) |
| `t` | client (mutual-TLS) certificates |
| `q` | quit |

Also mouse-driven - wheel to scroll, right-click a row for a context
menu of the same actions. See [Mouse](#mouse) below.

`o` cycles which column sorts the currently loaded page (captured order
- the default, newest-first or search-relevance order - then status,
size, time taken, method, host, path); `O` reverses whichever column is
active. Client-side, on top of whatever List/Search already returned:
loading more or re-searching keeps the same sort applied. Status-code
color-coding (2xx green through 5xx red, the same convention Burp and
Caido use) isn't in the table itself - `bubbles/table`, the terminal
table widget this UI is built on, has no way to color one cell without
corrupting the whole row's layout (confirmed, not guessed: its column-
width fitting counts every character of a color code as visible text).
It's in the detail view's title instead, where that constraint doesn't
apply.

### Detail view

`tab` switches request/response, `p` toggles pretty-printed,
syntax-highlighted JSON on the response (keys/strings/numbers/booleans
colored, matching most editors - 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), `w` views captured WebSocket messages if this entry's
connection was upgraded (see WebSocket below), `esc` back.

### WebSocket

A `ws://` or `wss://` request that gets a matching `101 Switching
Protocols` back stops being one-shot request/response - mitmux relays
every frame byte-for-byte unmodified in both directions (this is
capture, not tampering) while decoding each one's payload for display.
Press `w` from an upgraded entry's detail view to see them: direction,
opcode (text/binary/close/ping/pong), size, and a preview; `enter` on
a row shows that frame's full decoded payload. One row per frame, not
per reassembled logical message - a message fragmented across several
frames (rare in real-world WebSocket traffic: JSON events, chat
messages, game state are almost always single-frame) shows up as
several rows rather than being stitched back together.

### 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.

### Import

`I` from the history list reads a HAR file and inserts its entries into
history, tagged `source:import` so they're easy to find or filter out
later (`source:import` in search). Works with a HAR from mitmux itself
or from anywhere else that produces one - Chrome/Firefox DevTools, Burp,
Postman. An entry that fails to convert is skipped rather than aborting
the whole import; the status line reports how many, if any. Imported
entries are always shown as "reconstructed" (never "exact") - they're
rebuilt from HAR's structured fields, not the literal bytes that were
actually on the wire for the original request, same situation an
HTTP/2 capture is already in.

### 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, three more things
are configurable before `ctrl+r` starts the attack - all normal-mode-only
shortcuts, available from any pane:

- `a` cycles the **attack mode**: Sniper, Battering ram, Pitchfork,
  Cluster bomb - Burp's own four, same semantics. Sniper fuzzes one
  marked position at a time through a single shared payload set, every
  other position held at its base value. Battering ram sends the same
  payload, from that same single set, into every marked position at
  once. Pitchfork and Cluster bomb are inherently per-position - that's
  their whole point - so they need one payload set per marked position
  instead of one shared set: put them in the same Payloads pane,
  separated by a line containing exactly `---`, in position order.
  Pitchfork walks all sets in lockstep, one request per index, stopping
  at the shortest set's length. Cluster bomb tries every combination
  (the last position cycles fastest), so its request count is the
  product of every set's length - capped at 1000 requests like every
  other mode, checked before anything is sent.
- `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 or bodies (`a` add, `enter`/`e` edit, `d` delete, `space`
toggle enabled, Part field to choose header vs body). Matching is
literal-substring by default, or regex if the rule's Regex toggle is
on. Header 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. Body rules run directly against the
raw body bytes; a body larger than the capture cap is left unmodified
rather than partially rewritten, and Content-Length is recomputed
automatically when a body rule changes a body's length.

### 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.

### Client (mutual-TLS) certificates

Press `t` from the history view to manage which client certificate
mitmux presents when an upstream server's TLS handshake requests one
- a target requiring mutual TLS otherwise fails the handshake before
any request/response ever happens. `a` adds one: a name, a host
pattern (same substring-or-regex model as scope and match-and-replace
rules), and paths to a PEM certificate file and its matching PEM
private key. The files are read once, at save time, and their content
- not the paths - is what's stored and later presented, so a cert
keeps working even if the original file moves or is deleted
afterward. `space` toggles one on/off, `d` deletes it. Applies to
proxied HTTPS traffic and to Repeater/Intruder resends against
`https://` targets alike; a host matching no configured certificate
just handshakes without one, same as if this feature didn't exist.

### Mouse

This is a real terminal application (any terminal, not just tmux - the
name is a naming convention, not a runtime dependency) with genuine
mouse support, not just a keyboard-only TUI:

- **Wheel** scrolls whatever's focused - the history/rules/scope
  tables, a Detail/Comparer/Decoder-output pane, or a Repeater/
  Intruder text editor.
- **Right-click** a row in the history, rules, or scope list opens a
  context menu of the same actions the keyboard shortcuts already do
  (view/repeater/intruder/flag/delete for history, edit/enable-disable/
  delete for rules and scope). Click an item, or navigate with
  `j`/`k`/arrows and `enter`; `esc` or right-clicking again dismisses
  it without doing anything.

One honest limitation, not an oversight: the menu always acts on the
row that's currently *selected*, not necessarily the exact row your
cursor happens to be over when you right-click. The underlying table
widget doesn't expose its own scroll position, so there's no reliable
way to map a click's screen coordinates back to a specific row without
reaching into that library's private internals - which this
deliberately doesn't do, rather than risk silently selecting the wrong
one. Scroll or navigate to the row you want first; wheel scroll and
keyboard navigation both work exactly as you'd expect.

If you're running inside tmux, its own mouse mode needs to be on too
(`set -g mouse on` in `.tmux.conf`) for mouse events to reach mitmux at
all - tmux forwards them to whichever pane is focused once that's set,
the same way it does for nvim or any other mouse-aware terminal app,
nothing mitmux-specific to configure. If you ever want to select and
copy on-screen text with the mouse instead (which an app with mouse
mode on normally intercepts), most terminal emulators let you hold
Shift while click-dragging to fall back to the terminal's own native
selection.

## 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:

- Intruder: sequential sending only (no concurrent workers), capped at
  1000 requests per attack across all four modes
- `mitmuxd -install-ca` prints per-OS trust-store install steps; it
  never runs them for you (see Quick start above for why)
- WebSocket messages are captured one row per frame, not reassembled
  from fragments (rare in real-world traffic) - see WebSocket below
- No active or passive vulnerability scanning, no plugin system - this
  is a manual-testing tool, not a scanner

## License

GPL-3.0. See [LICENSE](LICENSE).

Copyright (C) 2026 srdusr