review-003: the assembly-layer full review — F-1/F-2 fixed, the residue dispositioned
- Review 003 written (docs/reviews/003-assembly-layer-implementation- review.md): the batch-2 assembly layer post-landing, the review-002 template — correctness/security/smells/coverage over the five batch-2 modules + the batch-2 consumer/producer diff surface. Baseline: 375 default / 620 all-features green, clippy/fmt/doc clean, both wasm checks clean, publish dry-run completes, llvm-cov 93.68% lines / 91.01% functions. - F-1 (major, fixed): the cred-bearing door swallowed every in-band refusal's mapped reply code (the CONNECT/BIND session-open errors and the accept-stage Err were bare closes) — a vanilla client got no reply where ADR-007 pins a reply-code-shaped refusal. Fixed with a write_refusal_reply helper (the door's boundary codec, RFC placeholder BND.ADDR) at all three sites, pinned by cred_door_in_band_refusal_surfaces_the_reply_code. - F-2 (major, fixed): UdpFrontDoor::serve_on returned Ok(()) on a fatal relay-socket I/O failure against its own doc contract — the Io exit now surfaces as Err; the idle/association/session exits stay clean. - Findings recorded, not fixed (the release-polish round's residue): D-1 (associate_with_token 0% covered), D-2 (the composed-egress failure matrix), D-3 (the io-kind refusal-table arms), S-1 (the UDP door's learned client address), N-1..N-8 (doc lines, posture notes, coverage residue). Verification: cargo test (375) + --all-features (620) green, clippy --all-targets --all-features -D warnings clean, fmt --check clean, doc --no-deps clean, both wasm checks clean, publish dry-run completes.
This commit is contained in:
1 parent
04c76e3a41
commit
2db05172ef
3 files changed
+797
-8
No files matched your search
@@ -0,0 +1,682 @@
|
||||
---
|
||||
status: open
|
||||
last_updated: 2026-09-26 (the review written against the post-gate tree
|
||||
`04c76e3` + the review's inline fixes; F-1/F-2 fixed in this change)
|
||||
reviewed_artifacts:
|
||||
- src/door.rs
|
||||
- src/local/mod.rs
|
||||
- src/local/producer_door.rs
|
||||
- src/composed/mod.rs
|
||||
- src/consumer.rs (the batch-2 additions: TokenClient + the
|
||||
*_with_token twins)
|
||||
- src/producer.rs (spot-check: the gate/builder surface)
|
||||
- src/rfc1928/ (spot-check: the cred-bearing door's forked surface)
|
||||
- docs/architecture/ (overview, wire, producer, consumer, rfc1928,
|
||||
README, decisions 003–014)
|
||||
- tests/ (all batch-2 suites read; coverage attributed)
|
||||
tool: manual source read + cargo test/clippy/fmt/doc/wasm/publish-dry-run + cargo-llvm-cov + alktunnels 0.2.0 surface cross-checks
|
||||
reviewer: general implementation review (correctness + security + smells + coverage)
|
||||
scope_note: the batch-2 assembly layer only — the five new modules plus
|
||||
the batch-2 additions to consumer.rs/producer.rs; the batch-1 core
|
||||
was reviewed by review 002 and is not re-litigated here
|
||||
---
|
||||
|
||||
# Review 003 — The Assembly-Layer Implementation (Post-Landing Full Review)
|
||||
|
||||
## Purpose
|
||||
|
||||
General review of the batch-2 assembly layer after landing — the
|
||||
eight-task group (`tasks/implementation/{local-*,door-*,composed-*,
|
||||
assembly-integration-suite,review-assembly-layer}.md`, commits
|
||||
`a867f0c..04c76e3`). The narrow review
|
||||
(`tasks/implementation/review-assembly-layer.md`) checked the eight
|
||||
gate criteria (never-binds, seam contracts, door fidelity, graduation
|
||||
shape, conventions, feature hygiene, POC fidelity, docs sync) and
|
||||
fixed R-1/R-2 inline; this one asks the four questions that review
|
||||
did not, following review 002's template:
|
||||
|
||||
1. **Correctness** — hazards in the landed door/provider behavior
|
||||
itself: teardown races, pump-lifetime paths, protocol-state
|
||||
corruption, error-shape fidelity at the door boundary.
|
||||
2. **Security** — resource exhaustion on the *kernel-socket* paths
|
||||
the batch-2 code newly opens (doors, producer backend), gate
|
||||
fidelity, panic surfaces, log/credential handling.
|
||||
3. **Code smells** — convention compliance plus the structural smells
|
||||
an 8-task implementation round leaves behind (dead surface,
|
||||
duplication, doc drift, test-side posture).
|
||||
4. **Coverage** (`cargo-llvm-cov`) — important gaps, attributed to
|
||||
specific behaviors and their risk.
|
||||
|
||||
Method note: line-level over the five batch-2 modules + the batch-2
|
||||
diff surface of `consumer.rs`/`producer.rs`, with cross-checks against
|
||||
alktunnels 0.2.0 (`make_forwarded_op_handler`, `ForwardedHandler`,
|
||||
`TunnelListener`, `TakenHalves`) and the ADR set 003–014. The RFC
|
||||
1928 core and the protocol-layer pump are re-read only where batch 2
|
||||
calls into them.
|
||||
|
||||
## Verification Baseline
|
||||
|
||||
All commands on the reviewed tree (post-fix; the review change itself):
|
||||
|
||||
- `cargo test`: **375 green** (default; the batch-2 suites are
|
||||
feature-gated out of the default build).
|
||||
- `cargo test --all-features`: **620 green** (up 5 from the gate's
|
||||
615 — the review's pinning test + the UDP-door exit change).
|
||||
- `cargo clippy --all-targets --all-features -- -D warnings`: clean.
|
||||
- `cargo fmt --check`: clean.
|
||||
- `cargo doc --no-deps`: clean (0 warnings).
|
||||
- `cargo publish --dry-run --allow-dirty`: completes.
|
||||
- Wasm checks: default + `--features composed` clean (check + clippy);
|
||||
`local` on wasm fails with exactly the expected mio unsupported-wasm
|
||||
error (the alktty posture, observed at the gate and re-verified).
|
||||
- `cargo llvm-cov --all-features`: **93.68% lines / 91.01% functions /
|
||||
92.97% regions** (6485 covered / 410 missed).
|
||||
|
||||
### Coverage breakdown
|
||||
|
||||
| Module | Lines | Functions | Regions |
|
||||
|---|---:|---:|---:|
|
||||
| composed/mod.rs | 77.32% | 65.31% | 80.45% |
|
||||
| consumer.rs | 95.25% | 93.60% | 93.88% |
|
||||
| door.rs | 81.45% | 81.08% | 79.05% |
|
||||
| egress.rs | 99.67% | 100.00% | 99.70% |
|
||||
| error.rs | 100.00% | 100.00% | 97.48% |
|
||||
| local/mod.rs | 84.91% | 74.29% | 86.94% |
|
||||
| local/producer_door.rs | 76.98% | 84.21% | 77.46% |
|
||||
| params.rs | 100.00% | 100.00% | 100.00% |
|
||||
| producer.rs | 95.70% | 91.83% | 96.74% |
|
||||
| rfc1928/ (all five) | 96.5–100% | 95.65–100% | 78.6–99.6% |
|
||||
| wire.rs | 96.58% | 97.06% | 97.19% |
|
||||
| **TOTAL** | **93.68%** | **91.01%** | **92.97%** |
|
||||
|
||||
Healthy overall; the batch-2 modules carry the lower numbers (D-1,
|
||||
D-2, N-8 attribute them). The protocol core's residue is unchanged
|
||||
from review 002's attribution; the assembly layer's gaps are the
|
||||
findings below.
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Severity | Count | IDs |
|
||||
|----------|------:|-----|
|
||||
| Critical | 0 | — |
|
||||
| Major | 2 | F-1, F-2 |
|
||||
| Minor | 4 | S-1, D-1, D-2, D-3 |
|
||||
| Observation | 8 | N-1..N-8 |
|
||||
|
||||
No critical findings. The two majors (F-1, F-2) are door-boundary
|
||||
fidelity/teardown corrections, both fixed in the review change:
|
||||
F-1 is the cred-bearing door silently swallowing the in-band
|
||||
refusals' mapped reply codes (a vanilla client got a bare EOF where
|
||||
ADR-007 pins a reply-code-shaped refusal); F-2 is the UDP door's
|
||||
fatal-I/O swallow (`serve_on` returned `Ok(())` on a relay-socket
|
||||
failure the docs call fatal). One coverage finding (D-2) is the
|
||||
batch-2 analogue of review 002's D-2 (the spec'd surface unexercised);
|
||||
the rest are small.
|
||||
|
||||
Prefixes follow the review-002 legend: `F` (correctness, fixed
|
||||
inline — continuing 002's `C` family with the batch's review-fixed
|
||||
disposition), `S` (security posture), `D` (dead surface/coverage), `N`
|
||||
(observations/notes/smells).
|
||||
|
||||
---
|
||||
|
||||
# Part A — Findings
|
||||
|
||||
## F-1 [major, fixed] — The cred-bearing door swallowed every in-band refusal's mapped reply code: a vanilla client got a bare EOF where the RFC (and ADR-007's failure mapping) promises a reply-code-shaped refusal
|
||||
|
||||
**Files**: `src/door.rs` (`cred_bearing`'s CONNECT/BIND session-open
|
||||
error arms + `bind_quiet_and_pipe`'s accept-stage error arm;
|
||||
pre-fix state at git `04c76e3`)
|
||||
|
||||
**Problem**: ADR-007 pins the failure mapping as RFC-faithful *end to
|
||||
end*: factory failure → reply#1's mapped code; accept-stage failure →
|
||||
reply#2's mapped code. The producer honors this in-band — but the
|
||||
cred-bearing door sat *between* the in-band conversation and the
|
||||
vanilla client and dropped the mapped code on the floor at three
|
||||
sites:
|
||||
|
||||
1. `connect_with_token` refused (dial refused per target policy —
|
||||
the in-band CONNECT reply carried 0x05/0x02/0x03/…) → the door
|
||||
returned `DoorExit::Open(err.to_string())` and the vanilla
|
||||
connection closed with **no reply at all**.
|
||||
2. `bind_with_token` refused (the in-band BIND command gate's 0x02 —
|
||||
an identity with `socks5:open` but not `socks5:bind`) → same: bare
|
||||
EOF where a vanilla client expects reply#1 `ConnectionNotAllowed`.
|
||||
3. `await_inbound()`'s accept-stage failure (`Err`) — the quiet
|
||||
wait's accept refused per policy — the door closed the connection
|
||||
with no reply#2.
|
||||
|
||||
The producer-side in-band paths (the pump handler, `serve_socks5_over`)
|
||||
write the RFC reply before returning the session error; the door —
|
||||
the one surface whose whole job is RFC codec translation — discarded
|
||||
it. The observable: `curl --proxy socks5://user:pass@host` against a
|
||||
target-policy refusal hangs-then-drops instead of printing
|
||||
"proxy CONNECT aborted" with a SOCKS error — and, worse, a BIND
|
||||
command-gate refusal (the ADR-006 security boundary working as
|
||||
designed) was indistinguishable from a crashed proxy. The door's own
|
||||
module doc promises "the RFC codec lives only here" and "zero protocol
|
||||
translation" of *payloads* — refusal re-encoding is the codec's job.
|
||||
|
||||
**Fix (landed in this review change)**: a `write_refusal_reply` helper
|
||||
(the door's boundary codec, best-effort — a failed write means the
|
||||
client is gone either way) re-encodes `SocksSessionError::Rfc(
|
||||
ReplyError)`-shaped refusals to the vanilla client with the RFC's
|
||||
placeholder BND.ADDR (`0.0.0.0:0`, the same form `reply_error` uses),
|
||||
at all three sites (CONNECT open, BIND open, the accept-stage `Err`).
|
||||
Non-reply-code shapes (open/transport/I-O) stay a bare close (nothing
|
||||
RFC-shaped to encode). Pinned by `cred_door_in_band_refusal_
|
||||
surfaces_the_reply_code` (a `socks5:open`-only credential BINDs →
|
||||
reply#1 carries 0x02 + the placeholder address, within a timeout —
|
||||
the old shape failed the test by never writing a reply).
|
||||
|
||||
**Lift**: closes the last wire-fidelity gap between the in-band and
|
||||
vanilla surfaces. Every other door path re-encodes its reply
|
||||
(`pipe_connect_plane`, `pipe_bind_plane`, the UDP door's codec); the
|
||||
refusal paths were the asymmetric ones.
|
||||
|
||||
---
|
||||
|
||||
## F-2 [major, fixed] — `UdpFrontDoor::serve_on` returned `Ok(())` on a fatal relay-socket I/O failure, against its own contract
|
||||
|
||||
**Files**: `src/door.rs` (`UdpFrontDoor::serve_on`'s final `Ok(())`,
|
||||
pre-fix at `04c76e3`); doc: "Runs until the association ends (the
|
||||
in-band channel EOF, the client idle bound, **or a fatal I/O
|
||||
failure**)"
|
||||
|
||||
**Problem**: the two-pump `select!` resolves with a `UdpDoorExit`; an
|
||||
`Io` exit (a real relay-socket failure — `recv_from` erroring on a
|
||||
non-idle socket) is documented as one of the run's fatal outcomes,
|
||||
but the method mapped *every* exit to `Ok(())`. The caller
|
||||
(assembly wiring, the `serve` wrapper) cannot distinguish "the
|
||||
association ended cleanly" from "the relay socket died" — the exact
|
||||
distinction the doc's contract claims to carry. The TCP door has the
|
||||
same return shape but its per-accept isolation posture makes
|
||||
per-connection I/O failure genuinely non-fatal (the loop continues);
|
||||
the UDP door is one association per call — an `Io` exit there *is*
|
||||
the call's outcome. Latent: nothing in the suite exercised a
|
||||
relay-socket error, so the swallow passed every test.
|
||||
|
||||
**Fix (landed in this review change)**: the exit match surfaces
|
||||
`UdpDoorExit::Io` as `Err(err)`; the idle bound, the association end,
|
||||
and a session error stay clean `Ok(())` (the association's own ends —
|
||||
each already logs at debug). All UDP-door tests unchanged and green
|
||||
(the idle-bound and teardown tests assert clean exits, which the
|
||||
match preserves).
|
||||
|
||||
**Lift**: the caller-visible contract matches the doc; an assembly
|
||||
monitoring `serve()` can now distinguish fatal from graceful.
|
||||
|
||||
---
|
||||
|
||||
## S-1 [minor] — The UDP door's reply path trusts the recorded client address for the association's whole lifetime: a spoofed first datagram hijacks every subsequent reply
|
||||
|
||||
**Files**: `src/door.rs` (`UdpFrontDoor::serve_on`'s `client_addr`
|
||||
capture — the first inbound datagram's source wins permanently)
|
||||
|
||||
**Problem**: the door serves one association per relay socket and
|
||||
"learns" the client's source address from the first datagram, using it
|
||||
for every reply. Two residual shapes:
|
||||
|
||||
- An off-path spoofed datagram arriving first (before the real
|
||||
client's) pins the replies to the spoofer's address. For the
|
||||
documented posture this is near-moot: the relay socket is bound
|
||||
loopback by the builder's posture (a wildcard bind is the caller's
|
||||
explicit variant), and an attacker who can inject datagrams into the
|
||||
loopback path owns the host anyway. But a deployment that *does*
|
||||
spell the wildcard bind gets a one-datagram reply-hijack primitive
|
||||
per association.
|
||||
- A client that changes source port mid-association (NAT rebinding)
|
||||
silently stops receiving replies (replies go to the learned
|
||||
address).
|
||||
|
||||
**Recommendation**: accepted-uncovered posture note rather than a fix
|
||||
— RFC 1928's own ASSOCIATE has the same per-association peer
|
||||
expectation (the RFC's relay is expected to drop datagrams from
|
||||
non-expected peers; learning-on-first is the v1 shape the POC
|
||||
validated). One doc line on `serve_on` naming the residual ("the
|
||||
client's source address is learned from the first inbound datagram
|
||||
and pinned for the association's lifetime — a wildcard-bind deployment
|
||||
accepts the off-path-first-datagram shape, the RFC's own posture")
|
||||
so the next reader does not mistake it for a bug. Filing now so the
|
||||
residual is a named decision, not an accident.
|
||||
|
||||
## D-1 [minor] — `associate_with_token` remains 0%-covered dead surface (the batch-2 completeness twin with no caller)
|
||||
|
||||
**Files**: `src/consumer.rs:450-475` (`associate_with_token` —
|
||||
uncovered; contrast `connect_with_token`/`bind_with_token`, both
|
||||
covered via the door tests), the doc claim: "the session family's
|
||||
completeness … and the wrapper-aware deployment's surface"
|
||||
|
||||
**Problem**: the TCP door refuses ASSOCIATE before any session; the
|
||||
UDP door rides `AssociateSession::associate` (no token — the UDP door
|
||||
is NoAuth by construction). No test drives the token twin. This is
|
||||
review 002 D-1's shape again — spec'd machinery with zero callers —
|
||||
but here the *doc* already carries the honest posture (the door does
|
||||
not call it), so this is not doc-vs-code drift. Still: a
|
||||
constructor-twin whose negotiation body could drift from
|
||||
`associate`'s (e.g. the DST.ADDR form) would pass the suite.
|
||||
|
||||
**Fix (small)**: one test mirroring `bind_session_with_token_round_
|
||||
trip`'s shape (drive `associate_with_token` directly against the
|
||||
token-provider topology) — or an explicit accepted-uncovered note on
|
||||
the method. Either closes the review-002-D-1-class trigger before it
|
||||
fires a third time.
|
||||
|
||||
## D-2 [minor] — `ComposedEgress`'s failure/edge matrix is the batch-2 coverage gap: the LRU's structural `Err` arm, `relay_read`'s sender-closed exit, and the eviction-vs-live-relay accounting are untested
|
||||
|
||||
**Files**: `src/composed/mod.rs` (module total 77.32% lines / 65.31%
|
||||
functions — the suite's lowest), `tests/composed-egress.rs` (6 tests,
|
||||
all happy-path or one-shot EOF)
|
||||
|
||||
**Problem**: the composed egress carries several load-bearing failure
|
||||
shapes that no test drives:
|
||||
|
||||
- `entry_for`'s post-insert invariant `Err` (the structural
|
||||
diagnostic; unreachable by construction — pinned nowhere, not even
|
||||
by an `unreachable`-class comment beyond its own doc).
|
||||
- `relay_read`'s `tx.send(..).is_err()` early return (the shared
|
||||
queue closed with the egress — the stage exiting mid-relay). The
|
||||
`Inbound::Ended` path *is* tested (`closed_queue_yields_err_...`),
|
||||
but the sender-closed path is a different exit.
|
||||
- The interaction between LRU eviction and `live_relays`: evicting an
|
||||
entry drops its `TableEntry` (session + write half), which EOFs the
|
||||
tunnel and ends the relay task → an `Ended` marker → the count
|
||||
decrements. An association that has evicted *all* of its (capped)
|
||||
tunnels while parked in `next_reply` would then trip the
|
||||
`ever_opened && live_relays == 0` association-end — correct, but the
|
||||
eviction-drives-association-end composition is unpinned (the
|
||||
existing tests evict and re-dial within a live set).
|
||||
- `inner_bytes`' oversize guard (`EgressError::General` on a reply
|
||||
over the joint bound) — the tunnel codec caps at 65535 inner bytes
|
||||
upstream, so the guard should be unreachable, but nothing pins the
|
||||
guard fires (or that the tunnel codec's cap makes it unreachable).
|
||||
|
||||
**Recommendation**: one small test round before release polish: (a)
|
||||
an egress with `max_tunnels: 1` whose single tunnel's far side EOFs →
|
||||
the association-end `Err` fires through the eviction path; (b) a unit
|
||||
probe on the invariant-`Err` arm's message shape (or fold it into
|
||||
(a)). The module's 17 uncovered functions are nearly all these two
|
||||
regions.
|
||||
|
||||
## D-3 [minor] — The `local` providers' refusal-mapping tables are partially uncovered: `dial_error_of`'s `TimedOut`/`PermissionDenied` arms and `bind_accept`'s `Shortage`-arm splits are compile-checked only
|
||||
|
||||
**Files**: `src/local/mod.rs:76-81` (`dial_error_of`'s
|
||||
`NetworkUnreachable`/`HostUnreachable`/`TimedOut`/`PermissionDenied`
|
||||
arms — unreachable from the loopback tests, which can only produce
|
||||
`ConnectionRefused`), `src/local/mod.rs:253-259` (the bind-map's
|
||||
`PermissionDenied`/`AddrNotAvailable`/`_` arms — the tests drive
|
||||
`AddrInUse` only), `src/local/mod.rs:108-110` (the "no addresses
|
||||
tried" terminal — unreachable: the resolved set is never empty after
|
||||
the empty-check), `src/local/mod.rs:63-64` (the domain-resolve
|
||||
"no addresses" arm), `src/local/mod.rs:214` (`try_recv_from`'s
|
||||
non-`WouldBlock` error arm)
|
||||
|
||||
**Problem**: the task notes record the mapping table as the
|
||||
deliverable, and `to_reply_error()` is table-tested — but the *io
|
||||
error kind → SocksDialError* / *io kind → BindAcceptFactoryError*
|
||||
splits themselves are only half-driven. A refactor that swaps
|
||||
`TimedOut` into the `_` arm (losing the 0x03 mapping) would pass the
|
||||
suite. Same class as review 002's N-8 single-arm cosmetics, but
|
||||
these are security-adjacent (the refusal vocabulary *is* the target
|
||||
policy's observable surface).
|
||||
|
||||
**Fix (small)**: table-driven unit tests on `dial_error_of` and the
|
||||
bind-map's closure (they are pure functions of `io::ErrorKind` —
|
||||
construct errors of each kind with `io::Error::new`); two tests, no
|
||||
new harness. The two structural arms (`no addresses tried`, the
|
||||
resolve-empty arm) stay accepted-uncovered with a note each.
|
||||
|
||||
## N-1 [observation] — `ProducerDoorGuard::close`'s `drop(self)`-then-await shape is correct but non-obvious: one doc line would prevent a "simplification" that deadlocks
|
||||
|
||||
**Files**: `src/local/producer_door.rs:194-202`
|
||||
|
||||
**Problem**: `close` drops `self` (releasing the `watch::Receiver`)
|
||||
*before* awaiting the loop task — load-bearing: the loop's
|
||||
`done_tx.closed()` only resolves when *all* receivers are dropped, so
|
||||
holding the receiver across the await would deadlock the graceful
|
||||
stop. The code is correct; the doc says "ends the accept loop
|
||||
gracefully" without naming why the drop precedes the await. A future
|
||||
refactor that "simplifies" the `drop(self)` away (it looks redundant
|
||||
next to the field-`None`ing above it) reintroduces a hang.
|
||||
|
||||
**Fix (one line)**: extend `close`'s doc: "the receiver is dropped
|
||||
before the await — the loop's `done_tx.closed()` fires only when
|
||||
every receiver is gone; holding it here would deadlock." Zero
|
||||
behavior change.
|
||||
|
||||
## N-2 [observation] — `bindfirst_accept`'s factory errors map to reply#1 codes whose vocabulary is wider than the producer's gate: `NotAllowed`/`Shortage` from a *listen resource* deserve a doc cross-check against the producer-side `bind_policy` gate
|
||||
|
||||
**Files**: `src/composed/mod.rs:718-820` (the factory's
|
||||
`Refused`/`NotAllowed`/`Shortage` mappings)
|
||||
|
||||
**Problem**: the composed factory returns `BindAcceptFactoryError`
|
||||
variants that the producer maps to reply#1 codes — including
|
||||
`NotAllowed` (0x02), the same code the ADR-006 command gate uses for
|
||||
the ACL refusal. On the channels path the two layers stack: an
|
||||
identity without `socks5:bind` never reaches the factory (the gate
|
||||
precedes the seam), so a factory-side `NotAllowed` can only mean the
|
||||
*tunnel* layer's per-listener ACL — semantically distinct, wire-identical.
|
||||
That is RFC-faithful (0x02 is the ruleset code) and the composed
|
||||
variant's per-listener ACL is a real policy source (ADR-007's
|
||||
per-listener ACL for free), so the behavior is correct — but nothing
|
||||
in the composed module's doc *names* the stacking ("the factory's
|
||||
0x02 can also mean the tunnel layer's ACL, distinct from the command
|
||||
gate's"). An operator debugging a refused BIND sees one code with two
|
||||
policy sources.
|
||||
|
||||
**Recommendation**: one doc line on `bindfirst_accept` naming the
|
||||
layering. No code change.
|
||||
|
||||
## N-3 [observation] — `FrontDoor::serve_on`'s per-accept task holds the session-cap permit for the connection's whole life, but the idle-first-byte timeout does not release early: a 256-cap door parks at "cap reached" for connections that are merely slow *readers*
|
||||
|
||||
**Files**: `src/door.rs:338-374` (the accept loop + `Semaphore`),
|
||||
`src/door.rs:393-401` (the idle-first-byte gate)
|
||||
|
||||
**Problem**: the permit is acquired at accept and dropped at task
|
||||
end — correct for the cap's purpose (bounding concurrent sessions).
|
||||
The residual: a connection that passes the idle gate (sent one byte)
|
||||
but is then slow consuming the producer's conversation holds its
|
||||
permit for the RFC conversation's full lifetime — the cap counts
|
||||
*accepted* connections, not *active* sessions. That is the cap
|
||||
working as designed (ADR-014 knob 2 bounds concurrent *connections*,
|
||||
which is the slowloris-relevant resource), so this is a naming
|
||||
residual only: the builder doc says "max-concurrent-sessions" where
|
||||
the mechanism is "max concurrent *connections*" (including idle-past-
|
||||
first-byte ones). Same shape on `ProducerDoor`.
|
||||
|
||||
**Recommendation**: one doc word ("connections" not "sessions") on
|
||||
both builders, or leave as-is — the knob's name is the docs'
|
||||
vocabulary, not a contract. Zero code.
|
||||
|
||||
## N-4 [observation] — The cred-bearing door's `local_rfc1929_half` check closure writes the mapped identity through a `std::sync::Mutex` witness even when the check rejects: the `None` slot survives a failed check (harmless, but the witness's semantics are "last lookup", not "accepted identity")
|
||||
|
||||
**Files**: `src/door.rs:530-557`
|
||||
|
||||
**Problem**: the checker sets `*slot = mapped` (including `None` on a
|
||||
miss) before returning `accepted`; the later `.take().ok_or(...)`
|
||||
correctly treats `None` as rejected. Residual: a checker that ran
|
||||
twice (upstream's `accept_password_auth` calls it once per
|
||||
conversation — fine here) would let a later miss overwrite an
|
||||
earlier hit. Reachable only through API misuse (the closure runs
|
||||
once per conversation by construction). Harmless as landed; the
|
||||
"the check IS the table lookup — one table, one truth" doc claim is
|
||||
accurate for the single-call shape.
|
||||
|
||||
**Recommendation**: none. Recorded to bound re-review (the shape is
|
||||
the sync-witness pattern's own; a second call site would need
|
||||
`Option::take`-semantics discipline).
|
||||
|
||||
## N-5 [observation] — The two doors' accept loops duplicate the idle-peek + semaphore-cap + per-accept-isolation shape (`FrontDoor::serve_on` vs `ProducerDoor`'s `serve_on_parts`) — a third door shape should extract it
|
||||
|
||||
**Files**: `src/door.rs:338-374`, `src/local/producer_door.rs:261-294`
|
||||
|
||||
**Problem**: ~35 lines of structural duplication (the
|
||||
`Semaphore::try_acquire_owned` cap refusal, the `spawn` +
|
||||
`_permit`-guard shape, the accept-error isolation) — the same N-5
|
||||
class as review 002's harness consolidation, across a feature
|
||||
boundary this time (`door.rs` and `local/producer_door.rs` could
|
||||
share a helper; both are `local`-gated so no feature-gate obstacle).
|
||||
Deliberately not fixed inline: the two loops differ in the producer
|
||||
injection (`serve_socks5_over` vs the variant split) and a third
|
||||
consumer doesn't exist yet; a premature extraction would trade two
|
||||
readable loops for one parameterized one.
|
||||
|
||||
**Fix (small, when a third shape lands)**: a
|
||||
`crate::local::accept_loop`-shaped helper (idle gate + cap + spawn +
|
||||
isolation) consumed by both. Until then, the duplication is honest.
|
||||
|
||||
## N-6 [observation] — `InbandBridge::recv`'s 16 KiB scratch allocates per call (per datagram when the queue is empty) — bounded, but the retained-capacity note from review 002 S-1 applies one layer up
|
||||
|
||||
**Files**: `src/door.rs` (`InbandBridge::recv`'s `vec![0u8; 16 * 1024]`)
|
||||
|
||||
**Problem**: each decode call allocates a fresh 16 KiB read buffer
|
||||
(the `AssociateSession::recv_datagram` shape it mirrors does the
|
||||
same). Bounded per call, dropped per call — fine; the S-1-style
|
||||
residual (a 65507-byte frame's `BytesMut` retaining ~64 KiB of
|
||||
capacity in the bridge's reader for the association's lifetime) is
|
||||
inherited from `DatagramReader`'s design and already recorded in
|
||||
review 002 S-1. Per-association bounded, unshrunk — same accepted
|
||||
posture, now with two owners (the session and the door's bridge).
|
||||
|
||||
**Recommendation**: none (the review-002 posture covers it). One line
|
||||
on `InbandBridge`'s doc pointing at the shared posture, if the next
|
||||
touch lands.
|
||||
|
||||
## N-7 [observation] — `ProducerDoor::builder()` requires `.producer` but not `.bind` — the bind decision moves to `serve()`, an asymmetry with `FrontDoor`'s builder (where `.bind` is required at build time)
|
||||
|
||||
**Files**: `src/local/producer_door.rs:144-155` + `222-239` vs
|
||||
`src/door.rs:279-288` + `305-309`
|
||||
|
||||
**Problem**: two builder shapes for sibling types: `FrontDoor`'s
|
||||
missing-bind is a `build()`-time error (`DoorBuildError::
|
||||
MissingBindAddr`); `ProducerDoor`'s is a `serve()`-time error
|
||||
(`ProducerDoorError::MissingBindAddr`). The producer door's split
|
||||
exists for a reason (its doc says so — `serve_on` needs no bind), but
|
||||
`FrontDoor` could have made the same split and didn't. The
|
||||
asymmetry is documented on both sides; a deployer writing both
|
||||
shapes hits the difference on the first compile error and reads two
|
||||
docs.
|
||||
|
||||
**Recommendation**: none (both postures are typed, convention-2
|
||||
compliant, and documented). Recorded to bound re-litigation: the
|
||||
split-vs-eager-bind choice is per-type, not a pattern to propagate.
|
||||
|
||||
## N-8 [observation] — Coverage residue worth one line each (or accepted-uncovered with a note), beyond D-2/D-3
|
||||
|
||||
The `--all-features --show-missing-lines` sweep attributed; these are
|
||||
the residuals beyond the findings (each small, named so the next
|
||||
coverage run needs no re-derivation):
|
||||
|
||||
- `door.rs:160-168` — `CredentialTable::len`/`is_empty`: observability
|
||||
accessors, no caller (one probe or an accepted-uncovered note).
|
||||
- `door.rs:194-196` — `DoorExit::LocalRfc`'s Display arm (the
|
||||
cred-bearing door's local-RFC exit is reached in tests, but that
|
||||
*variant's* arm render is not asserted): cosmetic.
|
||||
- `door.rs:305-309` — `FrontDoor::serve` (the bind-then-serve
|
||||
wrapper; every test drives `serve_on`): one test or a
|
||||
"the suite drives `serve_on`" note.
|
||||
- `door.rs:414` — the `(Some(_), None)` unreachable-by-construction
|
||||
arm: accepted-uncovered by design (convention 2's exhaustive-match
|
||||
shape), fine.
|
||||
- `door.rs:483-487` — `raw_pipe`'s open-failure exit (a NoAuth door
|
||||
whose channel open fails): one probe would pin it.
|
||||
- `door.rs:595-611` — `pipe_connect_plane`'s Domain-reply form + the
|
||||
`None`-reply + write-failure arms: the suite drives v4 replies
|
||||
only; a domain-reply unit probe or accepted-uncovered.
|
||||
- `door.rs:636-656` — `pipe_bind_plane`'s Domain-form reply wire
|
||||
(the BIND door's domain-BND re-encode): the suite's BIND tests
|
||||
drive IP forms; the Domain arm is hand-written wire code with no
|
||||
test — the batch-2 suite's most substantive single-arm gap.
|
||||
- `door.rs:722` — `write_refusal_reply`'s `_ => return` arm (a
|
||||
non-reply-code error stays a bare close): one probe.
|
||||
- `door.rs:755-758` — `UdpDoorExit`'s Display arms: cosmetic.
|
||||
- `door.rs:810-814` — `UdpFrontDoor::serve` (same as the TCP door's).
|
||||
- `door.rs:842-845` — `serve_on`'s spent-`split` arm (`Ok(())` on a
|
||||
spent association): one probe.
|
||||
- `door.rs:886` — the `client_addr`-already-set skip; `door.rs:908-
|
||||
913` — the reply-before-first-inbound park (`continue` when no
|
||||
client address is known): both single-arm, timing-shaped.
|
||||
- `door.rs:941` — the `Io`-exit-as-`Err` arm (F-2's new shape): the
|
||||
fix landed untested (a relay-socket read error is not producible
|
||||
over a test duplex without a poisoned socket) — accepted-uncovered
|
||||
with this note, or a `UdpSocket`-pair probe.
|
||||
- `door.rs:1005-1048` — the bridge's error arms (a decode garbage in
|
||||
a *later* batch frame, the `Address` variant): the unit tests pin
|
||||
the happy shapes; one garbage-later-batch probe.
|
||||
- `consumer.rs:1145-1147` — `TokenClient`'s Debug: cosmetic.
|
||||
- `consumer.rs:1185` — the spent-shape assert's `err().map(to_string)`
|
||||
formatting arm: cosmetic.
|
||||
- `local/mod.rs:63-64, 108-110, 214, 257-259` — D-3's terminal arms.
|
||||
- `local/producer_door.rs:71-72` — `BackendExit::Session`'s Display
|
||||
arm; `:121-124` — the builder's `idle_first_byte`/`max_sessions`
|
||||
setters (the tests drive both — via `serve()`'s defaults only);
|
||||
`:200` — `close`'s already-closed arm; `:222-239` — `serve`'s
|
||||
`MissingBindAddr`/`Bind` error arms; `:269-271` — the accept-error
|
||||
continue arm; `:314` — the idle-peek `Io` arm.
|
||||
- `composed/mod.rs` — D-2's matrix (the module's own gap).
|
||||
- `egress.rs:518` — the sync-`Err` factory test's panic arm (the
|
||||
match's `Ok` arm — unreachable in the passing test): cosmetic.
|
||||
|
||||
The headline gaps: the BIND door's Domain-form reply wire (D-2's
|
||||
neighbor), the composed egress's failure matrix, and the
|
||||
`local/mod.rs` mapping-table arms.
|
||||
|
||||
---
|
||||
|
||||
# Part B — Non-findings (verified correct, recorded to bound the re-review)
|
||||
|
||||
- **The never-binds posture survived batch 2** (re-swept): the
|
||||
protocol layer names no kernel-socket type; every bind is a
|
||||
`local`-gated constructor's explicit decision; the default +
|
||||
composed wasm checks stay clean; `local`-on-wasm fails with the
|
||||
expected mio error. The doors' binds are builder-`.bind()`-driven;
|
||||
`LocalEgress::bind`'s loopback and `bind_accept`'s explicit host
|
||||
are the doc-pinned explicit shapes.
|
||||
- **The cred-bearing door's re-origination holds under adversarial
|
||||
reading**: the door's RFC half consumes greeting + credentials +
|
||||
one command; the in-band conversation is fresh (the never-pipes-
|
||||
raw pin); the token is the mapped identity's id riding the open-op
|
||||
`auth_token` payload (CF-005 precedence (1)); the password never
|
||||
crosses the channel and never reaches a log line (swept: no
|
||||
credential is ever in a `tracing` call).
|
||||
- **The UDP door's InbandBridge carries the ADR-003 reader-state
|
||||
contract** (the gate review's R-1 fix verified independently):
|
||||
the pending queue drains in order, partial frames survive call
|
||||
boundaries, EOF mid-frame fails loud — the three unit pins all
|
||||
fail against a per-call reader, and the end-to-end doubled-echo
|
||||
test guards the class through the real stack.
|
||||
- **The composed accept's teardown-at-birth is structural**: the
|
||||
`TunnelListener` rides inside the write payload (`ForwardedOwner`);
|
||||
the disjoint-capture trap is doc-pinned and the whole-struct
|
||||
rebinding is in place; the client-gone select! drop tears the
|
||||
listener and the oneshot down together; a correlation miss drops
|
||||
the forwarded channel at birth (pinned).
|
||||
- **The forwarded handler's inline pump await (R-02) is real**: the
|
||||
handler's task lives exactly as long as the data plane (either-
|
||||
EOF completion), the wrapper's inline await tracks it — verified
|
||||
against alktunnels' `make_forwarded_op_handler` (the inline
|
||||
`pump.await`), not just the doc comments.
|
||||
- **The composed egress's readiness contract is count-based and
|
||||
sound**: the `ever_opened` guard (the assembly suite's drift fix)
|
||||
is correct — a never-opened table parks (the stage's channel
|
||||
branch governs); the ended-relay `Err` fires only after a tunnel
|
||||
existed and every relay ended; `saturating_sub` bounds the count;
|
||||
the closed-queue test awaits the `Err` (never a hot `Ok(None)`).
|
||||
- **The sync-bound wrinkle's Sync proofs are no-unsafe**: both
|
||||
`SyncHalves` and `OwnedWrite` ride `Mutex<T: Send>` (`Sync` for
|
||||
`T: Send`, std guarantee); poisoned locks unwrap via `into_inner`
|
||||
(convention 2); contention is structurally impossible (one pump
|
||||
task at a time — the doc says so and the pump shapes confirm).
|
||||
- **No panics in non-test code** (re-swept across all of `src/`,
|
||||
including the batch-2 modules): every `unwrap`/`expect`/`panic!`
|
||||
hit is in a `#[cfg(test)]` region or a test file. The batch-2
|
||||
`expect` sweep (gate review R-2) held through the review's read.
|
||||
- **Wire-driven allocation is bounded in the batch-2 code**: the
|
||||
doors' scratch buffers are fixed (1-byte peek, 64 KiB relay, 16 KiB
|
||||
bridge chunk); `parse_udp_request`'s domain arm stays 255-bounded;
|
||||
the UDP door's 65535-byte recv is the kernel's datagram bound; the
|
||||
cred-bearing door's username/password reads are 1-byte-bounded by
|
||||
the RFC 1929 length fields (the forked codec's bounded-read
|
||||
posture). No unbounded wire-driven allocation exists.
|
||||
- **The doors' failure isolation is real**: per-accept spawn, the
|
||||
accept loop continues on per-connection failure (both doors + the
|
||||
producer door, each pinned); a cap-refused accept closes the
|
||||
socket with no channel opened; the idle gate fires before any
|
||||
channel opens (witnessed by the channel-count probes).
|
||||
- **`pump_bidi` is awaited inline everywhere in batch 2** (the
|
||||
CONNECT/BIND doors' `copy_bidirectional` against taken in-band
|
||||
streams; the composed accept's middle pump; the producer door
|
||||
rides `serve_socks5_over`'s own inline await) — no
|
||||
spawn-and-forget data plane exists.
|
||||
- **MSRV, wasm, publish**: `cargo publish --dry-run` completes; both
|
||||
wasm checks clean; `rust-version = "1.88"` unchanged (the batch-2
|
||||
additions are plain 2021-edition async/await; no let-else/OnceLock
|
||||
or other MSRV-relevant surface).
|
||||
- **License/provenance posture unchanged**: `src/rfc1928/LICENSE`
|
||||
and the module docs still carry the full fork-point statement;
|
||||
batch 2 touched no forked file except `server.rs`'s already-pinned
|
||||
`accept_password_auth` consumption (doc-level only).
|
||||
|
||||
---
|
||||
|
||||
# Part C — What's Good
|
||||
|
||||
- **The door boundary is where the complexity belongs, and it is
|
||||
contained.** The entire RFC-codec surface (1929 negotiation,
|
||||
reply re-encoding, the UDP header codec) lives in `door.rs`
|
||||
(1135 lines, 70% tests) behind one feature gate; the protocol
|
||||
layer's growth for all of it was `TokenClient` + three constructor
|
||||
twins + a `reply_addr` accessor. The F-1 fix was possible *because*
|
||||
the codec lives at the door — one helper closed all three sites.
|
||||
- **The provider/producer split is honest.** `ProducerDoor` is pure
|
||||
accept-loop assembly over the landed `serve_socks5_over` (zero
|
||||
new protocol-layer code, the module doc's claim verified line by
|
||||
line); the ADR-014 knobs are two `local` types (`peek` gate,
|
||||
`Semaphore`) with door-side refusals — the protocol layer never
|
||||
learned about timers, exactly per ADR-014.
|
||||
- **The Sync-bound wrinkle's resolution is textbook**: no unsafe, no
|
||||
new trait, two small adapters with the Sync proof in the type
|
||||
system, the owner riding inside the payload so the compiler
|
||||
enforces the teardown — and the whole thing is pinned by a
|
||||
compile-shape test against the real alktunnels surface.
|
||||
- **The assembly integration suite is the batch's real deliverable.**
|
||||
It surfaced the one genuine implementation drift
|
||||
(`ComposedEgress`'s birth-killed association) that every
|
||||
per-component suite missed, and it pins the compositions the
|
||||
component tasks could not (doors + native + composed concurrent,
|
||||
the idle-bound teardown matrix, the domain-resolution pin). The
|
||||
mixed-assembly tests are exactly the roles-not-sides posture made
|
||||
executable.
|
||||
- **The docs discipline held through an 8-task batch**: the
|
||||
reconciliations the tasks recorded (the `ever_opened` fix, the
|
||||
TokenClient door-only contract, the one-command-per-connection
|
||||
posture, producer.md's local-backend rewrite) were all verified
|
||||
against the tree; the review found no further spec-vs-code drift
|
||||
beyond the ones already reconciled.
|
||||
|
||||
---
|
||||
|
||||
# Suggested Disposition
|
||||
|
||||
The batch-2 gate closed with the crate feature-complete; this review
|
||||
found the residue a full-surface read always finds. Disposition:
|
||||
|
||||
| Item | Action | Shape |
|
||||
|---|---|---|
|
||||
| F-1, F-2 | **Fixed in this review change** | the refusal re-encoding + the UDP-door exit match + the pinning test |
|
||||
| D-1 | one test or an accepted-uncovered note | the `associate_with_token` probe (or the note) in the release-polish round |
|
||||
| D-2 | one small test round before release polish | the composed-egress failure matrix (2 tests) |
|
||||
| D-3 | two table-driven unit tests | the `io::ErrorKind` → vocabulary tables |
|
||||
| S-1 | accepted-uncovered posture note | one doc line on `UdpFrontDoor::serve_on` |
|
||||
| N-1 | one doc line | the `close` drop-before-await note |
|
||||
| N-2 | one doc line | the `bindfirst_accept` 0x02 layering note |
|
||||
| N-3 | one doc word (or none) | "connections" vs "sessions" on both builders |
|
||||
| N-4 | none | recorded to bound re-review |
|
||||
| N-5 | defer | extract when a third accept-loop shape lands |
|
||||
| N-6 | none | covered by review 002 S-1's posture |
|
||||
| N-7 | none | recorded to bound re-review |
|
||||
| N-8 | batch the cosmetics with D-2/D-3's tests | the release-polish round |
|
||||
|
||||
The natural home for the residue is the release-polish task (publish
|
||||
prep, OQ-SK2-08) the batch-2 decomposition deferred — the review's
|
||||
test-shaped items ride alongside it; the doc-shaped items are
|
||||
one-line changes in the same round.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/reviews/002-protocol-core-implementation-review.md` (the
|
||||
template; the C/D/S/N legend; the N-5/N-8 lineage this review's
|
||||
D/N items extend)
|
||||
- `tasks/implementation/review-assembly-layer.md` (the narrow gate —
|
||||
its R-1 fix is this review's F-class context; its R-2 sweep is the
|
||||
no-panic posture re-verified in Part B)
|
||||
- docs/architecture/decisions/003/005/006/007/008/009/014 (the ADRs
|
||||
the batch-2 surface implements)
|
||||
- alktunnels 0.2.0 (`producer.rs` `make_forwarded_op_handler` — the
|
||||
R-02 inline-await cross-check; `ForwardedHandler`'s ownership
|
||||
shape)
|
||||
- ADR-014 (the accept-path defense obligations the doors inherit —
|
||||
the F-2/S-1/N-3 posture context)
|
||||
- The POC crates (`/workspace/alksocks-{connect,udp,bind}-poc` — the
|
||||
fidelity sources the gate review diffed; this review's POC-drift
|
||||
check found no further divergence)
|
||||
+42
-8
@@ -480,7 +480,14 @@ async fn cred_bearing(
|
||||
.await
|
||||
{
|
||||
Ok(session) => session,
|
||||
Err(err) => return DoorExit::Open(err.to_string()),
|
||||
Err(err) => {
|
||||
// The in-band refusal's mapped code is re-encoded to
|
||||
// the vanilla client (the RFC codec at the door
|
||||
// boundary — ADR-003): a dial refusal is reply#1's
|
||||
// code, never a bare EOF.
|
||||
write_refusal_reply(&mut stream, &err).await;
|
||||
return DoorExit::Open(err.to_string());
|
||||
}
|
||||
};
|
||||
pipe_connect_plane(stream, session).await
|
||||
}
|
||||
@@ -490,7 +497,10 @@ async fn cred_bearing(
|
||||
.await
|
||||
{
|
||||
Ok(session) => session,
|
||||
Err(err) => return DoorExit::Open(err.to_string()),
|
||||
Err(err) => {
|
||||
write_refusal_reply(&mut stream, &err).await;
|
||||
return DoorExit::Open(err.to_string());
|
||||
}
|
||||
};
|
||||
pipe_bind_plane(stream, session).await
|
||||
}
|
||||
@@ -663,11 +673,13 @@ async fn bind_quiet_and_pipe(
|
||||
let plane = match session.await_inbound().await {
|
||||
Ok(Some(plane)) => plane,
|
||||
Ok(None) => return DoorExit::PlaneEnded,
|
||||
// An accept-stage refusal (reply#2's mapped code was written
|
||||
// in-band; the producer's pump ended the session): the door
|
||||
// closes the vanilla connection — the conversation ends where
|
||||
// the in-band one did.
|
||||
Err(_) => {
|
||||
// An accept-stage refusal (the mapped reply#2 code): the
|
||||
// vanilla client gets that code re-encoded door-side (the RFC
|
||||
// codec at the boundary — never a bare EOF), then the
|
||||
// connection closes — the conversation ends where the in-band
|
||||
// one did.
|
||||
Err(err) => {
|
||||
write_refusal_reply(&mut stream, &err).await;
|
||||
session.close().await;
|
||||
return DoorExit::PlaneEnded;
|
||||
}
|
||||
@@ -699,6 +711,22 @@ async fn write_vanilla_reply(stream: &mut TcpStream, reply: &[u8]) -> std::io::R
|
||||
stream.flush().await
|
||||
}
|
||||
|
||||
/// Re-encode an in-band session error's refusal to the vanilla client
|
||||
/// (the door's RFC codec at the boundary): an error carrying an RFC
|
||||
/// reply code gets that code as the command's reply (`Succeeded`-form
|
||||
/// encoding with the error's code + the placeholder BND.ADDR — a
|
||||
/// refused reply carries a zero address per the RFC); any other error
|
||||
/// shape (open/transport/I-O) stays a bare close (nothing to encode).
|
||||
/// Best-effort: a failed write means the client is gone either way.
|
||||
async fn write_refusal_reply(stream: &mut TcpStream, err: &SocksSessionError) {
|
||||
let code = match err {
|
||||
SocksSessionError::Rfc(SocksError::ReplyError(code)) => *code,
|
||||
_ => return,
|
||||
};
|
||||
let reply = new_reply(&code, SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0));
|
||||
let _ = write_vanilla_reply(stream, &reply).await;
|
||||
}
|
||||
|
||||
/// The UDP front door (feature `local`, the POC #2 test-8 shape): the
|
||||
/// ASSOCIATE door — a real UDP relay socket bridged onto one in-band
|
||||
/// association. See the module docs for the shape and the codec
|
||||
@@ -908,7 +936,13 @@ impl UdpFrontDoor {
|
||||
};
|
||||
tracing::debug!("udp front door: association ended: {exit}");
|
||||
let _ = association.shutdown().await;
|
||||
Ok(())
|
||||
// A socket I/O failure is the door's fatal outcome (surfaced to
|
||||
// the caller); the idle bound, the association's end, and a
|
||||
// session error are the association's own ends — a clean return.
|
||||
match exit {
|
||||
UdpDoorExit::Io(err) => Err(err),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -607,6 +607,79 @@ async fn bind_door_vanilla_conversation_reply_fidelity() {
|
||||
drop(app);
|
||||
}
|
||||
|
||||
/// The in-band refusal's reply code is re-encoded to the vanilla
|
||||
/// client (the RFC codec at the door boundary): an identity carrying
|
||||
/// `socks5:open` only issues BIND through the cred-bearing door —
|
||||
/// the in-band command gate refuses 0x02, and the door re-encodes
|
||||
/// that code as reply#1 (never a bare EOF; the refused reply carries
|
||||
/// the placeholder zero address).
|
||||
#[tokio::test]
|
||||
async fn cred_door_in_band_refusal_surfaces_the_reply_code() {
|
||||
let dial = alksocks::tcp_dial();
|
||||
let table = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
|
||||
username: "open-only".to_string(),
|
||||
password: "pw".to_string(),
|
||||
identity: Identity {
|
||||
id: "door-open-only".to_string(),
|
||||
scopes: vec![alksocks::SOCKS5_OPEN_SCOPE.to_string()],
|
||||
resources: Default::default(),
|
||||
},
|
||||
}]);
|
||||
|
||||
let topo = harness::wire_consumer_side_with_provider(
|
||||
dial,
|
||||
None,
|
||||
None,
|
||||
Some(consumer_identity()),
|
||||
Arc::new(DoorTokenProvider {
|
||||
bind_scoped: vec![],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||
let addr = listener.local_addr().expect("door addr");
|
||||
let door = FrontDoor::builder()
|
||||
.credentials(table)
|
||||
.bind(addr)
|
||||
.build()
|
||||
.expect("the door builds");
|
||||
let _serve = tokio::spawn({
|
||||
let client = Arc::clone(&topo.consumer);
|
||||
async move {
|
||||
door.serve_on(listener, client)
|
||||
.await
|
||||
.expect("the door serves")
|
||||
}
|
||||
});
|
||||
|
||||
let mut vanilla = VanillaClient::dial(addr).await;
|
||||
let method = timeout(BOUND, vanilla.greet_password())
|
||||
.await
|
||||
.expect("greeting");
|
||||
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_PASSWORD);
|
||||
let status = timeout(BOUND, vanilla.send_credentials("open-only", "pw"))
|
||||
.await
|
||||
.expect("credential exchange");
|
||||
assert_eq!(status, 0x00, "accepted");
|
||||
|
||||
// BIND through the open-only identity: the in-band command gate's
|
||||
// 0x02 rides the door's re-encoded reply (the placeholder BND.ADDR
|
||||
// — a refused reply carries a zero address).
|
||||
let (code, advertised) = timeout(BOUND, vanilla.bind_v4([127, 0, 0, 1], 0))
|
||||
.await
|
||||
.expect("the refusal reply arrives (never a bare EOF)");
|
||||
assert_eq!(
|
||||
code,
|
||||
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
|
||||
"the in-band gate's 0x02 is re-encoded door-side"
|
||||
);
|
||||
assert!(
|
||||
matches!(advertised, TargetAddr::Ip(sa) if sa.port() == 0 && sa.ip().is_unspecified()),
|
||||
"a refused reply carries the placeholder BND.ADDR: {advertised:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The idle-first-byte timeout (ADR-014 knob 1): an accepted
|
||||
/// connection that sends no first byte within the window is closed
|
||||
/// door-side.
|
||||
|
||||
Reference in new issue
Block a user