diff options
| -rw-r--r-- | PLAN.md | 58 | ||||
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | internal/proxy/certdownload_test.go | 24 | ||||
| -rw-r--r-- | internal/proxy/proxy.go | 39 |
4 files changed, 142 insertions, 1 deletions
@@ -378,3 +378,61 @@ 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. + +## Licensing, packaging, and browser/mobile support + +Licensed GPL-3.0 (see `LICENSE`) - deliberate for a security tool +specifically: keeps derivatives open, a common and well-regarded choice +in that community, and doesn't foreclose the author dual-licensing the +code commercially later (remains available as sole copyright holder, +independent of the public license) or relicensing outright in the +future (unconstrained for as long as the codebase has no outside +contributors - the harder case only starts once other people's +copyrighted changes are in it). + +Added a `-version` flag to both binaries (`internal/version`, set via +`-ldflags` at build time, defaulting to "dev" otherwise) and a +`Makefile` (`make build`/`make test`/`make release`/`make install`). +Cross-platform support turned out to already be ~95% there by accident +of earlier choices - pure-Go SQLite (no CGO), `os.UserConfigDir()` +instead of a hardcoded XDG path, nothing Linux-specific anywhere in the +codebase - so making it explicit was verification and packaging work, +not a rewrite: `make release` was run for real and produced correctly- +formatted binaries (confirmed with `file`, not just an exit code) for +linux/darwin/windows/freebsd across amd64+arm64 where applicable, all +`CGO_ENABLED=0`. Linux remains the only platform actually run during +development, though - macOS/Windows/FreeBSD compile clean and pass `go +vet` but haven't touched real hardware, documented honestly as such in +the README's Platforms section rather than as a tested claim. + +CA certificate distribution for browsers/mobile: `mitmuxd` now +recognizes the magic hostname `mitmux.cert` on its plain-HTTP proxy +path and serves its own CA certificate as a download +(`internal/proxy/proxy.go`'s `serveCACert`/`isCertDownloadHost`) - +`http://mitmux.cert/` from any client already configured to proxy +through mitmux gets the cert with `Content-Type: +application/x-x509-ca-cert`, which triggers iOS/Android's native +"install this certificate" prompt directly. Same idea as mitmproxy's +own `http://mitm.it/`, arrived at independently rather than reusing +their domain - `.cert` isn't a registered TLD, so it can never collide +with a real site. Deliberately HTTP-only: fetching it over HTTPS would +require the client to already trust mitmux's CA to MITM that +connection, the exact chicken-and-egg problem this exists to solve. +This matters most for mobile devices, which otherwise have no +convenient way to get a certificate file onto the device at all short +of emailing it to yourself or similar. Verified live: fetched +`http://mitmux.cert/` through a real running proxy, confirmed the +downloaded bytes are byte-identical to the actual `ca.pem` on disk, +confirmed a request with an explicit port and path still matches, +confirmed normal proxying to an unrelated host is unaffected, and +confirmed the cert-download request itself never gets recorded to +history (it's answered before `record()` is ever reached). + +Any standard proxy-switcher extension (FoxyProxy, etc.) or a phone/ +tablet's own Wi-Fi proxy setting already works with mitmux exactly like +it would with Burp/ZAP/Caido - mitmux is a normal forward proxy +speaking the standard protocol, nothing proxy-switcher-specific to +support. No code needed here, just documented clearly in the README's +Quick start (previously this was implied but never actually spelled +out for a phone/tablet setup, which is a real, common daily workflow +this tool hadn't explicitly walked through before). @@ -144,13 +144,33 @@ shorter `-socket /tmp/mitmux.sock` (and the matching `-socket` to 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`. + 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. 4. **Open the TUI** (in another terminal - the daemon keeps running independently): diff --git a/internal/proxy/certdownload_test.go b/internal/proxy/certdownload_test.go new file mode 100644 index 0000000..aae5f48 --- /dev/null +++ b/internal/proxy/certdownload_test.go @@ -0,0 +1,24 @@ +package proxy + +import "testing" + +func TestIsCertDownloadHost(t *testing.T) { + tests := []struct { + host string + want bool + }{ + {"mitmux.cert", true}, + {"mitmux.cert:80", true}, + {"MITMUX.CERT", true}, + {"Mitmux.Cert:8080", true}, + {"example.com", false}, + {"notmitmux.cert", false}, + {"mitmux.cert.evil.com", false}, + {"", false}, + } + for _, tt := range tests { + if got := isCertDownloadHost(tt.host); got != tt.want { + t.Errorf("isCertDownloadHost(%q) = %v, want %v", tt.host, got, tt.want) + } + } +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 2e80ed2..523ca01 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -31,6 +31,7 @@ import ( "net" "net/http" "net/url" + "strings" "sync" "time" @@ -611,6 +612,10 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "mitmux: request must use absolute-form URI (configure as a proxy, not a target)", http.StatusBadRequest) return } + if isCertDownloadHost(r.URL.Host) { + s.serveCACert(w) + return + } host := r.URL.Host dial := func(ctx context.Context) (net.Conn, string, error) { return dialUpstreamPlain(ctx, host, s.UpstreamProxy) @@ -618,6 +623,40 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) { s.forward(dial, r.URL.Scheme, r.URL.Host, w, r) } +// certDownloadHost is a magic hostname mitmux intercepts and answers +// itself, serving its own CA certificate - reachable over plain HTTP +// from any client configured to use mitmux as its proxy, including a +// mobile browser, which otherwise has no easy way to get a file onto +// the device to trust as a CA at all. Deliberately not a real, +// resolvable domain (".cert" isn't a registered TLD) so it can never +// collide with an actual site someone meant to visit - the same idea +// as mitmproxy's own http://mitm.it/, arrived at independently rather +// than reusing their domain. HTTP only, on purpose: fetching this over +// HTTPS would require the client to already trust mitmux's CA to MITM +// that very connection - exactly the chicken-and-egg problem this page +// exists to solve, so intercepting it on the CONNECT/TLS path wouldn't +// make sense and isn't attempted. +const certDownloadHost = "mitmux.cert" + +func isCertDownloadHost(hostPort string) bool { + host := hostPort + if h, _, err := net.SplitHostPort(hostPort); err == nil { + host = h + } + return strings.EqualFold(host, certDownloadHost) +} + +// serveCACert answers with the CA certificate as a download. The +// content type (application/x-x509-ca-cert) is what makes iOS and +// Android offer to install it as a trusted certificate directly from +// the browser's download prompt, rather than just saving a plain file. +func (s *Server) serveCACert(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/x-x509-ca-cert") + w.Header().Set("Content-Disposition", `attachment; filename="mitmux-ca.pem"`) + w.WriteHeader(http.StatusOK) + w.Write(s.ca.CertPEM) +} + func stripHopByHop(h http.Header) { for _, k := range hopByHopHeaders { h.Del(k) |