door: the TCP front door — NoAuth raw pipe + RFC 1929 cred-bearing + BIND

The consumer's TCP front door (feature `local`, ADR-005/ADR-014): the
ssh `-D` local exposure vanilla clients point at.

- `src/door.rs`: `FrontDoor::builder().credentials(...).bind(...)`
  → `serve`/`serve_on`. The NoAuth door is the literal raw pipe
  (accept → channel BiStream via copy_bidirectional, zero protocol
  translation). The cred-bearing door terminates RFC 1929 locally
  (the forked accept_password_auth shape) and re-originates in-band
  via the token-carrying consumer sessions — the vanilla replies are
  re-encoded at the door (the RFC codec boundary, ADR-003), the
  password never crosses the channel as a SOCKS password. The BIND
  door rides BindSession with reply#1 fidelity (the advertised
  address passes through unchanged). UDP ASSOCIATE at the TCP door
  refuses RFC-faithfully (0x07 — the UDP door's task). The ADR-014
  accept-path defense: the idle-first-byte window (peek-based,
  door-side close) + the max-sessions cap (semaphore) as constructor
  config; per-accept failure isolation (the loop continues).
- The identity mechanism (decided + recorded in the task Notes): the
  per-credential identity rides the open-op `auth_token` payload
  (CF-005 precedence (1)) — the token is the mapped identity's id;
  the producer resolves it through ServingConfig.identity_provider.
  No per-credential ChannelClient.
- `src/consumer.rs`: `TokenClient` (the token-carrying open surface
  over the taken CallConnection + shared manager) and the
  `*_with_token` twins of the three session constructors through the
  open_with_token_and_negotiate prologue (teardown-at-birth
  preserved); `Socks5ChannelSession::reply_addr` (the CONNECT
  reply's BND.ADDR for the door's re-encoding). Existing surfaces
  untouched.
- `tests/door-tcp.rs`: 10 integration tests — the NoAuth raw-pipe
  round-trip (the POC #1 curl shape), good/bad credentials (the
  establisher witness sees the mapped identity, never the password;
  a rejection opens no channel), the never-pipes-raw-post-auth pin,
  the BIND door's vanilla conversation, the idle timeout, the
  session cap, failure isolation, and the native-session/BIND-token
  regression guards. Harness: wire_consumer_side_with_provider (the
  producer's identity provider configurable).

Verification: cargo test (375) + --all-features (423) green; clippy
-D warnings clean (default + all-features); fmt --check clean; wasm
check + wasm clippy green; default graph stays net-free.
This commit is contained in:
glm-5.3-flash committed 2026-09-25 12:37:23 +00:00
1 parent f3c371310c
commit b8fd75b9f3
6 files changed
+1905 -18

No files matched your search

+126 -11
View File
@@ -1,7 +1,7 @@
---
id: implementation/door-tcp
name: Front doors — the TCP door (raw-pipe + RFC 1929 cred-bearing) and the BIND door
status: pending
status: completed
depends_on: [implementation/local-providers]
scope: moderate
risk: medium
@@ -89,23 +89,23 @@ Deliverables:
## Acceptance Criteria
- [ ] `cargo test` passes; `cargo test --all-features` passes
- [ ] `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt
- [x] `cargo test` passes; `cargo test --all-features` passes
- [x] `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt
--check` clean
- [ ] Default wasm checks stay green (door code is `local`-gated)
- [ ] The NoAuth door is a literal raw pipe (zero protocol
- [x] Default wasm checks stay green (door code is `local`-gated)
- [x] The NoAuth door is a literal raw pipe (zero protocol
translation) — pinned by test
- [ ] The cred-bearing door terminates RFC 1929 locally and
- [x] The cred-bearing door terminates RFC 1929 locally and
re-originates in-band (never pipes raw post-auth) — pinned by
test (a vanilla client's command bytes must not reach the
producer as greeting bytes)
- [ ] The credential→identity mapping is the static config table
- [x] The credential→identity mapping is the static config table
(ADR-005); the password never crosses the channel as a SOCKS
password — pinned by test (the producer's establisher witness
sees the mapped identity, never the password)
- [ ] The idle-first-byte timeout + session cap land as constructor
- [x] The idle-first-byte timeout + session cap land as constructor
config — pinned by tests
- [ ] Per-connection failure isolation — pinned by test
- [x] Per-connection failure isolation — pinned by test
## References
@@ -123,8 +123,123 @@ Deliverables:
## Notes
> To be filled by implementation agent
> Filled by the implementation agent (2026-09-25).
- **The identity mechanism — decided (the task's open choice)**: the
per-credential identity rides the open-op call's `auth_token`
payload (CF-005 precedence (1)), never a per-credential
`ChannelClient`. Mechanically: the door wraps the client's
channel-0 `CallConnection` once (`TokenClient::wrap` — the raw
`call_with_payload` surface, since the token is a payload-level
field, ADR-017 §7) and each credential's open rides the payload
`auth_token`; the producer's `ServingConfig.identity_provider`
resolves it and the ACL gate reads the resolved scopes exactly as
for any per-call opener identity. The token is the mapped
identity's *id* (the resolution seam's vocabulary — the password
never rides it). Rationale: one transport, one call connection,
many identities — a per-credential `ChannelClient` would need a
full transport connection per credential, which the ssh `-D`
single-socket posture cannot afford.
- **New consumer surface (`src/consumer.rs`)**: `TokenClient` (the
token-carrying open surface over the taken `CallConnection` +
shared manager) and the `*_with_token` twins of the three session
constructors (`Socks5ChannelSession::connect_with_token`,
`BindSession::bind_with_token`,
`AssociateSession::associate_with_token`) through a
`open_with_token_and_negotiate` prologue (the mirror of
`open_and_negotiate`: open-with-token → adopt → negotiate →
teardown-at-birth on failure). `Socks5ChannelSession` gained the
`reply_addr` field + accessor (the CONNECT reply's BND.ADDR,
captured at `connect` — the door's vanilla-reply re-encoding rides
it). The existing constructor surfaces are untouched.
- **The door (`src/door.rs`, feature `local`)**:
- `FrontDoor::builder().credentials(...).bind(...).build()` →
`serve(client)`; `serve_on(listener, client)` is the pre-bound
seam (tests/assembly wire their own listener — the bind decision
stays the caller's).
- **The NoAuth door**: accept → `open_channel` →
`copy_bidirectional(accepted_socket, BiStream)` — the literal
raw pipe; zero translation (POC #1's shape).
- **The cred-bearing door**: the forked
`accept_password_auth` shape locally (`negotiate_auth(&[
PasswordAuthentication])` → `read_username_password` → check →
`accept`/`reject`), the checker closing over the static
`CredentialTable` (`user:pass → Identity`, the ADR-005 static
mapping — the check and the mapping are one lookup), then
`read_command` for the re-origination's parsed target, then the
in-band sessions with the mapped identity's id as the token.
Replies are re-encoded to the vanilla client (the RFC codec at
the door boundary only, ADR-003): the CONNECT reply from
`reply_addr()`, the BIND door's reply#1 from `reply1()
.bind_addr`, reply#2 from `await_inbound()`'s plane peer; the
post-reply data planes ride `copy_bidirectional` against the
taken in-band streams. **Never pipes raw post-auth** — the
vanilla client's greeting/command bytes were consumed by the
door's RFC half; the in-band path re-speaks them from the parsed
target (pinned by test: the cred-bearing CONNECT round-trip
succeeding at all is the pin — raw piping would misparse the
command bytes as a greeting).
- **The BIND door**: same listener shape, riding
`BindSession::bind_with_token`; the quiet wait rides the
session's `await_inbound`; reply#1's advertised address passes
through unchanged (the door adds nothing).
- UDP ASSOCIATE through the TCP door: refused RFC-faithfully at
the door (reply code 0x07 — the UDP door's task, not this one).
- **The accept-path defense (ADR-014)**: the idle-first-byte
window (a `peek`-based readability wait before any protocol
work — an accepted connection silent past the window is closed
door-side) and the max-sessions cap (a `Semaphore`; an accept
beyond the bound is refused immediately, no channel opened) as
constructor config. Defaults: 30s / 256.
- **Failure isolation**: per-accept `tokio::spawn`; failures log +
isolate — the accept loop continues (the alktunnels posture).
- **Harness**: `wire_consumer_side_with_provider` added (the
consumer-side topology with the producer's
`ServingConfig.identity_provider` configurable — the CF-005
precedence (1) seam the token sessions ride);
`wire_consumer_side_with` delegates unchanged.
- **Test-provider note**: the test `DoorTokenProvider` resolves
`door-`-prefixed tokens (the tests' credential-table identity ids)
to open-scoped identities (bind-scoped when listed). The token→
identity resolution is producer policy, not wire shape.
## Summary
> To be filled on completion
> Filled on completion (2026-09-25).
Implemented the consumer's TCP front door (feature `local`): the
NoAuth door (the literal raw pipe — accept → channel `BiStream` via
`copy_bidirectional`, zero protocol translation), the cred-bearing
door (RFC 1929 terminates locally through the forked
`accept_password_auth` shape; the static credential→identity table
maps the accepted credential; the SOCKS conversation re-originates
in-band via the token-carrying consumer sessions with the vanilla
replies re-encoded at the door; the password never crosses the
channel as a SOCKS password), and the BIND door (reply#1 fidelity —
the advertised address passes through unchanged; the quiet wait +
reply#2 + the pass-through data plane). The identity mechanism
decided + recorded: the per-credential identity rides the open-op
`auth_token` payload (CF-005 precedence (1)) via the new
`TokenClient` (the client's call connection taken once per door,
shared manager), resolved producer-side through
`ServingConfig.identity_provider`. The ADR-014 accept-path defense
landed as constructor config: the idle-first-byte window (peek-based,
door-side close) + the max-sessions cap (semaphore-refused accepts),
plus per-accept failure isolation (the loop continues). New
consumer-session surface: `TokenClient` + the `*_with_token`
constructor twins + `Socks5ChannelSession::reply_addr` (existing
surfaces untouched). 10 integration tests in `tests/door-tcp.rs`
pin: the NoAuth raw-pipe round-trip (the POC #1 curl shape), the
cred-bearing good/bad-credential paths (the witness sees the mapped
identity id, never the password; a rejection opens no channel), the
never-pipes-raw-post-auth pin, the BIND door's vanilla conversation
(reply#1/reply#2 fidelity + the data plane), the idle timeout, the
session cap, failure isolation, and the regression guards (native
sessions beside a NoAuth door; `bind_with_token` driven directly).
Verification: `cargo test` (375 green across 10 targets) +
`cargo test --all-features` (423 green), `cargo clippy --all-targets
-- -D warnings` clean (default + `--all-features`), `cargo fmt
--check` clean, both wasm checks (`cargo check` + `cargo clippy -D
warnings` on `wasm32-unknown-unknown`) green, and the default
dependency graph stays net-free (the door rides `local` alone).