Skip to content

Commit ca6b3af

Browse files
committed
docs: per-request policy and go-mitmproxy migration
Update CLAUDE.md and README with per-request policy details and go-mitmproxy migration notes. Move completed plan files to docs/plans/completed/.
1 parent 4b8da0f commit ca6b3af

4 files changed

Lines changed: 23 additions & 27 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,21 @@ Extends phantom swap to handle OAuth credentials bidirectionally. Static credent
160160
| Protocol | Credential injection | Content inspection | Policy granularity |
161161
|----------|---------------------|-------------------|--------------------|
162162
| HTTP/HTTPS | Built-in MITM, phantom swap | Full request/response | Per-request (allow-once = one HTTP request) |
163-
| gRPC | Header phantom swap (Content-Type detection) | Request/response metadata | Per-connection in practice (see gRPC caveat below) |
163+
| gRPC | Header phantom swap via go-mitmproxy Addon hooks (per HTTP/2 stream) | Request/response metadata | Per-request (each HTTP/2 stream is a separate policy check) |
164164
| WebSocket | Handshake headers + text frame phantom swap | Text frame deny + redact rules | Per-connection (one upgrade = one session) |
165165
| SSH | Jump host, key from vault | N/A | Per-connection (channels belong to one session) |
166166
| IMAP/SMTP | AUTH command proxy, phantom password swap | N/A | Per-connection (one mailbox session) |
167167
| DNS | N/A | Deny-only (NXDOMAIN). See DNS design note below. | Per-query deny, other verdicts resolved at SOCKS5 |
168-
| QUIC/HTTP3 | HTTP/3 MITM via quic-go | Full HTTP/3 request/response | Per-connection (see QUIC caveat below) |
168+
| QUIC/HTTP3 | HTTP/3 MITM via quic-go | Full HTTP/3 request/response | Per-request (each HTTP/3 request triggers policy check) |
169169
| APNS | Connection-level allow/deny (port 5223) | N/A | Per-connection |
170170

171-
**Per-request policy evaluation** applies to HTTP/HTTPS. Policy is re-evaluated for every HTTP request, so "Allow Once" permits a single HTTP request and subsequent requests on the same keep-alive connection re-trigger the approval flow. When a per-request approval resolves to "Always Allow" or "Always Deny", the `RequestPolicyChecker` persists the new rule to the policy store via its `PersistRuleFunc` callback and swaps in a freshly compiled engine, so subsequent requests match via the fast path instead of re-entering the approval flow. A fast path skips per-request checks when the SOCKS5 CONNECT matched an explicit allow rule (`RuleMatch`, not default verdict) so normally allowed destinations incur no extra overhead. WebSocket, SSH, and IMAP/SMTP remain connection-level on purpose: per-message or per-command policy on those would blow past the broker's 5/min per-destination rate limit and break normal usage.
171+
**Per-request policy evaluation** applies to HTTP/HTTPS, gRPC-over-HTTP/2, and QUIC/HTTP3. Policy is re-evaluated for every HTTP request (or HTTP/2 stream, or HTTP/3 request), so "Allow Once" permits a single request and subsequent requests on the same connection re-trigger the approval flow. When a per-request approval resolves to "Always Allow" or "Always Deny", the `RequestPolicyChecker` persists the new rule to the policy store via its `PersistRuleFunc` callback and swaps in a freshly compiled engine, so subsequent requests match via the fast path instead of re-entering the approval flow. A fast path skips per-request checks when the SOCKS5 CONNECT matched an explicit allow rule (`RuleMatch`, not default verdict) so normally allowed destinations incur no extra overhead. WebSocket, SSH, and IMAP/SMTP remain connection-level on purpose: per-message or per-command policy on those would blow past the broker's 5/min per-destination rate limit and break normal usage.
172172

173-
**gRPC caveat**: honest gRPC rides over HTTP/2 and enters goproxy via the HTTP/2 PRI preface upgrade. goproxy v1.8.3 defaults to `AllowHTTP2 == false`, so real HTTP/2 streams are rejected at the goproxy layer and never reach `injectCredentials` per stream. Requests that do reach the handler are HTTP/1.1-shaped (possibly carrying a gRPC content-type header) and go through per-request policy normally. Treat gRPC-over-HTTP/2 as effectively per-connection until `AllowHTTP2` is enabled and the H2Transport is wired to call back into the per-request check.
173+
**MITM library:** HTTPS interception uses go-mitmproxy (`github.com/lqqyt2423/go-mitmproxy`). The `SluiceAddon` struct in `internal/proxy/addon.go` implements go-mitmproxy's `Addon` interface. `Requestheaders` fires per HTTP/2 stream, giving true per-request policy for gRPC and other HTTP/2 traffic. `Request` handles credential injection (three-pass phantom swap). `Response` handles OAuth token interception.
174174

175-
**QUIC caveat**: the per-request machinery in `QUICProxy.buildHandler` is in place but currently unreachable in production because `EvaluateQUIC` only returns Allow or Deny (never Ask), so the UDP dispatch loop in `server.go` always passes a nil checker. Unit tests exercise the mechanism directly via `RegisterExpectedHostWithChecker`.
175+
**QUIC per-request:** `EvaluateQUICDetailed` returns Ask when an ask rule matches. The UDP dispatch loop creates a `RequestPolicyChecker` and passes it to `buildHandler`, which calls `CheckAndConsume` per HTTP/3 request.
176176

177-
See `internal/proxy/request_policy.go`, `internal/policy/engine.go` (`EvaluateDetailed`), and `internal/proxy/inject.go` (`injectCredentials`).
177+
See `internal/proxy/request_policy.go`, `internal/policy/engine.go` (`EvaluateDetailed`, `EvaluateQUICDetailed`), and `internal/proxy/addon.go` (`SluiceAddon`).
178178

179179
## Implementation Details
180180

README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,18 +317,14 @@ sluice audit verify # check hash chain integrity
317317
| Protocol | Credential Injection | Content Inspection | Policy Granularity |
318318
|----------|---------------------|--------------------|--------------------|
319319
| HTTP/HTTPS | MITM phantom swap | Full request/response | Per-request (allow-once = one HTTP request) |
320-
| gRPC | Header phantom swap | Metadata | Per-connection in practice (see gRPC note below) |
320+
| gRPC | Header phantom swap (per HTTP/2 stream) | Metadata | Per-request (each HTTP/2 stream checked independently) |
321321
| WebSocket | Handshake + text frames | Text frame content | Per-connection |
322322
| SSH | Jump host, key from vault | -- | Per-connection |
323323
| IMAP/SMTP | AUTH command proxy | -- | Per-connection |
324324
| DNS | -- | Deny-only (NXDOMAIN for denied domains). See note below. | Per-query deny |
325-
| QUIC/HTTP3 | HTTP/3 MITM | Full request/response | Per-connection (see QUIC note below) |
325+
| QUIC/HTTP3 | HTTP/3 MITM | Full request/response | Per-request (each HTTP/3 request checked independently) |
326326

327-
**Per-request policy:** HTTP/HTTPS evaluates policy on every HTTP request. "Allow Once" permits exactly one request, so a second request on the same keep-alive connection re-triggers the approval flow. When a per-request approval resolves to "Always Allow" or "Always Deny", the new rule is persisted to the store and the engine is recompiled so subsequent requests match via the fast path. Destinations matched by an explicit allow rule take a fast path that skips per-request checks entirely. WebSocket, SSH, and IMAP/SMTP remain per-connection on purpose: per-message or per-command approvals would hit the broker's rate limit and break normal usage.
328-
329-
**gRPC note:** real gRPC rides over HTTP/2 and enters goproxy via the HTTP/2 PRI preface. goproxy v1.8.3 defaults to `AllowHTTP2 == false`, so HTTP/2 streams are rejected at the goproxy layer and do not reach the per-request handler per stream. HTTP/1.1-shaped requests with a gRPC content-type header go through per-request policy normally. Treat honest gRPC-over-HTTP/2 as per-connection for now.
330-
331-
**QUIC note:** the per-request infrastructure in `QUICProxy.buildHandler` is present but currently unreachable because the UDP dispatch loop only reaches the QUIC path for connections that matched an explicit allow rule, so it always passes a nil checker. Tests exercise the mechanism directly.
327+
**Per-request policy:** HTTP/HTTPS, gRPC-over-HTTP/2, and QUIC/HTTP3 all evaluate policy on every request. "Allow Once" permits exactly one request (or HTTP/2 stream, or HTTP/3 request), so subsequent requests on the same connection re-trigger the approval flow. When a per-request approval resolves to "Always Allow" or "Always Deny", the new rule is persisted to the store and the engine is recompiled so subsequent requests match via the fast path. Destinations matched by an explicit allow rule take a fast path that skips per-request checks entirely. WebSocket, SSH, and IMAP/SMTP remain per-connection on purpose: per-message or per-command approvals would hit the broker's rate limit and break normal usage.
332328

333329
**DNS policy design**: The DNS interceptor only blocks explicitly denied domains (returns NXDOMAIN). All other verdicts (allow, ask, default) are forwarded to the upstream resolver. This is intentional. Policy enforcement for "ask" destinations happens at the SOCKS5 CONNECT layer, not DNS. Blocking DNS for "ask" destinations would prevent the TCP connection from ever reaching the approval flow. The DNS interceptor populates a reverse cache (IP -> hostname) so the SOCKS5 handler can recover hostnames from IP-only CONNECT requests sent by tun2proxy. For TLS connections, SNI from the ClientHello provides an additional hostname recovery path.
334330

docs/plans/20260408-per-request-policy.md renamed to docs/plans/completed/20260408-per-request-policy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ Add a per-connection `RequestPolicyChecker` that HTTP handlers call before forwa
172172

173173
### Task 8: [Final] Update documentation
174174

175-
- [ ] Update CLAUDE.md: document per-request policy for HTTP/HTTPS/QUIC, note that WebSocket/SSH/IMAP stay connection-level
176-
- [ ] Update README.md protocol table
177-
- [ ] Move this plan to `docs/plans/completed/`
175+
- [x] Update CLAUDE.md: document per-request policy for HTTP/HTTPS/QUIC, note that WebSocket/SSH/IMAP stay connection-level
176+
- [x] Update README.md protocol table
177+
- [x] Move this plan to `docs/plans/completed/`
178178

179179
## Post-Completion
180180

docs/plans/20260412-close-per-request-gaps.md renamed to docs/plans/completed/20260412-close-per-request-gaps.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,20 +210,20 @@ Three changes to close remaining per-request policy gaps on the `per-request-pol
210210

211211
### Task 12: Verify acceptance criteria
212212

213-
- [ ] Verify HTTP/2 per-request policy works (real gRPC-over-HTTP/2 stream fires per-request check)
214-
- [ ] Verify QUIC/HTTP3 per-request Ask works
215-
- [ ] Verify all existing HTTP/1.1 per-request behavior is preserved
216-
- [ ] Verify credential injection works on HTTP/2 streams
217-
- [ ] Verify WebSocket upgrade still works
218-
- [ ] Run full test suite: `go test ./... -v -timeout 120s`
219-
- [ ] Run e2e tests: `go test -tags=e2e ./e2e/ -v -count=1 -timeout=300s`
213+
- [x] Verify HTTP/2 per-request policy works (real gRPC-over-HTTP/2 stream fires per-request check) -- `TestHTTP2PerRequestPolicyAndInjection` (broker called >=2 times for two HTTP/2 streams on same connection), `TestHTTP2PerRequestDenySecondStream` (first stream 200, second stream 403 with per-stream granularity)
214+
- [x] Verify QUIC/HTTP3 per-request Ask works -- `TestQUICProxy_PerRequestAllowOnceConsumed` (seed credit consumed, second request denied), `TestQUICProxy_PerRequestDenyReturns403` (explicit deny beats seed credit), `TestQUICProxy_ExplicitAllowSkipsChecker`
215+
- [x] Verify all existing HTTP/1.1 per-request behavior is preserved -- `TestFullSOCKS5MITMPipeline`, `TestFullSOCKS5MITMPipelineMultipleBindings`, `TestProxyAskWithBrokerAllowOnce`, `TestProxyAskWithBrokerDeny`, `TestProxyAskWithBrokerAlwaysAllow`, `TestProxyWithByteDetectionHTTPS`, `TestProxyNonStandardPortWithBinding`
216+
- [x] Verify credential injection works on HTTP/2 streams -- `TestHTTP2PerRequestPolicyAndInjection` (both HTTP/2 streams receive `Authorization: Bearer <real-secret>` via binding injection)
217+
- [x] Verify WebSocket upgrade still works -- `TestWSProxy_PhantomTokenReplacement`, `TestWSProxy_UnboundPhantomStripped`, `TestWSProxy_BinaryFramePassthrough`, `TestWSProxy_ControlFramePassthrough`, `TestWSProxy_ContentDenyClosesConnection`, `TestWSProxy_ContentRedactInResponse`
218+
- [x] Run full test suite: `go test ./... -v -timeout 120s` -- all 12 packages pass
219+
- [x] Run e2e tests: `go test -tags=e2e ./e2e/ -v -count=1 -timeout=300s` -- 41 pass, 2 skip (Apple Container requires root), 0 fail. Per-request e2e: `TestPerRequestAllowOnceBlocksSecondRequest`, `TestPerRequestAlwaysAllowPermitsBoth`, `TestPerRequestDenyBlocksFirst`, `TestPerRequestAllowOnceReAsks`
220220

221221
### Task 13: [Final] Update documentation
222222

223-
- [ ] Update CLAUDE.md: remove gRPC and QUIC caveats, document go-mitmproxy as MITM library
224-
- [ ] Update README.md: update protocol table (gRPC and QUIC now per-request)
225-
- [ ] Remove `docs/superpowers/specs/2026-04-12-close-per-request-gaps-design.md`
226-
- [ ] Move this plan to `docs/plans/completed/`
223+
- [x] Update CLAUDE.md: remove gRPC and QUIC caveats, document go-mitmproxy as MITM library
224+
- [x] Update README.md: update protocol table (gRPC and QUIC now per-request)
225+
- [x] Remove `docs/superpowers/specs/2026-04-12-close-per-request-gaps-design.md`
226+
- [x] Move this plan to `docs/plans/completed/`
227227

228228
## Post-Completion
229229

0 commit comments

Comments
 (0)