- 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
121 lines
5.9 KiB
Markdown
121 lines
5.9 KiB
Markdown
---
|
|
id: implementation/core-consumer-connect
|
|
name: "Core: consumer half, Socks5ChannelSession (CONNECT) + e2e tests"
|
|
status: completed
|
|
depends_on: [implementation/core-producer-connect]
|
|
scope: narrow
|
|
risk: low
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
Implement the CONNECT consumer session in `src/consumer.rs` per
|
|
consumer.md §The Channels-Native Sessions: `Socks5ChannelSession`,
|
|
the typed client that opens `channels/socks5/sub` and drives the
|
|
client side of the RFC conversation in-stream (POC #1's
|
|
`use_stream` shape).
|
|
|
|
Deliverables (port from `/workspace/alksocks-connect-poc/src/
|
|
consumer.rs`, adapted to the crate's params/error surface):
|
|
|
|
1. **`Socks5ChannelSession::connect(client, target)`**: opens the
|
|
channel via `ChannelClient::open_channel(OP_SOCKS5_OPEN, params,
|
|
SOCKS5_ALPN)` (trivial params, ADR-001), then drives the forked
|
|
client typestate's `use_stream` path over the channel's
|
|
`BiStream` (generic over the backing stream, and the POC #1 fit:
|
|
`Socks5Stream` impls `AsyncRead + AsyncWrite` itself, so the
|
|
session over a `BiStream` is the intended use).
|
|
2. **Target construction** via `ToTargetAddr` (IP-string forms parse
|
|
to IP targets; domain form passes through unresolved, per
|
|
ADR-012).
|
|
3. **Post-reply payload pipe**: after the reply, the session is a
|
|
raw byte pipe. Expose `split()` (read/write halves) and
|
|
`into_inner()` (the underlying `Socks5Stream<BiStream>` for
|
|
advanced use). 0 B framing (CONNECT).
|
|
4. **Teardown ownership** (alktunnels ADR-005 posture, inherited):
|
|
the session owns the channel; on Drop it closes/joins. No
|
|
`Clone` (compile-asserted, since aliasing a session would alias its
|
|
teardown; the alktunnels `compile_fail` doctest shape).
|
|
5. **Error surface**: open failures are the typed
|
|
`SocksOpenError` reasons (ADR-049 §4: a refused session never
|
|
yields a phantom channel); a refused target is an RFC reply code
|
|
surfaced through the session type (consumer.md §Error surface).
|
|
6. **Tests** (end-to-end through the harness): connect → echo
|
|
round-trip (payload bytes both directions); refused dial → the
|
|
RFC reply code surfaced as a typed session error; FORBIDDEN open
|
|
→ typed open error, no session; half-close semantics (dropping
|
|
the write half EOFs the producer's pump cleanly); domain-form
|
|
target passes through unresolved (assert the wire bytes, the
|
|
echo dial ignores the target).
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] `Socks5ChannelSession` with `connect`/`split`/`into_inner`
|
|
per the POC shape
|
|
- [ ] Teardown ownership on Drop; no `Clone` (compile-asserted)
|
|
- [ ] Typed error surface (open errors vs reply-code refusals)
|
|
- [ ] E2E tests through the harness (round-trip, refusals,
|
|
half-close, domain pass-through)
|
|
- [ ] `cargo test` green; clippy/fmt clean; both wasm checks clean
|
|
|
|
## References
|
|
|
|
- docs/architecture/consumer.md §The Channels-Native Sessions
|
|
- docs/architecture/decisions/003-udp-handle-swap.md (the framing
|
|
mirror, where CONNECT is the 0 B case)
|
|
- POC source: `/workspace/alksocks-connect-poc/src/consumer.rs`
|
|
- alktunnels template: `/workspace/@alkdev/alktunnels/src/consumer.rs`
|
|
(the teardown-ownership + no-Clone posture)
|
|
|
|
## Notes
|
|
|
|
> Filled by the implementation agent (2026-09-23):
|
|
|
|
- The session shape is exactly the POC's, adapted to the crate
|
|
surface: `Socks5Stream::use_stream(bidi, None, Config::default())`
|
|
over `BiStream::from_joined(recv, send)`, then `request(TCPConnect,
|
|
target)`. `Config::default()` is the no-auth posture (the
|
|
`skip_auth` knob stays internal to the fork; the identity seam is
|
|
the open op's, CF-005/CF-006; no RFC-facing auth on this path).
|
|
- `split()` returns borrowed halves (`tokio::io::split` over a
|
|
`&mut Socks5Stream<BiStream>`; the `SplitPipe` type alias), not
|
|
owned ones: the session keeps teardown ownership for every shape,
|
|
including the split-driven one. The owned path is `into_inner()`
|
|
(`TakenInner { session, inner }`; the session rides along as the
|
|
teardown owner).
|
|
- Refusal teardown: an in-stream refusal (Rfc error) tears the
|
|
adopted channel down inside `connect` before the error returns
|
|
(teardown-at-birth; the phantom-channel property, tested both
|
|
sides). The producer's own handler-exit teardown races the
|
|
consumer's, and the leak-check probe polls past that race.
|
|
- Drop is the sync-reap posture (`teardown_channel` is sync;
|
|
`Drop` cannot await) + the structural EOF of the dropped pipe; the
|
|
graceful path is `close()` (write-half shutdown first, then reap).
|
|
- Harness: added the consumer-side topology (`wire_consumer_side[_with]`,
|
|
where the consumer dials with a real `ChannelClient` and the producer
|
|
accepts with the real registration) plus `witnessing_echo_dial`
|
|
(the dial-witness target-form probe). The `allow(dead_code)` attr
|
|
rides the alktunnels harness posture (each test binary compiles the
|
|
harness and uses a different subset).
|
|
- No `Clone` is compile-asserted by the doctest; a runtime probe
|
|
pins `Send` (the `Clone` bound itself is the doctest's job).
|
|
|
|
## Summary
|
|
|
|
Implemented `Socks5ChannelSession` (CONNECT) in `src/consumer.rs`:
|
|
`connect`/`channel_id`/`split`/`into_inner`/`close` + `Drop` teardown
|
|
ownership and the `SplitPipe`/`TakenInner` companions, with the
|
|
typed error surface (`SocksOpenError` for opens, the RFC reply code
|
|
through `SocksSessionError::Rfc` for refused targets) and
|
|
teardown-at-birth on the in-stream refusal path. E2E suite
|
|
(`tests/consumer-connect.rs`, 9 tests over the new consumer-side
|
|
harness topology): echo round-trip + CF-006 witness, target forms
|
|
(IP-string parses / domain passes through, per ADR-012 and wire-observed
|
|
at the dial), refused dial → typed RFC error + no phantom channel,
|
|
FORBIDDEN open → typed open error + no session, drop teardown +
|
|
connection health, half-close EOF semantics, `into_inner` pairing,
|
|
and the no-`Clone`/`Send` compile shape (the `compile_fail` doctest
|
|
pins `!Clone`). Verification: `cargo test` (219 incl. the doctest),
|
|
clippy `-D warnings` (all targets), fmt, and both wasm checks clean. |