Files
glm-5.3-flash e196ed43e3 docs: remove em dashes from repo prose and forbid them going forward
- 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
2026-11-03 19:31:53 +00:00

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).