From 7b9bf73e46102969d5802300469862296f979ef6 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:11:00 +0200 Subject: Multiple proxy listeners and upstream proxy chaining Closes 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"). Server.Addr became Server.Addrs; ListenAndServe binds every address up front - before any of them start serving - so a bad address fails startup immediately rather than leaving the daemon partially listening, and rolls back already-opened listeners if a later one fails to bind. All addresses share the same handler/history/CA/rules: one logical proxy reachable on more than one address, not several independent proxies in one process. Upstream proxy chaining: -upstream-proxy host:port (optional http:// prefix, stripped for convenience) routes every outbound connection through another HTTP CONNECT proxy instead of dialing origins directly. dialViaProxy does the CONNECT handshake to the upstream and hands back a plain net.Conn as if it were a direct connection; dialUpstreamTLS (CONNECT/HTTPS path) and dialUpstreamPlain (plain-HTTP path) both take an upstreamProxy parameter and route through it when set. The two paths need different handling: CONNECT/HTTPS is transparent below the tunnel (once the CONNECT handshake succeeds, TLS and the request on top of it look identical to a direct connection, so roundTripH2 and the H1 read side need no changes at all), but plain HTTP has to send an absolute-form request line to the upstream proxy instead of origin-form - so roundTripH1 gained a proxyForm parameter, and forward() selects it based on scheme=="http" && UpstreamProxy!="". Chaining into another intercepting/MITM proxy (including another mitmuxd) needs that proxy's own CA trusted too, or TLS verification fails - this is inherent to chaining MITM proxies, not a gap here, and confirmed live below rather than left as a guess. internal/proxy/dialer_test.go: dialViaProxy against a real local CONNECT stub (not a mock) - direct dial, successful tunnel-and-echo through a proxy, and a proxy that refuses the CONNECT with a non-200. All three exercise the actual network code path, not just the string-building around it. Verified live: started a daemon with two -listen addresses, sent requests through both, confirmed a single shared history; killed it mid-flight with SIGTERM and confirmed both listeners closed cleanly; started it with one bad address in the list and confirmed startup failed immediately with the already-bound port released, no lingering process. For chaining: sent plain HTTP and HTTPS through a downstream mitmuxd configured with -upstream-proxy pointing at a genuine passthrough CONNECT stub (tunnels raw bytes, doesn't MITM) and got real content back on both; separately chained through a second mitmuxd instance and got the expected "certificate signed by unknown authority" error, cleanly recorded in history rather than hanging. go build/vet/gofmt/test/mod tidy all clean. --- cmd/mitmuxd/main.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) (limited to 'cmd/mitmuxd/main.go') diff --git a/cmd/mitmuxd/main.go b/cmd/mitmuxd/main.go index 9c77516..baaba85 100644 --- a/cmd/mitmuxd/main.go +++ b/cmd/mitmuxd/main.go @@ -14,6 +14,7 @@ import ( "os/signal" "path/filepath" "runtime" + "strings" "syscall" "time" @@ -24,13 +25,25 @@ import ( ) func main() { - listen := flag.String("listen", "127.0.0.1:8080", "proxy listen address") + listen := flag.String("listen", "127.0.0.1:8080", "proxy listen address(es) - comma-separated for more than one, e.g. \"127.0.0.1:8080,127.0.0.1:8081\"") caDir := flag.String("ca-dir", "", "directory for CA cert/key and history db (default: XDG config dir)") dbPath := flag.String("db", "", "path to history database (default: /history.db)") socketPath := flag.String("socket", "", "control socket path (default: $XDG_RUNTIME_DIR/mitmux.sock, else /mitmux.sock)") installCA := flag.Bool("install-ca", false, "generate the CA if needed, print OS-specific trust-store install steps, and exit (doesn't start the proxy)") + upstreamProxy := flag.String("upstream-proxy", "", "chain all outbound connections through this HTTP CONNECT proxy (host:port, optional http:// prefix) instead of dialing origins directly") flag.Parse() + var listenAddrs []string + for _, a := range strings.Split(*listen, ",") { + if a = strings.TrimSpace(a); a != "" { + listenAddrs = append(listenAddrs, a) + } + } + if len(listenAddrs) == 0 { + log.Fatalf("-listen: no addresses given") + } + upstream := strings.TrimPrefix(strings.TrimSpace(*upstreamProxy), "http://") + dir := *caDir if dir == "" { d, err := ca.Dir() @@ -80,11 +93,14 @@ func main() { defer os.Remove(sockFile) log.Printf("control socket: %s", sockFile) - srv := proxy.New(*listen, root, db) + srv := proxy.New(listenAddrs, root, db, upstream) + if upstream != "" { + log.Printf("chaining outbound connections through upstream proxy %s", upstream) + } hub := ipc.NewHub() srv.OnEntry = ipc.LogAndBroadcast(hub) - ipcSrv := ipc.NewServer(db, hub, srv, *listen) + ipcSrv := ipc.NewServer(db, hub, srv, strings.Join(listenAddrs, ", ")) go func() { if err := ipcSrv.Serve(sockLn); err != nil { log.Printf("control socket: %v", err) -- cgit v1.2.3