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/repeat.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/repeat.go')
| -rw-r--r-- | internal/proxy/repeat.go | 15 |
1 files changed, 11 insertions, 4 deletions
diff --git a/internal/proxy/repeat.go b/internal/proxy/repeat.go index f682b99..8e7836d 100644 --- a/internal/proxy/repeat.go +++ b/internal/proxy/repeat.go @@ -36,7 +36,7 @@ func (s *Server) sendRaw(ctx context.Context, scheme, host string, raw []byte, s started := time.Now() method, path := parseRequestLine(raw) - conn, err := dialForRepeat(ctx, scheme, host) + conn, err := s.dialForRepeat(ctx, scheme, host) if err != nil { return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, false, false, 0, err.Error(), source) } @@ -101,8 +101,11 @@ func (s *Server) recordRaw(started time.Time, duration time.Duration, scheme, ho } // dialForRepeat connects to host for scheme, forcing HTTP/1.1 over ALPN -// when TLS is involved (see Repeat's doc comment for why). -func dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { +// when TLS is involved (see Repeat's doc comment for why), presenting a +// client certificate if one is configured for hostname (see +// clientCertFor) - a resent or fuzzed request against a mutual-TLS host +// needs one just as much as a live proxied request does. +func (s *Server) dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { nd := &net.Dialer{Timeout: 10 * time.Second} if scheme != "https" { hostPort := host @@ -122,7 +125,11 @@ func dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { if err != nil { return nil, err } - conn := tls.Client(raw, &tls.Config{ServerName: hostname, NextProtos: []string{"http/1.1"}}) + cfg := &tls.Config{ServerName: hostname, NextProtos: []string{"http/1.1"}} + if cert := s.clientCertFor(hostname); cert != nil { + cfg.Certificates = []tls.Certificate{*cert} + } + conn := tls.Client(raw, cfg) if err := conn.HandshakeContext(ctx); err != nil { raw.Close() return nil, err |