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:
1 parent
f3c371310c
commit
b8fd75b9f3
6 files changed
+1905
-18
No files matched your search
@@ -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).
|
||||
Reference in new issue
Block a user