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/ipc | |
| 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/ipc')
| -rw-r--r-- | internal/ipc/ipc.go | 75 | ||||
| -rw-r--r-- | internal/ipc/server.go | 47 |
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) |