|
| 1 | +# Fix MITM cert generation for SNI-deferred connections |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +When a SOCKS5 CONNECT arrives with a raw IP (no hostname) and the connection is SNI-deferred, the MITM proxy generates a TLS certificate for the raw IP address. This cert lacks IP SANs, causing `x509: cannot validate certificate for <IP> because it doesn't contain any IP SANs` errors on the upstream connection. |
| 6 | + |
| 7 | +The `sniAwareTLSConfig` callback in goproxy already handles this for normal connections by reading SNI from the TLS ClientHello and regenerating the cert with the hostname. But for SNI-deferred connections, the custom connect handler peeks the ClientHello bytes before they reach goproxy, and the MITM layer may not see the SNI. |
| 8 | + |
| 9 | +Observed in production: OpenClaw's Telegram client falls back to direct IP connections (`149.154.167.220`) when DNS-resolved IPs are unreachable. These IP-only connections get SNI-deferred, pass through the MITM, but the upstream TLS handshake fails because the MITM cert is for the IP. |
| 10 | + |
| 11 | +## Context |
| 12 | + |
| 13 | +- SNI deferral: `internal/proxy/server.go` (handleConnect, sniPolicyCheck) |
| 14 | +- SNI extraction: `internal/proxy/sni.go` (extractSNI, peekSNI) |
| 15 | +- MITM cert generation: `internal/proxy/inject.go` (sniAwareTLSConfig, GetConfigForClient) |
| 16 | +- DNS reverse cache: `internal/proxy/dns_reverse.go` |
| 17 | + |
| 18 | +## Development Approach |
| 19 | + |
| 20 | +- **Testing approach**: Regular (code first, then tests) |
| 21 | +- CRITICAL: every task MUST include new/updated tests |
| 22 | +- CRITICAL: all tests must pass before starting next task |
| 23 | + |
| 24 | +## Testing Strategy |
| 25 | + |
| 26 | +- Unit tests: `internal/proxy/sni_test.go` for SNI extraction |
| 27 | +- Integration tests: `internal/proxy/server_test.go` for SNI-deferred MITM connections |
| 28 | +- Verify on knuth with OpenClaw's Telegram fallback IP behavior |
| 29 | + |
| 30 | +## Implementation Steps |
| 31 | + |
| 32 | +### Task 1: Investigate SNI flow through custom connect handler to MITM |
| 33 | + |
| 34 | +**Files:** |
| 35 | +- Read: `internal/proxy/server.go` (handleConnect, sniPolicyCheck) |
| 36 | +- Read: `internal/proxy/inject.go` (sniAwareTLSConfig) |
| 37 | + |
| 38 | +- [ ] Trace the data flow: when sniPolicyCheck peeks ClientHello bytes and returns `io.MultiReader(buf, reader)`, verify that goproxy's TLS layer still receives the ClientHello (including SNI) |
| 39 | +- [ ] Check if `sniAwareTLSConfig.GetConfigForClient` is called and receives the SNI hostname |
| 40 | +- [ ] Identify whether the peeked bytes are consumed before goproxy reads them |
| 41 | +- [ ] Document the exact failure point |
| 42 | +- [ ] Run tests |
| 43 | + |
| 44 | +### Task 2: Fix cert generation for SNI-deferred IP connections |
| 45 | + |
| 46 | +**Files:** |
| 47 | +- Modify: `internal/proxy/server.go` |
| 48 | +- Modify: `internal/proxy/inject.go` |
| 49 | +- Test: `internal/proxy/server_test.go` |
| 50 | + |
| 51 | +- [ ] After SNI extraction in sniPolicyCheck, pass the recovered hostname to the MITM layer |
| 52 | +- [ ] Option A: update the CONNECT target in the goproxy context so the MITM uses hostname instead of IP |
| 53 | +- [ ] Option B: store the SNI hostname in the connection context and use it in sniAwareTLSConfig |
| 54 | +- [ ] Option C: replace the IP in the dial target with the SNI hostname before routing to the injector |
| 55 | +- [ ] Ensure the DNS reverse cache is populated (already done in sniPolicyCheck) |
| 56 | +- [ ] Write test: SNI-deferred IP connection produces cert with hostname SAN |
| 57 | +- [ ] Write test: SNI-deferred IP connection successfully MITMs upstream TLS |
| 58 | +- [ ] Run tests |
| 59 | + |
| 60 | +### Task 3: Verify acceptance criteria |
| 61 | + |
| 62 | +- [ ] Deploy to knuth, restart stack |
| 63 | +- [ ] Verify OpenClaw's Telegram fallback IP connections work through MITM |
| 64 | +- [ ] Verify no `IP SANs` errors in sluice logs |
| 65 | +- [ ] Verify credential injection works for IP-only Telegram connections |
| 66 | +- [ ] Run full test suite: `go test ./... -v -timeout 30s` |
| 67 | + |
| 68 | +### Task 4: [Final] Update documentation |
| 69 | + |
| 70 | +- [ ] Update CLAUDE.md SNI deferral section to note the cert generation fix |
| 71 | +- [ ] Move this plan to `docs/plans/completed/` |
| 72 | + |
| 73 | +## Post-Completion |
| 74 | + |
| 75 | +**Manual verification:** |
| 76 | +- Force OpenClaw to use IP-only Telegram connection (block DNS, let fallback kick in) |
| 77 | +- Verify MITM cert has correct hostname SAN |
| 78 | +- Check sluice logs for clean SNI -> ALLOW flow without cert errors |
0 commit comments