Files
alksocks/tasks/implementation/door-tcp.md
T
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

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
implementation/local-providers
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:

  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

  • cargo test passes; cargo test --all-features passes
  • 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 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_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).