- 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
13 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| implementation/door-tcp | Front doors: the TCP door (raw-pipe + RFC 1929 cred-bearing) and the BIND door | completed |
|
moderate | medium | component | 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:
src/door.rs(featurelocal), 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 channelBiStream(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'sauth_tokenpayload (CF-005 precedence (1): payloadauth_token→identity_providerresolution) or a per-credentialChannelClient; decide at implementation against the landed alkcall 0.8.0 surface (CallConnection::call_with_payloadcarriesauth_token;ServingConfig.identity_providerresolves it producer-side) and record the choice in Notes. The door checks credentials itself (no forkedAuthMethodimpl 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).serveruns the accept loop; per-connection failures are logged + isolated (the loop continues, matching the alktunnels per-accept isolation posture).
- The NoAuth door (POC #1's raw-pipe shape): bind a real
- 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
cargo testpasses;cargo test --all-featurespassescargo clippy --all-targets -- -D warningsclean;cargo fmt --checkclean- Default wasm checks stay green (door code is
local-gated) - The NoAuth door is a literal raw pipe (zero protocol translation), pinned by test
- 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 (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)
- The idle-first-byte timeout + session cap land as constructor config, pinned by tests
- 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_tokenprecedence)
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_tokenpayload (CF-005 precedence (1)), never a per-credentialChannelClient. Mechanically: the door wraps the client's channel-0CallConnectiononce (TokenClient::wrap, the rawcall_with_payloadsurface, since the token is a payload-level field, ADR-017 §7) and each credential's open rides the payloadauth_token; the producer'sServingConfig.identity_providerresolves 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-credentialChannelClientwould need a full transport connection per credential, which the ssh-Dsingle-socket posture cannot afford. - New consumer surface (
src/consumer.rs):TokenClient(the token-carrying open surface over the takenCallConnection+ shared manager) and the*_with_tokentwins of the three session constructors (Socks5ChannelSession::connect_with_token,BindSession::bind_with_token,AssociateSession::associate_with_token) through aopen_with_token_and_negotiateprologue (the mirror ofopen_and_negotiate: open-with-token → adopt → negotiate → teardown-at-birth on failure).Socks5ChannelSessiongained thereply_addrfield + accessor (the CONNECT reply's BND.ADDR, captured atconnect; the door's vanilla-reply re-encoding rides it). The existing constructor surfaces are untouched. - The door (
src/door.rs, featurelocal):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_authshape locally (negotiate_auth(&[ PasswordAuthentication])→read_username_password→ check →accept/reject), the checker closing over the staticCredentialTable(user:pass → Identity, the ADR-005 static mapping; the check and the mapping are one lookup), thenread_commandfor 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 fromreply_addr(), the BIND door's reply#1 fromreply1() .bind_addr, reply#2 fromawait_inbound()'s plane peer; the post-reply data planes ridecopy_bidirectionalagainst 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'sawait_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 (aSemaphore; 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_provideradded (the consumer-side topology with the producer'sServingConfig.identity_providerconfigurable (the CF-005 precedence (1) seam the token sessions ride);wire_consumer_side_withdelegates unchanged. - Test-provider note: the test
DoorTokenProviderresolvesdoor--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).