77//! during the normal mock-consumed run because the mock is on loopback and
88//! `NO_PROXY` covers it — the blackhole isn't even exercised).
99//!
10- //! This test asserts that a non-loopback destination fails *while the blackhole
11- //! is armed*. The destination is a local HTTP server on `127.0.0.2`, a loopback
12- //! alias that `NO_PROXY` does NOT cover. With the blackhole active the proxy
13- //! intercepts the connection and blocks it; without the blackhole the request
14- //! would reach the server directly and succeed, turning this test RED.
10+ //! This test asserts that a destination the blackhole's `NO_PROXY` does NOT
11+ //! cover fails *while the blackhole is armed*. The destination is a local HTTP
12+ //! server on `[::1]` (IPv6 loopback), which `NO_PROXY` (`127.0.0.1, localhost`)
13+ //! does not list. With the blackhole active the proxy intercepts the
14+ //! connection and blocks it; without the blackhole the request would reach the
15+ //! server directly and succeed, turning this test RED.
1516//!
1617//! A positive control (disarmed blackhole) runs first, proving the fixture is
17- //! valid and `127.0.0.2 ` is reachable.
18+ //! valid and `[::1] ` is reachable.
1819//!
1920//! Design constraints (see issue #211):
2021//! - An unresolvable host would keep the request failing even when the guard
2122//! is disarmed (DNS failure masquerading as the blackhole), so we must use
2223//! a reachable address.
2324//! - A real external host would make a live network call the moment the guard
2425//! disarms, and on any runner that cannot reach that host the connect error
25- //! again masquerades as the blackhole. A local loopback alias avoids both
26+ //! again masquerades as the blackhole. A local loopback address avoids both
2627//! problems — it is always reachable when unblocked and never reaches the
2728//! real network.
28- //! - This depends on reqwest's `NO_PROXY` matching by exact IP (holds in the
29- //! pinned reqwest/hyper-util; recheck on upgrade) and on the OS routing
30- //! `127/8` as loopback (default on Linux; macOS needs an explicit alias —
31- //! the test fails loudly, never skips, if `127.0.0.2` cannot be bound).
29+ //! - The address must NOT be covered by `NO_PROXY`, yet must exist on every
30+ //! platform. An arbitrary `127/8` alias is not portable: macOS only binds
31+ //! `127.0.0.1` by default. `[::1]` (IPv6 loopback) is present by default
32+ //! on Linux, macOS, and Windows, and `NO_PROXY` only lists `127.0.0.1` and
33+ //! `localhost`, so the blackhole still intercepts it.
34+ //! - This depends on hyper-util's proxy matcher having no implicit loopback
35+ //! bypass — a host is only proxied-around when literally listed in
36+ //! `NO_PROXY` (verified in the pinned hyper-util; recheck on upgrade).
3237
3338use std:: io:: { Read , Write } ;
3439use std:: net:: TcpListener ;
@@ -39,7 +44,7 @@ use icaptcha_client::{obtain_proof, Challenge, IcaptchaCfg};
3944mod support;
4045
4146/// A minimal HTTP server that responds to iCaptcha challenge and answer
42- /// requests on a loopback alias NOT covered by NO_PROXY.
47+ /// requests on the IPv6 loopback address NOT covered by NO_PROXY.
4348///
4449/// When the blackhole is working, the proxy should intercept these requests
4550/// and `obtain_proof` should fail. When the blackhole is disarmed, the
@@ -92,21 +97,21 @@ fn make_cfg(url: &str) -> IcaptchaCfg {
9297
9398#[ test]
9499fn obtain_proof_blackhole_tripwire ( ) {
95- // Bind to 127.0.0.2, a loopback alias NOT covered by NO_PROXY. Linux
96- // routes the whole 127/8 block as loopback; macOS does not alias it by
97- // default (add one with `sudo ifconfig lo0 alias 127.0.0.2`). Fail
98- // loudly on platforms that cannot bind it: a silent skip would report
99- // this tripwire as green while exercising none of the negative control,
100- // recreating the very gap it exists to close.
101- let listener = TcpListener :: bind ( "127.0.0.2:0" ) . expect (
102- "cannot bind 127.0.0.2 for the blackhole tripwire. Linux routes \
103- 127/8 as loopback; on macOS add the alias first \
104- (`sudo ifconfig lo0 alias 127.0.0.2`). This test fails rather than \
105- silently skipping so a platform without the alias cannot count the \
106- negative control as passed.",
100+ // Bind to [::1], IPv6 loopback, which NO_PROXY (127.0.0.1, localhost)
101+ // does not cover. Unlike an arbitrary 127/8 alias, IPv6 loopback is
102+ // present by default on Linux, macOS, and Windows, so the fixture is
103+ // portable across platforms. Fail loudly if it cannot be bound: a
104+ // silent skip would report this tripwire as green while exercising
105+ // none of the negative control, recreating the very gap it exists to
106+ // close.
107+ let listener = TcpListener :: bind ( ( "::1" , 0 ) ) . expect (
108+ "cannot bind [::1] for the blackhole tripwire. IPv6 loopback (::1) \
109+ is present by default on Linux, macOS, and Windows. This test fails \
110+ rather than silently skipping so a platform without it cannot count \
111+ the negative control as passed.",
107112 ) ;
108113 let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
109- let url = format ! ( "http://127.0.0.2 :{port}" ) ;
114+ let url = format ! ( "http://[::1] :{port}" ) ;
110115 serve_icaptcha ( listener) ;
111116
112117 // ── Positive control: disarmed blackhole → obtain_proof succeeds ──────
@@ -128,7 +133,7 @@ fn obtain_proof_blackhole_tripwire() {
128133 let result = obtain_proof ( & cfg, Some ( solve) ) ;
129134
130135 let err = result. expect_err (
131- "blackhole tripwire: obtain_proof succeeded against 127.0.0.2 , \
136+ "blackhole tripwire: obtain_proof succeeded against [::1] , \
132137 meaning the proxy blackhole did not intercept the request; \
133138 the no-live-call guard is disarmed",
134139 ) ;
0 commit comments