srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy/repeat.go
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-16 22:57:00 +0200
committersrdusr <[email protected]>2026-06-16 22:57:00 +0200
commit23c8ab359c2108654d57176e233d2c099b398f31 (patch)
tree3e2d1c66d987b4b771da69e67bd8a0c9ad2fc3db /internal/proxy/repeat.go
parent6114567258bcad0517a0d881168711aaacdba5d5 (diff)
downloadmitmux-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.go15
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