Skip to content

fix(icaptcha): harden mock-consumed guard with proxy observer and blackhole tripwire - #214

Open
Gravirei wants to merge 6 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-211-icaptcha-mock-consumed-guard-gaps
Open

fix(icaptcha): harden mock-consumed guard with proxy observer and blackhole tripwire#214
Gravirei wants to merge 6 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-211-icaptcha-mock-consumed-guard-gaps

Conversation

@Gravirei

@Gravirei Gravirei commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens the iCaptcha mock-consumed guard with a proxy observer (catches fire-and-forget leaks) and a blackhole tripwire (detects if the proxy guard is silently disarmed).

Motivation & context

Closes #211

Kind of change

  • Security fix
  • Tests / CI

What changed

  • icaptcha-client: Replaced the closed-port proxy with a ProxyObserver (TCP listener counting connections) so fire-and-forget leaks whose errors are discarded are caught
  • icaptcha-client: Added a blackhole tripwire test asserting obtain_proof fails against 127.0.0.2 while the proxy is armed; goes RED if .no_proxy() or a custom connector is added
  • icaptcha-client: Added mockito dev-dependency

How a reviewer can verify

cargo test -p icaptcha-client
cargo clippy -p icaptcha-client --all-targets -- -D warnings
cargo fmt --check -p icaptcha-client

Before you request review

  • Scope is one logical change; no unrelated churn
  • cargo test --workspace passes locally
  • New behavior is covered by tests (required for fixes)
  • cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings are clean
  • Commit titles use Conventional Commits
  • Docs / .env.example updated if behavior or config changed (or N/A)
  • Checked existing PRs so this isn't a duplicate

Protocol & signing impact

N/A - test-only crate change

Summary by CodeRabbit

  • Tests
    • Added a “tripwire” integration check that validates proof requests are blocked when the proxy blackhole/egress guard is enabled.
    • Introduced positive/negative controls to confirm expected behavior when the guard is disarmed vs. armed.
    • Added an observer-based assertion to detect and fail tests if unintended loopback/network traffic occurs.
    • Improved test isolation by temporarily overriding proxy-related environment settings and restoring them automatically after each test.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 48435a96-921a-4b94-bb79-8bb4c8310255

📥 Commits

Reviewing files that changed from the base of the PR and between 82796d2 and b9fff1a.

📒 Files selected for processing (1)
  • crates/icaptcha-client/tests/icaptcha_mock_consumed.rs

📝 Walkthrough

Walkthrough

The PR adds RAII proxy-environment helpers, strengthens mock-consumed egress verification with loopback connection observation, and introduces a tripwire test covering direct success versus blackhole-induced connection failure.

Changes

iCaptcha proxy interception tests

Layer / File(s) Summary
Proxy environment guards
crates/icaptcha-client/tests/support/mod.rs
Proxy variables can be captured, overridden for blackhole routing, cleared for direct connections, and restored automatically.
Mock-consumed egress observation
crates/icaptcha-client/tests/icaptcha_mock_consumed.rs
A loopback proxy observer counts connection attempts, and the test asserts that obtain_proof produces no additional observed connections while verifying mock endpoint consumption.
Blackhole interception tripwire
crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs
A local 127.0.0.2 server verifies direct success, while the armed blackhole must cause obtain_proof to fail with a connect-style error.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TripwireTest
  participant obtain_proof
  participant LocalICaptchaServer
  participant BlackholeProxy
  TripwireTest->>LocalICaptchaServer: Start local server on 127.0.0.2
  TripwireTest->>obtain_proof: Run with proxy disabled
  obtain_proof->>LocalICaptchaServer: Request challenge and answer
  LocalICaptchaServer-->>obtain_proof: Return JSON responses
  TripwireTest->>obtain_proof: Run with blackhole enabled
  obtain_proof->>BlackholeProxy: Attempt proxied connection
  BlackholeProxy-->>obtain_proof: Return connection failure
Loading

Possibly related PRs

  • Gitlawb/node#184: Added the mock-consumed no-live-call test that this PR extends with proxy helpers and connection observation.

Suggested labels: kind:test, crate:icaptcha

Suggested reviewers: kevincodex1, beardthelion

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: hardening the icaptcha mock-consumed guard with observer and tripwire tests.
Description check ✅ Passed The description follows the template and includes summary, motivation, changes, verification steps, and impact notes.
Linked Issues check ✅ Passed The PR addresses #211 by adding a connection-counting proxy observer and a blackhole tripwire test that fails when the guard is bypassed.
Out of Scope Changes check ✅ Passed The changes stay within the test-hardening scope and do not introduce unrelated code or behavioral changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Gravirei
Gravirei marked this pull request as ready for review July 17, 2026 15:57
Copilot AI review requested due to automatic review settings July 17, 2026 15:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The observer half does what #211 asked. I verified it by mutation at 9c731c8: injecting a swallowed fire-and-forget leak (let _ = client.get("http://leak.example.com/exfil").send();, discarded error) into obtain_proof makes the mock-consumed test go RED with the observer counting one connection, exactly the case the old closed-port blackhole missed. That's the real win here.

The tripwire, though, is vacuous as committed: it passes whether or not the blackhole is armed. Details below, each checked by running it.

Findings

  • [P1] Fix the tripwire fixture's Content-Length so the test actually locks the blackhole
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:54
    serve_icaptcha declares Content-Length: 78 for an 86-byte challenge body and 42 for a 40-byte answer body. Both are wrong, and it makes the tripwire test nothing. I ran it three ways: with the blackhole disarmed (proxy env cleared) and the committed lengths, obtain_proof returns Err("parse iCaptcha challenge") because the truncated body fails to deserialize, so assert!(result.is_err()) passes with the blackhole off. Correct the lengths to 40/86 and disarmed now returns Ok("PROOF-TRIP"); armed returns a connect error. So the fix is load-bearing and the current test is not. Derive the length from the body instead of hardcoding: bind each body to a variable and use body.len() in the format string.

  • [P2] Assert the tripwire error is a connect/proxy failure, not bare is_err()
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:109
    Even with the lengths fixed, result.is_err() accepts any failure, so a future fixture bug can re-vacuate the test the same way this one did. I confirmed the armed-blackhole error downcasts to reqwest::Error with is_connect() == true ("tcp connect error: Connection refused"), so narrowing the assertion to that shape is both accurate and self-defending: err.chain().any(|c| c.downcast_ref::<reqwest::Error>().is_some_and(|e| e.is_connect())). As a bonus it fails today's parse-error vacuity directly.

  • [P2] State the observer's coverage scope, or drive the branches it can't see
    crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:14
    The observer only witnesses leaks issued on the one path the test drives: a single passing round, no PoW, no Continue/Failed, no non-2xx. A leak on any of those branches makes no connection during the run and the zero-count assertion stays green, yet the test is named ..._makes_no_live_call and the header claims "no leak reached the network" without qualification. It also can't see a transport that ignores proxy env (.no_proxy() or a raw TcpStream). #211 asked for exactly this: either drive those branches or state that the guarantee covers only the exercised path and proxy-honoring transports. Add that caveat to the header (and ideally an observer-armed test driving a Continue round and a non-2xx response).

  • [P3] Retitle to test(icaptcha): ...
    The diff is test-only (two tests/ files plus a mockito dev-dependency; no src/ change), so fix( would have release-please cut a patch release for it. Test-only changes here use test(...) (ad7c2b2, c296e23); CONTRIBUTING.md line 55 notes prefixes drive the bump.

  • [P3] arm_blackhole is near-duplicated across the two test files
    They differ only in the proxy target (the observer port vs the closed :1). A tests/support/mod.rs exporting one arm_blackhole(target: &str) both files pull in via mod support; removes the duplication. Minor; fine to leave for two small binaries if you'd rather.

One note, not a change request: the Cargo.lock version bumps (0.5.0 -> 0.5.1 across five crates) are not churn from this PR. Those crates' manifests are already 0.5.1 on main; only the lockfile was stale, and regenerating it to add mockito correctly synced them. Leave them as they are. A one-line mention in the PR description that the lockfile picked up an unrelated sync would be tidy, nothing more.

The observer work is a genuine improvement; once the tripwire actually exercises the blackhole it'll be a solid pair.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Make the blackhole tripwire’s successful fixture valid
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:50
    The challenge body is 86 bytes but advertises Content-Length: 78 (and the 40-byte answer advertises 42). If a future client bypasses the proxy, it reaches the local listener but receives truncated JSON, so obtain_proof returns an error and the is_err() assertion still passes. That leaves the test green with the blackhole disarmed—the exact regression this PR is intended to catch. Derive the header from body.len() (or otherwise make the direct fixture valid) so an unblocked flow succeeds.

  • [P2] Assert that the tripwire failed at the proxy connection
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:109
    Even with valid response lengths, result.is_err() accepts any unrelated failure: a malformed fixture, changed request protocol, or solver error would again leave the test green after the proxy guard was bypassed. The test should require the expected connect/proxy failure (for example by finding a reqwest::Error with is_connect() in the error chain), or include an explicit unarmed positive control.

@Gravirei
Gravirei force-pushed the fix/issue-211-icaptcha-mock-consumed-guard-gaps branch from 9c731c8 to 87c716e Compare July 18, 2026 20:08
@Gravirei
Gravirei requested review from beardthelion and jatmn July 18, 2026 20:11

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beardthelion
beardthelion dismissed their stale review July 19, 2026 00:08

Superseded — re-reviewed the current head 87c716e.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 87c716e3. The tripwire rework is load-bearing: disarmed, the flow returns Ok("PROOF-TRIP") (a real positive control); armed, it fails with a reqwest connect error. Injecting a swallowed fire-and-forget leak into obtain_proof makes the observer count 1 and the mock-consumed test go red. Content-Length now derives from body.len(), the assertion narrows to a connect error, the observer-scope caveat is stated, and arm_blackhole is deduped into support/mod.rs. Prior findings resolved.

@kevincodex1 ready to merge.

@kevincodex1

Copy link
Copy Markdown
Contributor

hi @Gravirei thaks for this contribution please rebase to main and fix conflicts

@Gravirei
Gravirei force-pushed the fix/issue-211-icaptcha-mock-consumed-guard-gaps branch from 87c716e to e45bf31 Compare July 22, 2026 13:39
@Gravirei
Gravirei requested a review from kevincodex1 July 22, 2026 13:41

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Rebase this branch onto the current main before merge
    e45bf312033f9a51cba239dcd9cdc383838266d7
    The PR head is based on e14c2d3f3f4269c61ac8f8ae228223f46d22c078, while main is now b484a24955548257bb4f3fa92f460ba8fd00eafe; GitHub currently marks the PR blocked, and a maintainer has requested a rebase. The current merge tree is clean, so this is branch-state drift rather than a code conflict, but please rebase before merging so the final change can be reviewed against the current base.

  • [P2] Synchronize the proxy observer before asserting that no egress occurred
    crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:63
    The counter is incremented only after the detached accept thread is scheduled, but the test reads it immediately after obtain_proof returns. A future fire-and-forget/background request can connect to the listener backlog after that read (or before the accept thread runs), so the test exits green even though the leak this guard is meant to catch occurred. Add a bounded drain/acknowledgement or another lifecycle synchronization before asserting a zero count.

@Gravirei
Gravirei force-pushed the fix/issue-211-icaptcha-mock-consumed-guard-gaps branch from 926f978 to aa0056e Compare July 23, 2026 06:07
@Gravirei
Gravirei requested a review from jatmn July 23, 2026 06:15

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

[P1] arm_blackhole does not clear REQUEST_METHOD, which disables all proxies

Severity: High
Files: crates/icaptcha-client/tests/support/mod.rs:3-15
Also affects: crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:115, crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:74

In the pinned hyper-util 0.1.20 (src/client/proxy/matcher.rs:228-236), Builder::from_env() sets:

is_cgi: std::env::var_os("REQUEST_METHOD").is_some(),

and build() (matcher.rs:304-311) returns an empty matcher when is_cgi is true, ignoring every proxy variable. arm_blackhole sets ALL_PROXY, HTTPS_PROXY, HTTP_PROXY, and NO_PROXY in both cases, but it does not remove REQUEST_METHOD.

Impact: If the test runner, CI wrapper, or developer environment happens to set REQUEST_METHOD, the blackhole is silently disarmed. icaptcha_mock_consumed.rs still passes because its mock lives on 127.0.0.1 and direct loopback access satisfies the mock-consumed assertions; the proxy observer merely sees zero connections, which is indistinguishable from success. Only the tripwire test fails, and then with a confusing "obtain_proof succeeded" message rather than a clear "blackhole disabled" signal.

Fix: Clear REQUEST_METHOD in arm_blackhole:

std::env::remove_var("REQUEST_METHOD");

or save/restore it the same way InsecureEnv in src/lib.rs handles GITLAWB_ICAPTCHA_INSECURE.


[P2] ProxyObserver::flush() can under-count leaked connections

Severity: Medium–High
File: crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:96-107

The flush is supposed to guarantee that every leaked connection initiated during obtain_proof has been counted before the zero-leak assertion. It does not provide that guarantee:

  1. Sentinel-ordering race. flush() opens a sentinel self-connection and assumes that once the sentinel is accepted, all prior connections have been counted. TCP backlogs are not FIFO; a leaked connection whose handshake completes after the sentinel's can remain in the backlog while the sentinel is accepted first. flush() then reads count == 1, subtracts one, and reports 0.
  2. Unconditional saturating_sub(1). If the self-connect fails (backlog saturated, accept thread stalled), no sentinel is counted, yet line 106 still subtracts one, turning a single real leak into a reported 0.
  3. Ignored 100 ms timeout. recv_timeout errors are discarded; if the accept thread has not processed the sentinel within 100 ms on a loaded runner, the subtraction again under-counts.

This is particularly problematic because the test is explicitly designed to catch "fire-and-forget leaks whose transport error is discarded" (icaptcha_mock_consumed.rs:16-17). Those leaks are exactly the kind that can still be handshaking when obtain_proof returns, so the flush race undermines the guard's purpose.

Fix direction: Make flush() wait until the count is stable (e.g., repeatedly flush and verify the non-flush count does not change across a bounded interval), or close the listener and drain the backlog before reading the final count. Also subtract the sentinel only when its ack is actually received.


[P3] Blackhole tripwire is not portable to macOS

Severity: Medium
File: crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:69-70

The tripwire binds a listener on 127.0.0.2. The file's own comment notes that macOS may need an explicit loopback alias, but the test uses .expect(...) rather than skipping or failing fast with a helpful message. On a stock macOS runner the test panics before it exercises the blackhole.

Worse, if the bind succeeds but the address is unreachable (or if a future runner configuration changes), the assertion only checks reqwest::Error::is_connect() (icaptcha_blackhole_tripwire.rs:97-102). A direct connect failure to an unreachable 127.0.0.2 is indistinguishable from a proxy-blocked connect failure, so the test can pass for the wrong reason.

Fix direction: Gate the test to Linux, mark it #[ignore = "requires 127.0.0.2 loopback alias"], or add a reachability/positive-control check that verifies 127.0.0.2 is reachable before asserting the blackhole behavior.


[P4] Tripwire lacks a disarmed-blackhole positive control

Severity: Medium
File: crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:66-103

The tripwire asserts that obtain_proof fails when the blackhole is armed, but it never asserts the opposite: that the same request succeeds when the blackhole is disarmed. Without that positive control, a platform where 127.0.0.2 is unreachable (see P3) or a future change that makes obtain_proof fail for unrelated reasons will keep the test green while the guard is not actually being exercised.

Fix direction: Add a companion test (or an initial phase in the same test) that clears proxy variables, verifies 127.0.0.2 is reachable, and asserts obtain_proof returns Ok("PROOF-TRIP").


[P5] arm_blackhole mutates process-global env vars without restoration

Severity: Low–Medium
File: crates/icaptcha-client/tests/support/mod.rs:3-15

The helper overwrites eight proxy-related environment variables and never restores them. This is safe today because each integration-test file contains a single #[test] and Cargo compiles each file as a separate binary. It is fragile, however: adding a second test to either file would leak the mutated env into that test, and any child process or library code that inspects proxy vars inside the test will see the blackhole values.

Fix direction: Convert arm_blackhole into a drop-guard type that captures prior values (or None if absent) and restores them on drop, mirroring InsecureEnv in crates/icaptcha-client/src/lib.rs:392-413.


[P6] Tripwire fixture reads full buffer instead of bytes returned

Severity: Low
File: crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:47-51

let mut buf = [0; 4096];
if stream.read(&mut buf).is_err() { continue; }
let request = String::from_utf8_lossy(&buf);

The code discards the byte count returned by read and converts the entire 4096-byte buffer, including trailing NULs. contains("/v1/answer") still works because NUL is valid UTF-8, but the server is technically parsing uninitialized padding. This is brittle if the fixture ever needs to inspect headers or body length.

Fix direction: Use the count returned by read:

let n = stream.read(&mut buf).unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);

[P7] Branch is one commit behind main

Severity: Process / Low
Evidence: git log --oneline HEAD..main shows 111cff7 infra: add production fly configs for gitlawb-node-2 (sjc) and gitlawb-node-3 (nrt).

GitHub reports the PR as mergeable, so this is branch-state drift rather than a code conflict. Rebasing onto the current main before merge keeps the final change reviewed against the latest base.


Gravirei added 3 commits July 24, 2026 12:20
…ackhole tripwire (Gitlawb#211)

Replace the closed-port proxy with a ProxyObserver (TCP listener with
acknowledged accept-loop flushing) so fire-and-forget leaks whose error
is discarded are caught and the count is synchronized before assertion.
Add a blackhole tripwire asserting obtain_proof fails against 127.0.0.2
with the proxy armed, narrowed to a connect-level error to prevent
vacuous passes. Share arm_blackhole across both test files via a
tests/support module.

Document the observer's coverage scope: it only witnesses leaks on the
single exercised happy path; error/retry branches and non-proxy
transports are outside its guarantee.
@Gravirei
Gravirei force-pushed the fix/issue-211-icaptcha-mock-consumed-guard-gaps branch from 096c9c1 to 53091ba Compare July 24, 2026 06:20
@Gravirei
Gravirei requested a review from jatmn July 24, 2026 06:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/icaptcha-client/tests/support/mod.rs (1)

40-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Global env-var guards are fragile if a second #[test] is later added to a binary using them.

EnvGuard/arm_blackhole/disarm_proxy_env mutate process-wide state. Today each consuming test file has exactly one #[test], so there's no race, but cargo test runs multiple tests within a binary concurrently by default — a future second test in the same file calling either helper would race on these env vars. Consider a doc comment noting this constraint, or guarding with serial_test, to prevent a silent regression later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/icaptcha-client/tests/support/mod.rs` around lines 40 - 79, Add an
explicit synchronization safeguard around the process-wide environment mutations
in EnvGuard, arm_blackhole, and disarm_proxy_env, using the project’s
established serial-test mechanism if available. Ensure any tests invoking these
helpers cannot run concurrently within the same binary; otherwise document the
single-test-per-binary constraint directly on the helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/icaptcha-client/tests/support/mod.rs`:
- Around line 40-79: Add an explicit synchronization safeguard around the
process-wide environment mutations in EnvGuard, arm_blackhole, and
disarm_proxy_env, using the project’s established serial-test mechanism if
available. Ensure any tests invoking these helpers cannot run concurrently
within the same binary; otherwise document the single-test-per-binary constraint
directly on the helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f9638444-b11b-415e-b33c-d3fbbe31d202

📥 Commits

Reviewing files that changed from the base of the PR and between 111cff7 and 53091ba.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs
  • crates/icaptcha-client/tests/icaptcha_mock_consumed.rs
  • crates/icaptcha-client/tests/support/mod.rs

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Parse the complete HTTP request before selecting the fixture response
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:51
    A TCP read is not message-framed, so it can return before the complete request line (including /v1/answer) arrives. The fixture then treats an answer request as a challenge request and returns challenge JSON; the direct positive control fails to deserialize AnswerResult even though the proxy behavior is correct. Read through the HTTP headers/request line before dispatching, or use an HTTP test server, so normal TCP segmentation cannot make this tripwire intermittently fail.

@Gravirei
Gravirei requested a review from jatmn July 26, 2026 04:57

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one low-priority test-coverage issue.

Findings

  • [P3] Narrow the observer's fire-and-forget guarantee
    crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:82
    stabilise declares success after the first 100 ms interval in which the detached accept loop has not incremented count. A regression can spawn a fire-and-forget proxy request on the exercised path just before obtain_proof returns, but schedule its connection after that interval; both samples are zero, the test passes, and the leaked DID/answer/API-key request is never observed. Conversely, a repeating leak that keeps changing the counter never lets this unbounded loop return. That makes the new test non-load-bearing for the discarded-error/background egress it says it detects. Use a bounded acknowledgement/lifecycle barrier for the request work being tested (or narrow the assertion to connections already accepted), rather than treating one quiet polling interval as proof that the flow has no outstanding egress.

…re/after obtain_proof

The stabilise() polling loop cannot prove no more connections are coming,
only that none arrived in the last 100ms. A carefully timed background
leak could schedule its connection after stabilise stabilises and the test
would pass despite a live egress.  Replace with a simple counter read
before and after obtain_proof.  Connections accepted during the synchronous
call are counted; a leak whose TCP handshake completes after the counter
is read is outside the assertion window — this is now documented as a
known limitation of the single-threaded test harness.

Also narrow the fire-and-forget claim: the observer catches leaks that a
closed-port blackhole would miss (because it counts connections regardless
of error propagation), but the timing race inherent in a detached accept
thread means the guarantee is best-effort for post-return handshakes.
@Gravirei
Gravirei requested a review from jatmn July 27, 2026 05:59

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Do not let the blackhole tripwire report success when it never ran
    crates/icaptcha-client/tests/icaptcha_blackhole_tripwire.rs:96
    When TcpListener::bind("127.0.0.2:0") fails (the documented macOS / missing-loopback-alias case), the test prints SKIP and returns. Cargo still reports the case as passed, so a platform that cannot bind 127.0.0.2 gets zero coverage of the negative control while CI stays green. That recreates the original gap this tripwire was added to close: a future .no_proxy() or connector bypass can ship undetected anywhere the test silently no-ops. Please fail fast with a clear message, gate the test with #[cfg(...)] / #[ignore = "..."], or otherwise make skipped platforms visible instead of counting them as success.

  • [P2] Synchronize the proxy observer before asserting zero egress
    crates/icaptcha-client/tests/icaptcha_mock_consumed.rs:124
    The latest head removes stabilise() and reads the counter immediately after obtain_proof returns, while the accept loop still runs on a detached thread. A fire-and-forget leak whose TCP handshake is still in the listener backlog—or completes just after the synchronous call returns—can therefore pass with leak_count == 0, which is exactly the class #211 asked this guard to catch. The module comment now documents a post-return window, but that narrows the guarantee rather than replacing the lifecycle barrier previously requested. Please add a bounded drain/acknowledgement (or join/barrier with the accept thread) before the zero-count assertion, or narrow the test name/docs to match what is actually proved.

  • [P3] Document the single-test-per-binary constraint on env helpers
    crates/icaptcha-client/tests/support/mod.rs:48
    arm_blackhole and disarm_proxy_env mutate process-wide proxy variables and restore them on drop. Each consuming integration test binary currently has exactly one #[test], so this is safe today, but cargo test runs multiple tests in the same binary concurrently by default. A future second test in either file would race on these vars without serial_test or an explicit comment on the helpers. Please document that constraint on the public helpers (or add the project's serial-test guard if one exists).

  • [P3] Retitle to test(icaptcha): ...
    PR title / lead commit
    The diff is test-only (tests/ plus lockfile sync; no src/ change). This repo uses Conventional Commits prefixes for release-please versioning, and prior test-only icaptcha work here uses test(...). fix(...) can trigger an unnecessary product patch release for harness-only changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind:bug Defect fix — wrong or unsafe behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

icaptcha mock-consumed guard: swallowed dual-contact leak is invisible, and the blackhole has no tripwire test

5 participants