Skip to content

[draft] mitm-df: outbound (TLS termination, fronts table, audit log)#263

Draft
garmr-ulfr wants to merge 1 commit into
mitm-df-foundationfrom
mitmfront-outbound
Draft

[draft] mitm-df: outbound (TLS termination, fronts table, audit log)#263
garmr-ulfr wants to merge 1 commit into
mitm-df-foundationfrom
mitmfront-outbound

Conversation

@garmr-ulfr
Copy link
Copy Markdown
Collaborator

Summary

Stacked on top of #262. Once #262 lands, this PR introduces the actual mitm-df outbound that uses the foundation's name-constrained CA.

  • TCP outbound registered as "mitm-df". DialContext returns the user side of an in-memory net.Pipe — no listening port.
  • tls.Server with GetCertificate callback driven by the actual ClientHello SNI (not the router-supplied destination), so a sniff/routing mismatch can't mint the wrong leaf. Callback runs: deny-list check → fronts lookup → audit record → mitmca.CA.SignLeaf(sni).
  • Fronts table maps user SNIs to fronted_sni / verify_san / optional redirect_addr. Match semantics: exact-or-strict-subdomain (vercel.app matches *.vercel.app but not notvercel.app or vercel.app.evil.com).
  • JSONL audit log records every decision (allow, deny, no-match, error) with {ts, sni, fronted_sni, decision, reason}. Empty audit_log_path disables.
  • Egress uTLS uses HelloCustom + ApplyPreset so the user's negotiated ALPN is the only one offered to the front. Named uTLS presets (HelloChrome_Auto, etc.) bake an ALPN extension into the replayed ClientHello that overrides Config.NextProtos; HelloCustom is the only path that inherits cleanly. Without this, every fronted flow would leak [h2, http/1.1] regardless of the inner request shape — breaks h1-only flows like googlevideo / alive.github.com.
  • CA auto-generated on first boot at the configured ca.cert_path / ca.key_path if absent (delegates to mitmca.GenerateCA); reloaded on subsequent boots via mitmca.LoadCA which refuses certs without NameConstraints.

Files

  • option/mitmdf.goMITMDFOutboundOptions + nested MITMDFCAOptions / MITMDFFrontEntry.
  • protocol/mitmdf/
    • outbound.go — registration, NewOutbound, DialContext, the serve goroutine, the GetCertificate callback, CA bootstrap, verifyFrontSAN callback for the egress.
    • fronts.goFrontEntry parsing, suffix-aware matcher, deny-list match.
    • audit.go — append-only JSONL with sync.Mutex, nil-safe.
    • utls_preset.go — preset map + HelloCustom/ApplyPreset ALPN-rewrite helper.
    • fronts_test.go, audit_test.go — unit coverage.
  • test/e2e/mitmdf_test.goTestMITMDFE2E (full pipeline through an in-process fronting server: leaf chains to local CA, captured egress SNI = front, captured egress ALPN = [h2], bytes round-trip, audit allow) + TestMITMDFDenyList (deny-list path: no mint, audit deny).
  • testdata/options/mitmdf_client.json — example config; passes lantern-box check.
  • constant/proxy.go, protocol/register.go, go.mod — type constant, registration, utls promoted to direct dep.

Out of scope (follow-ups)

  • Hardware-backed KeyStore (keystore_macos.go, keystore_android.go, …) — the KeyStore interface is already in place on the foundation branch; per-platform variants behind build tags.
  • SPKI pinning to known CDN intermediates (pinner.go per architecture doc).
  • Config-server kill switch + weekly fronts validation CI — cross-repo.
  • Per-platform trust-store install commands for the CA.
  • Old-platform refusal (Android < 9, Windows < 10).
  • Leaf-cert cache (mitmca mints per-handshake; ECDSA P-256 keygen is cheap).

Test plan

  • go build ./...
  • go vet ./...
  • go test ./protocol/mitmdf/... ./internal/mitmca/...
  • go test ./test/e2e/ -run TestMITMDF -v
  • lantern-box check --config testdata/options/mitmdf_client.json — generates a CA at the configured paths with PermittedDNSDomainsCritical: true, CA:TRUE pathlen:0, key mode 0600.

🤖 Generated with Claude Code

Adds a TCP outbound that terminates the user's TLS handshake with a
leaf cert minted by a per-device, name-constrained CA, then re-encrypts
to a configured fronting target:

  - DialContext returns the user side of an in-memory pipe (no listening
    port); the proxy side runs tls.Server with a GetCertificate callback
    driven by the actual ClientHello SNI.
  - The signing CA (internal/mitmca) carries an RFC 5280 NameConstraints
    extension (PermittedDNSDomainsCritical, MaxPathLen 0). Generated
    per-device on first boot with the key persisted in PKCS#8 mode 0600
    via atomic temp+rename. Even with the key in hand, an attacker
    cannot mint leaves outside the configured permitted_domains.
  - Fronts table maps user SNI -> fronted_sni / verify_san /
    redirect_addr. A deny list short-circuits GetCertificate before any
    signature is produced; every decision is recorded to a JSONL audit
    log.
  - Egress uses uTLS HelloCustom + ApplyPreset so the user's negotiated
    ALPN is the only one offered to the front. Named uTLS presets bake
    an ALPN extension into the replayed ClientHello that overrides
    Config.NextProtos; HelloCustom is the only path that inherits
    cleanly.

Unit tests cover the CA, fronts matcher, deny list, concurrent
audit-log writes, and uTLS preset mapping. The e2e suite spins up an
in-process fronting server and exercises the full pipeline plus a
deny-list-blocked variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton added a commit that referenced this pull request May 15, 2026
PR #263 (mitmfront-outbound) lands the actual outbound under
protocol/mitmdf/ rather than internal/mitmdf/ — sibling to the
existing protocol/ transports (algeneva, lanturn, samizdat, unbounded,
water). Update the architecture doc to match:

- Component diagram: MITM box now labeled protocol/mitmdf
- Package layout block: mitmdf/ moves out from under internal/ to
  protocol/, with a one-line note explaining why mitmca stays under
  internal/ (private helper, consumed only by mitmdf)
- Implementation-order list: the three mitmdf paths now reference
  protocol/mitmdf/...
- Drop session.go from the layout (its responsibilities fold into
  outbound.go's serve goroutine — matches what #263 actually shipped)
- Add utls_preset.go to the layout with a note about the
  HelloCustom + ApplyPreset ALPN-inheritance detail that PR #263 caught
- Add test/e2e/mitmdf_test.go to the layout (#263 ships it; the
  previous draft of this doc deferred it)

No code changes; doc-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant