- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
245 lines
13 KiB
Markdown
245 lines
13 KiB
Markdown
---
|
|
id: implementation/door-tcp
|
|
name: "Front doors: the TCP door (raw-pipe + RFC 1929 cred-bearing) and the BIND door"
|
|
status: completed
|
|
depends_on: [implementation/local-providers]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
The consumer's TCP front door (feature `local`): the ssh `-D` local
|
|
exposure, a real `socks5://127.0.0.1:port` endpoint that vanilla
|
|
clients (curl, browsers, tun2proxy) point at. Two variants (ADR-005),
|
|
plus the BIND door's reply#2 fidelity. The door is the
|
|
vanilla↔ACL boundary; the RFC codec lives only here.
|
|
|
|
Deliverables:
|
|
|
|
1. **`src/door.rs`** (feature `local`), the door surface per
|
|
consumer.md §The Front Doors + overview.md §Module Map:
|
|
- **The NoAuth door** (POC #1's raw-pipe shape): bind a real
|
|
`TcpListener`; per accepted connection, pipe it raw onto a
|
|
channel `BiStream` (`copy_bidirectional`, **zero protocol
|
|
translation**). The producer runs the whole RFC conversation
|
|
through the pipe; the door adds nothing. This is the
|
|
loopback-only / wrapper-aware shape.
|
|
- **The cred-bearing door** (RFC 1929): the door terminates the
|
|
local RFC conversation itself: `accept_password_auth`-shaped
|
|
(the forked surface: `Socks5ServerProtocol::start` →
|
|
`negotiate_auth(&[PasswordAuthentication])` →
|
|
`read_username_password` → check → `accept`/`reject`), then
|
|
maps the accepted credential through the **static config
|
|
table** (`user:pass →` channel identity/endpoint selection,
|
|
ADR-005) and **re-originates the SOCKS conversation in-band**
|
|
via the consumer sessions (`Socks5ChannelSession::connect` /
|
|
`BindSession::bind`), piping the vanilla client's post-auth
|
|
bytes onto the session's pipe. **Do NOT pipe raw after auth**:
|
|
the vanilla client's command bytes were consumed by the door's
|
|
own RFC 1929 half; the in-band path re-speaks them from the
|
|
parsed target. The SOCKS password never crosses the channel as
|
|
a SOCKS password; the channel identity never reaches the
|
|
vanilla client.
|
|
- **The identity mechanism**: the credential→identity mapping
|
|
yields an alkcall `Identity` (scopes included); the door opens
|
|
the producer channel with it. The landed consumer sessions take
|
|
`&ChannelClient`, so the door's per-credential identity rides the
|
|
open-op call's `auth_token` payload (CF-005 precedence (1):
|
|
payload `auth_token` → `identity_provider` resolution) or a
|
|
per-credential `ChannelClient`; decide at implementation
|
|
against the landed alkcall 0.8.0 surface
|
|
(`CallConnection::call_with_payload` carries `auth_token`;
|
|
`ServingConfig.identity_provider` resolves it producer-side)
|
|
and record the choice in Notes. The door checks credentials
|
|
itself (no forked `AuthMethod` impl needed, per ADR-005).
|
|
- **The BIND door**: same listener shape; a vanilla BIND
|
|
conversation rides `BindSession` (reply#1's advertised address
|
|
passes through to the vanilla client unchanged, because the
|
|
conversation is the client's own; the door adds nothing,
|
|
consumer.md §The BIND door's reply#2 fidelity). The BIND door
|
|
rides the same door variant split (NoAuth raw-pipe vs
|
|
cred-bearing re-origination).
|
|
- **The accept path owns the idle defense** (ADR-014): an
|
|
accepted connection that sends no first byte within a
|
|
configured window is closed door-side (an accepted socket is
|
|
not a channel, so the cap regime does not bound it); plus
|
|
per-credential/per-peer session caps and accept-queue bounds as
|
|
constructor config. Keep the v1 shape simple: an idle-first-byte
|
|
timeout + a max-concurrent-sessions cap are the two knobs.
|
|
- **Door variant selection** is constructor config (ADR-005).
|
|
- Builder shape per consumer.md §Wiring: `FrontDoor::builder()
|
|
.credentials(cred_table).bind("127.0.0.1:1080").build()` →
|
|
`serve(channel_client)` (the NoAuth door omits `.credentials`).
|
|
`serve` runs the accept loop; per-connection failures are
|
|
logged + isolated (the loop continues, matching the alktunnels
|
|
per-accept isolation posture).
|
|
2. **Tests** (`tests/door-tcp.rs`): a vanilla RFC 1928 client (the
|
|
differential-suite raw-client pattern) through the NoAuth door →
|
|
CONNECT round-trip end to end (the POC #1 curl shape); the
|
|
cred-bearing door: good creds → RFC 1929 success → CONNECT
|
|
round-trip; bad creds → RFC 1929 rejection, no channel opened
|
|
(assert via the identity witness / channel count); the BIND door:
|
|
a vanilla BIND conversation through the door (the POC #5 door
|
|
shape); the idle-first-byte timeout closes a silent connection;
|
|
the session cap refuses beyond the bound; per-connection failure
|
|
isolation (one dead client doesn't kill the door).
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] `cargo test` passes; `cargo test --all-features` passes
|
|
- [x] `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt
|
|
--check` clean
|
|
- [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
|
|
- [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)
|
|
- [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 in that the producer's establisher witness
|
|
sees the mapped identity, never the password)
|
|
- [x] The idle-first-byte timeout + session cap land as constructor
|
|
config, pinned by tests
|
|
- [x] Per-connection failure isolation, pinned by test
|
|
|
|
## References
|
|
|
|
- docs/architecture/decisions/005-auth-split-by-door.md (the door
|
|
auth posture), 014-idle-timeout-posture.md (the door's accept-path
|
|
defense), 003 (the RFC codec at doors only)
|
|
- docs/architecture/consumer.md §The Front Doors, §Wiring
|
|
- docs/architecture/overview.md §Module Map (`door.rs`)
|
|
- POC references: `/workspace/alksocks-connect-poc/tests/
|
|
connect_poc.rs` (`front_door_raw_pipe_vanilla_client`), `/workspace/
|
|
alksocks-bind-poc/tests/bind_poc.rs` (`bind_vanilla_front_door`)
|
|
- Landed surfaces: `src/consumer.rs` (the session types),
|
|
`src/rfc1928/server.rs` (`accept_password_auth`, the forked
|
|
cred-bearing shape), alkcall CF-005 (`auth_token` precedence)
|
|
|
|
## Notes
|
|
|
|
> 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, since 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**, since 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, since 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
|
|
|
|
> 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). |