srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ipc
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/ipc
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/ipc')
-rw-r--r--internal/ipc/ipc.go75
-rw-r--r--internal/ipc/server.go47
2 files changed, 115 insertions, 7 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go
index d581313..68217c2 100644
--- a/internal/ipc/ipc.go
+++ b/internal/ipc/ipc.go
@@ -12,6 +12,7 @@ import (
"sync"
"time"
+ "mitmux/internal/clientcert"
"mitmux/internal/proxy"
"mitmux/internal/rules"
"mitmux/internal/scope"
@@ -69,6 +70,13 @@ type Request struct {
ScopeRule *scope.Rule `json:"scope_rule,omitempty"`
ScopeRuleID int64 `json:"scope_rule_id,omitempty"`
+ // For "clientcert_add": the new certificate (always an add, same
+ // reasoning as scope rules above). For "clientcert_delete"/
+ // "clientcert_toggle": ClientCertID (and RuleEnabled for toggle)
+ // identify the target.
+ ClientCert *clientcert.Cert `json:"client_cert,omitempty"`
+ ClientCertID int64 `json:"client_cert_id,omitempty"`
+
// For "set_flagged" and "delete_entry": ID identifies the history
// entry. "clear_history" needs no fields at all.
Flagged bool `json:"flagged,omitempty"`
@@ -99,13 +107,14 @@ type ImportEntry struct {
// Response is sent by the daemon to a client.
type Response struct {
- Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "scope_rules", "intrude_result", "intrude_done", "import_done", "status", "flagged", "deleted", "cleared", or "error"
- Entries []store.Summary `json:"entries,omitempty"` // for "list"
- Detail *EntryDetail `json:"detail,omitempty"` // for "get" and "repeat"
- New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push)
- Rules []rules.Rule `json:"rules,omitempty"` // for "rules"
- ScopeRules []scope.Rule `json:"scope_rules,omitempty"` // for "scope_rules"
- Status *StatusMsg `json:"status,omitempty"` // for "status"
+ Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "scope_rules", "intrude_result", "intrude_done", "import_done", "status", "flagged", "deleted", "cleared", or "error"
+ Entries []store.Summary `json:"entries,omitempty"` // for "list"
+ Detail *EntryDetail `json:"detail,omitempty"` // for "get" and "repeat"
+ New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push)
+ Rules []rules.Rule `json:"rules,omitempty"` // for "rules"
+ ScopeRules []scope.Rule `json:"scope_rules,omitempty"` // for "scope_rules"
+ ClientCerts []clientcert.Cert `json:"client_certs,omitempty"` // for "client_certs"
+ Status *StatusMsg `json:"status,omitempty"` // for "status"
// For "import_done": how many entries were actually inserted (a
// per-entry insert failure is skipped, not fatal to the batch).
@@ -444,6 +453,58 @@ func (c *Client) scopeRoundTrip(req Request) ([]scope.Rule, error) {
return resp.ScopeRules, nil
}
+// ListClientCerts returns every client certificate, enabled or not.
+func (c *Client) ListClientCerts() ([]clientcert.Cert, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.clientCertRoundTrip(Request{Type: "clientcert_list"})
+}
+
+// AddClientCert adds cert and returns its assigned ID.
+func (c *Client) AddClientCert(cert clientcert.Cert) (int64, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ saved, err := c.clientCertRoundTrip(Request{Type: "clientcert_add", ClientCert: &cert})
+ if err != nil {
+ return 0, err
+ }
+ if len(saved) == 0 {
+ return 0, errors.New("clientcert_add: daemon returned no certificate")
+ }
+ return saved[0].ID, nil
+}
+
+// DeleteClientCert removes a client certificate.
+func (c *Client) DeleteClientCert(id int64) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ _, err := c.clientCertRoundTrip(Request{Type: "clientcert_delete", ClientCertID: id})
+ return err
+}
+
+// SetClientCertEnabled toggles a client cert without touching its
+// content.
+func (c *Client) SetClientCertEnabled(id int64, enabled bool) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ _, err := c.clientCertRoundTrip(Request{Type: "clientcert_toggle", ClientCertID: id, RuleEnabled: enabled})
+ return err
+}
+
+func (c *Client) clientCertRoundTrip(req Request) ([]clientcert.Cert, error) {
+ if err := c.enc.Encode(req); err != nil {
+ return nil, err
+ }
+ var resp Response
+ if err := c.dec.Decode(&resp); err != nil {
+ return nil, err
+ }
+ if resp.Type == "error" {
+ return nil, errors.New(resp.Error)
+ }
+ return resp.ClientCerts, nil
+}
+
// Subscribe opens a dedicated connection that streams newly captured
// history entries as they happen. The returned channel is closed when
// the connection ends; call the returned close func to stop early.
diff --git a/internal/ipc/server.go b/internal/ipc/server.go
index c890a54..4db27b9 100644
--- a/internal/ipc/server.go
+++ b/internal/ipc/server.go
@@ -9,6 +9,7 @@ import (
"regexp"
"sync"
+ "mitmux/internal/clientcert"
"mitmux/internal/proxy"
"mitmux/internal/rules"
"mitmux/internal/scope"
@@ -347,6 +348,52 @@ func (s *Server) handleConn(conn net.Conn) {
}
enc.Encode(Response{Type: "scope_rules"})
+ case "clientcert_list":
+ certs, err := s.db.ListClientCerts()
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "client_certs", ClientCerts: certs})
+
+ case "clientcert_add":
+ if req.ClientCert == nil {
+ enc.Encode(Response{Type: "error", Error: "clientcert_add: missing certificate"})
+ continue
+ }
+ cert := *req.ClientCert
+ if cert.IsRegex {
+ if _, err := regexp.Compile(cert.Pattern); err != nil {
+ enc.Encode(Response{Type: "error", Error: "invalid regex: " + err.Error()})
+ continue
+ }
+ }
+ if _, err := cert.TLSCertificate(); err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ id, err := s.db.AddClientCert(cert)
+ if err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ cert.ID = id
+ enc.Encode(Response{Type: "client_certs", ClientCerts: []clientcert.Cert{cert}})
+
+ case "clientcert_delete":
+ if err := s.db.DeleteClientCert(req.ClientCertID); err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "client_certs"})
+
+ case "clientcert_toggle":
+ if err := s.db.SetClientCertEnabled(req.ClientCertID, req.RuleEnabled); err != nil {
+ enc.Encode(Response{Type: "error", Error: err.Error()})
+ continue
+ }
+ enc.Encode(Response{Type: "client_certs"})
+
case "subscribe":
sub := s.hub.subscribe()
defer s.hub.unsubscribe(sub)