diff options
| author | srdusr <[email protected]> | 2026-06-16 22:57:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-06-16 22:57:00 +0200 |
| commit | 23c8ab359c2108654d57176e233d2c099b398f31 (patch) | |
| tree | 3e2d1c66d987b4b771da69e67bd8a0c9ad2fc3db /internal/proxy/proxy.go | |
| parent | 6114567258bcad0517a0d881168711aaacdba5d5 (diff) | |
| download | mitmux-23c8ab359c2108654d57176e233d2c099b398f31.tar.gz mitmux-23c8ab359c2108654d57176e233d2c099b398f31.zip | |
Client (mutual-TLS) certificates
Adds internal/clientcert: a cert/key pair matched to hosts by the same
substring-or-regex pattern model as scope.Rule, so mitmux can present
a client certificate on an upstream TLS handshake that requires one -
the previous behavior was a hard handshake failure with no way to
authenticate. Wired into both places mitmux dials an https:// upstream
over its own TLS client connection: proxy.go's handleConnect (live
proxied traffic) and repeat.go's dialForRepeat (Repeater/Intruder
resends), both through a new Server.clientCertFor(host) helper.
Stored in a new client_certs table, mirroring the existing scope_rules
persistence pattern. The TUI (`t` from history) is add-only like
scope, for the same reason: delete and re-add covers changing
anything, and it's a rarely-touched, low-cardinality list. The add
form takes cert/key file paths and reads them once at save time - PEM
content, not the path, is what's stored and later presented, so a
cert keeps working even if the original file moves afterward.
Verified live against a real mutual-TLS-requiring origin server:
without a matching cert the handshake correctly fails; with one
configured, the origin receives it and the request succeeds; toggling
it off reproduces the failure, confirming the enable/disable path
works end to end.
Diffstat (limited to 'internal/proxy/proxy.go')
| -rw-r--r-- | internal/proxy/proxy.go | 43 |
1 files changed, 38 insertions, 5 deletions
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index aec724a..bc22bcf 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -39,6 +39,7 @@ import ( "golang.org/x/net/http2" "mitmux/internal/ca" + "mitmux/internal/clientcert" "mitmux/internal/rules" "mitmux/internal/scope" "mitmux/internal/store" @@ -228,7 +229,7 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) { client.SetDeadline(time.Time{}) dial := func(ctx context.Context) (net.Conn, string, error) { - return dialUpstreamTLS(ctx, hostPort, hostname, s.UpstreamProxy) + return dialUpstreamTLS(ctx, hostPort, hostname, s.UpstreamProxy, s.clientCertFor(hostname)) } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.forward(dial, "https", hostname, w, r) @@ -256,16 +257,22 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) { // HTTP/2 and HTTP/1.1 over ALPN and letting the server pick. Chaining // through another proxy is transparent to everything from here on: once // the CONNECT tunnel is up, TLS and the request/response on top of it -// look identical to a direct connection. -func dialUpstreamTLS(ctx context.Context, hostPort, sni, upstreamProxy string) (net.Conn, string, error) { +// look identical to a direct connection. cert, if non-nil, is presented +// during the handshake for servers that require mutual TLS - see +// clientCertFor. +func dialUpstreamTLS(ctx context.Context, hostPort, sni, upstreamProxy string, cert *tls.Certificate) (net.Conn, string, error) { raw, err := dialViaProxy(ctx, hostPort, upstreamProxy) if err != nil { return nil, "", err } - conn := tls.Client(raw, &tls.Config{ + cfg := &tls.Config{ ServerName: sni, NextProtos: []string{http2.NextProtoTLS, "http/1.1"}, - }) + } + if cert != nil { + cfg.Certificates = []tls.Certificate{*cert} + } + conn := tls.Client(raw, cfg) if err := conn.HandshakeContext(ctx); err != nil { raw.Close() return nil, "", err @@ -273,6 +280,32 @@ func dialUpstreamTLS(ctx context.Context, hostPort, sni, upstreamProxy string) ( return conn, conn.ConnectionState().NegotiatedProtocol, nil } +// clientCertFor returns the client certificate configured for host, if +// any - see clientcert.FindFor. Errors (a bad DB read, an unparseable +// PEM pair) are logged and treated as "no certificate configured" rather +// than failing the connection outright: a broken client-cert config +// shouldn't take down otherwise-working proxying for that host. +func (s *Server) clientCertFor(host string) *tls.Certificate { + if s.store == nil { + return nil + } + certs, err := s.store.ListClientCerts() + if err != nil { + log.Printf("list client certs: %v", err) + return nil + } + c := clientcert.FindFor(certs, host) + if c == nil { + return nil + } + tc, err := c.TLSCertificate() + if err != nil { + log.Printf("client cert %q: %v", c.Name, err) + return nil + } + return &tc +} + // dialUpstreamPlain connects to a plain (non-TLS) upstream for the // non-CONNECT proxy path, which is always HTTP/1.1. Unlike the TLS/ // CONNECT path, chaining here means dialing the upstream proxy's own |