srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy/proxy.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/proxy.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/proxy.go')
-rw-r--r--internal/proxy/proxy.go43
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