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
This commit is contained in:
1 parent
9ca8c52601
commit
e196ed43e3
115 files changed
+3593
-3531
No files matched your search
+35
-35
@@ -4,15 +4,15 @@ last_updated: 2026-09-25 (the assembly layer landed + the cross-cutting
|
||||
assembly integration suite; the protocol core landed + the cross-cutting
|
||||
integration suite 2026-09-24; the rfc1928 doc's targeted fork 2026-09-22; the
|
||||
vendor extraction amendment ADR-011 §Amendment 2026-09-21; Phase 1
|
||||
initial draft 2026-09-20 — the OQ-SK ledger promoted into ADRs
|
||||
initial draft 2026-09-20; the OQ-SK ledger promoted into ADRs
|
||||
001-012 and open-questions.md)
|
||||
---
|
||||
|
||||
# alksocks — Architecture
|
||||
# alksocks: Architecture
|
||||
|
||||
A SOCKS5 (RFC 1928) producer/consumer protocol crate on alkcall
|
||||
channels: an arbitrary-egress proxy service that never binds a port
|
||||
unless explicitly configured to — the `alk/socks5`-family
|
||||
unless explicitly configured to. It is the `alk/socks5`-family
|
||||
producer/consumer protocol crate, the sibling of alktty
|
||||
(`alk/tty`) and alktunnels (`alk/tunnel`). Where alktunnels
|
||||
realizes the `-L`/`-R` forwarding flavors, alksocks realizes the
|
||||
@@ -21,34 +21,34 @@ architecture spec docs and the ADRs.
|
||||
|
||||
## Current State
|
||||
|
||||
Phase 1 (Architecture) — the **protocol core and the assembly layer
|
||||
Phase 1 (Architecture): the **protocol core and the assembly layer
|
||||
are implemented**. Phase
|
||||
0 (`docs/research/phase-0.md`) is converged (2026-09-14) and
|
||||
POC-complete (all numbered POCs passed or were dropped with the scope
|
||||
decision); the OQ-SK ledger's Phase 1 inputs are collected and
|
||||
promoted into ADRs 001–014. All docs are `draft` — they transition
|
||||
promoted into ADRs 001–014. All docs are `draft`; they transition
|
||||
to `reviewed` when their open questions resolve and the architecture
|
||||
review passes.
|
||||
|
||||
The landed surface: the RFC 1928 core in `src/rfc1928/` (the targeted
|
||||
fork, ADR-013 — the vendor group's six tasks landed and passed the
|
||||
fork, ADR-013; the vendor group's six tasks landed and passed the
|
||||
extraction review), the ASSOCIATE datagram frame codec (`src/wire.rs`,
|
||||
ADR-003), the open-op params + typed error surface (`src/params.rs` /
|
||||
`src/error.rs`, ADR-001), the three seam types (`src/egress.rs`,
|
||||
ADR-004), the producer half (`src/producer.rs` — the session-gate
|
||||
ADR-004), the producer half (`src/producer.rs`: the session-gate
|
||||
establisher, the command-dispatch pump, the three commands, the scope
|
||||
gates), and the consumer half (`src/consumer.rs` — the
|
||||
gates), and the consumer half (`src/consumer.rs`: the
|
||||
connect/associate/bind session types with teardown ownership). The
|
||||
assembly layer (batch 2) landed with it: the `local` providers
|
||||
(`src/local/` — tcp_dial / udp_egress / bind_accept, the producer-side
|
||||
`ProducerDoor`, ADR-008/009), the front doors (`src/door.rs` — the
|
||||
assembly layer (batch 2) landed with it: the `local` providers in
|
||||
`src/local/` (tcp_dial / udp_egress / bind_accept, the producer-side
|
||||
`ProducerDoor`, ADR-008/009), the front doors in `src/door.rs` (the
|
||||
NoAuth raw-pipe + RFC 1929 cred-bearing TCP door, the BIND door, the
|
||||
UDP door, ADR-005/003/014), and the composed providers
|
||||
(`src/composed/` — the direct-op egress + the bind-first accept over
|
||||
UDP door, ADR-005/003/014), and the composed providers in
|
||||
`src/composed/` (the direct-op egress + the bind-first accept over
|
||||
alktunnels 0.2.0, ADR-007/008). Both cross-cutting integration suites
|
||||
pin the behavior end to end: `tests/core-integration.rs` (the
|
||||
protocol core) and `tests/assembly-integration.rs` (the assembly
|
||||
layer — the full `-D` story through the doors, the composed variant
|
||||
layer: the full `-D` story through the doors, the composed variant
|
||||
set, mixed assembly, the assembly teardown matrix).
|
||||
|
||||
## Documents
|
||||
@@ -56,33 +56,33 @@ set, mixed assembly, the assembly teardown matrix).
|
||||
| Document | Status | Description |
|
||||
|----------|--------|-------------|
|
||||
| [overview.md](overview.md) | draft | Crate purpose, the resource model in brief, dependencies, feature gates, module map |
|
||||
| [wire.md](wire.md) | draft | The wire format: the open op (trivial params — ADR-001), the phase model, the data planes (CONNECT pass-through / ASSOCIATE frames / BIND phases), sentinels |
|
||||
| [wire.md](wire.md) | draft | The wire format: the open op (trivial params, ADR-001), the phase model, the data planes (CONNECT pass-through / ASSOCIATE frames / BIND phases), sentinels |
|
||||
| [producer.md](producer.md) | draft | The producer half: the session-gate establisher, command dispatch, the three seams (dial/egress/accept), scope gates, the local backend |
|
||||
| [consumer.md](consumer.md) | draft | The consumer half: the channels-native sessions (connect/associate/bind), the front doors, door auth (ADR-005), teardown ownership |
|
||||
| [vendor.md](rfc1928.md) | draft | The RFC 1928 core: the targeted fork of fast-socks5's `T`-generic subset (ADR-013; the extraction mechanics in ADR-011) — the fork's scope, the deliberate modifications, the maintenance posture |
|
||||
| [vendor.md](rfc1928.md) | draft | The RFC 1928 core: the targeted fork of fast-socks5's `T`-generic subset (ADR-013; the extraction mechanics in ADR-011), covering the fork's scope, the deliberate modifications, the maintenance posture |
|
||||
| [bast.md](bast.md) | draft | The BAST document for the ASSOCIATE datagram frame (ADR-003); pass-through stages have no binary framing to describe |
|
||||
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-SK-01..09 promoted; new questions as OQ-SK2-06.. — the SK2 numbering is not contiguous from 01, working-draft numbers were consumed) |
|
||||
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-SK-01..09 promoted; new questions as OQ-SK2-06.., where the SK2 numbering is not contiguous from 01 because working-draft numbers were consumed) |
|
||||
|
||||
## ADR Table
|
||||
|
||||
| ADR | Title | Status |
|
||||
|-----|-------|--------|
|
||||
| [001](decisions/001-alpn-and-open-op.md) | ALPN, Open Op, and Trivial Params | Accepted |
|
||||
| [002](decisions/002-protocol-base.md) | Protocol Base — the Vendored fast-socks5 Typestate Subset | Accepted |
|
||||
| [003](decisions/003-udp-handle-swap.md) | The Handle Swap — one Channel per Association, Frames In-Band | Accepted |
|
||||
| [004](decisions/004-dial-callback-shape.md) | The Dial-Callback Shape — Function, not Trait, at Three Seams | Accepted |
|
||||
| [005](decisions/005-auth-split-by-door.md) | Auth Mapping — "Both," Split by the Door | Accepted |
|
||||
| [006](decisions/006-access-control.md) | Access-Control Posture — the Open Gate Plus the Command Gate | Accepted |
|
||||
| [007](decisions/007-bind-in-base.md) | BIND in the Base Crate — the Phase Model Extended | Accepted |
|
||||
| [008](decisions/008-provider-defaults.md) | Egress/Accept Provider Defaults — Local Socket and Composed (Direct Op) | Accepted |
|
||||
| [009](decisions/009-feature-gates.md) | Feature-Gate Layout — `local` and the Composition Boundary | Accepted (amended 2026-09-21) |
|
||||
| [010](decisions/010-channel-capacity.md) | Channel Capacity — Proxy-Workload Recommendation, Not a Wire Change | Accepted |
|
||||
| [011](decisions/011-inline-vendor.md) | the Inline Vendor — fast-socks5's `T`-Generic Subset, In-Tree | Superseded by ADR-013 (extraction mechanics remain authoritative) |
|
||||
| [002](decisions/002-protocol-base.md) | Protocol Base: the Vendored fast-socks5 Typestate Subset | Accepted |
|
||||
| [003](decisions/003-udp-handle-swap.md) | The Handle Swap: one Channel per Association, Frames In-Band | Accepted |
|
||||
| [004](decisions/004-dial-callback-shape.md) | The Dial-Callback Shape: Function, not Trait, at Three Seams | Accepted |
|
||||
| [005](decisions/005-auth-split-by-door.md) | Auth Mapping: "Both," Split by the Door | Accepted |
|
||||
| [006](decisions/006-access-control.md) | Access-Control Posture: the Open Gate Plus the Command Gate | Accepted |
|
||||
| [007](decisions/007-bind-in-base.md) | BIND in the Base Crate: the Phase Model Extended | Accepted |
|
||||
| [008](decisions/008-provider-defaults.md) | Egress/Accept Provider Defaults: Local Socket and Composed (Direct Op) | Accepted |
|
||||
| [009](decisions/009-feature-gates.md) | Feature-Gate Layout: `local` and the Composition Boundary | Accepted (amended 2026-09-21) |
|
||||
| [010](decisions/010-channel-capacity.md) | Channel Capacity: Proxy-Workload Recommendation, Not a Wire Change | Accepted |
|
||||
| [011](decisions/011-inline-vendor.md) | the Inline Vendor: fast-socks5's `T`-Generic Subset, In-Tree | Superseded by ADR-013 (extraction mechanics remain authoritative) |
|
||||
| [012](decisions/012-target-policy.md) | Target Policy and the Virtual-Subnet Translation Layer | Accepted |
|
||||
| [013](decisions/013-targeted-fork.md) | The Targeted Fork — the Extracted Subset Is Owned, Not Vendored | Accepted |
|
||||
| [013](decisions/013-targeted-fork.md) | The Targeted Fork: the Extracted Subset Is Owned, Not Vendored | Accepted |
|
||||
|
||||
Upstream ADRs (alkcall/alktunnels/alknet) are referenced by their own
|
||||
numbers and are NOT ported — the alkcall originals at
|
||||
numbers and are NOT ported; the alkcall originals at
|
||||
`/workspace/@alkdev/alkcall/docs/architecture/decisions/` remain
|
||||
authoritative for the shared surfaces (establishment, channels wire
|
||||
format, `pump_bidi`, identity seam, the channel caps).
|
||||
@@ -98,7 +98,7 @@ format, `pump_bidi`, identity seam, the channel caps).
|
||||
(the handle swap, ADR-003).
|
||||
2. **The RFC conversation is the data plane.** One ALPN, one open
|
||||
op, trivial params (ADR-001); the phase model needs no mode byte
|
||||
— the reply synchronizes the switch; no in-band control
|
||||
(the reply synchronizes the switch); no in-band control
|
||||
vocabulary (the escape hatch is a new ALPN, never a framing
|
||||
change).
|
||||
3. **The seams are functions, not traits** (ADR-004): DialFn /
|
||||
@@ -109,7 +109,7 @@ format, `pump_bidi`, identity seam, the channel caps).
|
||||
seam; the front door = RFC 1929 + the credential→identity static
|
||||
mapping.
|
||||
5. **Two grants, clean separation** (ADR-006): `socks5:open`
|
||||
(egress) and `socks5:bind` (ingress listeners) — never implied by
|
||||
(egress) and `socks5:bind` (ingress listeners), never implied by
|
||||
each other; target policy = the dialer.
|
||||
6. **Backpressure and limits are inherited, not redefined** (the
|
||||
alkcall channels invariants); the one-channel-per-session
|
||||
@@ -117,14 +117,14 @@ format, `pump_bidi`, identity seam, the channel caps).
|
||||
posture is ADR-010 (config, not wire).
|
||||
7. **Wasm-clean default crate** (ADR-009): the default crate is
|
||||
protocol-only; the RFC 1928 core is an extraction with zero
|
||||
kernel-net code — there is no `net` feature (ADR-011 §Amendment);
|
||||
kernel-net code; there is no `net` feature (ADR-011 §Amendment);
|
||||
`local` is opt-in and non-wasm; `composed` rides alktunnels'
|
||||
wasm-clean default (verified with the wasm check at
|
||||
implementation).
|
||||
8. **The RFC 1928 core is owned** (ADR-013, superseding ADR-011's
|
||||
vendor posture): the extracted `T`-generic subset is a targeted
|
||||
fork (`src/rfc1928/`, fork point `5b18d4d`) — project
|
||||
conventions apply unconditionally, the lint-allow/verbatim
|
||||
fork (`src/rfc1928/`, fork point `5b18d4d`); project
|
||||
conventions apply unconditionally, and the lint-allow/verbatim
|
||||
apparatus is dissolved; `/workspace/fast-socks5` stays the
|
||||
differential-testing behavioral reference and the only place the
|
||||
full upstream surface (the kernel-net region) exists; nothing
|
||||
@@ -152,5 +152,5 @@ format, `pump_bidi`, identity seam, the channel caps).
|
||||
- Siblings: alktty (`/workspace/@alkdev/alktty`),
|
||||
alktunnels (`/workspace/@alkdev/alktunnels`)
|
||||
- Reference checkout: `/workspace/fast-socks5` (v1.0.0, MIT,
|
||||
third-party — the differential-testing reference)
|
||||
third-party; the differential-testing reference)
|
||||
- RFC 1928, RFC 1929; tun2proxy (`/workspace/tun2proxy`)
|
||||
+19
-19
@@ -3,11 +3,11 @@ status: draft
|
||||
last_updated: 2026-09-20 (Phase 1 initial draft)
|
||||
---
|
||||
|
||||
# alksocks — BAST Document for the `alk/socks5` Wire Format
|
||||
# alksocks: BAST Document for the `alk/socks5` Wire Format
|
||||
|
||||
This document is the **BAST (Binary Abstract Syntax Tree)**
|
||||
specification for the binary portions of the `alk/socks5` wire
|
||||
format — per the alk* family BAST convention (alktunnels AGENTS.md
|
||||
format, per the alk* family BAST convention (alktunnels AGENTS.md
|
||||
convention 12, carried from alktty; alktunnels `bast.md` is the
|
||||
precedent), the mandatory ASSOCIATE
|
||||
datagram framing ([ADR-003](decisions/003-udp-handle-swap.md)) is
|
||||
@@ -28,36 +28,36 @@ feeds it.
|
||||
## Scope
|
||||
|
||||
The `alk/socks5` wire format is: a trivial JSON open op on the
|
||||
channels call plane (a UTF-8 JSON object — `{}`; specified in
|
||||
channels call plane (a UTF-8 JSON object, `{}`; specified in
|
||||
[wire.md](wire.md) §The Open Op and
|
||||
[ADR-001](decisions/001-alpn-and-open-op.md)), and a data plane
|
||||
inside the channel `BiStream` that is *mostly* RFC 1928 wire rules
|
||||
plus raw pass-through.
|
||||
|
||||
Per the BAST format spec, a BAST document describes **binary data
|
||||
layouts** — not JSON payloads. The RFC 1928 conversation itself
|
||||
layouts**, not JSON payloads. The RFC 1928 conversation itself
|
||||
(negotiation, command, replies, the RFC UDP datagram header at front
|
||||
doors) is specified by RFC 1928 and implemented by the forked
|
||||
RFC 1928 codecs ([rfc1928.md](rfc1928.md)) — it is not re-specified
|
||||
RFC 1928 codecs ([rfc1928.md](rfc1928.md)); it is not re-specified
|
||||
here. This document covers exactly the alksocks-specific binary
|
||||
framing:
|
||||
|
||||
1. **The ASSOCIATE datagram frame** (`[len: u16 BE][addr + data]`) —
|
||||
1. **The ASSOCIATE datagram frame** (`[len: u16 BE][addr + data]`),
|
||||
the one framed stage (ADR-003). CONNECT's pass-through and BIND's
|
||||
pass-through have no framing for BAST to describe.
|
||||
|
||||
What this BAST deliberately does **not** cover:
|
||||
|
||||
- **The RFC 1928 wire rules** — method negotiation, command/reply
|
||||
- **The RFC 1928 wire rules**: method negotiation, command/reply
|
||||
encoding, the `addr` sub-layout (ATYP + addr + port), the RFC UDP
|
||||
datagram header (RSV/FRAG/ATYP/addr/port, front doors only). The
|
||||
RFC is normative; the forked codecs implement it.
|
||||
- **The open-op params JSON shape** (`{}`) — UTF-8 JSON on the call
|
||||
- **The open-op params JSON shape** (`{}`): UTF-8 JSON on the call
|
||||
plane; ADR-001.
|
||||
- **The channels 8-byte chunk header** — the channels layer's own
|
||||
- **The channels 8-byte chunk header**: the channels layer's own
|
||||
framing (alknet ADR-071/093); stripped transparently before the
|
||||
alksocks data plane sees bytes. Specified upstream, not here.
|
||||
- **The EOF sentinel** — the channels-level zero-length chunk
|
||||
- **The EOF sentinel**: the channels-level zero-length chunk
|
||||
(upstream); the alksocks codecs never emit it (the F-2 invariant).
|
||||
|
||||
## The BAST document
|
||||
@@ -81,13 +81,13 @@ What this BAST deliberately does **not** cover:
|
||||
}
|
||||
```
|
||||
|
||||
(Note: `addr + data` together are exactly `len` bytes — the two
|
||||
(Note: `addr + data` together are exactly `len` bytes; the two
|
||||
`encoding: length-prefixed` fields are the *decoder's* internal
|
||||
split of one frame's payload; on the wire `len` covers both, and
|
||||
`data`'s length is `len − len(addr)`, decodable because the ATYP
|
||||
determines the address length. The two-field split documents the
|
||||
decoder's contract, not extra wire bytes. **Joint constraint:** a
|
||||
frame is legal only when `len(addr) + len(data) ≤ 65535` — the
|
||||
frame is legal only when `len(addr) + len(data) ≤ 65535`; the
|
||||
per-field maxima are not independent (a 65507-byte datagram to a
|
||||
255-char domain would need 65766 frame bytes and must be rejected
|
||||
`Oversize` at frame time; wire.md §The Data Plane carries the
|
||||
@@ -95,7 +95,7 @@ arithmetic).)
|
||||
|
||||
## Annotations
|
||||
|
||||
### `AssociateFrame` — binary, exact
|
||||
### `AssociateFrame`: binary, exact
|
||||
|
||||
Maps 1:1 to the on-wire bytes: `len` as `uint16` big-endian (2
|
||||
bytes), then exactly `len` bytes = `addr + data`. The `addr`
|
||||
@@ -109,10 +109,10 @@ One frame per datagram, in both directions of the channel
|
||||
`BiStream`, for the whole datagram stage (association lifetime =
|
||||
channel lifetime). Frames are self-delimiting on the byte stream; a
|
||||
decoder is an incremental state machine across chunk boundaries
|
||||
(boundary preservation is mandatory — an empty datagram is a legal
|
||||
(boundary preservation is mandatory; an empty datagram is a legal
|
||||
zero-length payload, never EOF; alktunnels F-2's invariant, met
|
||||
structurally and stated at payload level: `len == 0` — zero inner
|
||||
bytes — is unreachable from a compliant peer, since `addr` is always
|
||||
structurally and stated at payload level: `len == 0`, zero inner
|
||||
bytes, is unreachable from a compliant peer, since `addr` is always
|
||||
≥ 7 bytes).
|
||||
|
||||
### Layering note
|
||||
@@ -120,7 +120,7 @@ bytes — is unreachable from a compliant peer, since `addr` is always
|
||||
The frame codec sits in the wire layer, applied **exactly once**:
|
||||
`Egress::next_reply` returns *inner* bytes (`addr + data`); the
|
||||
producer's frame writer adds the single length prefix (ADR-003's
|
||||
framing-once invariant — the double-framing bug class POC #2
|
||||
framing-once invariant; the double-framing bug class POC #2
|
||||
caught). The consumer side mirrors it: the session's send frames
|
||||
once, its reader state persists across receives. The BAST describes
|
||||
the wire inside the `BiStream` only; `pump_bidi` (ADR-050) is
|
||||
@@ -130,10 +130,10 @@ upstream's, not re-specified here.
|
||||
|
||||
- [ADR-003](decisions/003-udp-handle-swap.md) (the handle swap + the
|
||||
frame), [ADR-001](decisions/001-alpn-and-open-op.md) (the JSON
|
||||
open-op shape — outside BAST scope),
|
||||
open-op shape, outside BAST scope),
|
||||
[ADR-011](decisions/011-inline-vendor.md) (the forked codecs)
|
||||
- [wire.md](wire.md) (the normative prose; §The Data Plane)
|
||||
- alktunnels `bast.md` (the BAST precedent — same meta-schema, same
|
||||
- alktunnels `bast.md` (the BAST precedent: same meta-schema, same
|
||||
"binary framing only" scope rule; the length codec's original
|
||||
document) and alktunnels ADR-003 (the codec alksocks reuses)
|
||||
- POC #2 `poc-udp-associate-findings.md` (the frame's validation
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-25 (the cred-bearing door's call-connection
|
||||
contract reconciled — TokenClient's door-only posture, one command per
|
||||
accepted connection; the BIND spent posture reconciled — the
|
||||
contract reconciled: TokenClient's door-only posture, one command per
|
||||
accepted connection; the BIND spent posture reconciled: the
|
||||
take-guarded reply#2 reader, review 002 C-1/D-1; the landed session
|
||||
signatures reconciled 2026-09-24; Phase 1 initial draft 2026-09-20)
|
||||
---
|
||||
|
||||
# alksocks — The Consumer Half
|
||||
# alksocks: The Consumer Half
|
||||
|
||||
The consumer opens SOCKS5 channels against a produced service and
|
||||
speaks RFC 1928 inside them. Two consumption shapes, structurally
|
||||
separate (the Phase 0 scope resolution):
|
||||
|
||||
1. **The channels-native session** — a typed client that opens
|
||||
1. **The channels-native session**: a typed client that opens
|
||||
`channels/socks5/sub` and drives the RFC conversation in-stream
|
||||
(wrapper-aware; the POC #1 `use_stream` / POC #2 associate
|
||||
session shapes).
|
||||
2. **The front door** (optional, feature `local`) — a real local
|
||||
2. **The front door** (optional, feature `local`): a real local
|
||||
endpoint (TCP for CONNECT/BIND; UDP for ASSOCIATE) that vanilla
|
||||
clients point at; the door bridges each accepted
|
||||
connection/socket onto a channel, with the RFC codec living only
|
||||
@@ -33,35 +33,35 @@ Opens `channels/socks5/sub` via `ChannelClient::open_channel`
|
||||
(alkcall ADR-037/043), then drives the client side of the RFC
|
||||
conversation in-stream over the channel's `BiStream`:
|
||||
|
||||
- the forked client typestate's `use_stream` path — generic over
|
||||
- the forked client typestate's `use_stream` path, generic over
|
||||
the backing stream (POC #1: `Socks5Stream` impls
|
||||
`AsyncRead + AsyncWrite` itself, so the session over a `BiStream`
|
||||
is the intended use, not a hack);
|
||||
- target construction via `ToTargetAddr` (IP-string forms parse to
|
||||
IP targets; domain form passes through unresolved — ADR-012);
|
||||
- after the reply, the session is a raw byte pipe — the consumer
|
||||
IP targets; domain form passes through unresolved, ADR-012);
|
||||
- after the reply, the session is a raw byte pipe: the consumer
|
||||
reads/writes payload bytes directly (0 B framing, CONNECT).
|
||||
|
||||
### The associate session
|
||||
|
||||
The UDP ASSOCIATE client (POC #2's shape):
|
||||
|
||||
- opens the channel, negotiates, sends ASSOCIATE, reads the reply —
|
||||
- opens the channel, negotiates, sends ASSOCIATE, reads the reply;
|
||||
**receives the sentinel `BND.ADDR` and never uses it for
|
||||
addressing** (the handle is the channel; ADR-003);
|
||||
- `send_datagram(dst, bytes)` frames once
|
||||
(`[len: u16 BE][addr + data]`, the RFC 1928 address encoding) and
|
||||
writes the frame;
|
||||
- `recv_datagram()` returns `Some((source_addr, bytes))` per
|
||||
datagram — its incremental reader state (mid-frame position +
|
||||
datagram; its incremental reader state (mid-frame position +
|
||||
batched-datagram pending queue) **persists across calls** (a
|
||||
datagram split across channel chunks survives a caller-driven
|
||||
receive loop; POC #2's finding);
|
||||
- `shutdown()` half-closes the stream — the mux's implicit-EOF
|
||||
- `shutdown()` half-closes the stream; the mux's implicit-EOF
|
||||
sentinel reaches the producer's stage as EOF and the association
|
||||
ends (association lifetime = channel lifetime);
|
||||
- **teardown ownership**: the session owns the channel; on Drop it
|
||||
closes/joins — the alktunnels ADR-005 posture (the consumer
|
||||
closes/joins (the alktunnels ADR-005 posture: the consumer
|
||||
session type owns teardown), combined with ADR-004's
|
||||
ownership-rides-the-handle contract.
|
||||
|
||||
@@ -72,16 +72,16 @@ client `request()` reads reply#1; reply#2 is hand-read with the
|
||||
forked public `read_address` codec (reply#1's codec applied again;
|
||||
no second codec). The session then rides the pass-through data
|
||||
plane. The reply#2 phase is take-guarded: the one normative reader
|
||||
is `await_inbound` — the first read consumes the session's stream
|
||||
is `await_inbound`, and the first read consumes the session's stream
|
||||
(the returned plane owns it; the session stays the teardown owner),
|
||||
so a second call returns the family's spent shape (`Ok(None)`, the
|
||||
same posture the sibling sessions pin) and cannot re-read the live
|
||||
data plane; a failed read restores the inner (the producer ends the
|
||||
session on every post-reply#1 failure exit, so a re-call EOFs —
|
||||
session on every post-reply#1 failure exit, so a re-call EOFs;
|
||||
never a live data-plane re-read either way). The
|
||||
`reply1()` handle is a view (the advertised BND.ADDR + the raw
|
||||
pre-reply#2 stream), not a reader. The BIND client is a
|
||||
wrapper-aware-consumer convenience — vanilla clients reach BIND
|
||||
wrapper-aware-consumer convenience; vanilla clients reach BIND
|
||||
through the front door instead.
|
||||
|
||||
### Error surface
|
||||
@@ -94,18 +94,18 @@ code surfaced through the session type.
|
||||
|
||||
## The Front Doors (feature `local`, optional)
|
||||
|
||||
The consumer's optional local exposure — the ssh `-D` shape (POC #1:
|
||||
The consumer's optional local exposure, the ssh `-D` shape (POC #1:
|
||||
curl end-to-end; POC #2 test 8: the UDP door; POC #5 test 8: the
|
||||
BIND door):
|
||||
|
||||
- **The CONNECT/BIND door**: bind a real TCP listener; for each
|
||||
accepted connection, pipe it raw onto a channel `BiStream`
|
||||
(`copy_bidirectional`, **zero protocol translation** — POC #1's
|
||||
(`copy_bidirectional`, **zero protocol translation**, POC #1's
|
||||
literal 0 B pass-through conclusion). The door's credential half
|
||||
(below) is the only RFC work it does.
|
||||
- **The UDP door**: bind a real UDP socket; decode RFC-header'd
|
||||
datagrams (`parse_udp_request`), send `{addr, data}` frames
|
||||
in-band, return replies RFC-header'd (`new_udp_header`) — the RFC
|
||||
in-band, return replies RFC-header'd (`new_udp_header`): the RFC
|
||||
codec at the door boundary only (ADR-003). FRAG ≠ 0 → refuse (the
|
||||
v1 posture).
|
||||
- **The BIND door's reply#2 fidelity**: the local variant's real
|
||||
@@ -118,19 +118,19 @@ BIND door):
|
||||
translation; for loopback-only or wrapper-aware use.
|
||||
- **The cred-bearing door** (RFC 1929): a vanilla client carrying
|
||||
`user:pass` gets a real RFC 1929 conversation at the door; the
|
||||
door maps the accepted credential to *local* configuration —
|
||||
which channel identity/endpoint to open the producer channel
|
||||
with — via a **static config table** (ADR-005: `user:pass →`
|
||||
door maps the accepted credential to *local* configuration (which
|
||||
channel identity/endpoint to open the producer channel with) via
|
||||
a **static config table** (ADR-005: `user:pass →`
|
||||
channel identity/endpoint selection). The SOCKS password never
|
||||
crosses the channel as a SOCKS password; the channel identity
|
||||
never reaches the vanilla client. The door checks credentials
|
||||
itself (no forked `AuthMethod` impl needed — the door owns the
|
||||
itself (no forked `AuthMethod` impl needed; the door owns the
|
||||
accept loop). The mapped identity rides the open-op call's
|
||||
`auth_token` payload (CF-005 precedence (1)) via
|
||||
`TokenClient::wrap` — the client's channel-0 `CallConnection` is
|
||||
`TokenClient::wrap`; the client's channel-0 `CallConnection` is
|
||||
taken once per door (the door-only contract: a cred-bearing
|
||||
door's client is a door client; other shapes — the UDP door's
|
||||
association, channels-native sessions — ride their own
|
||||
door's client is a door client; other shapes, such as the UDP door's
|
||||
association and channels-native sessions, ride their own
|
||||
connections to the same producer registration, one registration,
|
||||
many connections). The vanilla conversation is
|
||||
**one command per accepted connection** (the door's RFC half
|
||||
@@ -159,10 +159,10 @@ door.serve(channel_client).await?;
|
||||
```
|
||||
|
||||
The landed session signatures: the constructors take `&ChannelClient`
|
||||
(shared — many sessions ride one client, the concurrency posture);
|
||||
(shared; many sessions ride one client, the concurrency posture);
|
||||
`send_datagram` takes `impl ToTargetAddr` (the CONNECT session's
|
||||
target-construction posture, ADR-012); `recv_datagram()` returns
|
||||
`Result<Option<(TargetAddr, Bytes)>, SocksSessionError>` — `None` is
|
||||
`Result<Option<(TargetAddr, Bytes)>, SocksSessionError>`; `None` is
|
||||
the channel EOF (the association's clean end), never an empty
|
||||
datagram (F-2: an empty payload is a legal datagram).
|
||||
|
||||
@@ -185,12 +185,12 @@ None currently blocking this document.
|
||||
|
||||
## References
|
||||
|
||||
- POC #1 (`poc-connect-wasm-findings.md` — the raw-pipe door, the
|
||||
`use_stream` fit), POC #2 (`poc-udp-associate-findings.md` — the
|
||||
- POC #1 (`poc-connect-wasm-findings.md`: the raw-pipe door, the
|
||||
`use_stream` fit), POC #2 (`poc-udp-associate-findings.md`: the
|
||||
associate session, the reader-state finding, the UDP door), POC #5
|
||||
(`poc-bind-findings.md` — the reply#2 reader, the BIND door)
|
||||
(`poc-bind-findings.md`: the reply#2 reader, the BIND door)
|
||||
- Upstream: alkcall ADR-037/043 (channel open), **alktunnels
|
||||
ADR-005** (consumer session owns teardown — the posture
|
||||
ADR-005** (consumer session owns teardown; the posture
|
||||
inherited), the CF-005/CF-006 seam
|
||||
- [producer.md](producer.md) (the other half),
|
||||
[wire.md](wire.md) (the normative wire)
|
||||
@@ -9,10 +9,10 @@ Accepted (2026-09-20, Phase 1)
|
||||
The crate rides alkcall channels: a produced SOCKS5 resource is an
|
||||
openable ALPN-op, opened by consumers via `ChannelClient` (alkcall
|
||||
ADR-037/043). The ALPN string is wire-stable once published
|
||||
(AGENTS.md conventions 5/16 — decide via ADR before the first
|
||||
(AGENTS.md conventions 5/16: decide via ADR before the first
|
||||
consumer). Phase 0's POCs (#1, #2, #5) all ran one ALPN
|
||||
(`alk/socks5`), one open op (`channels/socks5/sub`), with trivially
|
||||
empty params — the natural reading of "the RFC conversation is the
|
||||
empty params, the natural reading of "the RFC conversation is the
|
||||
data plane": the SOCKS5 target is unknown at open time (it arrives
|
||||
in-stream post-negotiation), so there is nothing for params to carry
|
||||
(`poc-connect-wasm-findings.md` §Wire/API notes).
|
||||
@@ -20,26 +20,26 @@ in-stream post-negotiation), so there is nothing for params to carry
|
||||
## Decision
|
||||
|
||||
- **ALPN: `alk/socks5`** (provisional → stable on first publish;
|
||||
the alk/ prefix per the alkcall ADR-004 ALPN-string convention —
|
||||
the alk/ prefix per the alkcall ADR-004 ALPN-string convention;
|
||||
AGENTS.md convention 16's "ADR-004/ADR-006" shorthand cites the
|
||||
naming family, of which ADR-004 is the operative one). One ALPN
|
||||
per protocol.
|
||||
- **One open op: `channels/socks5/sub`** — the SOCKS5 session op,
|
||||
- **One open op: `channels/socks5/sub`**: the SOCKS5 session op,
|
||||
flavor-form like alktunnels' (`…/sub`; alkcall ADR-047: openable
|
||||
ALPNs are operations). All three RFC 1928 commands (CONNECT,
|
||||
BIND, UDP ASSOCIATE) ride this one op, selected in-stream at
|
||||
command dispatch — the command is an RFC-1928 wire fact, not a
|
||||
wire-format choice of ours.
|
||||
- **Params: the trivial object** `{}` — schema `{type: object}`,
|
||||
additionalProperties false. Nothing else is needed at open time
|
||||
(the establisher is a session gate, not a dial — the target is
|
||||
command dispatch (the command is an RFC-1928 wire fact, not a
|
||||
wire-format choice of ours).
|
||||
- **Params: the trivial object** `{}` (schema `{type: object}`,
|
||||
additionalProperties false). Nothing else is needed at open time
|
||||
(the establisher is a session gate, not a dial; the target is
|
||||
unknown). Extensibility: if a later need surfaces per-session
|
||||
options (e.g. a requested local bind hint), that is an
|
||||
**additive** params schema change while no params were ever
|
||||
required; consumers that send `{}` stay valid. Params are a
|
||||
self-contained JSON object identifying the produced resource
|
||||
(alkcall ADR-039's shape, following alktunnels ADR-001's template).
|
||||
- **Reply: `{"channel_id": <u32>}`** — the Sub-typed operation
|
||||
- **Reply: `{"channel_id": <u32>}`**: the Sub-typed operation
|
||||
reply, as with alktunnels. No extra reply fields in v1 (the
|
||||
establishment phase is a session gate and has nothing to project;
|
||||
the alkcall 0.8.0 `with_reply_field` seam remains available
|
||||
@@ -49,12 +49,12 @@ in-stream post-negotiation), so there is nothing for params to carry
|
||||
|
||||
- The wire surface is minimal and stable: one ALPN + one op + empty
|
||||
params + one reply field. The one-way-door surface is the ALPN
|
||||
string, the op id, and the (trivial) params shape — all frozen at
|
||||
string, the op id, and the (trivial) params shape, all frozen at
|
||||
first publish.
|
||||
- Command-level differentiation (CONNECT vs ASSOCIATE vs BIND)
|
||||
lives in the RFC 1928 stream and in the ACL scope check at
|
||||
command dispatch (see ADR-006), not in the op set. No
|
||||
per-command ops — that would re-encode the RFC's own command byte
|
||||
per-command ops: that would re-encode the RFC's own command byte
|
||||
in our wire format for no benefit.
|
||||
- POC-validated: all three POCs served CONNECT/ASSOCIATE/BIND on
|
||||
the same registration (`connect_still_served_on_the_same_
|
||||
@@ -62,7 +62,7 @@ in-stream post-negotiation), so there is nothing for params to carry
|
||||
|
||||
## References
|
||||
|
||||
- OQ-SK-01 (params context), OQ-SK-03 (the handle swap — why no
|
||||
- OQ-SK-01 (params context), OQ-SK-03 (the handle swap, why no
|
||||
correlation params), OQ-SK-08 (BIND rides the same op)
|
||||
- POC #1 `poc-connect-wasm-findings.md` (the trivial-params finding),
|
||||
POC #2 `poc-udp-associate-findings.md` (one-op regression guard),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# ADR-002: Protocol Base — the Vendored fast-socks5 Typestate Subset
|
||||
# ADR-002: Protocol Base: the Vendored fast-socks5 Typestate Subset
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; the extracted-and-forked posture is
|
||||
OQ-SK-04's resolution — the extraction mechanics in
|
||||
OQ-SK-04's resolution: the extraction mechanics in
|
||||
[ADR-011](011-inline-vendor.md), the ownership posture in
|
||||
[ADR-013](013-targeted-fork.md))
|
||||
|
||||
@@ -13,7 +13,7 @@ Phase 0 evaluated the protocol-base candidates: fast-socks5 (the
|
||||
full-fidelity RFC 1928 implementation, explicit typestate server API,
|
||||
generic over `T`), iroh-socks5 (structurally the closest sibling, but
|
||||
iroh-coupled and an RFC subset), and a hand-rolled codec (rejected as
|
||||
anti-prior-art — alknet's client hand-rolls and it is the known
|
||||
anti-prior-art; alknet's client hand-rolls, and it is the known
|
||||
weakness). POC #1 confirmed the load-bearing property: fast-socks5's
|
||||
`Socks5ServerProtocol<T, states::*>` accepts a channels `BiStream`
|
||||
directly (`T: AsyncRead + AsyncWrite + Unpin`), and POC #4 measured
|
||||
@@ -24,13 +24,13 @@ functionally complete (a full RFC 1928 CONNECT conversation passes
|
||||
## Decision
|
||||
|
||||
- **The protocol layer is built on fast-socks5's explicit typestate
|
||||
API** — `Socks5ServerProtocol<T, …>` (negotiate/finish_auth/
|
||||
API**: `Socks5ServerProtocol<T, …>` (negotiate/finish_auth/
|
||||
read_command/reply_success/reply_error), the client side
|
||||
`Socks5Stream<S>::use_stream` / the associate session, and the
|
||||
pure codecs (`new_udp_header`/`parse_udp_request`).
|
||||
- **Only the explicit API is used.** The legacy
|
||||
`Socks5Server`/`Socks5Socket`/`Incoming` bind-based API is
|
||||
deprecated upstream and is out — never forked into the default
|
||||
deprecated upstream and is out; never forked into the default
|
||||
path.
|
||||
- **Base = v1.0.0, extracted in-tree and forked** (ADR-011 for the
|
||||
extraction mechanics and the two carried changes; ADR-013 for the
|
||||
@@ -38,7 +38,7 @@ functionally complete (a full RFC 1928 CONNECT conversation passes
|
||||
`/workspace/fast-socks5` remains the read-only reference for
|
||||
differential testing of RFC edge cases; the fork point is noted
|
||||
in the module docs and the bundled license notice.
|
||||
- **Refusals map faithfully per RFC 1928** — `ReplyError` codes
|
||||
- **Refusals map faithfully per RFC 1928**: `ReplyError` codes
|
||||
(`as_u8`/`from_u8`) are the error vocabulary at the command plane;
|
||||
refusals are never collapsed into generic errors (AGENTS.md
|
||||
convention 2). POC #1 validated the mapping (dial refusal → 0x05).
|
||||
@@ -46,11 +46,11 @@ functionally complete (a full RFC 1928 CONNECT conversation passes
|
||||
## Consequences
|
||||
|
||||
- The crate owns its protocol code in-tree (the fork), so BIND and
|
||||
the `new_reply` publication ride the same tree — no upstream waits.
|
||||
the `new_reply` publication ride the same tree; no upstream waits.
|
||||
- The typestate's interception points are the layering seams:
|
||||
command-read (the dial/accept dispatch), `transfer` (the raw
|
||||
two-pump), and — where the default handlers are unusable (UDP
|
||||
ASSOCIATE's unconditional bind) — direct typestate driving. The
|
||||
two-pump), and, where the default handlers are unusable (UDP
|
||||
ASSOCIATE's unconditional bind), direct typestate driving. The
|
||||
interception-point map is spec'd in [producer.md](../producer.md).
|
||||
- iroh-socks5's design ideas (the `{addr, data}` frame, control-EOF
|
||||
association lifetime, FRAG≠0 refusal) were adopted at the design
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR-003: The Handle Swap — one Channel per Association, Frames In-Band
|
||||
# ADR-003: The Handle Swap: one Channel per Association, Frames In-Band
|
||||
|
||||
## Status
|
||||
|
||||
@@ -9,12 +9,12 @@ validated end to end 2026-09-14)
|
||||
|
||||
UDP ASSOCIATE is the hard case for the no-bind requirement: RFC 1928
|
||||
replies with a UDP relay address (`BND.ADDR`/`BND.PORT`) and the
|
||||
client sends datagrams to it — which seems to demand a producer-side
|
||||
client sends datagrams to it, which seems to demand a producer-side
|
||||
kernel bind, the one thing the protocol layer must never do.
|
||||
|
||||
The design's load-bearing reading of RFC 1928 (OQ-SK-03, settled
|
||||
2026-09-13): the ASSOCIATE reply's `BND.ADDR`/`BND.PORT` is *just the
|
||||
handle for the relay*, and one relay serves the whole association —
|
||||
handle for the relay*, and one relay serves the whole association:
|
||||
the per-datagram destination is carried in the datagram header
|
||||
(ATYP/addr/port), not in the handle. The relay port was never "a port
|
||||
that goes to one place"; it is a multiplexing endpoint. Swapping the
|
||||
@@ -26,27 +26,27 @@ including the front door.
|
||||
|
||||
The in-band (channels-native) UDP ASSOCIATE shape is:
|
||||
|
||||
- **One channel per association** — the CONNECT shape; no second
|
||||
- **One channel per association**: the CONNECT shape; no second
|
||||
channel, no correlation params (the op's params stay trivial,
|
||||
ADR-001).
|
||||
- **Per-datagram destinations ride in-band frames:** every datagram
|
||||
is `[len: u16 BE][addr + data]` — the boundary-preserving
|
||||
is `[len: u16 BE][addr + data]`, the boundary-preserving
|
||||
length codec shaped on alktunnels' (`frame_datagram`/
|
||||
`DatagramReader`, re-implemented in first-party `src/wire.rs`;
|
||||
behavior pinned by the same F-2 tests)
|
||||
over the RFC address encoding (`TargetAddr::to_be_bytes()` — the
|
||||
over the RFC address encoding (`TargetAddr::to_be_bytes()`, the
|
||||
RFC UDP header minus RSV/FRAG). The RFC header codec
|
||||
(`new_udp_header`/`parse_udp_request`) exists only at vanilla
|
||||
front-door boundaries. RSV/FRAG do not ride the tunnel wire —
|
||||
front-door boundaries. RSV/FRAG do not ride the tunnel wire;
|
||||
fragmentation is refused at front doors (FRAG ≠ 0 → refuse, the
|
||||
iroh-socks5 v1 posture; it cannot even be expressed in-band).
|
||||
- **Association lifetime = channel lifetime** — channel EOF is the
|
||||
- **Association lifetime = channel lifetime**: channel EOF is the
|
||||
end-of-life signal; no extra control vocabulary (POC #2 confirmed
|
||||
post-reply garbage is refused structurally: the frame's first byte
|
||||
is ATYP, so any post-reply control byte parses as a garbage ATYP
|
||||
and fails the association loud).
|
||||
- **The reply's `BND.ADDR`/`BND.PORT` on the in-band path are pure
|
||||
RFC-compatibility surface** — the sentinel `127.0.0.1:0` (POC #2's
|
||||
RFC-compatibility surface** (the sentinel `127.0.0.1:0`, POC #2's
|
||||
shape). Only a front door with a real socket reports a real
|
||||
address.
|
||||
- **Producer egress is the `EgressFactory` seam** (ADR-004, the
|
||||
@@ -55,7 +55,7 @@ The in-band (channels-native) UDP ASSOCIATE shape is:
|
||||
via the alktunnels direct op) are policy, not protocol.
|
||||
- **The datagram stage is `select!`-driven** with the egress
|
||||
readiness contract (next_reply is `Pending` when nothing is
|
||||
ready — an instantly-ready `Ok(None)` starves a biased select into
|
||||
ready; an instantly-ready `Ok(None)` starves a biased select into
|
||||
livelock; POC #2's readiness findings are load-bearing API design
|
||||
for the crate). Framing is applied exactly once, by the wire
|
||||
layer: `Egress::next_reply` returns *inner* bytes; the producer's
|
||||
@@ -64,7 +64,7 @@ The in-band (channels-native) UDP ASSOCIATE shape is:
|
||||
reader state persists.** The associate session's
|
||||
`send_datagram` frames once; its `recv_datagram`'s incremental
|
||||
reader state (mid-frame position + batched-datagram pending
|
||||
queue) persists across calls — a datagram split across channel
|
||||
queue) persists across calls; a datagram split across channel
|
||||
chunks survives a caller-driven receive loop (POC #2: the first
|
||||
cut dropped the reader state per call; the 2-byte-prefix chunk
|
||||
test exposed it). This is part of the datagram stage's
|
||||
@@ -76,22 +76,22 @@ The in-band (channels-native) UDP ASSOCIATE shape is:
|
||||
BIND need no socket at all; ASSOCIATE's "bind" is virtualized as
|
||||
the channel (the handle swap). Local binds remain optional
|
||||
assembly-layer shapes (front doors; the local egress variant).
|
||||
- The datagram framing is **wire-stable once published** — the
|
||||
- The datagram framing is **wire-stable once published**; the
|
||||
one-way door is now closed with empirical ground under it (open
|
||||
cost ~0.8 ms per composed destination, measured; LRU + dedup
|
||||
pinned by test).
|
||||
- Boundary preservation is mandatory (empty datagram ≠ EOF —
|
||||
- Boundary preservation is mandatory (empty datagram ≠ EOF;
|
||||
alktunnels F-2's lesson transfers directly; the frame shape meets
|
||||
it structurally). The BAST document for the frame is
|
||||
[bast.md](../bast.md).
|
||||
|
||||
## References
|
||||
|
||||
- OQ-SK-03 (the full reasoning trail), OQ-SK-09 (fan-out bound —
|
||||
- OQ-SK-03 (the full reasoning trail), OQ-SK-09 (fan-out bound;
|
||||
see ADR-010)
|
||||
- POC #2 `poc-udp-associate-findings.md` (the validation record +
|
||||
the readiness/framing/ownership findings)
|
||||
- alktunnels ADR-003 (the length codec + F-2), alktunnels
|
||||
ADR-007 (the direct op — the composed egress vehicle)
|
||||
- iroh-socks5 eval (`docs/research/iroh-socks5-eval.md` — the frame
|
||||
ADR-007 (the direct op, the composed egress vehicle)
|
||||
- iroh-socks5 eval (`docs/research/iroh-socks5-eval.md`, the frame
|
||||
design prior art)
|
||||
@@ -1,9 +1,9 @@
|
||||
# ADR-004: The Dial-Callback Shape — Function, not Trait, at Three Seams
|
||||
# ADR-004: The Dial-Callback Shape: Function, not Trait, at Three Seams
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; shape resolved empirically — POC #1
|
||||
(2026-09-13), POC #2 (2026-09-14), POC #5 (2026-09-15))
|
||||
Accepted (2026-09-20, Phase 1; shape resolved empirically, with
|
||||
POC #1 (2026-09-13), POC #2 (2026-09-14), POC #5 (2026-09-15))
|
||||
|
||||
## Context
|
||||
|
||||
@@ -14,23 +14,23 @@ inversion point for the three I/O planes a SOCKS5 server touches:
|
||||
the CONNECT dial, the ASSOCIATE datagram egress, and the BIND
|
||||
accept. alktunnels resolved the same question for its establisher
|
||||
with the no-trait finding (OQ-TN-05 → alktunnels ADR-004): "produce
|
||||
boxed halves for a resource" is a function, not a trait — two real
|
||||
boxed halves for a resource" is a function, not a trait; two real
|
||||
implementations existed as callbacks, and the trait added nothing.
|
||||
|
||||
The POCs extended that finding to all three SOCKS5 seams:
|
||||
|
||||
- **CONNECT**: the dial cannot live in the establisher — the target
|
||||
- **CONNECT**: the dial cannot live in the establisher; the target
|
||||
is unknown at open time (it arrives in-stream at command read).
|
||||
The injected `DialFn(target) -> TargetHandle {read, write}`
|
||||
closure is called at the command-read interception point
|
||||
(`poc-connect-wasm-findings.md` §Structural findings). Per-target
|
||||
refusal is an RFC reply code; session-level refusal stays
|
||||
channels-level.
|
||||
- **ASSOCIATE**: the `EgressFactory` — called once per association;
|
||||
- **ASSOCIATE**: the `EgressFactory`, called once per association;
|
||||
the datagram stage owns the egress; stage exit drops it
|
||||
(`poc-udp-associate-findings.md` §The per-association egress
|
||||
instance).
|
||||
- **BIND**: the `BindAcceptFactory` — one instance per session; its
|
||||
- **BIND**: the `BindAcceptFactory`, one instance per session; its
|
||||
future owns the listener/tunnel and is dropped by the quiet
|
||||
wait's `select!` on the control-stream branch (the client-gone
|
||||
teardown; `poc-bind-findings.md` §The quiet wait is
|
||||
@@ -40,20 +40,20 @@ The POCs extended that finding to all three SOCKS5 seams:
|
||||
|
||||
- **No dialer/egress/accept trait.** All three seams are
|
||||
function-shaped callbacks injected at registration/assembly:
|
||||
- `DialFn(target) -> TargetHandle` (boxed halves, `+ Sync` —
|
||||
satisfies the `ChannelPlan` payload bound; the bound binds
|
||||
handle/plan payloads downstream — the SOCKS5 establisher
|
||||
- `DialFn(target) -> TargetHandle` (boxed halves, `+ Sync`,
|
||||
satisfying the `ChannelPlan` payload bound; the bound binds
|
||||
handle/plan payloads downstream; the SOCKS5 establisher
|
||||
itself carries no plan, `Establishment::default()`)
|
||||
- `EgressFactory() -> impl Egress` (per-association instance;
|
||||
`next_reply` readiness contract: `Pending` when nothing is
|
||||
ready)
|
||||
- `BindAcceptFactory` (per-session; the future owns its resources
|
||||
end-to-end — the ownership-rides-the-handle lesson)
|
||||
end-to-end, the ownership-rides-the-handle lesson)
|
||||
- **Ownership rides the handle structurally.** Plan/handle payloads
|
||||
own their channel sessions and socket halves inside the payload
|
||||
(the third witness of the teardown-at-birth pattern: POC #2's
|
||||
egress table, alktunnels ADR-005, POC #5's `BindTargetHandle`).
|
||||
The compiler should enforce it — no `_` bindings of owning
|
||||
The compiler should enforce it: no `_` bindings of owning
|
||||
values.
|
||||
- **The trait re-evaluation threshold is the same as alktunnels
|
||||
ADR-004's**: revisit if three real implementations exist as trait
|
||||
@@ -69,7 +69,7 @@ The POCs extended that finding to all three SOCKS5 seams:
|
||||
the module map in [overview.md](../overview.md)).
|
||||
- The composed providers (alktunnels direct-op egress, bind-first
|
||||
listen accept) are assembly-layer compositions, not crate
|
||||
dependencies of the protocol layer — the protocol crate depends
|
||||
dependencies of the protocol layer; the protocol crate depends
|
||||
on alkcall only (see [overview.md](../overview.md) §Dependencies;
|
||||
the composed-provider helpers that reference alktunnels types live
|
||||
behind the composition feature, ADR-009).
|
||||
@@ -79,7 +79,7 @@ The POCs extended that finding to all three SOCKS5 seams:
|
||||
|
||||
## References
|
||||
|
||||
- OQ-SK-01 (the dial policy — the A/B/C options; this ADR closes
|
||||
- OQ-SK-01 (the dial policy, the A/B/C options; this ADR closes
|
||||
the dialer-trait-vs-callback half), OQ-SK-03 (the egress seam),
|
||||
OQ-SK-08 (the accept seam)
|
||||
- POC #1/#2/#5 findings (the three seams' empirical records)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# ADR-005: Auth Mapping — "Both," Split by the Door
|
||||
# ADR-005: Auth Mapping: "Both," Split by the Door
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; the posture settled 2026-09-13 —
|
||||
OQ-SK-02 — and POC #1 validated the identity seam end to end)
|
||||
Accepted (2026-09-20, Phase 1; the posture settled 2026-09-13
|
||||
(OQ-SK-02), and POC #1 validated the identity seam end to end)
|
||||
|
||||
## Context
|
||||
|
||||
Two auth mechanisms are real: alkcall's identity seam (the per-call
|
||||
opener identity arrives on the open-op hooks — alkcall ledger
|
||||
opener identity arrives on the open-op hooks; alkcall ledger
|
||||
CF-005/CF-006) and RFC 1929's in-band username/password. Phase 0
|
||||
resolved the posture: both mechanisms are real, each on its own side
|
||||
of the door — the consumer's optional front door (where the vanilla
|
||||
of the door: the consumer's optional front door (where the vanilla
|
||||
world meets the ACL world). A producer-side password would add
|
||||
nothing the identity seam doesn't already do, and the ACL system is
|
||||
strictly better than a shared secret.
|
||||
|
||||
POC #1's wiring finding (the accept-side install hook must call
|
||||
`channel0_conn.set_identity(resolved)` — the transport identity does
|
||||
`channel0_conn.set_identity(resolved)`; the transport identity does
|
||||
not flow to channel-0 dispatch automatically) is a producer-harness
|
||||
requirement worth surfacing in the spec, not a protocol-layer
|
||||
concern.
|
||||
@@ -27,12 +27,12 @@ concern.
|
||||
- **Producer side: identity only.** The channels-native open is
|
||||
authorized by the alkcall ACL/identity seam. The producer's
|
||||
in-band RFC conversation runs NoAuth via the typestate's normal
|
||||
path — `accept_no_auth` (the greeting and method selection still
|
||||
path: `accept_no_auth` (the greeting and method selection still
|
||||
happen; POC #2/#5's validated shape: `accept_no_auth →
|
||||
read_command`). The negotiation is real but the auth decision is
|
||||
vacuous in-band because authorization already happened at the
|
||||
open gate. **`skip_auth_this_is_not_rfc_compliant` is excluded
|
||||
from the in-band path** — it wraps a stream already marked
|
||||
from the in-band path**; it wraps a stream already marked
|
||||
authenticated, skipping the greeting entirely; feeding it a
|
||||
client that speaks the RFC greeting would misparse the greeting
|
||||
as command bytes. (At most it belongs to a raw-pipe door that
|
||||
@@ -41,26 +41,26 @@ concern.
|
||||
where vanilla clients need it.** The door owns credential
|
||||
presentation: a vanilla client carrying `user:pass` gets a real
|
||||
RFC 1929 conversation at the door. The door then maps the
|
||||
accepted credential to *local* configuration — which channel
|
||||
identity/endpoint to open the producer channel with — and that
|
||||
accepted credential to *local* configuration (which channel
|
||||
identity/endpoint to open the producer channel with), and that
|
||||
identity reaches the producer via the CF-005/CF-006 seam. **The
|
||||
SOCKS password never crosses the channel as a SOCKS password; the
|
||||
channel identity never reaches the vanilla client.**
|
||||
- **No fast-socks5 `AuthMethod` impl is required.** The door checks
|
||||
credentials itself before/around handing the stream to the
|
||||
typestate — the door is this crate's code; the trait was designed
|
||||
typestate; the door is this crate's code; the trait was designed
|
||||
for embedders who don't own the accept loop.
|
||||
- **The door's credential→identity mapping shape: static config.**
|
||||
The mapping is a caller-supplied configuration table
|
||||
(`username/password → channel identity or endpoint selection`)
|
||||
— static, declarative, assembly-layer. A pluggable lookup
|
||||
(`username/password → channel identity or endpoint selection`),
|
||||
static, declarative, assembly-layer. A pluggable lookup
|
||||
(callback) remains possible as an additive extension if a
|
||||
deployment needs dynamic resolution; v1 ships the static shape
|
||||
because the front door's use cases (tun2proxy's fixed
|
||||
`user:pass@host`, curl's `--proxy-user`) are single-credential or
|
||||
small-table shapes.
|
||||
- **The NoAuth door remains valid** for loopback-only or
|
||||
wrapper-aware use (POC #1's raw-pipe shape — zero protocol
|
||||
wrapper-aware use (POC #1's raw-pipe shape: zero protocol
|
||||
translation); the cred-bearing door is the same bridge plus the
|
||||
greeting half. Door variant selection is an assembly-layer
|
||||
choice.
|
||||
@@ -70,15 +70,15 @@ concern.
|
||||
- Vanilla-client compatibility at the front door is real (RFC 1929),
|
||||
without polluting the channels-native path with shared secrets.
|
||||
- The producer-side optional local backend (a front door in the
|
||||
other direction — vanilla clients → a locally-served SOCKS5
|
||||
other direction: vanilla clients → a locally-served SOCKS5
|
||||
resource) offers the same door surface: door = vanilla↔ACL
|
||||
boundary, same code shape.
|
||||
- The mapping is not wire-visible: no params, no reply fields, no
|
||||
ALPN changes — the door is entirely assembly-layer.
|
||||
ALPN changes; the door is entirely assembly-layer.
|
||||
|
||||
## References
|
||||
|
||||
- OQ-SK-02 (the full reasoning: "replace or second-gate" dissolved —
|
||||
- OQ-SK-02 (the full reasoning: "replace or second-gate" dissolved;
|
||||
the mechanisms occupy different planes)
|
||||
- POC #1 `poc-connect-wasm-findings.md` (the identity-seam wiring
|
||||
finding; the raw-pipe front door)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# ADR-006: Access-Control Posture — the Open Gate Plus the Command Gate
|
||||
# ADR-006: Access-Control Posture: the Open Gate Plus the Command Gate
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; scope gates POC-validated in shape —
|
||||
Accepted (2026-09-20, Phase 1; scope gates POC-validated in shape:
|
||||
POC #1 (`socks5:open`), POC #5 (`socks5:bind` at command dispatch);
|
||||
consequence asymmetry pinned by the integration suite, 2026-09-24)
|
||||
|
||||
## Context
|
||||
|
||||
SOCKS5 is arbitrary-egress by nature — the open gate plus the target
|
||||
SOCKS5 is arbitrary-egress by nature; the open gate plus the target
|
||||
policy are the security boundary (AGENTS.md convention 12).
|
||||
alktunnels resolved the sibling posture (its ADR-006): whatever ACL
|
||||
governs the resource governs everything reachable through it, plus
|
||||
whatever policy the SOCKS5 implementation itself applies downstream.
|
||||
For SOCKS5, BIND adds an *inbound* capability — the accept-side twin
|
||||
of arbitrary egress — so the open gate must distinguish it
|
||||
For SOCKS5, BIND adds an *inbound* capability, the accept-side twin
|
||||
of arbitrary egress, so the open gate must distinguish it
|
||||
(OQ-SK-08's security note): an identity with proxy egress must not
|
||||
implicitly get ingress listeners. POC #5 validated exactly this
|
||||
distinction at command dispatch from the per-call opener identity.
|
||||
@@ -23,10 +23,10 @@ distinction at command dispatch from the per-call opener identity.
|
||||
|
||||
- **The open gate is the boundary.** The open op's
|
||||
`AccessControl.required_scopes` is `["socks5:open"]`
|
||||
(`SOCKS5_OPEN_SCOPE`) — scope-gated via
|
||||
(`SOCKS5_OPEN_SCOPE`), scope-gated via
|
||||
`ChannelCore::register_openable` (the registry runs the ACL
|
||||
before the open handler). Scope-string convention:
|
||||
`<alpn-family>:<capability>` (`socks5:open`, `socks5:bind`) —
|
||||
`<alpn-family>:<capability>` (`socks5:open`, `socks5:bind`),
|
||||
following alktty's `TTY_OPEN_SCOPE` / alktunnels'
|
||||
`tunnel:open`/`tunnel:direct` shape. Stable once published.
|
||||
- **The command gate: BIND requires `socks5:bind` at command
|
||||
@@ -38,7 +38,7 @@ distinction at command dispatch from the per-call opener identity.
|
||||
`ConnectionNotAllowed` (0x02) refusal while CONNECT still serves
|
||||
the same identity (POC #5 test 4).
|
||||
- **Target policy is the dialer's concern.** No target allowlists
|
||||
in the base crate — the dial callback refuses (per-target refusal
|
||||
in the base crate; the dial callback refuses (per-target refusal
|
||||
is an RFC reply code; ADR-004's seams). The dialer-refuses shape
|
||||
avoids a second policy layer (alktunnels OQ-TN-08's resolution
|
||||
pattern, evidence-backed). Whatever ACL governs the socks5
|
||||
@@ -49,23 +49,23 @@ distinction at command dispatch from the per-call opener identity.
|
||||
by the `tunnel:direct` grant's ACL (POC #2 validated); the dial
|
||||
callback governs per-destination checks.
|
||||
- **Ownership**: optionally consult `OwnershipProvider` for
|
||||
resource ownership (alknet ADR-050's seam) — a registration-time
|
||||
resource ownership (alknet ADR-050's seam), a registration-time
|
||||
choice, not a protocol concern.
|
||||
- **Hub relaying**: the hub's terminate-and-re-produce applies per
|
||||
hop (alkcall ADR-051's `ChannelRelay`/`HubLegTemplate`); the hub's
|
||||
ACL governs the final dial — the hub is the network boundary. No
|
||||
ACL governs the final dial; the hub is the network boundary. No
|
||||
socks5-specific relay work.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Two grants, clean separation: `socks5:open` (egress) and
|
||||
`socks5:bind` (ingress listeners) — never implied by each other
|
||||
`socks5:bind` (ingress listeners), never implied by each other
|
||||
(the alktunnels ADR-006 Amendment 1 posture: a capability on its
|
||||
own scope). The implication asymmetry is pinned by the integration
|
||||
suite (`tests/core-integration.rs`, the identity/scope matrix):
|
||||
`socks5:open` alone serves CONNECT + ASSOCIATE and is refused BIND
|
||||
(0x02) at the command gate; `socks5:bind` alone grants *nothing*
|
||||
on the op — the open gate's `required_scopes = ["socks5:open"]`
|
||||
on the op: the open gate's `required_scopes = ["socks5:open"]`
|
||||
fails closed for every command (the establisher never runs, no
|
||||
phantom channel); both scopes, granted explicitly, serve
|
||||
everything. A BIND-capable deployment grants both.
|
||||
@@ -73,7 +73,7 @@ distinction at command dispatch from the per-call opener identity.
|
||||
same op (ADR-001): the command gate is the mechanism, and it is
|
||||
POC-validated (POC #5).
|
||||
- The allowlist-hook variant (a declarative per-target policy at
|
||||
registration) is *not* shipped in v1 — the dial callback is the
|
||||
registration) is *not* shipped in v1: the dial callback is the
|
||||
policy point; see OQ-SK-05's residue (OQ-SK2-07 in the tracker)
|
||||
for the virtual-subnet translation-layer proposal, which is the
|
||||
same dial-callback shape.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR-007: BIND in the Base Crate — the Phase Model Extended
|
||||
# ADR-007: BIND in the Base Crate: the Phase Model Extended
|
||||
|
||||
## Status
|
||||
|
||||
@@ -15,7 +15,7 @@ asks for a listener on the producer's network; the producer replies
|
||||
replies #2 with the peer address before the data plane. The use case
|
||||
that makes it real (FTP active mode is the canonical vanilla reason)
|
||||
plus the composed variant's hub story were found in Phase 0
|
||||
(OQ-SK-08), and POC #5 validated the whole shape end to end — both
|
||||
(OQ-SK-08), and POC #5 validated the whole shape end to end: both
|
||||
accept variants, the scope gate, the failure mapping, the vanilla
|
||||
front door.
|
||||
|
||||
@@ -28,8 +28,8 @@ Nothing here is a one-way door.
|
||||
|
||||
- **BIND ships in the base crate** (protocol layer), phase-model
|
||||
style: request → reply#1 carrying `BND.ADDR` → quiet wait (any
|
||||
control byte after reply#1 is protocol garbage; the session ends —
|
||||
exactly two exits: the accept, or the control stream's EOF/
|
||||
control byte after reply#1 is protocol garbage; the session ends,
|
||||
with exactly two exits: the accept, or the control stream's EOF/
|
||||
garbage/error) → exactly one inbound accept → reply#2 carrying the
|
||||
peer address → raw pass-through data plane (zero per-packet
|
||||
headers in-band, same as CONNECT; only the ASSOCIATE datagram
|
||||
@@ -37,27 +37,27 @@ Nothing here is a one-way door.
|
||||
- **The accept seam is `BindAcceptFactory`** (ADR-004). Two
|
||||
provider-backed accept variants (plus the wrapper-aware in-band
|
||||
default when neither is wired), all POC #5-validated:
|
||||
1. **Local backend binds for real** (feature-gated) — the faithful
|
||||
1. **Local backend binds for real** (feature-gated): the faithful
|
||||
RFC shape: a real network-B address in reply#1 (the listener's
|
||||
`local_addr`), a real peer in reply#2, vanilla front-door
|
||||
clients work. **This is the default accept variant** for the
|
||||
optional local backend.
|
||||
2. **Composed (alktunnels listen resource)** — the listener lives
|
||||
2. **Composed (alktunnels listen resource)**: the listener lives
|
||||
on a bind-first listen resource (`register_tunnel_bindfirst_
|
||||
openable` + `BindFirstInjections`, alktunnels 0.2.0); per-
|
||||
listener ACL for free, hub-relayable. **Vanilla-fidelity with
|
||||
no stand-ins** since the graduation: the factory no longer
|
||||
carries an expected `bind_addr` — reply#1 advertises the real
|
||||
OS-chosen `bound` (the open-op reply's `bound` field,
|
||||
no stand-ins** since the graduation: the factory no longer
|
||||
carries an expected `bind_addr`; reply#1 advertises the real
|
||||
OS-chosen `bound` (the open-op reply's `bound` field,
|
||||
alkcall 0.8.0 reply projection); reply#2 carries the real peer
|
||||
(the forwarded op's `peer` param).
|
||||
3. **Wrapper-aware only** (zeros in reply#1, sentinel reply#2) —
|
||||
3. **Wrapper-aware only** (zeros in reply#1, sentinel reply#2),
|
||||
retained for completeness as the in-band default when neither
|
||||
provider is wired: the handle-swap reading applied to BIND
|
||||
(the reply is RFC-compatibility surface; the data plane is the
|
||||
channel).
|
||||
- **The establishment bound covers only the open-op round trip.**
|
||||
The accept wait lives in the teardown-governed pump phase — FTP-
|
||||
The accept wait lives in the teardown-governed pump phase; FTP-
|
||||
length BIND waits stop resolving as spurious establishment
|
||||
refusals (the bind-first graduation's stated second win;
|
||||
`poc-bind-findings.md` §Establishment-bound, superseded in shape).
|
||||
@@ -71,7 +71,7 @@ Nothing here is a one-way door.
|
||||
- The RFC 1928 command surface is complete: CONNECT + BIND +
|
||||
ASSOCIATE, all three on one op (POC #5 test 7 regression-guarded
|
||||
the other two).
|
||||
- The composed accept path composes against alktunnels 0.2.0 — the
|
||||
- The composed accept path composes against alktunnels 0.2.0: the
|
||||
bind-first establisher + forwarded op (ADR-008) are the substrate;
|
||||
the POC's pinned workaround (reply#1 first, the tunnel open rides
|
||||
the accept future) is retired with the graduation.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# ADR-008: Egress/Accept Provider Defaults — Local Socket and Composed (Direct Op)
|
||||
# ADR-008: Egress/Accept Provider Defaults: Local Socket and Composed (Direct Op)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; the A/B assembly-layer choice —
|
||||
OQ-SK-01's residue — with the composed vehicle settled by
|
||||
Accepted (2026-09-20, Phase 1; the A/B assembly-layer choice
|
||||
(OQ-SK-01's residue), with the composed vehicle settled by
|
||||
alktunnels 0.2.0 ADR-007)
|
||||
|
||||
## Context
|
||||
@@ -11,12 +11,12 @@ alktunnels 0.2.0 ADR-007)
|
||||
OQ-SK-01's dialer-trait-vs-callback half is closed (ADR-004); what
|
||||
remained was the **default provider set**: where does the dial
|
||||
callback come from in the base crate's optional shapes? Option A
|
||||
(dial via alktunnels — the composed shape), Option B (dial directly
|
||||
— local TCP), Option C (injected dialer trait — rejected, ADR-004).
|
||||
(dial via alktunnels, the composed shape), Option B (dial directly,
|
||||
local TCP), Option C (injected dialer trait, rejected, ADR-004).
|
||||
alktunnels 0.2.0 removed Option A's blocker (the direct op
|
||||
`channels/tunnel/direct` is the dynamic-target channel — one open
|
||||
`channels/tunnel/direct` is the dynamic-target channel: one open
|
||||
per CONNECT target, what it was graduated for), and POC #2 measured
|
||||
its per-destination open cost (~0.8 ms, loopback) — fine for DNS +
|
||||
its per-destination open cost (~0.8 ms, loopback), fine for DNS +
|
||||
browsing.
|
||||
|
||||
## Decision
|
||||
@@ -25,7 +25,7 @@ browsing.
|
||||
- **Local-socket providers** (behind the `local` feature): a real
|
||||
`TcpStream` dial (CONNECT), a real `UdpSocket` egress
|
||||
(ASSOCIATE, one socket per association, per-datagram
|
||||
`send_to`/`recv_from` — no flow table; domain destinations
|
||||
`send_to`/`recv_from`; no flow table; domain destinations
|
||||
resolve at the egress, the ssh `-D` semantic), a real
|
||||
`TcpListener` accept (BIND, ADR-007 variant 1). These are the
|
||||
faithful-RFC shapes and the simplest assembly story.
|
||||
@@ -33,10 +33,10 @@ browsing.
|
||||
dial callback opens an alktunnels direct-op channel naming the
|
||||
target (`open_direct`, `Substrate::Tcp`/`Substrate::Udp`); the
|
||||
UDP table is a lazy destination→tunnel table (open on first
|
||||
datagram, LRU-evict at a cap — POC #2's `ComposedEgress` shape).
|
||||
datagram, LRU-evict at a cap, POC #2's `ComposedEgress` shape).
|
||||
Hub relaying is per-target terminate-and-re-produce
|
||||
(alkcall ADR-051).
|
||||
- **Neither provider is a dependency of the protocol layer** —
|
||||
- **Neither provider is a dependency of the protocol layer**;
|
||||
both are assembly-layer wiring (ADR-004); the features exist so
|
||||
the providers ship *in the crate* rather than forcing every
|
||||
deployment to hand-roll them. The feature gates and module
|
||||
@@ -46,11 +46,11 @@ browsing.
|
||||
The feature-gated providers are convenience constructors over
|
||||
the same seams. A missing callback means that command refuses
|
||||
RFC-faithfully (`CommandNotSupported`-class reply), never a
|
||||
panic — the command gate is the open handler's dispatch, and a
|
||||
panic: the command gate is the open handler's dispatch, and a
|
||||
provider-less command is an unprovided capability, not an error
|
||||
condition.
|
||||
- **Per-target ACL is the composed variant's benefit** (each
|
||||
destination rides its own `tunnel:direct`-governed channel —
|
||||
destination rides its own `tunnel:direct`-governed channel,
|
||||
ADR-006); the local variant's target policy is the dial
|
||||
callback itself.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# ADR-009: Feature-Gate Layout — `local` and the Composition Boundary
|
||||
# ADR-009: Feature-Gate Layout: `local` and the Composition Boundary
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; the wasm-clean default-crate invariant
|
||||
is the alk* family posture — AGENTS.md convention 4). Amended
|
||||
is the alk* family posture, AGENTS.md convention 4). Amended
|
||||
2026-09-21 (pre-decomposition): the vendor's `net` gate dissolves
|
||||
under ADR-011's extraction amendment — the vendor is
|
||||
under ADR-011's extraction amendment; the vendor is
|
||||
wasm-clean by construction; features collapse to `local` +
|
||||
`composed`; see §Amendment.
|
||||
|
||||
@@ -13,20 +13,20 @@ wasm-clean by construction; features collapse to `local` +
|
||||
|
||||
The default crate must compile to `wasm32-unknown-unknown` (the
|
||||
alktty/alktunnels precedent; the wasm shape is a sandboxed
|
||||
SOCKS5 consumer — pure byte framing, no local sockets needed
|
||||
SOCKS5 consumer (pure byte framing, no local sockets needed
|
||||
client-side; phase-0 OQ-SK-04's analysis). Two groups of
|
||||
platform-specific or sibling-dependent code need gating:
|
||||
|
||||
1. **The crate's own local backends** (dial/listen/egress providers
|
||||
touching real sockets — ADR-008's local variants).
|
||||
touching real sockets, ADR-008's local variants).
|
||||
2. **The composed providers** (the alktunnels-composed egress/accept
|
||||
helpers — ADR-008's composed variants), which need the
|
||||
helpers, ADR-008's composed variants), which need the
|
||||
alktunnels dependency.
|
||||
|
||||
The vendor originally contributed a third gate: its kernel-net
|
||||
region was to ride a default-on `net` feature (POC #4's measured
|
||||
shape, ~120 cfg lines across 5 files). ADR-011's 2026-09-21
|
||||
amendment pins the vendor as *extraction* — the kernel-net region is
|
||||
amendment pins the vendor as *extraction*; the kernel-net region is
|
||||
not copied, so that gate no longer exists (see §Amendment). POC #4's
|
||||
gate map survives as the extraction recipe, not as in-tree cfg.
|
||||
|
||||
@@ -38,26 +38,26 @@ gate map survives as the extraction recipe, not as in-tree cfg.
|
||||
feature-gated, never imported by the protocol layer.
|
||||
- **The default build has no `net` feature**: the extracted vendor
|
||||
contains zero kernel-net code, so the wasm-clean subset is the
|
||||
whole crate — there is nothing to gate (ADR-011 §Amendment).
|
||||
- **`local` (default-off)**: the crate's first-party local backends
|
||||
— the TCP dial, the UDP egress socket, the BIND listener, and the
|
||||
front doors (the consumer's ssh `-D` local exposure; the
|
||||
whole crate; there is nothing to gate (ADR-011 §Amendment).
|
||||
- **`local` (default-off)**: the crate's first-party local backends,
|
||||
namely the TCP dial, the UDP egress socket, the BIND listener, and
|
||||
the front doors (the consumer's ssh `-D` local exposure; the
|
||||
producer's local backend). Brings `tokio/net` + `socket2` into the
|
||||
graph. Non-wasm by design (enabling it on wasm is a build error,
|
||||
the alktty posture).
|
||||
- **`composed` (default-off)**: the composed providers over
|
||||
alktunnels 0.2.0 (direct-op egress, bind-first listen accept) —
|
||||
alktunnels 0.2.0 (direct-op egress, bind-first listen accept),
|
||||
the only feature that adds the alktunnels dependency. The
|
||||
protocol layer never references alktunnels types; the composed
|
||||
module does, injected at the assembly layer (ADR-004/ADR-008).
|
||||
- **tokio stays `default-features = false`** with the wasm-clean
|
||||
subset (`rt`, `sync`, `io-util`, `macros`, `time`) — **never
|
||||
subset (`rt`, `sync`, `io-util`, `macros`, `time`); **never
|
||||
`features = ["full"]`** (AGENTS.md convention 4); `net`-shaped
|
||||
additions ride `local` alone.
|
||||
|
||||
## Amendment (2026-09-21, pre-decomposition — the vendor's `net` gate dissolves)
|
||||
## Amendment (2026-09-21, pre-decomposition: the vendor's `net` gate dissolves)
|
||||
|
||||
The original decision carried a third feature — `net`
|
||||
The original decision carried a third feature, `net`
|
||||
(default-on), the vendored subset's kernel-net gate, with `local`
|
||||
implying it. ADR-011's extraction amendment dissolves it:
|
||||
|
||||
@@ -66,12 +66,12 @@ implying it. ADR-011's extraction amendment dissolves it:
|
||||
`Socks5Server`/`Incoming` API, `Socks5Datagram`'s kernel path, the
|
||||
DNS-resolve helper, the net-only tests), the `socks4` module, and
|
||||
the legacy `Config`/`Authentication` layer are not copied into the
|
||||
tree — the extraction is wasm-clean by construction, with no
|
||||
tree; the extraction is wasm-clean by construction, with no
|
||||
in-tree cfg gate.
|
||||
- Features collapse to two: **`local`** (first-party socket
|
||||
providers + front doors, ADR-008; brings `tokio/net` + `socket2`
|
||||
into the graph itself) and **`composed`** (the alktunnels
|
||||
providers). `local` no longer implies a separate `net` feature —
|
||||
providers). `local` no longer implies a separate `net` feature;
|
||||
it is the sole gate for socket/platform I/O.
|
||||
- POC #4's gate map survives as the *extraction recipe* (its
|
||||
ungated inventory is what to copy), not as in-tree cfg.
|
||||
@@ -81,24 +81,24 @@ implying it. ADR-011's extraction amendment dissolves it:
|
||||
|
||||
## Consequences
|
||||
|
||||
- Wasm consumers (the sandboxed adapter story — a product input,
|
||||
- Wasm consumers (the sandboxed adapter story; a product input,
|
||||
nothing waits on it) build the default crate clean; the protocol
|
||||
substance *is* the wasm-relevant surface.
|
||||
- The verification commands cover the gates: `cargo test` (default),
|
||||
`cargo test --all-features`, and both wasm checks
|
||||
(`cargo check/clippy --target wasm32-unknown-unknown`) — the
|
||||
(`cargo check/clippy --target wasm32-unknown-unknown`), the
|
||||
structural guard for the invariant.
|
||||
- The vendor's carried changes (BIND, `new_reply` publication) live
|
||||
in the extracted region where they touch codecs/typestate; nothing
|
||||
vendored is gated — the only feature-gated socket code is the
|
||||
vendored is gated; the only feature-gated socket code is the
|
||||
crate's own first-party `local` module.
|
||||
|
||||
## References
|
||||
|
||||
- OQ-SK-04 (resolved: the extracted-and-forked base — ADR-011/
|
||||
- OQ-SK-04 (resolved: the extracted-and-forked base; ADR-011/
|
||||
ADR-013), OQ-SK-01/03/08
|
||||
(the providers the features gate)
|
||||
- POC #4 `poc-connect-wasm-findings.md` (the measured gate — now the
|
||||
- POC #4 `poc-connect-wasm-findings.md` (the measured gate, now the
|
||||
extraction recipe),
|
||||
alktty's `local` feature (the posture precedent), alktunnels
|
||||
ADR-004 + overview.md (the same feature split)
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR-010: Channel Capacity — Proxy-Workload Recommendation, Not a Wire Change
|
||||
# ADR-010: Channel Capacity: Proxy-Workload Recommendation, Not a Wire Change
|
||||
|
||||
## Status
|
||||
|
||||
@@ -14,12 +14,12 @@ CONNECT channel per flow; composed UDP egress spends a downstream
|
||||
channel per destination; BIND spends an accept resource per
|
||||
listener). A proxy-shaped deployment will discover the cap in
|
||||
production unless the crate documents the posture. The caps are
|
||||
policy knobs — configurable without a wire change (the `u32`
|
||||
channel-ID wire space is ~4 billion) — so the resolution is config
|
||||
policy knobs, configurable without a wire change (the `u32`
|
||||
channel-ID wire space is ~4 billion), so the resolution is config
|
||||
plus documentation, never wire.
|
||||
|
||||
Considered and rejected: in-channel port multiplexing (a
|
||||
`[port: u16][len][payload]` per-chunk prefix) — it re-opens the
|
||||
`[port: u16][len][payload]` per-chunk prefix); it re-opens the
|
||||
demux-vs-phase trade ADR-003 (the handle swap) resolved, breaks
|
||||
the 0-B pass-through that lets the forked `transfer` drop in
|
||||
unchanged, imposes chunk boundaries on a stream RFC 1928 treats as
|
||||
@@ -28,14 +28,14 @@ convention (AGENTS.md #10).
|
||||
|
||||
## Decision
|
||||
|
||||
- **No wire change — rejected permanently.** The caps are policy
|
||||
- **No wire change (rejected permanently).** The caps are policy
|
||||
knobs (alkcall ADR-040/041); the resolution is config.
|
||||
- **Assembly-layer config is the mechanism**: a proxy-shaped
|
||||
deployment raises the per-connection/per-identity caps at
|
||||
registration; the default stays conservative for everyone else.
|
||||
- **Documented proxy-workload recommendation**: the crate docs
|
||||
recommend a raised cap for proxy deployments, with the memory
|
||||
arithmetic — a session's memory is bounded + predictable (CONNECT/
|
||||
arithmetic: a session's memory is bounded + predictable (CONNECT/
|
||||
ASSOCIATE are bounded-buffer pumps; BIND adds one listener + one
|
||||
accept), so the per-connection cap can rise safely relative to a
|
||||
general-purpose deployment. A concrete recommended figure is a
|
||||
@@ -44,12 +44,12 @@ convention (AGENTS.md #10).
|
||||
cap → per-identity cap → dial/accept policy. Relaxing the caps
|
||||
leans on the rest. The per-identity DoS brake is arguably
|
||||
*stronger* here (each SOCKS5 session maps to real producer-side
|
||||
resources — a dial, a downstream channel, an accept listener);
|
||||
resources (a dial, a downstream channel, an accept listener);
|
||||
the per-connection memory bound is arguably *weaker* (the three
|
||||
command shapes are well-known and bounded).
|
||||
- **Revisit trigger** (OQ-SK-09's): if a deployment hits a fixed
|
||||
upstream channel budget that config cannot raise, the additive
|
||||
escape is a *new multiplexed ALPN* — never a change to this
|
||||
escape is a *new multiplexed ALPN*, never a change to this
|
||||
ALPN's framing.
|
||||
|
||||
## Consequences
|
||||
@@ -57,7 +57,7 @@ convention (AGENTS.md #10).
|
||||
- Deployments get the guidance before they hit the cap; no protocol
|
||||
surface changes.
|
||||
- The rejected multiplexing proposal has a recorded rationale and a
|
||||
revisit trigger — the escape hatch is additive (new ALPN), never
|
||||
revisit trigger; the escape hatch is additive (new ALPN), never
|
||||
a framing change.
|
||||
|
||||
## References
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# ADR-011: the Inline Vendor — fast-socks5's `T`-Generic Subset, In-Tree
|
||||
# ADR-011: the Inline Vendor: fast-socks5's `T`-Generic Subset, In-Tree
|
||||
|
||||
## Status
|
||||
|
||||
Superseded by [ADR-013](013-targeted-fork.md) (2026-09-22): the
|
||||
maintenance posture (the vendor framing — re-base/re-vendor
|
||||
maintenance posture (the vendor framing, re-base/re-vendor
|
||||
discipline, the verbatim/lint-allow apparatus) is retired; the
|
||||
extracted region is a targeted fork, module `rfc1928`. **The
|
||||
extraction mechanics here remain authoritative** — the cut table,
|
||||
extraction mechanics here remain authoritative**; the cut table,
|
||||
the two carried changes (BIND, `new_reply`), the §Amendment
|
||||
(extraction, not wholesale copy; the net gate dissolves) describe
|
||||
what was copied and what was not; only the post-copy maintenance
|
||||
@@ -16,57 +16,57 @@ OQ-SK-04's resolution), Amended 2026-09-21 (pre-decomposition).
|
||||
## Context
|
||||
|
||||
fast-socks5 is **third-party upstream** (v1.0.0, MIT;
|
||||
`/workspace/fast-socks5` is a read-only reference checkout — we do
|
||||
`/workspace/fast-socks5` is a read-only reference checkout; we do
|
||||
not own it, and it has not been forked). The wrapper's needs go
|
||||
beyond what it publishes:
|
||||
|
||||
1. **The wasm gate** — `tokio::net` + `socket2` (via `mio`) hard-error
|
||||
1. **The wasm gate**: `tokio::net` + `socket2` (via `mio`) hard-error
|
||||
on `wasm32-unknown-unknown`; the default crate must compile there
|
||||
(AGENTS.md convention 4). POC #4 measured the gate: ~120 changed
|
||||
lines across 5 files, all `#[cfg]` insertions; the wasm-clean
|
||||
subset is functionally complete (a full RFC 1928 CONNECT
|
||||
conversation passes `--no-default-features`).
|
||||
2. **BIND is absent** — `Socks5Command::TCPBind` is parsed but every
|
||||
2. **BIND is absent**: `Socks5Command::TCPBind` is parsed but every
|
||||
handler refuses it; POC #5 proved the wrapper must drive the
|
||||
typestate itself for the BIND shape (ADR-007).
|
||||
3. **The `run_udp_proxy_custom` bind seam** — the "custom" seam is
|
||||
3. **The `run_udp_proxy_custom` bind seam**: the "custom" seam is
|
||||
narrower than it looks: it calls `udp_bind_random_port`
|
||||
unconditionally and derives the reply port from that kernel
|
||||
socket; the channels path cannot use it (verified 2026-09-13).
|
||||
The channels path drives the typestate directly; making the bind
|
||||
optional / accepting a caller-supplied socket is an in-tree fix.
|
||||
4. **`new_reply` is private** — the reply encoder is private; the
|
||||
4. **`new_reply` is private**: the reply encoder is private; the
|
||||
BIND reply#2 writer otherwise hand-rolls the 10-byte reply
|
||||
(POC #5's courtesy item, now a vendor change).
|
||||
|
||||
Two facts settled the posture: the ownership premise was wrong
|
||||
(no cheap "file asks / PR" path), and the carried needs (2–4) exist
|
||||
regardless of the wasm question — so the vendor pays for itself
|
||||
regardless of the wasm question, so the vendor pays for itself
|
||||
without leaning on the adapter story. The change set is exactly the
|
||||
shape POC #4 measured as minimal. (Need 1 later dissolves —
|
||||
shape POC #4 measured as minimal. (Need 1 later dissolves,
|
||||
§Amendment; needs 3's handler is not extracted with it.)
|
||||
|
||||
## Decision
|
||||
|
||||
- **Extract the `T`-generic subset into this crate** (see §Amendment
|
||||
for the mechanical reading of "extract"): the codecs, the typestate
|
||||
machinery, the client `use_stream` path — the parts already generic
|
||||
machinery, the client `use_stream` path, the parts already generic
|
||||
over `T: AsyncRead + AsyncWrite + Unpin`. The extraction contains
|
||||
zero kernel-net code and is wasm-clean by construction.
|
||||
- **Carry the four needs in the same vendor**: the net gate, BIND
|
||||
(the typestate-driven shape, ADR-007), the `run_udp_proxy_custom`
|
||||
seam fix, and the `new_reply` publication. (Post-Amendment: the
|
||||
net gate dissolves — see §Amendment; BIND and `new_reply` are the
|
||||
net gate dissolves; see §Amendment; BIND and `new_reply` are the
|
||||
two that touch extracted code.)
|
||||
- **Note the vendor point (v1.0.0) in the vendored code**; keep the
|
||||
MIT license obligations (upstream is MIT; preserve the license
|
||||
header per file or a bundled notice).
|
||||
- **`/workspace/fast-socks5` stays the differential-testing
|
||||
reference** for RFC edge cases (reply-code mapping, domain
|
||||
addressing, fragmentation) — keep `/workspace/fast-socks5` for
|
||||
addressing, fragmentation); keep `/workspace/fast-socks5` for
|
||||
differential testing; never wait on an upstream merge.
|
||||
- **Offering the net-gate upstream as a courtesy PR stays optional**
|
||||
(POC #4: it is small enough to offer as-is) — never a wait, never
|
||||
(POC #4: it is small enough to offer as-is), never a wait, never
|
||||
a dependency.
|
||||
|
||||
## Consequences
|
||||
@@ -75,7 +75,7 @@ shape POC #4 measured as minimal. (Need 1 later dissolves —
|
||||
cadence, no feature waits, and the vendored code co-evolves with
|
||||
the wrapper (the carried changes are first-class, not patches).
|
||||
- The cost is maintenance of the extracted subset (the `T`-generic
|
||||
region + codecs — a fraction of upstream's ~4k LoC; POC #4's
|
||||
region + codecs, a fraction of upstream's ~4k LoC; POC #4's
|
||||
ungated inventory is the shopping list). The diff vs upstream is
|
||||
deliberate and bounded: the extraction cut + the carried changes +
|
||||
the license/provenance notes.
|
||||
@@ -86,7 +86,7 @@ shape POC #4 measured as minimal. (Need 1 later dissolves —
|
||||
- Differential testing against `/workspace/fast-socks5` guards the
|
||||
RFC edge cases the extracted code must keep passing.
|
||||
|
||||
## Amendment (2026-09-21, pre-decomposition — "extract" pinned, the `net` gate dissolves)
|
||||
## Amendment (2026-09-21, pre-decomposition: "extract" pinned, the `net` gate dissolves)
|
||||
|
||||
The decision's wording ("vendor the `T`-generic subset") supported
|
||||
two mechanical readings; this amendment pins the intent so the
|
||||
@@ -109,12 +109,12 @@ decomposition task is unambiguous:
|
||||
- **Why**: convention 17's letter ("vendor the `T`-generic subset");
|
||||
no crate consumer routes through the kernel-net region (the
|
||||
producer/consumer/doors drive the typestate and the pure codecs;
|
||||
the local backends are first-party tokio code — ADR-004/ADR-008);
|
||||
the local backends are first-party tokio code, ADR-004/ADR-008);
|
||||
and the BIND + `new_reply` needs touch only the extracted region
|
||||
(BIND's driving is wrapper-side code per POC #5 — "zero upstream
|
||||
(BIND's driving is wrapper-side code per POC #5, "zero upstream
|
||||
ask for BIND itself").
|
||||
- **The `net` feature gate dissolves.** With no kernel-net code
|
||||
in-tree, the extraction is wasm-clean by construction — no
|
||||
in-tree, the extraction is wasm-clean by construction; no
|
||||
`#[cfg]` gate needed inside the vendored files. Features collapse
|
||||
to two: `local` (the crate's first-party socket providers and
|
||||
front doors, ADR-008) and `composed` (the alktunnels providers).
|
||||
@@ -123,11 +123,11 @@ decomposition task is unambiguous:
|
||||
stays `default-features = false` with the wasm-clean subset
|
||||
(AGENTS.md convention 4); `tokio/net` rides `local` alone.
|
||||
- **The carried-changes list re-reads under extraction**: (1) the
|
||||
net gate — dissolves (no net code); (2) BIND — stays (the typestate
|
||||
net gate, dissolves (no net code); (2) BIND, stays (the typestate
|
||||
driving + reply#2 writer are in-tree; ADR-007); (3) the
|
||||
`run_udp_proxy_custom` bind seam — dissolves (the handler isn't
|
||||
`run_udp_proxy_custom` bind seam, dissolves (the handler isn't
|
||||
extracted; the typestate-driving shape POC #2 validated is the
|
||||
crate's ASSOCIATE path); (4) `new_reply` publication — stays
|
||||
crate's ASSOCIATE path); (4) `new_reply` publication, stays
|
||||
(it is inside the extracted codec region). Net: **two carried
|
||||
changes** touch extracted code.
|
||||
- **Extraction completeness is compiler-checked**: a missed
|
||||
@@ -139,10 +139,10 @@ decomposition task is unambiguous:
|
||||
upstream's `log` calls in the extracted region are dropped or
|
||||
re-pointed at the crate's `tracing` (the alk* convention); the
|
||||
extracted region's `anyhow::Context` error wrapping is dropped
|
||||
(error surface is the crate's `thiserror` types — the vendored
|
||||
(error surface is the crate's `thiserror` types; the vendored
|
||||
`SocksError`/`SocksServerError` stay the wire-level vocabulary);
|
||||
`async-trait` is dropped (its four upstream uses are all on the
|
||||
legacy `Authentication` layer, which is cut — the typestate's
|
||||
legacy `Authentication` layer, which is cut; the typestate's
|
||||
`AuthMethod<T>` traits are native async-fn-in-trait); the
|
||||
`socks4` module and the legacy `Config`/`Authentication` layer are
|
||||
out (above). These are extraction-time mechanical adjustments to
|
||||
@@ -157,8 +157,8 @@ wasm check a hard gate as in alktty/alktunnels.
|
||||
|
||||
- OQ-SK-04 (the resolved posture; the reasoning trail),
|
||||
OQ-SK-08 (the BIND + `new_reply` needs), OQ-SK-03 (the bind seam)
|
||||
- POC #4 + POC #1 (`poc-connect-wasm-findings.md` — the measured
|
||||
gate), POC #5 (`poc-bind-findings.md` — the BIND typestate
|
||||
- POC #4 + POC #1 (`poc-connect-wasm-findings.md`: the measured
|
||||
gate), POC #5 (`poc-bind-findings.md`: the BIND typestate
|
||||
driving + the `new_reply` finding)
|
||||
- ADR-002 (the protocol base), ADR-007 (BIND), ADR-009 (the feature
|
||||
gates); `/workspace/fast-socks5` (the reference checkout, v1.0.0)
|
||||
@@ -3,7 +3,7 @@
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-20, Phase 1; the dialer-refuses shape is
|
||||
OQ-SK-05's evidence-backed resolution — see ADR-006 — and the
|
||||
OQ-SK-05's evidence-backed resolution (see ADR-006), and the
|
||||
2026-09-14 virtual-subnet proposal is resolved as assembly-layer
|
||||
policy)
|
||||
|
||||
@@ -11,15 +11,15 @@ policy)
|
||||
|
||||
OQ-SK-05's residual Phase 1 questions: (a) does the base crate ship
|
||||
a per-target allowlist hook, or is target policy entirely the
|
||||
dialer's concern; (b) domain-form targets (RFC 1928 ATYP 0x03) —
|
||||
dialer's concern; (b) domain-form targets (RFC 1928 ATYP 0x03):
|
||||
resolve producer-side, refuse, or pass through unresolved; (c) the
|
||||
2026-09-14 virtual-subnet proposal (identity-scoped subnets so
|
||||
proxied resources become IP-addressable — "BND.ADDR ↔ channel id"
|
||||
proxied resources become IP-addressable ("BND.ADDR ↔ channel id"
|
||||
translation, a hub exposing proxied resources per identity under
|
||||
ACL).
|
||||
|
||||
The proposal's key property: the mechanism needs **no wire change**
|
||||
— DST.ADDR and BND.ADDR are arbitrary RFC fields, so virtual
|
||||
The proposal's key property: the mechanism needs **no wire change**;
|
||||
DST.ADDR and BND.ADDR are arbitrary RFC fields, so virtual
|
||||
addresses ride ordinary CONNECT/BIND requests; the translation layer
|
||||
is the dial callback (`dial(target)` matches a virtual subnet
|
||||
against a per-identity resource table).
|
||||
@@ -34,13 +34,13 @@ against a per-identity resource table).
|
||||
dialer-refuses.
|
||||
- **Domain-form targets: pass through unresolved to the dialer.**
|
||||
Resolution is the egress's/dialer's concern (POC #2 resolved the
|
||||
local-egress half: "the egress owns resolution" — the ssh `-D`
|
||||
local-egress half: "the egress owns resolution" (the ssh `-D`
|
||||
semantic). The protocol layer never resolves: a wasm producer may
|
||||
have no resolver at all, and the composed variant's targets ride
|
||||
the alktunnels direct op (producer-side resolution there, per
|
||||
alktunnels ADR-007's own posture). fast-socks5's `dns_resolve`
|
||||
config (and its `resolve_dns` step) is not part of the extracted
|
||||
vendor region at all — the crate's producer never resolves
|
||||
vendor region at all; the crate's producer never resolves
|
||||
(ADR-006/ADR-012), so the extraction drops both.
|
||||
- **The virtual-subnet translation layer is assembly-layer policy,
|
||||
not a crate feature.** The crate ships no translation-table
|
||||
@@ -48,12 +48,12 @@ against a per-identity resource table).
|
||||
wanting identity-scoped virtual subnets implements the
|
||||
subnet→resource table there (per-identity scoping = resources +
|
||||
ACL + target policy; hub exposure = the terminate-and-re-produce
|
||||
story — no new wire vocabulary). The full-tunnel composition (the
|
||||
story; no new wire vocabulary). The full-tunnel composition (the
|
||||
host routes the subnet into the proxy via tun2proxy) is where a
|
||||
virtual subnet becomes routable from vanilla apps:
|
||||
`connect(10.x.y.z:p)` is an ordinary CONNECT the dialer
|
||||
translates. tun2proxy's virtual-network/virtual-DNS support is
|
||||
the prior art to check *if and when* a deployment builds this —
|
||||
the prior art to check *if and when* a deployment builds this;
|
||||
the crate's obligation is only that the seams support it, which
|
||||
ADR-004's shape already does.
|
||||
|
||||
@@ -66,7 +66,7 @@ against a per-identity resource table).
|
||||
translation layer rides the dial callback; if a deployment
|
||||
materializes with concrete needs, a translation-table helper can
|
||||
be added additively (assembly-layer code, possibly later
|
||||
graduated into the crate behind a feature) — the seams don't
|
||||
graduated into the crate behind a feature); the seams don't
|
||||
change.
|
||||
- DNS posture is uniform: producer-side resolution everywhere
|
||||
(local egress resolves; composed egress hands the name to the
|
||||
@@ -77,11 +77,11 @@ against a per-identity resource table).
|
||||
- OQ-SK-05 (the full question set + the proposal's record),
|
||||
OQ-SK2-07 (the deferred helper question this ADR's resolution
|
||||
feeds), OQ-SK-01
|
||||
(the dial callback), OQ-SK-09 (the multiplexing rejection — the
|
||||
(the dial callback), OQ-SK-09 (the multiplexing rejection; the
|
||||
virtual-subnet idea that survives is recorded in the tracker)
|
||||
- POC #2 (`poc-udp-associate-findings.md` — the egress-owns-DNS
|
||||
- POC #2 (`poc-udp-associate-findings.md`: the egress-owns-DNS
|
||||
resolution), ADR-004 (the seams), ADR-006 (the ACL posture),
|
||||
ADR-008 (the provider defaults)
|
||||
- alktunnels phase-0-findings OQ-TN-08 (the target-policy
|
||||
dissolution precedent); tun2proxy (`/workspace/tun2proxy` — the
|
||||
dissolution precedent); tun2proxy (`/workspace/tun2proxy`, the
|
||||
virtual-network prior art + the endgame composition partner)
|
||||
@@ -1,17 +1,17 @@
|
||||
# ADR-013: The Targeted Fork — fast-socks5's `T`-Generic Subset Is Owned, Not Vendored
|
||||
# ADR-013: The Targeted Fork: fast-socks5's `T`-Generic Subset Is Owned, Not Vendored
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-22, post-extraction-review; supersedes the ADR-011
|
||||
maintenance posture — ADR-011 stays for the extraction mechanics and
|
||||
maintenance posture; ADR-011 stays for the extraction mechanics and
|
||||
the cut table; the ADR-011 §Amendment is subsumed here)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-011 pinned the inline-vendor posture as **extraction** (the
|
||||
`T`-generic subset copied in-tree, kernel-net region not copied; the
|
||||
net gate dissolving), and the vendor group — five implementation
|
||||
tasks plus the review gate — landed it in commits `57b107b..5b18d4d`,
|
||||
net gate dissolving), and the vendor group (five implementation
|
||||
tasks plus the review gate) landed it in commits `57b107b..5b18d4d`,
|
||||
with the review (`tasks/implementation/
|
||||
review-vendor-extraction.md`) as the fidelity gate: the file-by-file
|
||||
diff vs `/workspace/fast-socks5` v1.0.0 categorizes every hunk, and
|
||||
@@ -20,23 +20,23 @@ every review criterion passed.
|
||||
The review also made the maintenance posture's cost visible. The
|
||||
tree is now permanently diverged from upstream in five categories:
|
||||
|
||||
1. **The extraction cut** — the kernel-net region, `socks4`, the
|
||||
1. **The extraction cut**: the kernel-net region, `socks4`, the
|
||||
legacy `Config`/`Authentication` layer, `Socks5Datagram`, the
|
||||
convenience dialers, the DNS helper: gone.
|
||||
2. **The carried changes** — BIND's reply surface, the published
|
||||
2. **The carried changes**: BIND's reply surface, the published
|
||||
`new_reply`.
|
||||
3. **The panic translations** — `ReplyError::from_u8` (the review's
|
||||
3. **The panic translations**: `ReplyError::from_u8` (the review's
|
||||
F-1), `use_password_auth`, `reply_error`'s parse.
|
||||
4. **The error-pruning adjustments** — anyhow/log/async-trait out,
|
||||
4. **The error-pruning adjustments**: anyhow/log/async-trait out,
|
||||
error variants pruned, `MissingTargetAddr` added.
|
||||
5. **The lint-allow posture** — module-level `#[allow]`s preserving
|
||||
5. **The lint-allow posture**: module-level `#[allow]`s preserving
|
||||
upstream style (`u8::max_value()`, needless lifetimes,
|
||||
`match_like_matches_macro`, `io_other_error`, `wrong_self_
|
||||
convention`, the F-4 verbatim string) to keep the diff mechanical.
|
||||
|
||||
Category 5 exists solely to keep categories 1–4 reconcilable by a
|
||||
line-based diff. But a future re-base against an upstream release
|
||||
was never going to be a clean mechanical diff again — it was always
|
||||
was never going to be a clean mechanical diff again; it was always
|
||||
going to be manual reconciliation across all five categories. The
|
||||
vendor posture's maintenance apparatus (verbatim preservation, the
|
||||
lint allows, the re-vendor/adjustment-list discipline) pays a
|
||||
@@ -44,7 +44,7 @@ permanent tax to serve a re-base path that would never be used.
|
||||
|
||||
The "vendor" framing also produced a real defect: the review's
|
||||
F-1 (`ReplyError::from_u8`'s upstream `unreachable!()` arm kept
|
||||
verbatim — wire-reachable, a misbehaving peer could panic the
|
||||
verbatim; wire-reachable, a misbehaving peer could panic the
|
||||
consumer task) escaped precisely because the vendor posture
|
||||
maintained a second, weaker instruction set ("verbatim + adjustments
|
||||
list" instead of "project conventions, unconditionally"). The
|
||||
@@ -52,7 +52,7 @@ fidelity-vs-conventions tension is structural to the vendor framing;
|
||||
the review caught the escape, but the seam itself was the vendor
|
||||
framing's.
|
||||
|
||||
The fork point is now — post-review, post-F-1 — the cleanest line
|
||||
The fork point is now, post-review and post-F-1, the cleanest line
|
||||
the tree will ever present: the extraction provenance is in git
|
||||
history (`57b107b..5b18d4d`), the review is the recorded fidelity
|
||||
gate, and the five divergence categories are documented in ADR-011,
|
||||
@@ -76,8 +76,8 @@ vendor.md, and the review task. "The tree as of the review commit
|
||||
(now gone); "socks5" would stutter with the crate name
|
||||
(`alksocks::socks5`) and collide with the future module structure
|
||||
(the producer/consumer/adapter layers are also SOCKS5-shaped).
|
||||
`rfc1928` names what the module *is* — the RFC 1928 wire
|
||||
vocabulary and the state machines — permanent and collision-free.
|
||||
`rfc1928` names what the module *is* (the RFC 1928 wire
|
||||
vocabulary and the state machines), permanent and collision-free.
|
||||
- **The lint-allow posture dissolves.** The module-level `#[allow]`s
|
||||
and the "verbatim lint posture" module-doc paragraphs are removed;
|
||||
the suppressed smells are fixed per project conventions:
|
||||
@@ -100,7 +100,7 @@ vendor.md, and the review task. "The tree as of the review commit
|
||||
pins bytes, not upstream text, so it survives the rename
|
||||
untouched. Textual-diff discipline is retired; behavioral
|
||||
differential testing is retained.
|
||||
- **The cut table is re-read as the not-forked inventory** — the
|
||||
- **The cut table is re-read as the not-forked inventory**, the
|
||||
map of what to hand-port if a future need surfaces (e.g. the
|
||||
kernel-net region for a standalone vanilla-server example). The
|
||||
hand-port path replaces the re-vendor path: port against the
|
||||
@@ -109,7 +109,7 @@ vendor.md, and the review task. "The tree as of the review commit
|
||||
- **The honest cost**: the cheap upstream-fix path closes. A future
|
||||
fast-socks5 fix (a reply-code mapping correction, a codec fix)
|
||||
arrives as a hand-port against the reference checkout, not a
|
||||
diff absorb. Risk is assessed low — fast-socks5 is a quiet
|
||||
diff absorb. Risk is assessed low; fast-socks5 is a quiet
|
||||
project, and the extracted surface is the well-trodden RFC
|
||||
1928 path.
|
||||
|
||||
@@ -118,13 +118,13 @@ vendor.md, and the review task. "The tree as of the review commit
|
||||
- **Positive**: one instruction set (project conventions,
|
||||
unconditionally); the lint-allow tax and the adjustment-list
|
||||
apparatus dissolve; smells are fixed at the only cost that
|
||||
matters — a one-time cleanup verified by the existing suite.
|
||||
matters: a one-time cleanup verified by the existing suite.
|
||||
The F-1 seam class (conventions-vs-fidelity gaps) is closed
|
||||
structurally.
|
||||
- **Positive**: the module name is stable for the crate's future
|
||||
(no `alksocks::socks5` stutter, no `socks5::producer` collision
|
||||
pressure).
|
||||
- **Negative (accepted)**: the cheap upstream-fix path closes — a
|
||||
- **Negative (accepted)**: the cheap upstream-fix path closes; a
|
||||
future upstream fix is a hand-port against the reference
|
||||
checkout, guarded by the differential suite. Assessed low-risk
|
||||
(quiet upstream, well-trodden RFC surface).
|
||||
@@ -139,10 +139,10 @@ vendor.md, and the review task. "The tree as of the review commit
|
||||
## References
|
||||
|
||||
- ADR-011 (the extraction mechanics, the cut table, the carried
|
||||
changes — the mechanics remain authoritative), ADR-002 (the
|
||||
changes; the mechanics remain authoritative), ADR-002 (the
|
||||
protocol base), ADR-009 (feature gates, no `net` feature)
|
||||
- `tasks/implementation/review-vendor-extraction.md` (the fidelity
|
||||
gate + F-1), `docs/architecture/rfc1928.md` (the owned-core
|
||||
spec)
|
||||
- `/workspace/fast-socks5` (v1.0.0, MIT — the behavioral
|
||||
- `/workspace/fast-socks5` (v1.0.0, MIT, the behavioral
|
||||
reference for differential testing)
|
||||
@@ -1,10 +1,10 @@
|
||||
# ADR-014: The Idle-Timeout Posture — Teardown-Governed Phases, No Protocol-Layer Idle Bound
|
||||
# ADR-014: The Idle-Timeout Posture: Teardown-Governed Phases, No Protocol-Layer Idle Bound
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-24, review 002 C-2's decision task — the deliverable
|
||||
Accepted (2026-09-24, review 002 C-2's decision task: the deliverable
|
||||
is this decision, not code; it lands before the batch-2 `local`/
|
||||
`composed` doors exist, whose tasks may depend on it — ADR-008/
|
||||
`composed` doors exist, whose tasks may depend on it, per ADR-008/
|
||||
ADR-009's provider posture)
|
||||
|
||||
## Context
|
||||
@@ -13,33 +13,33 @@ Review 002's C-2 (major): the pre-command RFC handshake and the
|
||||
association have no timeout surface. After the channel opens, the
|
||||
pump handler's spawned task parks on the RFC handshake
|
||||
(`Socks5ServerProtocol::accept_no_auth` → `read_command`), and the
|
||||
datagram stage's loop exits only on EOF/error/data — no timer exists
|
||||
datagram stage's loop exits only on EOF/error/data; no timer exists
|
||||
anywhere in the protocol layer. The alkcall 10s establishment bound
|
||||
(`register_openable_with_establisher`'s default, `timeout: None`,
|
||||
alkcall ADR-049 §2) governs only the open-op round trip, not the
|
||||
in-stream conversation. A silent peer therefore holds — per idle
|
||||
channel — one spawned task and one channel entry (bounded buffers,
|
||||
in-stream conversation. A silent peer therefore holds, per idle
|
||||
channel, one spawned task and one channel entry (bounded buffers,
|
||||
ADR-040) indefinitely. BIND's quiet wait got an explicit decision
|
||||
(ADR-007: teardown-governed — "FTP-length waits stay legal"); the
|
||||
(ADR-007: teardown-governed, "FTP-length waits stay legal"); the
|
||||
handshake and the association lifetime got "inherited from the
|
||||
channel" — implicit, never decided.
|
||||
channel", implicit, never decided.
|
||||
|
||||
The mitigations are real: the 256-channel per-connection cap
|
||||
(ADR-040) and the per-identity cap (ADR-041) bound the count,
|
||||
per-session memory is bounded, and teardown fires on channel close —
|
||||
per-session memory is bounded, and teardown fires on channel close;
|
||||
nothing leaks, nothing grows. The aggregate remains: any identity
|
||||
with `socks5:open` can hold 256 parked tasks + 256 channel entries
|
||||
indefinitely *without sending a byte*. And batch 2's providers
|
||||
(`local`, `composed`, ADR-008) are about to route *vanilla clients*
|
||||
through these same phases via the front doors, where the peer is a
|
||||
kernel socket often outside the channel-cap regime entirely — a
|
||||
kernel socket often outside the channel-cap regime entirely; a
|
||||
slowloris-shaped hold (open, greet, stop) becomes the assembly
|
||||
layer's problem, and the protocol layer is where a timeout seam would
|
||||
live.
|
||||
|
||||
The review offered two options: (1) idle timeout is the substrate's/
|
||||
assembly's job — the protocol layer never times out, the DoS posture
|
||||
recorded explicitly; (2) a producer-side idle bound — an optional
|
||||
assembly's job: the protocol layer never times out, the DoS posture
|
||||
recorded explicitly; (2) a producer-side idle bound: an optional
|
||||
per-session inactivity timeout at the pump level (config on
|
||||
`Socks5ProducerBuilder`), racing the handshake/stage reads against a
|
||||
sleep, timing out as a clean EOF-shaped teardown.
|
||||
@@ -51,61 +51,61 @@ Two facts sharpen the choice beyond the review's sketch:
|
||||
idle CONNECT tunnel (`pump_bidi` parked on a quiet target), a
|
||||
quiet-but-alive association (association lifetime = channel
|
||||
lifetime, wire.md §UDP ASSOCIATE), BIND's quiet wait (ADR-007).
|
||||
The resource cost is identical to the pre-command hold — one
|
||||
The resource cost is identical to the pre-command hold, one
|
||||
parked task + one channel entry. A handshake-only idle bound
|
||||
protects only the pre-auth sliver while the dominant hold (the
|
||||
post-reply idle) stays contractually open — incoherent; a bound on
|
||||
post-reply idle) stays contractually open; incoherent. A bound on
|
||||
both is a full inactivity timeout, which contradicts the landed
|
||||
contracts (association lifetime = channel lifetime; FTP-length
|
||||
BIND waits stay legal, ADR-007). The unbounded-time aggregate is
|
||||
therefore a property of every idle session the protocol serves —
|
||||
therefore a property of every idle session the protocol serves:
|
||||
bounded by the cap regime, not by a timer.
|
||||
- **The two sides differ in composability.** The consumer sessions
|
||||
are plain futures at the caller — a caller wanting an idle bound
|
||||
wraps the constructor (or the session's reads) in
|
||||
are plain futures at the caller: a caller wanting an idle
|
||||
bound wraps the constructor (or the session's reads) in
|
||||
`tokio::time::timeout` itself; no decision is needed consumer-side.
|
||||
The producer's parked task lives inside the alkcall-spawned open
|
||||
handler — not caller-composable; only the producer-side question
|
||||
handler, not caller-composable; only the producer-side question
|
||||
needed an ADR.
|
||||
|
||||
Also recorded (an upstream-fidelity correction to the review's
|
||||
sketch): upstream's connect-timeout path maps its *dial* timeout to
|
||||
`ReplyError::ConnectionTimeout` → reply code 0x06 (TTL expired), not
|
||||
0x01 as the sketch suggested — the mapping table lives in
|
||||
0x01 as the sketch suggested; the mapping table lives in
|
||||
`src/rfc1928/mod.rs` (carried from `/workspace/fast-socks5`
|
||||
`util/stream.rs`). Relevant only if option 2 is ever built.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 1 — the protocol layer never times out.** Every phase of the
|
||||
**Option 1: the protocol layer never times out.** Every phase of the
|
||||
RFC 1928 conversation is teardown-governed: the pre-command
|
||||
handshake, the BIND quiet wait, and the association lifetime end on
|
||||
the channels-level sentinels (EOF / garbage / error), never on a
|
||||
protocol-layer inactivity timer. The posture is substrate-uniform —
|
||||
protocol-layer inactivity timer. The posture is substrate-uniform:
|
||||
the same for a channel peer and for a kernel socket a door relays.
|
||||
|
||||
- **No inactivity bound exists in `src/`.** The handshake and stage
|
||||
reads park like any other phase: no idle timer, no keepalive, no
|
||||
in-band expiry vocabulary. "No in-band control vocabulary"
|
||||
(wire.md) holds — this decision adds none.
|
||||
(wire.md) holds; this decision adds none.
|
||||
- **The DoS posture is explicit and layered** (the ADR-010 pattern:
|
||||
ACL → per-connection cap → per-identity cap → door/dial policy):
|
||||
- **Channels shapes**: parked-task count is bounded by the
|
||||
per-connection cap (ADR-040) and the per-identity cap (ADR-041);
|
||||
per-session memory is bounded; teardown fires on channel close.
|
||||
Time is unbounded by design — accepted, the same posture every
|
||||
Time is unbounded by design; accepted, the same posture every
|
||||
idle-but-open session already has.
|
||||
- **Doors** (the batch-2 obligation this ADR records before they
|
||||
exist): an accepted TCP connection is *not a channel* — the cap
|
||||
exist): an accepted TCP connection is *not a channel*; the cap
|
||||
regime does not bound a door's accepted-socket hold. The door's
|
||||
accept path owns its defense; the batch-2 door author inherits
|
||||
these obligations knowingly (the wire.md §UDP ASSOCIATE
|
||||
"inherits this bound knowingly" pattern):
|
||||
1. an **accept-stage idle bound** — an accepted connection that
|
||||
1. an **accept-stage idle bound**: an accepted connection that
|
||||
sends no first byte within a configured window is closed
|
||||
(door-observable, protocol-agnostic; this is the slowloris
|
||||
sliver the protocol layer will not bound);
|
||||
2. **per-credential / per-peer session caps** — the RFC 1929
|
||||
2. **per-credential / per-peer session caps**: the RFC 1929
|
||||
cred-bearing door's credential→identity config table
|
||||
(ADR-005) is the natural knob;
|
||||
3. **accept-queue / relay-task bounds** on the door's own accept
|
||||
@@ -115,10 +115,10 @@ the same for a channel peer and for a kernel socket a door relays.
|
||||
the producer-side local backend's accept loop, and a composed
|
||||
bind-first listener's front.
|
||||
- The protocol layer neither provides nor re-derives these knobs
|
||||
(limits are inherited, not redefined — AGENTS.md convention 10);
|
||||
(limits are inherited, not redefined, per AGENTS.md convention 10);
|
||||
no timeout helper ships speculatively.
|
||||
- **Relationship to ADR-007 (the BIND quiet wait): same regime.**
|
||||
ADR-007 decided the quiet wait's ends as part of BIND's shape — the
|
||||
ADR-007 decided the quiet wait's ends as part of BIND's shape: the
|
||||
`select!` needed exactly two exits, and "FTP-length waits stay
|
||||
legal" was the explicit call. ADR-014 records the *same* resolution
|
||||
for the phases ADR-007 left implicit. The principle is one and the
|
||||
@@ -129,33 +129,33 @@ the same for a channel peer and for a kernel socket a door relays.
|
||||
difference is only decidedness: ADR-007 decided one phase's ends by
|
||||
designing BIND; ADR-014 decides the rest by recording the inherited
|
||||
resolution as the decision. No new regime is introduced.
|
||||
- **Revisitable — not a one-way door.** A producer-side idle seam is
|
||||
- **Revisitable, not a one-way door.** A producer-side idle seam is
|
||||
additive if a deployment ever proves this posture insufficient: an
|
||||
optional per-session inactivity bound (builder config defaulting to
|
||||
`None` = this posture), enforced by racing the handshake/stage
|
||||
reads against a sleep timer (`tokio::time` is already in the
|
||||
wasm-clean dependency subset), timing out as a clean EOF-shaped
|
||||
teardown. No wire change, no API break, no spec contradiction —
|
||||
teardown. No wire change, no API break, no spec contradiction:
|
||||
the spec lines record the *default* posture, and an opt-in bound
|
||||
contradicts nothing. The RFC has no timeout reply code; a pre-reply
|
||||
timeout, if the seam is ever built, would surface as the EOF shape
|
||||
(or the closest mapped code — upstream's dial-timeout precedent is
|
||||
(or the closest mapped code; upstream's dial-timeout precedent is
|
||||
0x06, see Context). Revisit trigger: a deployment that cannot
|
||||
enforce the bound at its doors or assembly — a peer class outside
|
||||
both the cap regime and any door policy — with measured evidence
|
||||
enforce the bound at its doors or assembly (a peer class outside
|
||||
both the cap regime and any door policy), with measured evidence
|
||||
the aggregate hold is unacceptable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No code changes this round; the posture is recorded. The spec lines
|
||||
(wire.md §UDP ASSOCIATE's association-lifetime bullet;
|
||||
producer.md §The Open Handler) carry the decided posture — the
|
||||
producer.md §The Open Handler) carry the decided posture; the
|
||||
implicit residue is gone.
|
||||
- The batch-2 door tasks inherit the recorded accept-path obligations;
|
||||
this ADR is the timeout-posture gate those tasks may depend on
|
||||
(it precedes any `local`/`composed` door work).
|
||||
- Review 002 C-2 closes as decided. The adjacent S-1 residual (the
|
||||
datagram stage's hot-`Ok(None)` tolerance — unbounded *rate*, not
|
||||
datagram stage's hot-`Ok(None)` tolerance: unbounded *rate*, not
|
||||
unbounded *time*) stays its own accepted-uncovered posture; this
|
||||
decision does not touch it.
|
||||
- Consumer-side timeout policy stays where it always was: caller-
|
||||
@@ -167,7 +167,7 @@ the same for a channel peer and for a kernel socket a door relays.
|
||||
- docs/reviews/002-protocol-core-implementation-review.md §C-2 (the
|
||||
finding), §S-1 (the adjacent rate posture), §N-2 (the
|
||||
"inherits this bound knowingly" pattern this ADR extends)
|
||||
- [ADR-007](007-bind-in-base.md) (the quiet wait's decision — the
|
||||
- [ADR-007](007-bind-in-base.md) (the quiet wait's decision; the
|
||||
contrast case), [ADR-010](010-channel-capacity.md) (the
|
||||
layered-defense + config-not-protocol pattern this follows),
|
||||
[ADR-008](008-provider-defaults.md) / [ADR-009](009-feature-gates.md)
|
||||
@@ -179,7 +179,7 @@ the same for a channel peer and for a kernel socket a door relays.
|
||||
- [wire.md](../wire.md) §UDP ASSOCIATE,
|
||||
[producer.md](../producer.md) §The Open Handler (the spec lines
|
||||
updated)
|
||||
- POC findings (the stage's measured shapes — the "sufficient for
|
||||
- POC findings (the stage's measured shapes; the "sufficient for
|
||||
relay postures" lineage)
|
||||
- `/workspace/fast-socks5` (`src/util/stream.rs` — the dial-timeout
|
||||
- `/workspace/fast-socks5` (`src/util/stream.rs`: the dial-timeout
|
||||
mapping precedent, 0x06)
|
||||
@@ -1,46 +1,46 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-28 (OQ-SK2-09 recorded — the UDP door's
|
||||
last_updated: 2026-09-28 (OQ-SK2-09 recorded: the UDP door's
|
||||
peer-pin posture; option 2 landed in 0.1.0, option 3 deferred;
|
||||
OQ-SK2-06's trigger ownership checked — genuinely external; the
|
||||
OQ-SK2-06's trigger ownership checked, genuinely external; the
|
||||
OQ-SK ledger OQ-SK-01..09 promoted 2026-09-20; new questions
|
||||
continue as OQ-SK2-06..)
|
||||
---
|
||||
|
||||
# alksocks — Open Questions
|
||||
# alksocks: Open Questions
|
||||
|
||||
Centralized tracker. The Phase 0 OQ ledger (`docs/research/
|
||||
phase-0.md`, OQ-SK-01..09) is promoted here with statuses current as
|
||||
of the Phase 1 spec; new Phase 1 questions continue the OQ-SK2
|
||||
numbering (SK2 = the architecture-set ledger, distinct from the
|
||||
Phase 0 ledger's OQ-SK- prefix, which stays historical). Note: the
|
||||
SK2 numbering is not contiguous from 01 — OQ-SK2-01..05 were
|
||||
SK2 numbering is not contiguous from 01: OQ-SK2-01..05 were
|
||||
consumed in working drafts before the first committed set; the
|
||||
sequence starts at the numbers in use. Format per
|
||||
`docs/sdd_process.md`.
|
||||
|
||||
## Theme 1: Wire format and phases
|
||||
|
||||
### OQ-SK-01: Producer dial policy — what dials the target?
|
||||
### OQ-SK-01: Producer dial policy (what dials the target?)
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-01)
|
||||
- **Status**: resolved — [ADR-004](decisions/004-dial-callback-shape.md)
|
||||
- **Status**: resolved: [ADR-004](decisions/004-dial-callback-shape.md)
|
||||
(the shape), [ADR-008](decisions/008-provider-defaults.md) (the
|
||||
provider set)
|
||||
- **Priority**: high
|
||||
- **Resolution**: 2026-09-20 (Phase 1). The dialer-trait-vs-callback
|
||||
half closed on the POC evidence (function-not-trait at three
|
||||
seams — DialFn/EgressFactory/BindAcceptFactory, boxed halves,
|
||||
seams: DialFn/EgressFactory/BindAcceptFactory, boxed halves,
|
||||
ownership rides the handle). The provider-default half: the base
|
||||
crate ships local-socket providers (`local`) and composed
|
||||
providers (`composed`, the alktunnels direct op); no implicit
|
||||
dialer — registration always takes the callbacks explicitly.
|
||||
dialer; registration always takes the callbacks explicitly.
|
||||
- **Cross-references**: OQ-SK-03, OQ-SK-08, ADR-004, ADR-008
|
||||
|
||||
### OQ-SK-03: UDP ASSOCIATE without binding
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-03)
|
||||
- **Status**: resolved — [ADR-003](decisions/003-udp-handle-swap.md)
|
||||
- **Status**: resolved: [ADR-003](decisions/003-udp-handle-swap.md)
|
||||
- **Priority**: high
|
||||
- **Resolution**: 2026-09-20 (Phase 1; baseline settled 2026-09-13,
|
||||
POC #2 validated 2026-09-14). The handle swap: one channel per
|
||||
@@ -52,15 +52,15 @@ sequence starts at the numbers in use. Format per
|
||||
dedup pinned by test).
|
||||
- **Cross-references**: OQ-SK-01, ADR-003, ADR-008
|
||||
|
||||
### OQ-SK-08: BIND — the accept-side command
|
||||
### OQ-SK-08: BIND (the accept-side command)
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-08)
|
||||
- **Status**: resolved — [ADR-007](decisions/007-bind-in-base.md)
|
||||
- **Status**: resolved: [ADR-007](decisions/007-bind-in-base.md)
|
||||
- **Priority**: high
|
||||
- **Resolution**: 2026-09-20 (Phase 1; POC #5 validated 2026-09-15).
|
||||
BIND ships in the base crate, phase-model style; the accept
|
||||
variants: local listener (default, faithful), composed
|
||||
(alktunnels bind-first listen + forwarded op — vanilla-fidelity
|
||||
(alktunnels bind-first listen + forwarded op, vanilla-fidelity
|
||||
since the graduation), wrapper-aware (in-band default). The
|
||||
establishment bound covers only the open-op round trip; the
|
||||
accept wait lives in the teardown-governed pump phase.
|
||||
@@ -68,10 +68,10 @@ sequence starts at the numbers in use. Format per
|
||||
|
||||
## Theme 2: Crate architecture
|
||||
|
||||
### OQ-SK-02: Auth mapping — identity seam vs in-band SOCKS auth
|
||||
### OQ-SK-02: Auth mapping (identity seam vs in-band SOCKS auth)
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-02)
|
||||
- **Status**: resolved — [ADR-005](decisions/005-auth-split-by-door.md)
|
||||
- **Status**: resolved: [ADR-005](decisions/005-auth-split-by-door.md)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: 2026-09-20 (Phase 1; the posture settled
|
||||
2026-09-13). "Both," split by the door: producer = identity seam
|
||||
@@ -85,7 +85,7 @@ sequence starts at the numbers in use. Format per
|
||||
### OQ-SK-04: fast-socks5 wasm posture
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-04)
|
||||
- **Status**: resolved — [ADR-011](decisions/011-inline-vendor.md)
|
||||
- **Status**: resolved: [ADR-011](decisions/011-inline-vendor.md)
|
||||
(the extraction mechanics), [ADR-013](decisions/
|
||||
013-targeted-fork.md) (the ownership posture)
|
||||
- **Priority**: high
|
||||
@@ -97,14 +97,14 @@ sequence starts at the numbers in use. Format per
|
||||
changes (BIND, `new_reply` publication); fork point = the tree as
|
||||
of the extraction-review commit (`5b18d4d`), module `src/rfc1928/`;
|
||||
differential testing against `/workspace/fast-socks5` (behavioral
|
||||
reference — the cheap upstream-fix path closes; a future upstream
|
||||
reference; the cheap upstream-fix path closes; a future upstream
|
||||
fix is a hand-port, guarded by the suite).
|
||||
- **Cross-references**: ADR-002, ADR-009, ADR-011, ADR-013
|
||||
|
||||
### OQ-SK-07: POC register
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-07)
|
||||
- **Status**: resolved — the register is closed (process artifact,
|
||||
- **Status**: resolved: the register is closed (process artifact,
|
||||
not a design question)
|
||||
- **Priority**: low
|
||||
- **Resolution**: 2026-09-14 (Phase 0). All POCs resolved: #1, #2,
|
||||
@@ -117,13 +117,13 @@ sequence starts at the numbers in use. Format per
|
||||
### OQ-SK-09: Channel capacity for proxy workloads (the 256 default)
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-09)
|
||||
- **Status**: resolved — [ADR-010](decisions/010-channel-capacity.md)
|
||||
- **Status**: resolved: [ADR-010](decisions/010-channel-capacity.md)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: 2026-09-20 (Phase 1). No wire change (rejected
|
||||
permanently); assembly-layer config is the mechanism; the crate
|
||||
docs carry the proxy-workload recommendation with the memory
|
||||
arithmetic; in-channel port multiplexing rejected (the rationale +
|
||||
the revisit trigger recorded — the additive escape is a new
|
||||
the revisit trigger recorded; the additive escape is a new
|
||||
multiplexed ALPN).
|
||||
- **Cross-references**: OQ-SK2-08, ADR-010
|
||||
|
||||
@@ -132,7 +132,7 @@ sequence starts at the numbers in use. Format per
|
||||
### OQ-SK-05: Target policy and egress scoping
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-05)
|
||||
- **Status**: resolved — [ADR-006](decisions/006-access-control.md)
|
||||
- **Status**: resolved: [ADR-006](decisions/006-access-control.md)
|
||||
(the gate posture), [ADR-012](decisions/012-target-policy.md)
|
||||
(target policy + the virtual-subnet layer)
|
||||
- **Priority**: high
|
||||
@@ -147,14 +147,14 @@ sequence starts at the numbers in use. Format per
|
||||
### OQ-SK-06: noq client support
|
||||
|
||||
- **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-06)
|
||||
- **Status**: resolved — transferred to the alknet rewrite (the
|
||||
- **Status**: resolved: transferred to the alknet rewrite (the
|
||||
2026-09-14 scope decision)
|
||||
- **Priority**: low
|
||||
- **Resolution**: 2026-09-14 (Phase 0). The quinn/noq
|
||||
`AsyncUdpSocket` client wrapper is alknet's ancestor-shaped
|
||||
component (ADR-090 + the quinn-proxy POC live there); it neither
|
||||
produces nor consumes channels. This crate's client story is the
|
||||
channels-native sessions (POC-validated) — complete without it.
|
||||
channels-native sessions (POC-validated), complete without it.
|
||||
- **Cross-references**: ADR-002 (the base), phase-0 §Scope
|
||||
|
||||
### OQ-SK2-06: The sandboxed-wasm-consumer adapter story
|
||||
@@ -166,27 +166,27 @@ sequence starts at the numbers in use. Format per
|
||||
- **Priority**: low
|
||||
- **Impacts**: how much wasm surface to exercise in the default
|
||||
crate (a product/docs input). Blocks nothing in the protocol
|
||||
design — the wasm-clean invariant (ADR-009) holds regardless.
|
||||
- **Blocked on**: a concrete sandboxed-adapter use case — a TS/Python
|
||||
design; the wasm-clean invariant (ADR-009) holds regardless.
|
||||
- **Blocked on**: a concrete sandboxed-adapter use case: a TS/Python
|
||||
(or other) adapter embedding the alksocks consumer session in a
|
||||
wasm runtime. Tracker task id:
|
||||
`architecture/oq-sk2-06-wasm-adapter-story`
|
||||
(`tasks/architecture/oq-sk2-06-wasm-adapter-story.md`).
|
||||
- **Trigger ownership (checked 2026-09-28)**: the trigger is
|
||||
genuinely external — no planned suite-internal surface (alktty,
|
||||
genuinely external: no planned suite-internal surface (alktty,
|
||||
alktunnels, alkcall) needs a sandboxed alksocks consumer, and the
|
||||
`alknodes` assembly concept carries no named wasm-adapter plan.
|
||||
Nothing suite-side waits on this OQ, and this OQ waits on nothing
|
||||
suite-side (the adapter's substrate — the wasm-clean default crate
|
||||
— is complete and continuously gate-verified; a downstream glue
|
||||
suite-side (the adapter's substrate, the wasm-clean default crate,
|
||||
is complete and continuously gate-verified; a downstream glue
|
||||
crate can embed it today with zero further alksocks work). The
|
||||
Schrödinger-bind check (both sides deferring to each other
|
||||
forever) came up empty; the OQ stays an unowned external trigger
|
||||
by verified default. Re-check the ownership claim if a product
|
||||
surface names a sandboxed adapter — the amendment then converts
|
||||
surface names a sandboxed adapter; the amendment then converts
|
||||
this to an owned dependency.
|
||||
- **Resolution**: Not yet decidable. The use case isn't concrete yet
|
||||
— the adapter story is a product input whose shape would tell us
|
||||
- **Resolution**: Not yet decidable. The use case isn't concrete yet:
|
||||
the adapter story is a product input whose shape would tell us
|
||||
whether the wasm consumer needs surface beyond the
|
||||
channels-native session (e.g. bindings ergonomics). The
|
||||
architecture decision (wasm-clean default, ADR-009) is already
|
||||
@@ -200,7 +200,7 @@ sequence starts at the numbers in use. Format per
|
||||
- **Status**: deferred(scope)
|
||||
- **Door type**: two-way
|
||||
- **Priority**: low
|
||||
- **Impacts**: nothing in the crate — the seams already support the
|
||||
- **Impacts**: nothing in the crate; the seams already support the
|
||||
translation layer (the dial callback, ADR-004/ADR-012). This
|
||||
tracks whether a *translation-table helper* should graduate into
|
||||
the crate if a deployment materializes with concrete needs.
|
||||
@@ -209,7 +209,7 @@ sequence starts at the numbers in use. Format per
|
||||
at that point). Tracker task id:
|
||||
`architecture/oq-sk2-07-virtual-subnet-helper`
|
||||
(`tasks/architecture/oq-sk2-07-virtual-subnet-helper.md`).
|
||||
- **Resolution**: Not yet decidable. The use case isn't concrete —
|
||||
- **Resolution**: Not yet decidable. The use case isn't concrete:
|
||||
the mechanism is assembly-layer policy today (ADR-012), and a
|
||||
crate-level helper would need real deployment requirements to
|
||||
shape (subnet sizing, per-identity tables, hub exposure).
|
||||
@@ -236,7 +236,7 @@ sequence starts at the numbers in use. Format per
|
||||
the pre-publish session's escalation (2026-09-28): the residual is
|
||||
a real deployment concern (NAT is common), and the RFC's own drop
|
||||
rule was unimplemented
|
||||
- **Status**: partially resolved — option 2 landed in 0.1.0 (commit
|
||||
- **Status**: partially resolved: option 2 landed in 0.1.0 (commit
|
||||
`31fb11c`); option 3 deferred (below)
|
||||
- **Door type**: two-way (additive; the current shape is RFC-faithful)
|
||||
- **Priority**: low (post-fix: the common cases are covered)
|
||||
@@ -244,23 +244,23 @@ sequence starts at the numbers in use. Format per
|
||||
**IP** on the first parsed datagram and sends replies to the
|
||||
**latest-seen port** from that IP (the NAT port-rebind case keeps
|
||||
receiving); datagrams from other source IPs are dropped (RFC 1928's
|
||||
MUST, previously unimplemented — the spoof primitive shrank from
|
||||
MUST, previously unimplemented, so the spoof primitive shrank from
|
||||
any-source to same-IP off-path). The **remaining residual**: a full
|
||||
client IP change mid-association (a NAT *address* rebind) stops
|
||||
receiving replies — the RFC's expected-peer posture (the RFC pins
|
||||
receiving replies (the RFC's expected-peer posture: the RFC pins
|
||||
the expected IP per association too).
|
||||
- **Option 3 (the complete fix, deferred)**: per-ASSOCIATE relay
|
||||
sockets spawned by the TCP door, with the expected peer IP derived
|
||||
from the TCP control connection's remote address (the RFC's
|
||||
"acquire from the SOCKS server" reading) — RFC-faithful end to end
|
||||
"acquire from the SOCKS server" reading): RFC-faithful end to end
|
||||
and kills the same-IP spoof residual too. Deferred because it
|
||||
restructures the doors' client-correlation story (currently the
|
||||
loose assembly-layer wiring seam) and needs its own ADR — not
|
||||
loose assembly-layer wiring seam) and needs its own ADR; not
|
||||
buildable speculatively without a deployment that actually spells
|
||||
the wildcard bind *and* suffers address-level rebinds.
|
||||
- **Blocked on**: a concrete wildcard-bind deployment with
|
||||
address-level client mobility (QUIC connection migration is the
|
||||
likely driver — the channels association survives transport
|
||||
likely driver: the channels association survives transport
|
||||
migration while a pinned IP does not).
|
||||
- **Cross-references**: review 003 S-1, ADR-003 (the handle swap),
|
||||
ADR-014 (the doors own the accept-path defense), OQ-SK-03
|
||||
@@ -271,11 +271,11 @@ sequence starts at the numbers in use. Format per
|
||||
|----|--------|------------|--------------|
|
||||
| OQ-SK2-06 | deferred(scope) | a concrete sandboxed-adapter use case | `architecture/oq-sk2-06-wasm-adapter-story` |
|
||||
| OQ-SK2-07 | deferred(scope) | a concrete virtual-subnet deployment | `architecture/oq-sk2-07-virtual-subnet-helper` |
|
||||
| OQ-SK2-09 | partially resolved (option 3 deferred) | a wildcard-bind deployment with address-level client mobility | — (the posture is recorded here; a tracker task spawns with the deployment driver) |
|
||||
| OQ-SK2-09 | partially resolved (option 3 deferred) | a wildcard-bind deployment with address-level client mobility | none (the posture is recorded here; a tracker task spawns with the deployment driver) |
|
||||
|
||||
## References
|
||||
|
||||
- Phase 0 ledger: `docs/research/phase-0.md` §Open Questions
|
||||
(OQ-SK-01..09 — the full reasoning trails live there)
|
||||
(OQ-SK-01..09; the full reasoning trails live there)
|
||||
- The SDD process: `docs/sdd_process.md` (the OQ format; the
|
||||
deferred-OQ blocker-task half)
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-28 (OQ-SK2-09 indexed — the UDP door's peer-pin
|
||||
posture; OQ-SK2-06's trigger ownership checked; ADR-014 indexed —
|
||||
the idle-timeout posture; the targeted fork — ADR-013, the module is
|
||||
`src/rfc1928/`; the vendor extraction amendment — no `net` feature —
|
||||
2026-09-21; Phase 1 initial draft 2026-09-20 — the OQ-SK ledger
|
||||
last_updated: 2026-09-28 (OQ-SK2-09 indexed: the UDP door's peer-pin
|
||||
posture; OQ-SK2-06's trigger ownership checked; ADR-014 indexed: the
|
||||
idle-timeout posture; the targeted fork, ADR-013, the module is
|
||||
`src/rfc1928/`; the vendor extraction amendment, no `net` feature,
|
||||
2026-09-21; Phase 1 initial draft 2026-09-20; the OQ-SK ledger
|
||||
promoted into ADRs 001-012 and open-questions.md)
|
||||
---
|
||||
|
||||
# alksocks — Overview
|
||||
# alksocks: Overview
|
||||
|
||||
A SOCKS5 (RFC 1928) producer/consumer protocol crate on alkcall
|
||||
channels: an arbitrary-egress proxy service that **never binds a port
|
||||
@@ -16,7 +16,7 @@ unless explicitly configured to**, carrying the RFC conversation
|
||||
inside a channels data channel like any other produced resource. The
|
||||
sibling of alktty (`alk/tty`) and alktunnels (`alk/tunnel`) on the
|
||||
alkcall substrate; where alktunnels realizes the `-L`/`-R` forwarding
|
||||
flavors, alksocks realizes the `-D` flavor (dynamic / SOCKS5 proxy) —
|
||||
flavors, alksocks realizes the `-D` flavor (dynamic / SOCKS5 proxy),
|
||||
the one alktunnels explicitly deferred to composition. This document
|
||||
covers the crate's purpose, the resource model in brief, dependencies,
|
||||
the ALPN, feature gates, and the module map; component details are in
|
||||
@@ -33,9 +33,9 @@ front doors:
|
||||
`channels/socks5/sub` open op (registered via
|
||||
`ChannelCore::register_openable_with_establisher`, alkcall
|
||||
ADR-047/049). The establisher is a *session gate* (validates the
|
||||
opener identity; the SOCKS5 target is unknown at open time); the
|
||||
opener identity; the SOCKS5 target is unknown at open time); the
|
||||
open handler runs the RFC 1928 state machine in-stream over the
|
||||
channel's `BiStream` — the forked RFC 1928 core
|
||||
channel's `BiStream`; the forked RFC 1928 core
|
||||
([rfc1928.md](rfc1928.md)) drives negotiation, command read, and
|
||||
replies; the dial/egress/accept seams (ADR-004) supply the I/O
|
||||
planes; `alkcall::channels::pump_bidi` (ADR-050) is the data plane
|
||||
@@ -46,14 +46,14 @@ front doors:
|
||||
the associate session and the BIND reply#2 reader). *Wrapper-aware*
|
||||
means a client that knows the in-band conventions (the sentinel
|
||||
BND.ADDR, in-band frames) rather than a vanilla RFC 1928 client.
|
||||
Optionally it
|
||||
exposes the service locally: the **front door** — a real
|
||||
`socks5://127.0.0.1:port` endpoint relaying each accepted
|
||||
connection into a channel (the ssh `-D` local-exposure shape; the
|
||||
path curl/browser/tun2proxy point at).
|
||||
Optionally it
|
||||
exposes the service locally through the **front door**, a real
|
||||
`socks5://127.0.0.1:port` endpoint relaying each accepted
|
||||
connection into a channel (the ssh `-D` local-exposure shape; the
|
||||
path curl/browser/tun2proxy point at).
|
||||
- **The optional producer local backend**: the producer may serve
|
||||
from a real listener (feature-gated; the same door shape in the
|
||||
other direction — vanilla clients → a locally-served SOCKS5
|
||||
other direction: vanilla clients → a locally-served SOCKS5
|
||||
resource).
|
||||
|
||||
The defining posture (the crate's reason to exist as a separate
|
||||
@@ -76,7 +76,7 @@ binding decision belongs to the caller.
|
||||
SOCKS5 leg; alktunnels carries the tunnel; tun2proxy closes the
|
||||
loop.
|
||||
- The substrate-agnostic protocol layer: the forked RFC 1928 core is
|
||||
generic over `T: AsyncRead + AsyncWrite + Unpin` — a channels
|
||||
generic over `T: AsyncRead + AsyncWrite + Unpin`; a channels
|
||||
`BiStream` drops in directly (POC #1), a `TcpStream` drops in at
|
||||
the local backends. Neither substrate leaks into the protocol
|
||||
layer (the alktty `TtyBackend` / alktunnels pump-halves
|
||||
@@ -89,7 +89,7 @@ A SOCKS5 channel's life (full detail in [wire.md](wire.md) and
|
||||
|
||||
1. **Open (the establishment phase).** The consumer opens
|
||||
`channels/socks5/sub` with trivial params (ADR-001). The
|
||||
establisher is the session gate — it checks/records the opener
|
||||
establisher is the session gate: it checks/records the opener
|
||||
identity and does session-level policy; it never sees a target.
|
||||
Failure is a typed `channel:open_failed` (alkcall ADR-049); no
|
||||
phantom channel.
|
||||
@@ -99,8 +99,8 @@ A SOCKS5 channel's life (full detail in [wire.md](wire.md) and
|
||||
dispatch: CONNECT/ASSOCIATE require `socks5:open`; BIND requires
|
||||
`socks5:bind` (the command gate, ADR-006).
|
||||
3. **Data plane.** CONNECT: the dial seam (ADR-004) produces boxed
|
||||
halves; `pump_bidi` two-pumps channel ↔ target — raw
|
||||
pass-through, 0 B overhead. ASSOCIATE: the reply's BND.ADDR is a
|
||||
halves; `pump_bidi` two-pumps channel ↔ target (raw
|
||||
pass-through, 0 B overhead). ASSOCIATE: the reply's BND.ADDR is a
|
||||
pure RFC handle (the sentinel in-band); the datagram stage rides
|
||||
`[len: u16 BE][addr + data]` frames (ADR-003, the handle swap) to
|
||||
the egress seam. BIND: reply#1 → quiet wait → exactly one accept
|
||||
@@ -119,34 +119,34 @@ protocol layer; crate-level vocabulary is producer/consumer.
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
alksocks (default — wasm-clean)
|
||||
alksocks (default: wasm-clean)
|
||||
├── alkcall 0.10.1 (core types: Connection, ProtocolHandler, BiStream,
|
||||
│ AuthContext, Identity, AccessControl, OwnershipProvider;
|
||||
│ channels: ChannelCore/ChannelClient,
|
||||
│ register_openable_with_establisher, pump_bidi,
|
||||
│ Establishment/ChannelPlan + reply projection,
|
||||
│ ServingConfig identity seam — CF-005/006)
|
||||
│ the ServingConfig identity seam (CF-005/006))
|
||||
├── tokio (wasm-clean subset: rt, sync, io-util, macros, time)
|
||||
├── serde/serde_json (params schema), thiserror (error surface), tracing
|
||||
└── (no backend deps — sockets live in feature-gated modules)
|
||||
└── (no backend deps: sockets live in feature-gated modules)
|
||||
|
||||
alksocks (local feature) — non-wasm by design
|
||||
alksocks (local feature): non-wasm by design
|
||||
└── adds: tokio/net + socket2 (the feature's own socket providers) and
|
||||
the first-party local backends + front doors
|
||||
|
||||
alksocks (composed feature)
|
||||
└── adds: alktunnels 0.4.1 (the direct op egress; the bind-first
|
||||
listen accept; ADR-008's composed providers) — its default
|
||||
listen accept; ADR-008's composed providers), whose default
|
||||
crate is wasm-clean; the composed providers use only the
|
||||
ungated channels surface (verify with the wasm check)
|
||||
```
|
||||
|
||||
Same posture as alktty/alktunnels: the default crate is
|
||||
protocol-only and wasm-clean — the RFC 1928 core is an *extraction*
|
||||
protocol-only and wasm-clean; the RFC 1928 core is an *extraction*
|
||||
of fast-socks5's `T`-generic subset, containing zero kernel-net code,
|
||||
so there is no `net` feature at all (ADR-011 §Amendment);
|
||||
substrate I/O rides `local` alone (ADR-009 §Amendment).
|
||||
`fast-socks5` is NOT a Cargo dependency — it was extracted in-tree
|
||||
`fast-socks5` is NOT a Cargo dependency: it was extracted in-tree
|
||||
and forked (ADR-013); `/workspace/fast-socks5` stays as the
|
||||
differential-testing behavioral reference.
|
||||
|
||||
@@ -154,44 +154,44 @@ differential-testing behavioral reference.
|
||||
|
||||
| Feature | Contents | Wasm |
|
||||
|---------|----------|------|
|
||||
| *(default)* | the RFC 1928 core (`src/rfc1928/`, ADR-013 — codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types — protocol only; zero kernel-net code, wasm-clean by construction | yes |
|
||||
| *(default)* | the RFC 1928 core (`src/rfc1928/`, ADR-013: codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types; protocol only, zero kernel-net code, wasm-clean by construction | yes |
|
||||
| `local` | the crate's first-party local backends: TCP dial, UDP egress socket, BIND `TcpListener` accept, the front doors (consumer + producer local shapes); brings `tokio/net` + `socket2` into the graph | no (build error on wasm, the alktty posture) |
|
||||
| `composed` | the composed providers over alktunnels 0.4.1: direct-op egress (lazy destination→tunnel table), bind-first listen accept — adds the alktunnels dep | yes (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface — verify with the wasm check at implementation) |
|
||||
| `composed` | the composed providers over alktunnels 0.4.1: direct-op egress (lazy destination→tunnel table), bind-first listen accept; adds the alktunnels dep | yes (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface: verify with the wasm check at implementation) |
|
||||
|
||||
## Module Map
|
||||
|
||||
```
|
||||
src/
|
||||
lib.rs — re-exports; crate docs
|
||||
rfc1928/ — the RFC 1928 core: the targeted fork of
|
||||
lib.rs : re-exports; crate docs
|
||||
rfc1928/ : the RFC 1928 core: the targeted fork of
|
||||
fast-socks5's T-generic subset (ADR-013;
|
||||
extraction per ADR-011; fork point noted
|
||||
in-tree): consts, codecs, errors, typestate,
|
||||
client use_stream path; the fork's two
|
||||
carried changes (BIND reply surface,
|
||||
new_reply publication); no kernel-net code
|
||||
params.rs — the trivial params schema + SOCKS5_OPEN_SCOPE/
|
||||
params.rs : the trivial params schema + SOCKS5_OPEN_SCOPE/
|
||||
SOCKS5_BIND_SCOPE constants (ADR-001/006)
|
||||
wire.rs — the ASSOCIATE datagram frame codec, re-implemented
|
||||
wire.rs : the ASSOCIATE datagram frame codec, re-implemented
|
||||
in first-party code shaped on alktunnels' length
|
||||
codec (ADR-003; framing is wire-layer, applied
|
||||
exactly once)
|
||||
producer.rs — the open op spec (scope-gated), the session-gate
|
||||
producer.rs : the open op spec (scope-gated), the session-gate
|
||||
establisher, the command-dispatch pump handler
|
||||
(typestate driving + the three seams), teardown
|
||||
consumer.rs — the channels-native session types (connect/associate/
|
||||
consumer.rs : the channels-native session types (connect/associate/
|
||||
bind reply#2 reader), teardown ownership
|
||||
door.rs — the front doors (feature `local`): the raw-pipe
|
||||
door.rs : the front doors (feature `local`): the raw-pipe
|
||||
bridge + the RFC 1929 cred-bearing door + the
|
||||
credential→identity mapping (ADR-005); the UDP
|
||||
front door's RFC codec boundary (ADR-003)
|
||||
egress.rs — the egress/accept seam types (DialFn/EgressFactory/
|
||||
BindAcceptFactory contracts — ADR-004)
|
||||
local/ — feature `local`: the socket-backed provider
|
||||
egress.rs : the egress/accept seam types (DialFn/EgressFactory/
|
||||
BindAcceptFactory contracts, ADR-004)
|
||||
local/ : feature `local`: the socket-backed provider
|
||||
constructors (dial/egress/accept)
|
||||
composed/ — feature `composed`: the alktunnels-composed
|
||||
composed/ : feature `composed`: the alktunnels-composed
|
||||
providers (direct-op egress, bind-first accept)
|
||||
error.rs — SocksError + the typed open-error surface
|
||||
error.rs : SocksError + the typed open-error surface
|
||||
(alkcall ADR-049 §4)
|
||||
```
|
||||
|
||||
@@ -205,7 +205,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [001](decisions/001-alpn-and-open-op.md) | ALPN + open op + trivial params | `alk/socks5`, `channels/socks5/sub`, `{}` — the RFC conversation is the data plane |
|
||||
| [001](decisions/001-alpn-and-open-op.md) | ALPN + open op + trivial params | `alk/socks5`, `channels/socks5/sub`, `{}`: the RFC conversation is the data plane |
|
||||
| [002](decisions/002-protocol-base.md) | Protocol base | fast-socks5's explicit typestate API (extracted + forked, `src/rfc1928/`); faithful reply-code mapping |
|
||||
| [003](decisions/003-udp-handle-swap.md) | The handle swap | one channel per association; `{addr, data}` frames in-band; RFC codec at front doors only |
|
||||
| [004](decisions/004-dial-callback-shape.md) | The dial-callback shape | function-not-trait at three seams (dial/egress/accept); ownership rides the handle |
|
||||
@@ -213,7 +213,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
| [006](decisions/006-access-control.md) | Access-control posture | `socks5:open` gate + `socks5:bind` command gate; target policy = the dialer |
|
||||
| [007](decisions/007-bind-in-base.md) | BIND in the base crate | phase model extended (reply#1 → quiet wait → accept → reply#2 → pass-through); additive |
|
||||
| [008](decisions/008-provider-defaults.md) | Provider defaults | local-socket + composed (direct op) providers, both feature-gated; no implicit dialer |
|
||||
| [009](decisions/009-feature-gates.md) | Feature gates | `local` + `composed`; no `net` feature (the RFC 1928 core is wasm-clean by construction — ADR-011 §Amendment); default crate wasm-clean |
|
||||
| [009](decisions/009-feature-gates.md) | Feature gates | `local` + `composed`; no `net` feature (the RFC 1928 core is wasm-clean by construction; ADR-011 §Amendment); default crate wasm-clean |
|
||||
| [010](decisions/010-channel-capacity.md) | Channel capacity | no wire change; assembly-layer config + the documented proxy-workload recommendation |
|
||||
| [011](decisions/011-inline-vendor.md) | The extraction | the `T`-generic subset copied in-tree; two carried changes (BIND, `new_reply`); v1.0.0 noted (superseded as a posture by ADR-013) |
|
||||
| [013](decisions/013-targeted-fork.md) | The targeted fork | the extracted region is owned; `src/rfc1928/`; fork point `5b18d4d`; hand-port replaces re-vendor |
|
||||
@@ -225,13 +225,13 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
Open questions are tracked in [open-questions.md](open-questions.md).
|
||||
Key questions affecting this document:
|
||||
|
||||
- **OQ-SK2-06**: sandboxed-wasm-consumer adapter story (deferred(scope)
|
||||
— a product input, nothing waits on it; the trigger is external by
|
||||
- **OQ-SK2-06**: sandboxed-wasm-consumer adapter story (deferred(scope);
|
||||
a product input, nothing waits on it; the trigger is external by
|
||||
verified default)
|
||||
- **OQ-SK2-08**: recommended proxy-workload cap figure (open — a docs
|
||||
- **OQ-SK2-08**: recommended proxy-workload cap figure (open, a docs
|
||||
tuning note; ADR-010 pins the posture)
|
||||
- **OQ-SK2-09**: the UDP door's peer-pin posture (option 2 — the
|
||||
same-IP lock + reply-port refresh — landed in 0.1.0; option 3, the
|
||||
- **OQ-SK2-09**: the UDP door's peer-pin posture (option 2, the
|
||||
same-IP lock + reply-port refresh, landed in 0.1.0; option 3, the
|
||||
control-conn-derived expected peer, deferred on a concrete
|
||||
wildcard-bind deployment)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-24 (the open handler's phase lifetimes carry the
|
||||
decided timeout posture — ADR-014; Phase 1 initial draft 2026-09-20)
|
||||
decided timeout posture, ADR-014; Phase 1 initial draft 2026-09-20)
|
||||
---
|
||||
|
||||
# alksocks — The Producer Half
|
||||
# alksocks: The Producer Half
|
||||
|
||||
The producer exposes the SOCKS5 service as a produced, ACL-scoped
|
||||
resource: it registers the `channels/socks5/sub` open op (ADR-001)
|
||||
@@ -22,33 +22,33 @@ Registration uses `ChannelCore::register_openable_with_establisher`
|
||||
- the op spec `channels/socks5/sub` on the `alk/socks5` ALPN,
|
||||
`required_scopes = ["socks5:open"]` (`SOCKS5_OPEN_SCOPE`, ADR-006);
|
||||
- params schema `{type: object}`, `additionalProperties: false`
|
||||
(trivial — ADR-001);
|
||||
(trivial, ADR-001);
|
||||
- the establisher: validates/records the opener identity (the CF-005/
|
||||
CF-006 seam — ADR-005) and applies session-level policy. It runs
|
||||
`Establishment::default()`-shaped (no plan payload) — the dial
|
||||
CF-006 seam, ADR-005) and applies session-level policy. It runs
|
||||
`Establishment::default()`-shaped (no plan payload): the dial
|
||||
happens post-establishment, so there is nothing to thread
|
||||
establisher→handler (POC #1's wire note).
|
||||
|
||||
Failure semantics: a scope-less identity is `FORBIDDEN` at the
|
||||
registry ACL gate; establisher refusal is a typed
|
||||
`channel:open_failed` (`handler_error`-class or the mapped reason) —
|
||||
`channel:open_failed` (`handler_error`-class or the mapped reason),
|
||||
never a phantom channel. Session-level refusal is channels-level;
|
||||
target-level refusal is in-stream RFC (below).
|
||||
|
||||
**Harness wiring requirement** (POC #1, POC #2 re-witnessed): the
|
||||
accept-side install hook must call `channel0_conn.set_identity
|
||||
(resolved)` before building the `CallConnection` — the transport
|
||||
(resolved)` before building the `CallConnection`; the transport
|
||||
identity does not flow to channel-0 dispatch automatically. A real
|
||||
mTLS transport resolves the peer here. Without it, scope-gated opens
|
||||
fail closed with `FORBIDDEN "authentication required"` — correct
|
||||
fail closed with `FORBIDDEN "authentication required"`: correct
|
||||
behavior, but the wiring requirement is implicit.
|
||||
|
||||
## The Open Handler: RFC Conversation + Command Dispatch
|
||||
|
||||
The open handler runs the forked typestate over the channel's
|
||||
`BiStream` (`T`-generic — the POC #1 fit). Every phase is
|
||||
`BiStream` (`T`-generic, the POC #1 fit). Every phase is
|
||||
teardown-governed, never idle-expired (ADR-014): the handler parks on
|
||||
the handshake / stage reads like any other phase — no protocol-layer
|
||||
the handshake / stage reads like any other phase; no protocol-layer
|
||||
inactivity timer exists (the alkcall 10s establishment bound covers
|
||||
only the open-op round trip, ADR-049 §2); a silent peer holds the
|
||||
task + channel entry until the channels-level sentinels arrive, and
|
||||
@@ -57,28 +57,28 @@ wants an idle bound enforces it outside the protocol layer (an
|
||||
accept-path bound at a door; a caller-side timeout around a
|
||||
substrate-free `serve_socks5_over`):
|
||||
|
||||
1. **Negotiation**: `start(inner)` → `negotiate_auth(&methods)` —
|
||||
1. **Negotiation**: `start(inner)` → `negotiate_auth(&methods)`;
|
||||
NoAuth as a no-op (ADR-005: the producer side is identity-only;
|
||||
authorization already happened at the open gate).
|
||||
2. **Command read**: `read_command()` returns `(cmd, target)` —
|
||||
the dispatch point:
|
||||
2. **Command read**: `read_command()` returns `(cmd, target)`, the
|
||||
dispatch point:
|
||||
- **CONNECT** → the command gate (`socks5:open` already checked
|
||||
at the open gate — no extra check) → invoke `DialFn(target)`
|
||||
at the open gate, no extra check) → invoke `DialFn(target)`
|
||||
(ADR-004). Success: `reply_success` then the pass-through data
|
||||
plane. Refusal: `reply_error` with the mapped RFC code
|
||||
(dial-failure → `ConnectionRefused` 0x05; scope-class refusal
|
||||
→ `ConnectionNotAllowed` 0x02) — no phantom data plane.
|
||||
→ `ConnectionNotAllowed` 0x02); no phantom data plane.
|
||||
- **TCP BIND** → **the command gate**: require `socks5:bind`
|
||||
from the per-call opener identity (ADR-006) — an identity
|
||||
from the per-call opener identity (ADR-006); an identity
|
||||
without it gets `ConnectionNotAllowed` (0x02) while CONNECT
|
||||
still serves the same identity (POC #5 test 4). Then the BIND
|
||||
shape (ADR-007, below).
|
||||
- **UDP ASSOCIATE** → `reply_success(sentinel)` (the
|
||||
RFC-compatibility handle, ADR-003) then the datagram stage.
|
||||
3. **Data plane** per command (wire.md §The Data Plane):
|
||||
- CONNECT: `pump_bidi` inline inside the handler task (the
|
||||
returned `JoinHandle` tracks the data-plane lifetime — R-02,
|
||||
alkcall review 007,
|
||||
- CONNECT: `pump_bidi` inline inside the handler task (the
|
||||
returned `JoinHandle` tracks the data-plane lifetime: R-02,
|
||||
alkcall review 007,
|
||||
`/workspace/@alkdev/alkcall/docs/reviews/
|
||||
007-establishment-follow-ups-review.md`; early return =
|
||||
teardown-at-birth).
|
||||
@@ -101,7 +101,7 @@ substrate-free `serve_socks5_over`):
|
||||
|
||||
Contracts (POC-pinned, load-bearing):
|
||||
|
||||
- **Ownership rides the handle structurally** — handle payloads own
|
||||
- **Ownership rides the handle structurally**: handle payloads own
|
||||
their channel sessions/sockets; the compiler should enforce it (no
|
||||
`_` bindings of owning values; the third teardown-at-birth
|
||||
witness).
|
||||
@@ -109,21 +109,21 @@ Contracts (POC-pinned, load-bearing):
|
||||
frames once.
|
||||
- **The accept future owns its resources end-to-end**; the quiet
|
||||
wait's `select!` drop IS the client-gone teardown.
|
||||
- **No trait** — the re-evaluation threshold is three real
|
||||
- **No trait**: the re-evaluation threshold is three real
|
||||
implementations as trait impls (the alktunnels ADR-004 posture).
|
||||
|
||||
## Scope Gating (ADR-006)
|
||||
|
||||
- `SOCKS5_OPEN_SCOPE = "socks5:open"` — the open op's
|
||||
- `SOCKS5_OPEN_SCOPE = "socks5:open"`: the open op's
|
||||
`required_scopes`; egress capability (CONNECT + ASSOCIATE).
|
||||
- `SOCKS5_BIND_SCOPE = "socks5:bind"` — the BIND command gate,
|
||||
- `SOCKS5_BIND_SCOPE = "socks5:bind"`: the BIND command gate,
|
||||
checked at command dispatch from the per-call opener identity;
|
||||
ingress capability, never implied by `socks5:open`.
|
||||
- Target policy: the dialer/egress/accept callbacks refuse (RFC
|
||||
reply codes); no allowlist hook in v1 (ADR-006/ADR-012). Whatever
|
||||
ACL governs the socks5 resource governs everything reachable
|
||||
through it, plus the callbacks' policy.
|
||||
- Optional: consult `OwnershipProvider` for resource ownership — a
|
||||
- Optional: consult `OwnershipProvider` for resource ownership, a
|
||||
registration-time choice.
|
||||
|
||||
## Producer-Side Local Backend (feature `local`, optional)
|
||||
@@ -131,40 +131,40 @@ Contracts (POC-pinned, load-bearing):
|
||||
The optional shape where the producer serves from a real listener:
|
||||
a feature-gated backend binds `socks5://127.0.0.1:port` (or any
|
||||
address) and hands each accepted TCP connection to the landed
|
||||
substrate-free entry `Socks5Producer::serve_socks5_over` directly —
|
||||
substrate-free entry `Socks5Producer::serve_socks5_over` directly:
|
||||
the RFC conversation runs on the accepted socket (the NoAuth
|
||||
in-stream shape: greeting, command, reply, data plane, all on the
|
||||
accepted socket), and the seams dial/egress/accept locally. The same
|
||||
door shape in reverse: this is a *front door for the producer side*
|
||||
— vanilla clients reach a locally-served SOCKS5 resource through it
|
||||
door shape in reverse: this is a *front door for the producer side*;
|
||||
vanilla clients reach a locally-served SOCKS5 resource through it
|
||||
(`src/local/producer_door.rs`, the `ProducerDoor`/builder/guard
|
||||
surface). A cred-bearing variant (RFC 1929) would wrap
|
||||
`accept_password_auth` before `serve_socks5_over` — a documented
|
||||
`accept_password_auth` before `serve_socks5_over`, a documented
|
||||
follow-up, not landed.
|
||||
|
||||
The door's accept path owns the idle defense the protocol layer does
|
||||
not provide (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 — the cap regime does not bound it), plus the
|
||||
not a channel; the cap regime does not bound it), plus the
|
||||
max-concurrent-sessions cap (an accept beyond the bound is refused
|
||||
door-side, no session spawned) — the two constructor knobs.
|
||||
door-side, no session spawned); these are the two constructor knobs.
|
||||
|
||||
The producer-side local *backend* for the I/O planes (the
|
||||
local-socket providers — TCP dial, UDP egress, BIND listener) is
|
||||
local-socket providers: TCP dial, UDP egress, BIND listener) is
|
||||
ADR-008's variant set, also feature `local`. The BIND command gate
|
||||
on this path is the builder's `bind_policy` (there is no open-op
|
||||
identity on an accepted socket — the accepted socket is the whole
|
||||
caller; the pump handler's identity-derived gate does not apply).
|
||||
identity on an accepted socket; the accepted socket is the whole
|
||||
caller, and the pump handler's identity-derived gate does not apply).
|
||||
|
||||
## Wiring (Assembly Layer)
|
||||
|
||||
```rust
|
||||
let producer = Socks5Producer::builder()
|
||||
.dial(dial_fn) // DialFn — required for CONNECT
|
||||
.egress(egress_factory) // EgressFactory — required for ASSOCIATE
|
||||
.accept(bind_accept_factory) // BindAcceptFactory — required for BIND
|
||||
.dial(dial_fn) // DialFn: required for CONNECT
|
||||
.egress(egress_factory) // EgressFactory: required for ASSOCIATE
|
||||
.accept(bind_accept_factory) // BindAcceptFactory: required for BIND
|
||||
.build();
|
||||
// register(&core, ®istry, auth, witness) — the establisher rides
|
||||
// register(&core, ®istry, auth, witness): the establisher rides
|
||||
// the registration (the optional witness is the CF-006 test probe).
|
||||
producer.register(&core, ®istry, auth, None)?;
|
||||
```
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-24 (the keep-or-cut sweep + the refusal
|
||||
vocabulary variant — the fork's owned post-fork changes section;
|
||||
review 002 N-1/N-4; the targeted fork 2026-09-22 — ADR-013
|
||||
supersedes ADR-011's maintenance posture; the module is
|
||||
vocabulary variant: the fork's owned post-fork changes section;
|
||||
review 002 N-1/N-4; the targeted fork 2026-09-22, with ADR-013
|
||||
superseding ADR-011's maintenance posture; the module is
|
||||
`src/rfc1928/`, the fork point is `5b18d4d`, the lint-allow posture
|
||||
dissolves; extraction review F-1: `ReplyError::from_u8`'s panic arm
|
||||
mapped to `GeneralFailure`; Phase 1 initial draft 2026-09-20)
|
||||
---
|
||||
|
||||
# alksocks — The RFC 1928 Core (the targeted fork)
|
||||
# alksocks: The RFC 1928 Core (the targeted fork)
|
||||
|
||||
The crate owns its RFC 1928 protocol code in-tree: the **targeted
|
||||
fork** of fast-socks5's `T`-generic subset ([ADR-013](decisions/
|
||||
013-targeted-fork.md); the extraction mechanics in
|
||||
[ADR-011](decisions/011-inline-vendor.md); OQ-SK-04's resolution).
|
||||
This document is the fork's scope — what was extracted, what was
|
||||
not, the deliberate modifications — and the maintenance posture.
|
||||
This document is the fork's scope (what was extracted, what was
|
||||
not, the deliberate modifications) and the maintenance posture.
|
||||
The protocol base decision is ADR-002; the feature gating is
|
||||
ADR-009.
|
||||
|
||||
@@ -30,25 +30,25 @@ review-vendor-extraction.md`) is the recorded fidelity gate.
|
||||
|
||||
The code arrived by **extraction** (ADR-011 §Amendment): copy *only*
|
||||
the `T`-generic inventory from fast-socks5 v1.0.0 into the tree; the
|
||||
kernel-net handlers are **not copied** — they stay in
|
||||
kernel-net handlers are **not copied**; they stay in
|
||||
`/workspace/fast-socks5` (the read-only reference checkout).
|
||||
Concretely: POC #4's cfg map (its five-file inventory of what is
|
||||
kernel-net-gated) is the base of the extraction recipe — copy what
|
||||
kernel-net-gated) is the base of the extraction recipe: copy what
|
||||
POC #4 left *ungated*, minus the two recorded exclusions (the
|
||||
`socks4` module and the legacy `Config`/`Authentication` layer —
|
||||
see the not-forked inventory). The extraction contains zero
|
||||
`socks4` module and the legacy `Config`/`Authentication` layer; see
|
||||
the not-forked inventory). The extraction contains zero
|
||||
kernel-net code, so it is wasm-clean by construction; there is no
|
||||
in-tree `net`-shaped feature gate.
|
||||
|
||||
Post-extraction (ADR-013), the region is **owned code**: project
|
||||
conventions apply unconditionally, there is no upstream tree to
|
||||
stay reconcilable with, and the module is `src/rfc1928/` — named
|
||||
stay reconcilable with, and the module is `src/rfc1928/`, named
|
||||
for what it is (the RFC 1928 wire vocabulary and state machines),
|
||||
not for an ownership relation it no longer has.
|
||||
|
||||
## What was Extracted (now the forked core)
|
||||
|
||||
From fast-socks5 **v1.0.0** (MIT, third-party upstream —
|
||||
From fast-socks5 **v1.0.0** (MIT, third-party upstream;
|
||||
`/workspace/fast-socks5` is the read-only reference checkout; the
|
||||
fork point is noted in the module docs and the license notice):
|
||||
|
||||
@@ -58,34 +58,34 @@ fork point is noted in the module docs and the license notice):
|
||||
| The auth traits (`AuthMethod<T>`/`AuthMethodSuccessState<T>`) | the negotiation machinery (the crate uses `accept_no_auth` in-band; the traits stay) | `T`-generic |
|
||||
| `transfer` + `wait_on_tcp` | the typestate's own two-pump seam (the crate's data plane is `pump_bidi`; `transfer` is the interception-point shape) | `T`-generic |
|
||||
| The codecs: `new_udp_header`/`parse_udp_request`, `TargetAddr`/`to_be_bytes`/`read_address`, `ReplyError`/`SocksError`/`SocksServerError` | the RFC wire vocabulary everywhere (front doors, associate frames, reply#2 reader) | pure; `new_reply` published in-tree (fork change) |
|
||||
| The client types: `Socks5Stream<S>::use_stream` + the `AsyncRead/AsyncWrite` impls | the consumer half's stream session (ADR-002; POC #1's fit) — the first-party associate session in `consumer.rs` was informed by this shape | `T`-generic |
|
||||
| The client types: `Socks5Stream<S>::use_stream` + the `AsyncRead/AsyncWrite` impls | the consumer half's stream session (ADR-002; POC #1's fit); the first-party associate session in `consumer.rs` was informed by this shape | `T`-generic |
|
||||
| The non-net test modules (where extractable as unit tests) | RFC edge-case coverage rides in-tree | adjusted to the crate's test layout |
|
||||
|
||||
## What was NOT Forked (the cut, re-read as the hand-port map)
|
||||
|
||||
These stay in `/workspace/fast-socks5` (the differential-testing
|
||||
reference); the table below records the boundary — under the fork
|
||||
reference); the table below records the boundary; under the fork
|
||||
posture it is the **hand-port map**: what to port from the
|
||||
reference checkout if a future need surfaces (port it, adapt it to
|
||||
the crate's conventions, let the differential suite guard it —
|
||||
the crate's conventions, let the differential suite guard it;
|
||||
ADR-013). Rows 1–5 are POC #4's five-file cfg map (what POC #4
|
||||
gated is what was not extracted); the socks4 row and the legacy
|
||||
`Config`/`Authentication` layer are the recipe's two additions —
|
||||
`Config`/`Authentication` layer are the recipe's two additions:
|
||||
the socks4 module is out of RFC 1928 scope, and the legacy auth
|
||||
layer was left ungated by POC #4 (it compiles without net) but no
|
||||
extraction consumer reached it. None of it is forked:
|
||||
|
||||
| Upstream file | Region NOT in `src/rfc1928/` |
|
||||
|---|---|
|
||||
| `src/server.rs` | `run_tcp_proxy`, `run_udp_proxy`/`run_udp_proxy_custom`, `udp_bind_random_port`, the legacy `Socks5Server`/`Incoming`/`Socks5Socket` (struct + impls), `DnsResolveHelper` trait + impl, `handle_udp_*`/`transfer_udp`, the net-only test module — plus the legacy `Config<A>` + `Authentication` trait + impls (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`, `authenticate_callback`), which POC #4 left ungated but which only this cut region consumes (rows 1–5 are POC #4's gate map; the `Config`/`Authentication` layer and the socks4 row are this recipe's two additions) |
|
||||
| `src/server.rs` | `run_tcp_proxy`, `run_udp_proxy`/`run_udp_proxy_custom`, `udp_bind_random_port`, the legacy `Socks5Server`/`Incoming`/`Socks5Socket` (struct + impls), `DnsResolveHelper` trait + impl, `handle_udp_*`/`transfer_udp`, the net-only test module; plus the legacy `Config<A>` + `Authentication` trait + impls (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`, `authenticate_callback`), which POC #4 left ungated but which only this cut region consumes (rows 1–5 are POC #4's gate map; the `Config`/`Authentication` layer and the socks4 row are this recipe's two additions) |
|
||||
| `src/client.rs` | `Socks5Datagram` (struct + impl), the `Socks5Stream<TcpStream>` convenience impl (`connect`/`connect_with_password`/`connect_raw`) |
|
||||
| `src/util/stream.rs` | `tcp_connect*` fns (the `read_exact!`/`ready!` macros + `ConnectError` stay — `ConnectError` kept-with-note, review 001 C-1: constructed nowhere in-tree, kept as the hand-port map's companion for a future kernel-net port) |
|
||||
| `src/util/stream.rs` | `tcp_connect*` fns (the `read_exact!`/`ready!` macros + `ConnectError` stay; `ConnectError` kept-with-note, review 001 C-1: constructed nowhere in-tree, kept as the hand-port map's companion for a future kernel-net port) |
|
||||
| `src/util/target_addr.rs` | the `resolve_dns` method (the codec stays) |
|
||||
| `src/lib.rs` | the net-heavy test module (consts/errors/UDP-header codec stay) |
|
||||
| `src/socks4/` | the whole module (upstream's separate default-off `socks4` cargo feature; out of RFC 1928 scope) |
|
||||
|
||||
**Why none of it is needed in-tree** (ADR-011 §Amendment): no crate
|
||||
consumer routes through the kernel-net region — the producer drives
|
||||
consumer routes through the kernel-net region; the producer drives
|
||||
the typestate directly (the ASSOCIATE path is the POC #2
|
||||
typestate-driving shape; `run_udp_proxy_custom` is unusable on
|
||||
channels regardless), the consumer sessions use `use_stream` plus
|
||||
@@ -99,16 +99,16 @@ reference checkout, as an additive decision (ADR-013).
|
||||
|
||||
## The Deliberate Modifications (the fork's diff vs upstream v1.0.0)
|
||||
|
||||
The fork's diff vs upstream v1.0.0 is deliberate and bounded —
|
||||
The fork's diff vs upstream v1.0.0 is deliberate and bounded:
|
||||
**two changes touch the forked code**; the other two of the original
|
||||
four dissolve with the kernel-net region:
|
||||
|
||||
1. **BIND** — absent upstream (every handler refuses
|
||||
1. **BIND**: absent upstream (every handler refuses
|
||||
`Socks5Command::TCPBind`). The crate drives the typestate itself
|
||||
for the BIND shape (ADR-007); the fork carries the shape in-tree
|
||||
(`reply_success`'s `sock_addr` as the reply#1 carrier; the
|
||||
reply#2 writer using the published `new_reply`).
|
||||
2. **`new_reply` publication** — the reply encoder becomes public
|
||||
2. **`new_reply` publication**: the reply encoder becomes public
|
||||
(a `reply(code, addr)`-shaped constructor) so the BIND reply#2
|
||||
writer doesn't re-encode the 10/22-byte reply form.
|
||||
|
||||
@@ -116,10 +116,10 @@ Dissolved: the `net` feature gate (no net code in-tree) and the
|
||||
`run_udp_proxy_custom` bind seam (the handler isn't forked).
|
||||
|
||||
Plus the **owned-code post-fork changes** (the fork is first-class
|
||||
code — extend this list, never replace it; convention 17):
|
||||
code; extend this list, never replace it; convention 17):
|
||||
|
||||
- **The keep-or-cut sweep** (review 002 N-1): the producer-replaced
|
||||
machinery is cut from the typestate — the `auth_method_enums!`
|
||||
machinery is cut from the typestate: the `auth_method_enums!`
|
||||
macro (upstream's enum generator) with its generated
|
||||
`StandardAuthentication` enum + `allow_no_auth`,
|
||||
`skip_auth_this_is_not_rfc_compliant`, and `CheckResult`'s
|
||||
@@ -130,11 +130,11 @@ code — extend this list, never replace it; convention 17):
|
||||
typestate's). Kept with note (the hand-port map, ADR-013):
|
||||
`ConnectError` (constructed nowhere in-tree; the dial-error →
|
||||
reply-code mapping a future kernel-net port reuses) and
|
||||
`transfer`/`wait_on_tcp` (differential-suite-consumed — their
|
||||
`transfer`/`wait_on_tcp` (differential-suite-consumed; their
|
||||
semantics stay pinned there). The upstream `Bug` variant is
|
||||
likewise kept, its display intact.
|
||||
- **The refusal vocabulary variant** (review 002 N-4):
|
||||
`SocksServerError::Refused(&'static str)` — the honest
|
||||
`SocksServerError::Refused(&'static str)`: the honest
|
||||
log/observability vocabulary for the producer's in-stream policy
|
||||
refusals (the command gate + the four seam-refusal sites). Upstream
|
||||
has no such variant; it is an owned addition, never reaching the
|
||||
@@ -144,7 +144,7 @@ code — extend this list, never replace it; convention 17):
|
||||
|
||||
Plus the provenance notes: the fork point (upstream v1.0.0,
|
||||
extraction `57b107b..5b18d4d`, fork at the review commit) and the
|
||||
license notice (MIT, extended — ADR-013's license mechanics).
|
||||
license notice (MIT, extended; ADR-013's license mechanics).
|
||||
|
||||
Plus the **extraction-time adjustments** (mechanical edits applied
|
||||
at extraction, before the fork):
|
||||
@@ -154,8 +154,8 @@ at extraction, before the fork):
|
||||
out of RFC 1928 scope). `SocksError`'s `ReplySocks4Error` variant
|
||||
(also `#[cfg(feature = "socks4")]` upstream) goes with it.
|
||||
- The legacy `Config<A>`/`Authentication` auth layer is out
|
||||
(ungated upstream, but consumed only by the cut legacy/net region
|
||||
— `Socks5Server`/`Socks5Socket`/the `run_*` handlers; the
|
||||
(ungated upstream, but consumed only by the cut legacy/net region:
|
||||
`Socks5Server`/`Socks5Socket`/the `run_*` handlers; the
|
||||
typestate's own `accept_no_auth`/`accept_password_auth` are the
|
||||
crate's in-band auth surface). With it go `async-trait` (its four
|
||||
upstream uses are all on this layer), the example auth impls
|
||||
@@ -165,14 +165,14 @@ at extraction, before the fork):
|
||||
- Upstream `log` calls in the extracted region are dropped or
|
||||
re-pointed at the crate's `tracing`.
|
||||
- Upstream `anyhow` error wrapping (`Context` / `Other(#[from]
|
||||
anyhow::Error)`) is dropped — the crate's error surface is
|
||||
anyhow::Error)`) is dropped; the crate's error surface is
|
||||
`thiserror`-typed; the forked `SocksError`/`SocksServerError`
|
||||
stay the wire-level vocabulary.
|
||||
- The panic paths in the extracted region are translated (AGENTS.md
|
||||
convention 2 — no panics in library code): `ReplyError::from_u8`'s
|
||||
convention 2: no panics in library code): `ReplyError::from_u8`'s
|
||||
upstream `unreachable!()` arm for codes outside the RFC table
|
||||
maps unassigned codes (`0x09`–`0xFF`, RFC 1928 §6) to
|
||||
`GeneralFailure` instead — the value comes off the wire
|
||||
`GeneralFailure` instead; the value comes off the wire
|
||||
(`Socks5Stream::read_request_reply` feeds the raw reply byte
|
||||
there), so a misbehaving peer surfaces as a typed error, never a
|
||||
panic (the extraction review's F-1 correction).
|
||||
@@ -186,28 +186,28 @@ at extraction, before the fork):
|
||||
a request header would corrupt the conversation; byte-identical on
|
||||
a healthy stream). `reply_success`/`reply_error` were likewise
|
||||
converted at the fork review (001 F-1): replies are small,
|
||||
single-shot frames that must be all-or-nothing — a partial write
|
||||
single-shot frames that must be all-or-nothing; a partial write
|
||||
desynchronizes the conversation silently. (The
|
||||
`clippy::unused_io_amount` lint — deny-by-default — does flag bare
|
||||
`clippy::unused_io_amount` lint, deny-by-default, does flag bare
|
||||
`.write()` on `AsyncWriteExt`, but the `err_when`-chaining idiom
|
||||
type-infers the result through a generic blanket impl and suppresses
|
||||
it; the linter is not a substitute for the conversion.)
|
||||
|
||||
Plus the **post-fork convention fixes** (ADR-013 — the lint-allow
|
||||
Plus the **post-fork convention fixes** (ADR-013; the lint-allow
|
||||
posture dissolved, project conventions applied): the module-level
|
||||
`#[allow]`s removed and the suppressed smells fixed
|
||||
(`u8::max_value()` → `u8::MAX`, needless lifetimes elided,
|
||||
`match_like_matches_macro` applied, `io::Error::new(Other)` →
|
||||
`io::Error::other`, `wrong_self_convention` fixed on `AuthMethod::
|
||||
new`, the `ToSocketAddrs` error string re-worded — it no longer
|
||||
names the cut `resolve_dns()` — and the `#[allow(dead_code)]` on
|
||||
new`, and the `ToSocketAddrs` error string re-worded (it no longer
|
||||
names the cut `resolve_dns()`); the `#[allow(dead_code)]` on
|
||||
`AuthenticationMethod` swept). These are one-time, recorded for
|
||||
completeness; the fork is first-class code thereafter.
|
||||
|
||||
## Maintenance Posture
|
||||
|
||||
- **The fork is first-class code**: it co-evolves with the wrapper
|
||||
under the project conventions, unconditionally — no second
|
||||
under the project conventions, unconditionally: no second
|
||||
instruction set, no adjustment-list apparatus, no verbatim
|
||||
preservation (ADR-013).
|
||||
- **Differential testing**: `/workspace/fast-socks5` stays the
|
||||
@@ -215,19 +215,19 @@ completeness; the fork is first-class code thereafter.
|
||||
domain addressing, fragmentation). The crate's test suite covers
|
||||
the edge cases the forked code must keep passing; when in doubt,
|
||||
diff behavior against the reference checkout. The kernel-net
|
||||
region lives there too — the reference checkout is the only place
|
||||
region lives there too; the reference checkout is the only place
|
||||
the full upstream surface exists.
|
||||
- **Upstream posture**: fast-socks5 is third-party; nothing waits
|
||||
on it, nothing is absorbed from it. A future upstream fix that
|
||||
matters is **hand-ported** against the reference checkout and
|
||||
guarded by the differential suite (ADR-013's honest cost —
|
||||
guarded by the differential suite (ADR-013's honest cost,
|
||||
assessed low-risk: a quiet upstream, a well-trodden RFC surface).
|
||||
The offering-a-PR-upstream courtesy item is retired with the
|
||||
vendor posture (the net gate dissolved with the kernel-net
|
||||
region; there is no in-tree need to offer).
|
||||
- **Provenance is permanent**: the module docs and the bundled
|
||||
license notice always name the origin (fast-socks5 v1.0.0, the
|
||||
extraction range, the fork point) — orthogonal to ownership, kept
|
||||
extraction range, the fork point), orthogonal to ownership, kept
|
||||
so future readers can always see where the code came from
|
||||
(ADR-013).
|
||||
- **No second codec, ever**: the forked codecs are the only RFC
|
||||
@@ -252,10 +252,10 @@ None currently blocking this document.
|
||||
|
||||
- OQ-SK-04 (the resolved posture), OQ-SK-08 (BIND + `new_reply`),
|
||||
OQ-SK-03 (the bind seam)
|
||||
- POC #4 (`poc-connect-wasm-findings.md` — the measured gate), POC
|
||||
#5 (`poc-bind-findings.md` — the BIND driving + the `new_reply`
|
||||
- POC #4 (`poc-connect-wasm-findings.md`: the measured gate), POC
|
||||
#5 (`poc-bind-findings.md`: the BIND driving + the `new_reply`
|
||||
finding), POC #2 (the typestate-driving shape for ASSOCIATE)
|
||||
- `tasks/implementation/review-vendor-extraction.md` (the fidelity
|
||||
gate; F-1)
|
||||
- `/workspace/fast-socks5` (v1.0.0, MIT — the behavioral reference);
|
||||
- `/workspace/fast-socks5` (v1.0.0, MIT, the behavioral reference);
|
||||
`docs/research/iroh-socks5-eval.md` (why not the alternative base)
|
||||
+41
-41
@@ -1,19 +1,19 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-24 (the phase lifetimes' timeout posture decided —
|
||||
last_updated: 2026-09-24 (the phase lifetimes' timeout posture decided:
|
||||
teardown-governed, no protocol-layer idle bound, ADR-014; Phase 1
|
||||
initial draft 2026-09-20 — the POC-validated wire shapes promoted
|
||||
initial draft 2026-09-20; the POC-validated wire shapes promoted
|
||||
into the normative spec)
|
||||
---
|
||||
|
||||
# alksocks — Wire Format
|
||||
# alksocks: Wire Format
|
||||
|
||||
The `alk/socks5` wire surface. Two layers: the **open op** (JSON
|
||||
params on the channels call plane — trivial here, ADR-001) and the
|
||||
params on the channels call plane; trivial here, ADR-001) and the
|
||||
**data plane** (the RFC 1928 conversation + the data streams inside
|
||||
the channel `BiStream`). The channels layer strips its own 8-byte
|
||||
header transparently (alknet ADR-093 / alkcall ADR-035); alksocks
|
||||
owns everything inside the `BiStream` — which is, per the phase
|
||||
owns everything inside the `BiStream`, which is, per the phase
|
||||
model (ADR-003/ADR-007), the RFC conversation itself plus raw
|
||||
pass-through, with exactly one framed stage (ASSOCIATE datagrams).
|
||||
|
||||
@@ -23,14 +23,14 @@ Design decisions live in the ADRs ([001](decisions/001-alpn-and-open-op.md),
|
||||
[014](decisions/014-idle-timeout-posture.md)); this document is the normative
|
||||
WHAT. RFC 1928's own wire rules (method negotiation, command encoding,
|
||||
reply encoding, the RFC UDP datagram header) are specified by the RFC
|
||||
itself — this document covers the channels-specific surface and the
|
||||
itself; this document covers the channels-specific surface and the
|
||||
phase model's stage boundaries.
|
||||
|
||||
## The Open Op (`channels/socks5/sub`)
|
||||
|
||||
`Sub`-typed operation (alkcall ADR-047): the reply carries
|
||||
`{ "channel_id": <u32> }` once; everything else — the entire RFC 1928
|
||||
conversation and the data plane — flows on the channel's `BiStream`,
|
||||
`{ "channel_id": <u32> }` once; everything else (the entire RFC 1928
|
||||
conversation and the data plane) flows on the channel's `BiStream`,
|
||||
never in the call response stream. ALPN marker `alk/socks5` (ADR-001).
|
||||
|
||||
### Input (params)
|
||||
@@ -41,21 +41,21 @@ never in the call response stream. ALPN marker `alk/socks5` (ADR-001).
|
||||
|
||||
- Trivial: the SOCKS5 target is unknown at open time (it arrives
|
||||
in-stream post-negotiation); the establisher is a session gate, not
|
||||
a dial (POC #1's structural finding — ADR-004).
|
||||
a dial (POC #1's structural finding, ADR-004).
|
||||
- Schema: `{type: object}` with `additionalProperties: false`. Empty
|
||||
params stay valid forever; a future per-session option would be an
|
||||
additive schema extension (ADR-001).
|
||||
|
||||
### Reply / errors
|
||||
|
||||
- Success: `{ "channel_id": <u32> }` — the establishment phase (the
|
||||
- Success: `{ "channel_id": <u32> }`: the establishment phase (the
|
||||
session gate) succeeded, the open handler is running.
|
||||
- Failure: a typed `channel:open_failed` call error with
|
||||
`details: { reason, message }` — `reason ∈ dial_failed` /
|
||||
`details: { reason, message }` where `reason ∈ dial_failed` /
|
||||
`unknown_resource` / `resource_shortage` / `handler_error` /
|
||||
`timeout` (alkcall ADR-049 §3). The registry's ACL gate runs
|
||||
before the wrapper (`FORBIDDEN` for a scope-less identity —
|
||||
POC #1). **A failed open never returns a `channel_id`** — no
|
||||
before the wrapper (`FORBIDDEN` for a scope-less identity;
|
||||
POC #1). **A failed open never returns a `channel_id`**: no
|
||||
phantom channel. Session-level refusal is always channels-level;
|
||||
*target-level* refusal happens later, in-stream, as an RFC reply
|
||||
code (ADR-004).
|
||||
@@ -71,7 +71,7 @@ never in the call response stream. ALPN marker `alk/socks5` (ADR-001).
|
||||
|
||||
RFC 1928 is one command per connection, and control and data never
|
||||
interleave in either direction. The channel's `BiStream` therefore
|
||||
has a **phase structure with no mode byte** — the reply itself
|
||||
has a **phase structure with no mode byte**: the reply itself
|
||||
synchronizes the switch (POC #2 confirmed post-reply garbage is
|
||||
refused structurally; the demux-vs-phase trade is resolved by
|
||||
ADR-003/ADR-007):
|
||||
@@ -88,18 +88,18 @@ ADR-003/ADR-007):
|
||||
any post-reply control byte parses as a garbage ATYP and fails the
|
||||
association loud (`udp_undecodable_frame_ends_association`, POC
|
||||
#2). BIND: any byte in the quiet wait ends the session with no
|
||||
data plane (POC #5 — the RFC 1928 §3 reading).
|
||||
data plane (POC #5, the RFC 1928 §3 reading).
|
||||
- **No in-band control vocabulary.** The alktunnels resolution
|
||||
("no in-band control path, ever — the escape hatch is a new ALPN")
|
||||
("no in-band control path, ever; the escape hatch is a new ALPN")
|
||||
holds: no KEEPALIVE, no error frames, no flow-expiry frames. No
|
||||
idle/inactivity vocabulary either — every phase's lifetime is
|
||||
idle/inactivity vocabulary either: every phase's lifetime is
|
||||
teardown-governed (EOF / garbage / error sentinels), never
|
||||
protocol-layer timed (ADR-014; ADR-007 decided the BIND quiet
|
||||
wait's ends the same way). A deployment enforces idle policy at
|
||||
its edges, not in-band. Errors are EOF-shaped (channels-level) or
|
||||
reply-code-shaped (in-stream, pre-data-plane).
|
||||
- **One channel per SOCKS5 session** — no sub-demux, no flow key
|
||||
(AGENTS.md convention 10; the ASSOCIATE handle swap makes the
|
||||
- **One channel per SOCKS5 session** (no sub-demux, no flow key;
|
||||
AGENTS.md convention 10; the ASSOCIATE handle swap makes the
|
||||
channel the relay handle, ADR-003).
|
||||
|
||||
## The Data Plane
|
||||
@@ -107,7 +107,7 @@ ADR-003/ADR-007):
|
||||
### CONNECT: raw pass-through
|
||||
|
||||
Post-reply, the handler runs `alkcall::channels::pump_bidi(channel_
|
||||
bistream, target_read, target_write)` (ADR-050) — the canonical
|
||||
bistream, target_read, target_write)` (ADR-050), the canonical
|
||||
two-pump shape (alknet ADR-078); each pump shuts down the opposite
|
||||
sink on completion (half-close semantics fall out); EOF sentinels
|
||||
are the channels layer's. Zero protocol framing in-band: the RFC
|
||||
@@ -122,31 +122,31 @@ interception point with the parsed target. Per-target refusal:
|
||||
### UDP ASSOCIATE: the handle swap + framed datagram stage
|
||||
|
||||
The ASSOCIATE reply's `BND.ADDR`/`BND.PORT` is the sentinel
|
||||
`127.0.0.1:0` on the in-band path — pure RFC-compatibility surface;
|
||||
`127.0.0.1:0` on the in-band path (pure RFC-compatibility surface);
|
||||
the channel is the relay handle (ADR-003). The datagram stage rides
|
||||
the same channel:
|
||||
|
||||
- **Frame:** `[len: u16 BE][addr + data]` where `addr + data` is the
|
||||
RFC address encoding (`TargetAddr::to_be_bytes()` — ATYP + addr +
|
||||
RFC address encoding (`TargetAddr::to_be_bytes()`: ATYP + addr +
|
||||
port; the RFC UDP header minus RSV/FRAG). One frame per datagram,
|
||||
both directions. **A zero-length payload is a legal empty datagram
|
||||
(never EOF — the F-2 invariant, shaped on alktunnels ADR-003's
|
||||
codec, re-implemented in first-party `src/wire.rs`); `len == 0` —
|
||||
zero inner bytes — is unreachable from a compliant peer, since
|
||||
(never EOF; the F-2 invariant, shaped on alktunnels ADR-003's
|
||||
codec, re-implemented in first-party `src/wire.rs`); `len == 0`
|
||||
(zero inner bytes) is unreachable from a compliant peer, since
|
||||
`addr` is always ≥ 7 bytes, so the operative invariant lives at
|
||||
payload level.** The length covers
|
||||
`addr + data`, not data alone — the joint bound is the
|
||||
`addr + data`, not data alone; the joint bound is the
|
||||
constraint:** a frame is rejected (`Oversize`, at frame time)
|
||||
whenever `len(addr) + len(data) > 65535`. The UDP payload bound
|
||||
alone (65507) does *not* fit u16 unconditionally: a 65507-byte
|
||||
datagram to a 255-character domain is 65507 + 259 (max domain-form
|
||||
address: ATYP + length byte + 255 + port) = 65766 bytes of frame
|
||||
payload — a legal RFC 1928 datagram that would overflow u16. The
|
||||
payload, a legal RFC 1928 datagram that would overflow u16. The
|
||||
joint rejection keeps the length field honest: max data is 65276
|
||||
for domain-form destinations, 65516 for IPv6, 65528 for IPv4.
|
||||
Implementations must not use unchecked u16 arithmetic on the
|
||||
payload bound.
|
||||
- **RSV/FRAG do not ride the tunnel wire** — fragmentation cannot be
|
||||
- **RSV/FRAG do not ride the tunnel wire**: fragmentation cannot be
|
||||
expressed in-band; FRAG ≠ 0 is refused at front doors (the
|
||||
iroh-socks5 v1 posture).
|
||||
- **The RFC header codec (`new_udp_header`/`parse_udp_request`)
|
||||
@@ -157,7 +157,7 @@ the same channel:
|
||||
channels-level sentinel) ends the association; no extra control
|
||||
vocabulary (POC #2 test 4: a fresh associate succeeds immediately
|
||||
after a half-close teardown). No inactivity timer exists at the
|
||||
protocol layer — every phase (the pre-command handshake, the BIND
|
||||
protocol layer: every phase (the pre-command handshake, the BIND
|
||||
quiet wait, the association) is teardown-governed, never
|
||||
idle-expired (ADR-014); a deployment wanting an idle bound enforces
|
||||
it outside the protocol layer (door accept paths, caller-side
|
||||
@@ -165,7 +165,7 @@ the same channel:
|
||||
- **The stage is `select!`-driven** (channel reads ∥ egress replies),
|
||||
with the egress readiness contract: `next_reply` is `Pending` when
|
||||
nothing is ready (an instantly-ready `Ok(None)` starves a biased
|
||||
select into livelock — POC #2's load-bearing API finding).
|
||||
select into livelock; POC #2's load-bearing API finding).
|
||||
**Framing is applied exactly once, by the wire layer:**
|
||||
`Egress::next_reply` returns *inner* bytes (`addr + data`); the
|
||||
frame writer adds the single length prefix (the double-framing bug
|
||||
@@ -178,7 +178,7 @@ the same channel:
|
||||
open cost ~0.8 ms measured, per-target ACL for free).
|
||||
**Per-datagram `send` is awaited inline** (the stage interleaves
|
||||
sends with reply drains); a slow egress applies backpressure to
|
||||
the association, not to other channels — the batch-2
|
||||
the association, not to other channels; the batch-2
|
||||
composed-egress author inherits this bound knowingly.
|
||||
|
||||
### BIND: the phase model extended (ADR-007)
|
||||
@@ -187,13 +187,13 @@ the same channel:
|
||||
variant's real listener address; the sentinel on the wrapper-aware
|
||||
in-band path).
|
||||
- **The quiet wait**: no bytes are legal; the `select!` is
|
||||
single-shot with exactly two exits — the one inbound accept (from
|
||||
single-shot with exactly two exits: the one inbound accept (from
|
||||
the `BindAcceptFactory` seam, whose future owns the listener), or
|
||||
the control stream's EOF/garbage/error (the client-gone teardown
|
||||
riding the accept future's drop).
|
||||
- **reply#2 carries the peer address** (the real peer on both
|
||||
non-sentinel variants — local: `accept()`'s peer, byte-exact;
|
||||
composed: the forwarded op's `peer` param since the bind-first
|
||||
non-sentinel variants: local is `accept()`'s peer, byte-exact;
|
||||
composed is the forwarded op's `peer` param since the bind-first
|
||||
graduation, alktunnels `docs/research/tunnels-graduation.md`;
|
||||
sentinel `0.0.0.0:0` wrapper-aware).
|
||||
- **Phase 2 is raw pass-through** (`pump_bidi`, the CONNECT shape)
|
||||
@@ -204,7 +204,7 @@ the same channel:
|
||||
|
||||
### Sentinels and lifecycle (all commands)
|
||||
|
||||
- **EOF** is the channels-level zero-length chunk — per-direction
|
||||
- **EOF** is the channels-level zero-length chunk, per-direction
|
||||
(half-close); written by `pump_bidi`'s shutdown-on-completion or
|
||||
teardown. CONNECT/BIND data planes end on EOF; the ASSOCIATE
|
||||
association ends on EOF.
|
||||
@@ -212,7 +212,7 @@ the same channel:
|
||||
whole point); pump-phase failures are EOF-shaped (alkcall
|
||||
ADR-049 §6).
|
||||
- **BAST:** the ASSOCIATE datagram frame is the one binary framing
|
||||
beyond pass-through — its BAST document is [bast.md](bast.md).
|
||||
beyond pass-through; its BAST document is [bast.md](bast.md).
|
||||
|
||||
## Normative byte diagrams
|
||||
|
||||
@@ -227,7 +227,7 @@ UDP ASSOCIATE (the datagram stage, one direction's view):
|
||||
|
||||
```
|
||||
[len:u16 BE][ATYP + addr + port + data] [len:u16 BE][...] ...
|
||||
zero-length data: an empty datagram (legal payload, NOT EOF —
|
||||
zero-length data: an empty datagram (legal payload, NOT EOF;
|
||||
the F-2 invariant at payload level)
|
||||
len == 0 (no addr bytes): unreachable from a compliant peer
|
||||
EOF: the channels-level sentinel, outside the codec
|
||||
@@ -244,9 +244,9 @@ BIND (the phase sequence, one channel's view):
|
||||
## Open Questions
|
||||
|
||||
Open questions are tracked in [open-questions.md](open-questions.md).
|
||||
None currently open against this document — the datagram framing
|
||||
None currently open against this document: the datagram framing
|
||||
(the one-way door) is decided (ADR-003); the phase lifetimes'
|
||||
timeout posture is decided (ADR-014 — teardown-governed, no
|
||||
timeout posture is decided (ADR-014: teardown-governed, no
|
||||
protocol-layer idle bound); OQ-SK2-07 (the virtual-subnet
|
||||
translation layer) rides the dial callback and does not touch this
|
||||
surface.
|
||||
@@ -256,7 +256,7 @@ surface.
|
||||
- ADR-001 (op/params), ADR-003 (the handle swap + frame),
|
||||
ADR-004 (the seams + phase-model refusal paths), ADR-007 (BIND),
|
||||
ADR-011 (the forked codecs), ADR-014 (the teardown-governed
|
||||
phase lifetimes — no protocol-layer idle timeout)
|
||||
phase lifetimes; no protocol-layer idle timeout)
|
||||
- POC findings: `poc-connect-wasm-findings.md` (CONNECT pass-through,
|
||||
refusal mapping), `poc-udp-associate-findings.md` (the stage,
|
||||
readiness contract, framing-once invariant, association lifetime),
|
||||
@@ -266,4 +266,4 @@ surface.
|
||||
(channel open), ADR-049 (establishment/typed errors), ADR-050
|
||||
(`pump_bidi`); alktunnels ADR-003 (the length codec + F-2)
|
||||
- RFC 1928 (the protocol's own wire rules), iroh-socks5 eval
|
||||
(`docs/research/iroh-socks5-eval.md` — the frame prior art)
|
||||
(`docs/research/iroh-socks5-eval.md`, the frame prior art)
|
||||
+78
-78
@@ -1,52 +1,52 @@
|
||||
# Fuzzing alksocks: Research and Plan
|
||||
|
||||
**Status:** adopted — wave-1 targets (`datagram_codec`, `rfc1928_codec`,
|
||||
**Status:** adopted; wave-1 targets (`datagram_codec`, `rfc1928_codec`,
|
||||
`params_parse`) implemented, seeds committed, campaigns run clean (§7).
|
||||
Wave 2 (`session_pumps`) deferred to post-0.1 (§4). Campaigns run via the
|
||||
detached runner (§6) — **never as a foreground child of an agent
|
||||
detached runner (§6): **never as a foreground child of an agent
|
||||
session** (§5).
|
||||
**Date:** 2026-09-28
|
||||
**Upstream precedent:** `/workspace/@alkdev/alkcall/docs/research/fuzzing.md`
|
||||
(one real finding: the duplicate adopt/open destroying the live channel —
|
||||
(one real finding: the duplicate adopt/open destroying the live channel,
|
||||
fixed in 0.8.1) and
|
||||
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md` (wave-1/wave-2
|
||||
adopted, six harness-model findings, no crate bugs). Tooling, corpus
|
||||
policy, runner, and the no-hosted-CI posture are inherited from those
|
||||
docs wholesale — this doc does not re-litigate them; it scopes them to
|
||||
docs wholesale; this doc does not re-litigate them. It scopes them to
|
||||
this crate.
|
||||
|
||||
---
|
||||
|
||||
## 1. Should this crate be fuzzed?
|
||||
|
||||
**Yes — and alkcall's fuzz doc already assigned this to us.** Three
|
||||
**Yes, and alkcall's fuzz doc already assigned this to us.** Three
|
||||
lines, all pointing the same way:
|
||||
|
||||
1. **alkcall's fuzz doc §5 named this crate's surfaces as downstream
|
||||
targets.** Its "non-surfaces (deliberately)" note ends: *the channels
|
||||
layer carries opaque `Bytes` and the handler owns sub-stream framing
|
||||
(ADR-035) — those downstream parsers are downstream crates' fuzz
|
||||
targets.* Our `src/wire.rs` datagram codec **is** that named
|
||||
(ADR-035); those downstream parsers are downstream crates' fuzz
|
||||
targets. Our `src/wire.rs` datagram codec **is** that named
|
||||
downstream target (shaped on alktunnels' codec, which alktunnels
|
||||
fuzzed as its wave-1 first target — the same assignment, landed
|
||||
fuzzed as its wave-1 first target; the same assignment, landed
|
||||
here). Fuzzing it completes the upstream scope rather than
|
||||
duplicating it.
|
||||
|
||||
2. **We own an entire RFC handshake state machine fed by arbitrary
|
||||
peers — on both sides.** The targeted fork (`src/rfc1928/`, ADR-013)
|
||||
peers, on both sides.** The targeted fork (`src/rfc1928/`, ADR-013)
|
||||
is fully owned code with project conventions applying unconditionally
|
||||
(convention 17). Its parse surfaces — `read_address` (the address
|
||||
(convention 17). Its parse surfaces, namely `read_address` (the address
|
||||
codec), `negotiate_auth` (method negotiation), `read_command` (the
|
||||
command encoding), `read_username_password` (RFC 1929), the client's
|
||||
`read_request_reply` — are fed bytes produced by **arbitrary peers**
|
||||
command encoding), `read_username_password` (RFC 1929) and the client's
|
||||
`read_request_reply`, are fed bytes produced by **arbitrary peers**
|
||||
in both directions: the producer drives the server typestate against
|
||||
consumer bytes, and the consumer sessions + front doors drive the
|
||||
client path against producer bytes. An RFC state machine is exactly
|
||||
the shape the quinn-proto CVE class (RUSTSEC-2026-0037 — remote DoS
|
||||
the shape the quinn-proto CVE class (RUSTSEC-2026-0037, remote DoS
|
||||
via `unwrap()` in parsing, the upstream doc §1's strongest
|
||||
datapoint) lives in: hand-rolled state transitions, length arithmetic,
|
||||
and boundary handling. One fork-carried correction is already of this
|
||||
class — upstream's `ReplyError::from_u8` ended in `unreachable!()`
|
||||
class: upstream's `ReplyError::from_u8` ended in `unreachable!()`
|
||||
for unassigned reply codes; the fork made it a typed catch-all (the
|
||||
review's F-1) precisely because the value comes off the wire. A fuzz
|
||||
target is how that posture is *demonstrated* across the whole codec,
|
||||
@@ -54,13 +54,13 @@ lines, all pointing the same way:
|
||||
|
||||
3. **The differential suite's blind spot is exactly fuzzing's
|
||||
strength.** `tests/rfc1928-differential.rs` pins our fork's behavior
|
||||
against the reference checkout on *valid* RFC edge cases — curated
|
||||
against the reference checkout on *valid* RFC edge cases, curated
|
||||
inputs a human thought of. What no example-based suite provides:
|
||||
random *malformed* input exploration (the panic-on-attacker-bytes
|
||||
class), stateful interleavings, and resource-bound assertions under
|
||||
adversarial sequences. The fork is about to be published (0.1) as
|
||||
wire-stable; after that, targets are regression protection for a
|
||||
frozen surface — cheapest now.
|
||||
frozen surface (cheapest now).
|
||||
|
||||
**The honest caveat** (mirroring both upstream docs): the crate has no
|
||||
`unsafe` (checked at the crate root), the codec's decode direction is
|
||||
@@ -72,7 +72,7 @@ more than 255), and the fork's error vocabulary is typed end-to-end
|
||||
Expected yield is therefore not memory-safety bugs; it is (a) the
|
||||
demonstration that the infallibility/no-panic contracts hold on
|
||||
adversarial input, (b) the decoder-state bound assertion (alktunnels
|
||||
§1.3's finding class — a peer chunk amplifying into unbounded state),
|
||||
§1.3's finding class: a peer chunk amplifying into unbounded state),
|
||||
and (c) regression protection for the published wire surface. If the
|
||||
campaigns find nothing, that is the successful-negative-result posture
|
||||
alkcall's §1 codified.
|
||||
@@ -84,7 +84,7 @@ Identical to both upstream docs: **cargo-fuzz 0.13.2 + `arbitrary`
|
||||
`fuzz/rust-toolchain.toml`, the crate itself stays stable at MSRV 1.88.
|
||||
AFL optional/deferred; bolero deferred (the async-friction argument);
|
||||
libafl/honggfuzz/OSS-Fuzz not adopted. Sanitizer: `--sanitizer=address`
|
||||
with debug assertions (cargo-fuzz defaults) — no MSAN (no `unsafe` in
|
||||
with debug assertions (cargo-fuzz defaults); no MSAN (no `unsafe` in
|
||||
this crate).
|
||||
|
||||
**Layout (the upstream pattern, load-bearing):**
|
||||
@@ -97,7 +97,7 @@ fuzz/
|
||||
│ ├── datagram_codec.rs
|
||||
│ ├── rfc1928_codec.rs
|
||||
│ └── params_parse.rs
|
||||
├── shared/ alksocks-fuzz-shared — STABLE-toolchain library
|
||||
├── shared/ alksocks-fuzz-shared (STABLE-toolchain library)
|
||||
│ └── src/… invariant logic + corpus replay (§3)
|
||||
├── corpus/<target>/ committed hand-made seeds (§3)
|
||||
├── gen_fuzz_seeds.py deterministic seed generator (quiche pattern)
|
||||
@@ -106,16 +106,16 @@ fuzz/
|
||||
```
|
||||
|
||||
- **Invariant logic lives in `fuzz/shared/`**, not inline in the target
|
||||
binaries — the stable-toolchain corpus replay
|
||||
binaries: the stable-toolchain corpus replay
|
||||
(`cargo test --manifest-path fuzz/shared/Cargo.toml`) runs the SAME
|
||||
invariant functions the fuzzer runs, on stable, in the release
|
||||
verification checklist. This is alkcall §7.2 tier-3's deliverable
|
||||
minus automation; per alkcall §7.9 (adopted here identically) this
|
||||
repo has no hosted CI and will not get one — corpus replay as plain
|
||||
repo has no hosted CI and will not get one. Corpus replay as plain
|
||||
`cargo test` is the fuzz gate.
|
||||
- **Root `Cargo.toml` needs the explicit `[workspace]` table**
|
||||
(`members = ["."]`, `exclude = ["fuzz"]`) and `"fuzz/"` on the
|
||||
publish exclude list — without it cargo auto-discovers `fuzz/shared/`
|
||||
publish exclude list; without it cargo auto-discovers `fuzz/shared/`
|
||||
into the main workspace and the MSRV toolchain tries to build the
|
||||
nightly-consumed dev-deps (the exact failure alkcall hit).
|
||||
- **Corpus policy:** commit hand-made seeds, gitignore grown corpora
|
||||
@@ -129,23 +129,23 @@ Input: raw `&[u8]`. Drives `DatagramReader::feed` + `frame` /
|
||||
`frame_inner` / `addr_encoding_len`. This is the alktunnels wave-1
|
||||
`datagram_codec` target's shape, scoped to this crate's codec (the
|
||||
frame carries `addr + data` inner bytes and decodes to
|
||||
`(TargetAddr, payload)`, not opaque datagrams — the invariants follow
|
||||
`(TargetAddr, payload)`, not opaque datagrams; the invariants follow
|
||||
the inner structure). Invariants:
|
||||
|
||||
- **no-panic on any byte sequence** (the baseline).
|
||||
- **infallible reassembly**: `feed` never errors at the reassembly
|
||||
layer — every frame completes as `Ok` or as one of the four
|
||||
layer; every frame completes as `Ok` or as one of the four
|
||||
*decode-garbage* `WireError` variants (`EmptyFrame` / `BadAtyp` /
|
||||
`ShortAddress` / `DomainNotUtf8`); `Oversize`/`Address` are
|
||||
unreachable from `feed` (they are frame-time errors — a shape
|
||||
unreachable from `feed` (they are frame-time errors; a shape
|
||||
assertion pins the partition).
|
||||
- **empty-payload F-2 layering**: a decoded `(addr, empty)` pair comes
|
||||
only from a real `[len][addr]` frame whose payload region is empty —
|
||||
only from a real `[len][addr]` frame whose payload region is empty,
|
||||
never invented by the codec; a `len == 0` frame (zero inner bytes)
|
||||
always surfaces as `EmptyFrame`, never as an empty datagram.
|
||||
- **bad-ATYP loudness**: every `BadAtyp` error's byte is the actual
|
||||
first inner byte of its frame, and it is not one of the three
|
||||
RFC ATYPs (the post-reply control-byte semantic — any non-ATYP
|
||||
RFC ATYPs (the post-reply control-byte semantic: any non-ATYP
|
||||
control byte must fail loud, never guess).
|
||||
- **consumption accounting / splitting transparency**: feeding in
|
||||
3-byte slices yields exactly the datagram sequence the whole-input
|
||||
@@ -155,14 +155,14 @@ the inner structure). Invariants:
|
||||
well-framed input re-frames byte-for-byte.
|
||||
- **joint bound**: `frame` rejects iff
|
||||
`addr_encoding_len(addr) + payload.len() > MAX_INNER_LEN`, echoing
|
||||
`(total, max)` exactly — including the max-domain case where a legal
|
||||
`(total, max)` exactly, including the max-domain case where a legal
|
||||
RFC payload (65507 to a 255-char domain = 65766) must reject (the
|
||||
joint-bound reason the codec exists); `frame_inner` rejects iff
|
||||
`inner.len() > MAX_INNER_LEN`. The `checked_add` path (payload
|
||||
overflow wrapping the u16) rejects, never wraps.
|
||||
- **decoder-state bound**: `buf` + in-flight `current` stay bounded by
|
||||
the fed chunk size plus one frame (header + `MAX_INNER_LEN`) on any
|
||||
adversarial input — the alktunnels §1.3 finding-class assertion
|
||||
adversarial input, the alktunnels §1.3 finding-class assertion
|
||||
(state must never grow with input). The crate's `DatagramReader`
|
||||
exposes this via `is_mid_frame()` + a new `retained_bytes()`
|
||||
diagnostic (the alktunnels `retained_bytes()` posture).
|
||||
@@ -175,11 +175,11 @@ batched frames, split frames, garbage ATYP, non-UTF-8 domains, the
|
||||
### 3.2 `rfc1928_codec` (the forked RFC 1928 core, `src/rfc1928/`)
|
||||
|
||||
Input: raw `&[u8]`. Drives the fork's byte-level parsers over tokio
|
||||
duplex streams (the in-tree test shape — mock bytes, no sockets):
|
||||
duplex streams (the in-tree test shape, mock bytes, no sockets):
|
||||
|
||||
- **`read_address`** (the address codec, both directions consume it):
|
||||
no-panic on any ATYP/bytes; every failure is a typed `AddrError`
|
||||
(`IncorrectAddressType` for unknown ATYP — the F-1 class, already
|
||||
(`IncorrectAddressType` for unknown ATYP, the F-1 class, already
|
||||
fixed; never a panic); round-trip for every well-formed address
|
||||
(`to_be_bytes` → parse → equal).
|
||||
- **the server handshake** (`negotiate_auth` → `read_command` over a
|
||||
@@ -190,7 +190,7 @@ duplex streams (the in-tree test shape — mock bytes, no sockets):
|
||||
command + target.
|
||||
- **the client reply path** (`read_request_reply`): no-panic on any
|
||||
server byte sequence; unassigned reply codes map to the
|
||||
`GeneralFailure` catch-all (the F-1 pin — never `unreachable!()`);
|
||||
`GeneralFailure` catch-all (the F-1 pin, never `unreachable!()`);
|
||||
version mismatches are typed errors.
|
||||
- **`parse_udp_request`** (the RFC UDP header codec, front doors only
|
||||
but still wire-reachable): no-panic; garbage RSV / unknown ATYP /
|
||||
@@ -198,7 +198,7 @@ duplex streams (the in-tree test shape — mock bytes, no sockets):
|
||||
`new_udp_header`.
|
||||
- **auth sub-handshakes** (`read_username_password`, RFC 1929):
|
||||
no-panic; the u8 length fields bound allocations structurally
|
||||
(≤ 255 bytes — asserted); empty username/password are typed errors;
|
||||
(≤ 255 bytes, asserted); empty username/password are typed errors;
|
||||
round-trip accept/reject status bytes.
|
||||
- **`ReplyError::from_u8`** full-domain table: every `u8` maps, no
|
||||
panic, unassigned → catch-all (the direct probe of the F-1 class).
|
||||
@@ -222,16 +222,16 @@ generator) → `Socks5Params` deserialization + `socks5_open_spec()` +
|
||||
- **no-panic on any JSON-shaped input** (`deny_unknown_fields`
|
||||
dispatch, the registry-spec builder).
|
||||
- **the trivial-params pin**: `{}` parses, `Socks5Params::as_json()` ==
|
||||
`{}`, and unknown fields always reject (the loud-rejection posture —
|
||||
`{}`, and unknown fields always reject (the loud-rejection posture:
|
||||
the serde pin and the schema's `additionalProperties: false` agree).
|
||||
- **the spec is a constant**: `socks5_open_spec()`'s wire-visible
|
||||
fields (op id, ALPN, scopes, schemas) are identical on every
|
||||
execution — the open-op surface is wire-stable (ADR-001); the
|
||||
execution; the open-op surface is wire-stable (ADR-001); the
|
||||
reason-enum table equals the typed parse table (the schema/typed-view
|
||||
drift guard, mirrored from the in-suite test).
|
||||
- **the reason surface is a pass-through extraction, not a validated
|
||||
table**: `establishment_reason` yields `Some(details.reason)` iff the
|
||||
code is `channel:open_failed` and the reason is a string — any string
|
||||
code is `channel:open_failed` and the reason is a string; any string
|
||||
is surfaced as-is (the alktunnels harness-model finding #2, adopted
|
||||
here as the modeled contract); `SocksEstablishmentReason::parse`
|
||||
never panics and is total.
|
||||
@@ -239,29 +239,29 @@ generator) → `Socks5Params` deserialization + `socks5_open_spec()` +
|
||||
### 3.4 What is deliberately NOT a target (yet)
|
||||
|
||||
- **The producer pump (`serve_socks5_over`) / consumer sessions as
|
||||
stateful op-sequence targets** — wave 2, deferred (§4). The
|
||||
stateful op-sequence targets**: wave 2, deferred (§4). The
|
||||
handshake-level target (§3.2) covers the byte surface; the
|
||||
session-level interleavings (associate stage select-shape, BIND quiet
|
||||
wait, the two-pump CONNECT plane) are stateful shapes over a live
|
||||
channels stack — the alktunnels wave-2 pattern, worth its own session
|
||||
channels stack (the alktunnels wave-2 pattern), worth its own session
|
||||
post-0.1.
|
||||
- **The doors (`door.rs`, `local/`, `composed/`)** — feature-gated
|
||||
- **The doors (`door.rs`, `local/`, `composed/`)** are feature-gated
|
||||
assembly; their RFC-codec boundary (`parse_udp_request` /
|
||||
`new_udp_header`) is covered by the §3.2 target. The rest is I/O
|
||||
plumbing over the fuzzed cores, not parse surfaces.
|
||||
- **`front_door` RFC 1929 negotiation** — covered by §3.2's auth
|
||||
- **`front_door` RFC 1929 negotiation**: covered by §3.2's auth
|
||||
sub-handshake invariants (the door drives the same forked parsers).
|
||||
|
||||
## 4. Scope: two waves
|
||||
|
||||
**Wave 1 (this session):** the three targets above. All drive
|
||||
already-`pub` APIs — no `#[cfg(fuzzing)]` exposure needed (matching
|
||||
already-`pub` APIs; no `#[cfg(fuzzing)]` exposure needed (matching
|
||||
both upstream docs' experience across all eight targets to date). One
|
||||
diagnostic addition to the crate: `DatagramReader::retained_bytes()`
|
||||
(the decoder-state byte count §3.1's bound asserts against, in
|
||||
alktunnels' `retained_bytes()` style).
|
||||
|
||||
**Wave 2 (deferred — post-0.1):** `session_pumps`, the stateful
|
||||
**Wave 2 (deferred to post-0.1):** `session_pumps`, the stateful
|
||||
op-sequence target over the producer pump + consumer sessions in the
|
||||
alkcall `manager_routing` / alktunnels `session_pumps` style (live
|
||||
channels stack; ops over CONNECT/ASSOCIATE/BIND interleavings; the
|
||||
@@ -269,7 +269,7 @@ two-pump shutdown-on-completion contract, the egress readiness
|
||||
contract, and the datagram-stage exit matrix as invariants). Deferred
|
||||
because wave 1 covers the wire byte surface completely, the session
|
||||
harness is a large build, and the crate is one commit from its first
|
||||
publish — the alktunnels sequencing (wave 1 at 0.2.0's fuzz adoption,
|
||||
publish. The alktunnels sequencing (wave 1 at 0.2.0's fuzz adoption,
|
||||
wave 2 the same session) was possible there because its session shape
|
||||
was the crate's whole surface; here the byte-level targets carry the
|
||||
attacker-reachable load and the stateful session target is a separate
|
||||
@@ -282,108 +282,108 @@ wave 2 when scheduled.
|
||||
## 5. Operational isolation: campaigns never share fate with the agent session
|
||||
|
||||
**Adopted verbatim from the upstream docs (alkcall §7.6, alktunnels
|
||||
§5)** — an environment constraint, not a nice-to-have. Agent sessions
|
||||
§5)**: an environment constraint, not a nice-to-have. Agent sessions
|
||||
run fuzzer builds/campaigns through a bash tool that spawns the fuzzer
|
||||
as a child of the session's own process tree and cgroup; an OOM in a
|
||||
fuzz target must cost the fuzzer, never the session. Three layers, all
|
||||
mandatory: (1) libFuzzer soft limits (`-rss_limit_mb=2048`
|
||||
`-malloc_limit_mb=2048`) — never `ulimit -v` under ASAN; (2) fork mode
|
||||
`-malloc_limit_mb=2048`); never `ulimit -v` under ASAN; (2) fork mode
|
||||
(`-fork=1`) as blast-radius containment (crash/timeout/OOM kills only
|
||||
the short-lived child; the harness survives and persists the artifact;
|
||||
also the deadlock answer); (3) process detachment — the runner script
|
||||
also the deadlock answer); (3) process detachment: the runner script
|
||||
wraps `cargo fuzz run` in `setsid` + `nohup` + full redirection to a
|
||||
log file; poll, never `wait`.
|
||||
|
||||
Also inherited: the operational note that `cargo fuzz run` returns
|
||||
before its last fork-mode children fully exit — prefer `pgrep -af` to
|
||||
before its last fork-mode children fully exit; prefer `pgrep -af` to
|
||||
LIST, kill by explicit pid from the runner's echoed `pid=`, and never
|
||||
pattern-kill-and-recheck in one call.
|
||||
|
||||
## 6. No hosted CI
|
||||
|
||||
Per alkcall §7.9 / alktunnels §6 — the standing policy applies to this
|
||||
Per alkcall §7.9 / alktunnels §6, the standing policy applies to this
|
||||
repo identically: no hosted CI will exist; do not add Actions/Gitea
|
||||
Actions workflow files. **Corpus replay is the fuzz gate as plain
|
||||
`cargo test`**: `cargo test --manifest-path fuzz/shared/Cargo.toml`
|
||||
replays every committed seed through the invariant functions — add it
|
||||
replays every committed seed through the invariant functions, so add it
|
||||
to the release verification checklist (AGENTS.md). Campaigns run
|
||||
manually via the detached runner (§6) — e.g. before a release, or after
|
||||
manually via the detached runner (§6); e.g. before a release, or after
|
||||
touching `src/wire.rs`, `src/rfc1928/`, or `src/params.rs`.
|
||||
|
||||
## 7. Implementation log
|
||||
|
||||
- **2026-09-28 — proposal adopted** (this doc). Wave-1 targets
|
||||
- **2026-09-28: proposal adopted** (this doc). Wave-1 targets
|
||||
`datagram_codec` + `rfc1928_codec` + `params_parse`; wave 2 deferred
|
||||
(§4).
|
||||
- **2026-09-28 — `alkcall` bumped 0.8.0 → 0.8.1,
|
||||
- **2026-09-28: `alkcall` bumped 0.8.0 → 0.8.1,
|
||||
`alktunnels` 0.2.0 → 0.2.1.** Bug-fix releases, no API/wire change;
|
||||
alkcall's fix (the duplicate adopt/open destroying the live channel)
|
||||
is directly relevant to this crate's consumer sessions — every
|
||||
is directly relevant to this crate's consumer sessions: every
|
||||
session's teardown reaps via the manager, and the
|
||||
open-op-response-replayed scenario alkcall's changelog names is the
|
||||
consumer open path. Verification: full suite (default + all-features)
|
||||
green, clippy, fmt, wasm check (§8 commands).
|
||||
- **2026-09-28 — wave-1 implemented.** The `fuzz/` workspace laid out
|
||||
- **2026-09-28: wave-1 implemented.** The `fuzz/` workspace laid out
|
||||
per §2 (nightly-pinned subtree, stable `fuzz/shared` library, runner,
|
||||
dict, generator); the three targets over already-`pub` APIs — no
|
||||
dict, generator); the three targets over already-`pub` APIs, with no
|
||||
`#[cfg(fuzzing)]` exposure needed (the upstream docs' experience,
|
||||
now across eleven targets). One diagnostic addition to the crate:
|
||||
`DatagramReader::retained_bytes()` (§3.1's bound target). Seeds
|
||||
committed: 45 + 65 + 19 generated (fuzz/gen_fuzz_seeds.py) + 2
|
||||
campaign-found crash inputs promoted (`datagram_codec/seed-900`,
|
||||
`seed-901` — the §7 harness findings below).
|
||||
- **2026-09-28 — two harness-model defects caught by corpus replay
|
||||
`seed-901`, the §7 harness findings below).
|
||||
- **2026-09-28: two harness-model defects caught by corpus replay
|
||||
pre-campaign** (the alkcall §7.7 fuzzer-as-harness-checker pattern):
|
||||
1. **A `len == 0` frame is well-framed but decodes to
|
||||
`Err(EmptyFrame)`** — the re-frame round-trip first expected every
|
||||
`Err(EmptyFrame)`**: the re-frame round-trip first expected every
|
||||
well-framed frame to decode `Ok`. The frame count still holds over
|
||||
every frame; the byte-for-byte re-frame holds over the decodable
|
||||
ones; `EmptyFrame` is pinned as the only zero-inner-byte shape.
|
||||
The same arm widened for `ShortAddress`/`DomainNotUtf8`/
|
||||
`BadAtyp`: a well-framed frame can carry any addr-garbage shape —
|
||||
`BadAtyp`: a well-framed frame can carry any addr-garbage shape;
|
||||
the decode refuses the addr, and the raw frame is the only
|
||||
re-frame possible.
|
||||
2. **A fieldless `deny_unknown_fields` struct deserializes from ANY
|
||||
empty JSON composite** (the serde seq form `[]` included) — not
|
||||
empty JSON composite** (the serde seq form `[]` included), not
|
||||
only `{}`. Not wire-reachable (the registry's `type: object`
|
||||
input schema is the gate that keeps non-object forms from ever
|
||||
reaching the type), but the harness must model the serde reality:
|
||||
the trivial-params invariant is now "an empty composite parses,
|
||||
everything non-empty rejects, the canonical round-trip is `{}`."
|
||||
- **2026-09-28 — first campaign finding (fork-mode containment as
|
||||
designed): a third harness-model defect.** The `datagram_codec`
|
||||
- **2026-09-28: first campaign finding, fork-mode containment as
|
||||
designed (a third harness-model defect).** The `datagram_codec`
|
||||
campaign crashed at ~35 s on input `0005 0301 1203 03bb 00`
|
||||
(`BadAtyp(0)` shape) — the re-frame walk's `Err` arm (fixed in the
|
||||
(`BadAtyp(0)` shape); the re-frame walk's `Err` arm (fixed in the
|
||||
replay pass above) still omitted `BadAtyp`. Fixed; the crash input is
|
||||
committed as `seed-900`; the campaign relaunched clean.
|
||||
- **2026-09-28 — second campaign finding (also harness, fork-mode
|
||||
containment): the count walk's `declared >= 7` gate excluded short
|
||||
domains.** Input `0005 0301 1203 03bb 00` — a domain frame with a
|
||||
- **2026-09-28: second campaign finding (also harness, fork-mode
|
||||
containment); the count walk's `declared >= 7` gate excluded short
|
||||
domains.** Input `0005 0301 1203 03bb 00`, a domain frame with a
|
||||
1-byte domain (`addr_len = 5`) is a legal empty-payload frame, but
|
||||
the walk's `declared >= 7` gate (meant to skip addr-less frames)
|
||||
rejected it; the per-ATYP `addr_len` computation already does that
|
||||
job. The gate lowered to `>= 4` (the minimum ATYP + length-byte
|
||||
prefix the walk reads); the crash input is committed as `seed-901`;
|
||||
the campaign relaunched clean.
|
||||
- **2026-09-28 — wave-1 campaigns run (10-min detached runs, §6
|
||||
- **2026-09-28: wave-1 campaigns run (10-min detached runs, §6
|
||||
runner; `-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048
|
||||
-timeout=25`; codec `-max_len=65536`, rfc1928
|
||||
`-max_len=4096 -use_value_profile=1`, params
|
||||
`-dict=json.dict -use_value_profile=1). All three exited 0 with
|
||||
empty artifact directories — no crashes, hangs, OOMs, or leaks.**
|
||||
- `datagram_codec` (post-harness-fixes) — ~1.9M execs (~3k exec/s),
|
||||
empty artifact directories: no crashes, hangs, OOMs, or leaks.**
|
||||
- `datagram_codec` (post-harness-fixes), ~1.9M execs (~3k exec/s),
|
||||
33 fork jobs, `oom/timeout/crash: 0/0/0` throughout. Coverage
|
||||
saturated at 481 edges (ft 2222, corp 209) — the 2-byte-header
|
||||
saturated at 481 edges (ft 2222, corp 209); the 2-byte-header
|
||||
state machine is fully enumerated (the alktunnels wave-1
|
||||
saturation profile).
|
||||
- `rfc1928_codec` — ~2.0M execs (~3.6k exec/s), 33 fork jobs,
|
||||
- `rfc1928_codec`, ~2.0M execs (~3.6k exec/s), 33 fork jobs,
|
||||
`oom/timeout/crash: 0/0/0` throughout. Coverage saturated at 1429
|
||||
edges (ft 7987, corp 704) — the handshake state machines'
|
||||
edges (ft 7987, corp 704); the handshake state machines'
|
||||
byte-level shape space is fully enumerated; both protocol
|
||||
directions held across every adversarial sequence.
|
||||
- `params_parse` — ~3.1M execs (~5.5k exec/s), 33 fork jobs,
|
||||
- `params_parse`, ~3.1M execs (~5.5k exec/s), 33 fork jobs,
|
||||
`oom/timeout/crash: 0/0/0` throughout. Coverage 1512 edges (ft
|
||||
12970, in-memory corpus 2981 from the 19 committed seeds) — the
|
||||
12970, in-memory corpus 2981 from the 19 committed seeds); the
|
||||
serde/untagged shape space was still expanding at budget end; the
|
||||
spec-constant and pass-through-reason invariants held everywhere.
|
||||
- The decoder-state bound (§3.1) held everywhere:
|
||||
@@ -392,13 +392,13 @@ touching `src/wire.rs`, `src/rfc1928/`, or `src/params.rs`.
|
||||
committed seeds (the §2 policy).
|
||||
- **NO crate bugs found by wave 1.** All three findings this session
|
||||
were harness-model defects, each invisible to example-based review
|
||||
and caught by fuzzing/replay — the upstream "fuzzer as a
|
||||
and caught by fuzzing/replay: the upstream "fuzzer as a
|
||||
harness-checker" pattern, confirmed a third time (alkcall §7.7,
|
||||
alktunnels §7, here §7). The codec's contracts (infallible
|
||||
reassembly, the F-2 layering, the joint bound, the retained-bytes
|
||||
bound) and the RFC core's no-panic posture held across every
|
||||
adversarial input the campaigns generated.
|
||||
- (progress appended as implementation proceeds — see git log)
|
||||
- (progress appended as implementation proceeds; see git log)
|
||||
|
||||
## 8. Verification commands
|
||||
|
||||
@@ -425,11 +425,11 @@ Campaign flags per target: `datagram_codec -max_len=65536`;
|
||||
runner, corpus policy, no-CI posture):
|
||||
`/workspace/@alkdev/alkcall/docs/research/fuzzing.md`,
|
||||
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md`
|
||||
- alkcall 0.8.1 changelog — the finding our consumer open/adopt paths
|
||||
- alkcall 0.8.1 changelog, the finding our consumer open/adopt paths
|
||||
are exposed to
|
||||
- This crate: ADR-001 (op/params), ADR-003 (the frame codec), ADR-013
|
||||
(the targeted fork), `docs/architecture/wire.md`,
|
||||
`docs/architecture/rfc1928.md`
|
||||
- External: RUSTSEC-2026-0037 (quinn-proto — the maintainers'
|
||||
- External: RUSTSEC-2026-0037 (quinn-proto, the maintainers'
|
||||
fuzzing-coverage statement), rust-fuzz trophy case, cargo-fuzz Rust
|
||||
Fuzz Book (all linked from the upstream doc's §9)
|
||||
@@ -3,7 +3,7 @@ status: complete
|
||||
last_updated: 2026-09-13
|
||||
---
|
||||
|
||||
# alksocks research — iroh-socks5 as an alternative base
|
||||
# alksocks research: iroh-socks5 as an alternative base
|
||||
|
||||
Evaluation of `iroh-socks5` 0.5.0 (crates.io, published 2026-08-31 by
|
||||
Matthew Geddes) as a potential base for alksocks, in place of or in
|
||||
@@ -62,7 +62,7 @@ Module map:
|
||||
Better than the README's self-deprecation suggests ("LLM-assisted... All
|
||||
bugs are mine"). The wire codecs are clean, bounded, and unit-tested;
|
||||
the integration suite (`tests/integration.rs`, 12 tests under
|
||||
`test-utils`) exercises real iroh connections — direct and via-relay
|
||||
`test-utils`) exercises real iroh connections (direct and via-relay
|
||||
dials, CONNECT (IP + domain), refused-target error mapping, full UDP
|
||||
ASSOCIATE round-trip, BIND, allow-list accept/deny, greeting edge cases.
|
||||
Doc comments are genuinely explanatory. Notable specifics:
|
||||
@@ -72,29 +72,29 @@ Doc comments are genuinely explanatory. Notable specifics:
|
||||
serde (not hand-rolled enums) for the inner types.
|
||||
- **UDP header codec** (`socks5.rs` `parse_udp_packet`/
|
||||
`format_udp_packet`): correct ATYP handling, uses `get(..)?` bounds
|
||||
checks, propagates the FRAG byte, domain targets supported — same
|
||||
checks, propagates the FRAG byte, domain targets supported; same
|
||||
functional slot as fast-socks5's `new_udp_header`/`parse_udp_request`.
|
||||
- **Half-close propagation** in `relay_tcp`: each direction signals
|
||||
`finish()`/`shutdown()` on completion — a hand-rolled two-pump that
|
||||
`finish()`/`shutdown()` on completion, a hand-rolled two-pump that
|
||||
*does* shut down the opposite sink (the ADR-050 contract), then joins
|
||||
both halves (`tokio::join!` + propagate both errors).
|
||||
- **UDP association lifecycle**: the client side ends the association
|
||||
when the SOCKS5 control connection closes (`wait_until_closed`
|
||||
drains-and-detects EOF on the control TCP), not merely when the UDP
|
||||
socket goes quiet — the RFC's "TCP control connection closes →
|
||||
socket goes quiet; it implements the RFC's "TCP control connection closes →
|
||||
association ends" semantic, which many proxies get wrong.
|
||||
- **Reply-code mapping** is faithful where it matters (refused vs
|
||||
unreachable distinguishes `ErrorKind::ConnectionRefused`).
|
||||
|
||||
Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
|
||||
- **RFC 1929 username/password is absent** — no-auth only
|
||||
(`negotiate` writes `0xFF` if the client doesn't offer no-auth). Auth
|
||||
- **RFC 1929 username/password is absent** (no-auth only:
|
||||
`negotiate` writes `0xFF` if the client doesn't offer no-auth). Auth
|
||||
is the iroh `EndpointId` allow-list at the connection level.
|
||||
- **BIND's second-incoming-connection shape** accepts exactly one
|
||||
inbound connection and has no timeout or multi-accept handling (RFC
|
||||
1928 BIND is notoriously underspecified; this is a reasonable minimal
|
||||
read, but it binds a real `TcpListener` — the exact thing alksocks'
|
||||
read, but it binds a real `TcpListener`, the exact thing alksocks'
|
||||
no-bind principle forbids in the protocol layer).
|
||||
- **UDP relay is one socket + flow-table-free on the server side**: all
|
||||
datagrams from the association share one `UdpSocket`, replies are
|
||||
@@ -102,10 +102,10 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
back as `TargetAddr::Ip(from)`). Fine for the common case; no
|
||||
per-endpoint flow table like alknet's `Socks5UdpSocket` model.
|
||||
- **`allowlist_denies_unknown_peer`** closes the QUIC connection without
|
||||
an RFC reply — a vanilla client sees a hard close, not a SOCKS5 error
|
||||
an RFC reply; a vanilla client sees a hard close, not a SOCKS5 error
|
||||
code. (In alksocks this maps to the typed `channel:open_failed`
|
||||
establisher refusal, which POC #1 already handles better.)
|
||||
- Client-side UDP binds a real socket per association (necessarily —
|
||||
- Client-side UDP binds a real socket per association (necessarily:
|
||||
it's the front door for vanilla clients); server-side UDP likewise
|
||||
binds.
|
||||
|
||||
@@ -117,7 +117,7 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
+ `Router::accept(ALPN, handler)` is the same posture as alkcall's
|
||||
`register_openable` (ALPN-scoped produced resource, handler per
|
||||
connection). iroh 1.0's `ProtocolHandler` is even *single-method*
|
||||
(`accept(Connection)`) — structurally the open-handler shape POC #1
|
||||
(`accept(Connection)`), structurally the open-handler shape POC #1
|
||||
used. The crate's ALPN string convention (`iroh-socks5/1`) matches
|
||||
the alk/ ALPN convention too.
|
||||
2. **One stream per session, no sub-demux.** The `Request`/`Reply`
|
||||
@@ -129,10 +129,10 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
dials), `Client` = consumer half with the optional local TCP front
|
||||
door (`bind_listener`/`serve` is the ssh `-D` shape POC #1's
|
||||
`local_front_door` example validated). The *only* consumer path is
|
||||
the local bind — there is no in-band (channels-native) client session
|
||||
the local bind; there is no in-band (channels-native) client session
|
||||
type, which is where alksocks' wrapper differs.
|
||||
4. **Target resolution on the producing side.** Domains are carried
|
||||
unresolved (`TargetAddr::Domain`) and resolved at the exit node —
|
||||
unresolved (`TargetAddr::Domain`) and resolved at the exit node,
|
||||
the ssh `-D` semantic phase-0 §OQ-SK-05 notes, with a pluggable
|
||||
resolver (`DnsResolver`) and a privacy rationale (DoH so the exit
|
||||
host's network can't observe names) that mirrors the iroh-privacy
|
||||
@@ -144,13 +144,13 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
`client.rs` are written against `iroh::endpoint::{Connection,
|
||||
SendStream, RecvStream}` concretely; `relay.rs` signatures take
|
||||
`&mut SendStream`. Extracting a substrate-agnostic core would mean
|
||||
keeping only `protocol.rs` + `socks5.rs` (~550 LoC of codecs) — and
|
||||
keeping only `protocol.rs` + `socks5.rs` (~550 LoC of codecs); and
|
||||
those are the parts fast-socks5 already provides in stricter, more
|
||||
complete form (typestate, auth traits, reply-code enums, generic `T`).
|
||||
2. **Not generic over the stream.** The whole reason POC #1 could drop a
|
||||
channels `BiStream` into fast-socks5's `Socks5ServerProtocol<T>` with
|
||||
zero adaptation is fast-socks5's `T: AsyncRead + AsyncWrite + Unpin`
|
||||
genericity (AGENTS.md convention 7). iroh-socks5 has no such seam —
|
||||
genericity (AGENTS.md convention 7). iroh-socks5 has no such seam;
|
||||
the equivalent refactor would be a rewrite, not a fork.
|
||||
3. **RFC 1928 subset.** No username/password auth (OQ-SK-02's RFC-facing
|
||||
path), no auth-method negotiation machinery (fast-socks5's
|
||||
@@ -158,13 +158,13 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
(reply codes are bare `u8` consts). fast-socks5 is a conformant
|
||||
implementation with a test suite covering the RFC edge cases;
|
||||
iroh-socks5 is a pragmatic subset.
|
||||
4. **tokio "full" + iroh pin** (`iroh = "=1.0.3"`, exact pin) — heavy
|
||||
4. **tokio "full" + iroh pin** (`iroh = "=1.0.3"`, exact pin) is a heavy
|
||||
dependency for the wasm-clean default-crate invariant (OQ-SK-04);
|
||||
the vendored-subset posture would reduce it to the two codec files
|
||||
anyway, at which point fast-socks5's already-POC'd fork (POC #4:
|
||||
~120 cfg lines, functional wasm-clean subset) dominates.
|
||||
5. **No in-band client session type.** The only consumer path is the
|
||||
local TCP front door — there is no client half that speaks the
|
||||
local TCP front door; there is no client half that speaks the
|
||||
framed protocol from inside a process without a kernel listener
|
||||
(fast-socks5's `Socks5Stream::use_stream` slot). alksocks'
|
||||
consumer half needs exactly that; iroh-socks5 doesn't offer it.
|
||||
@@ -172,18 +172,18 @@ Weaknesses (acceptable for its scope, relevant if borrowed):
|
||||
### The valuable part: OQ-SK-03 evidence
|
||||
|
||||
iroh-socks5 implements channels-native UDP ASSOCIATE without binding a
|
||||
relay-visible port on the producer side beyond the real egress socket —
|
||||
relay-visible port on the producer side beyond the real egress socket;
|
||||
the exact question OQ-SK-03 poses. Its answers:
|
||||
|
||||
- **Datagram stage rides the same stream** (its "second channel" option
|
||||
is not used; the association's stream carries framed `Datagram`s
|
||||
post-reply). Matches the phase-model hunch, including "control stream
|
||||
goes silent after the reply" — nothing else is ever sent on the
|
||||
goes silent after the reply": nothing else is ever sent on the
|
||||
control stream after the ASSOCIATE reply; the client-side relay
|
||||
monitors the control connection's EOF (`wait_until_closed`) as the
|
||||
association's end-of-life, and the RFC's "post-reply control data is
|
||||
garbage" semantic holds.
|
||||
- **The datagram framing is a length prefix, not the RFC UDP header** —
|
||||
- **The datagram framing is a length prefix, not the RFC UDP header**:
|
||||
the RFC UDP header (`RSV/FRAG/ATYP/addr/port`) is applied only at the
|
||||
*front door* (the local UDP socket a vanilla client sends to), where
|
||||
it belongs: `relay_udp_client` parses/`format_udp_packet`s at the
|
||||
@@ -191,22 +191,22 @@ the exact question OQ-SK-03 poses. Its answers:
|
||||
This is a cleaner cut than phase-0's hunch (which put the length
|
||||
prefix inside the same stream as RFC headers): the alkcall-channel
|
||||
wire carries `{addr, data}` postcard frames (boundary-preserving by
|
||||
construction — an empty datagram is a frame with empty `data`, never
|
||||
construction (an empty datagram is a frame with empty `data`, never
|
||||
EOF; alktunnels F-2's requirement is met structurally), and RFC header
|
||||
encode/decode exists only where a vanilla-client socket boundary
|
||||
exists (the optional local backends on either side).
|
||||
- **Server-side `BND.ADDR` semantics**: the ASSOCIATE reply reports the
|
||||
*real* bound UDP socket address (`udp.local_addr()`) — because in
|
||||
*real* bound UDP socket address (`udp.local_addr()`), because in
|
||||
this design the server-side relay socket is real. On alksocks'
|
||||
channels path (no socket), phase-0's sentinel question remains open;
|
||||
iroh-socks5 sidesteps it by having the vanilla-facing side be the
|
||||
*client* half. A producer-side vanilla UDP front door (OQ-SK-03's
|
||||
local-backend-bridge option) would need the same real-socket
|
||||
treatment; the wrapper-aware in-band path can carry a sentinel or
|
||||
(better, per this evidence) no address at all — the framed `Datagram`
|
||||
(better, per this evidence) no address at all; the framed `Datagram`
|
||||
stream needs no `BND.ADDR` to function; the address is pure
|
||||
RFC-compatibility surface.
|
||||
- **No per-endpoint flow table on the producer side** — the relay
|
||||
- **No per-endpoint flow table on the producer side**; the relay
|
||||
socket is one `send_to`/`recv_from` loop with `TargetAddr` carried
|
||||
per datagram. This is *simpler* than alknet's flow-table model and
|
||||
sufficient: per-datagram addressing in the frame does the
|
||||
@@ -224,13 +224,13 @@ the exact question OQ-SK-03 poses. Its answers:
|
||||
| Stream genericity | `T: AsyncRead + AsyncWrite + Unpin` throughout | concrete iroh `SendStream`/`RecvStream` |
|
||||
| Server structure | explicit typestate + interception points | single `accept()` method, monolithic |
|
||||
| Auth extension seam | `AuthMethod` trait | none (allow-list only) |
|
||||
| UDP codec | public pure fns (`new_udp_header`, `parse_udp_request`) | public pure fns (`parse_udp_packet`, `format_udp_packet`) — equivalent |
|
||||
| UDP codec | public pure fns (`new_udp_header`, `parse_udp_request`) | public pure fns (`parse_udp_packet`, `format_udp_packet`), equivalent |
|
||||
| UDP ASSOCIATE over the transport | default handler binds sockets; custom seam narrow (phase-0 caveat) | clean framed-datagram design, no producer-side virtualization needed |
|
||||
| Producer dial seam | interception points (`run_tcp_proxy`, command-read typestate) | none (dial hardwired in `handle_connection`) |
|
||||
| Client session genericity | `Socks5Stream::use_stream` (any `T`) | none (local TCP front door only) |
|
||||
| wasm posture | fork verified minimal (POC #4) | moot — iroh + tokio-full are native-only |
|
||||
| wasm posture | fork verified minimal (POC #4) | moot (iroh + tokio-full are native-only) |
|
||||
| Tests | unit + examples; router/custom-auth examples | strong integration suite over real iroh infra |
|
||||
| License | MIT (third-party upstream; inline-vendored — OQ-SK-04) | MIT OR Apache-2.0 |
|
||||
| License | MIT (third-party upstream; inline-vendored, OQ-SK-04) | MIT OR Apache-2.0 |
|
||||
| LOC | ~4k | ~1.3k |
|
||||
|
||||
## Verdict
|
||||
@@ -240,20 +240,20 @@ stand); **adopt iroh-socks5's UDP design as prior art for OQ-SK-03**:
|
||||
|
||||
1. The framed `{addr, data}` datagram stream (no RFC header in-band, RFC
|
||||
header only at vanilla front doors) is the cleaner expression of the
|
||||
phase model — the RFC UDP header becomes a front-door codec concern,
|
||||
phase model: the RFC UDP header becomes a front-door codec concern,
|
||||
not a channels-wire concern.
|
||||
2. Per-datagram addressing with no producer-side flow table suffices
|
||||
for the *local egress* variant (revisit alknet's flow-table posture
|
||||
in light of this). Caveat added 2026-09-13: under *composed* egress
|
||||
(per-target alktunnels udp-tunnels, the OQ-SK-03 handle-swap
|
||||
discussion) the flow table returns as a destination→tunnel-channel
|
||||
table — a dial-callback policy, not a protocol-layer concern.
|
||||
3. Control-stream EOF ends the association (`wait_until_closed` shape —
|
||||
table, a dial-callback policy, not a protocol-layer concern.
|
||||
3. Control-stream EOF ends the association (`wait_until_closed` shape:
|
||||
on channels, the channel's EOF is the same signal; no extra control
|
||||
vocabulary needed).
|
||||
4. FRAG ≠ 0 refused is an acceptable v1 posture.
|
||||
|
||||
Attribution if code is reused (the two codec files are the candidates —
|
||||
Attribution if code is reused (the two codec files are the candidates,
|
||||
though fast-socks5 already covers them): MIT OR Apache-2.0 dual license,
|
||||
copyright Matthew Geddes; keep LICENSE-MIT/LICENSE-APACHE entries and a
|
||||
provenance note in any vendored file header.
|
||||
@@ -261,6 +261,6 @@ provenance note in any vendored file header.
|
||||
Also worth noting (posture corrected 2026-09-20: fast-socks5 is
|
||||
third-party upstream, so nothing is "filed" there as of right;
|
||||
iroh-socks5 is upstream's own crate): nothing blocking. Optionally a
|
||||
repository link on the crates.io entry would help discoverability — the
|
||||
repository link on the crates.io entry would help discoverability; the
|
||||
repo exists at `github.com/mattgeddes/iroh-socks5` but `repository` is
|
||||
null on crates.io.
|
||||
+256
-253
File diff suppressed because it is too large.
Load diff
@@ -1,14 +1,14 @@
|
||||
---
|
||||
status: complete
|
||||
last_updated: 2026-09-20 (ledger statuses updated — the graduation
|
||||
last_updated: 2026-09-20 (ledger statuses updated with the graduation
|
||||
landed; both alktunnels asks superseded in shape)
|
||||
---
|
||||
|
||||
# alksocks POC findings — channels-native SOCKS5 BIND
|
||||
# alksocks POC findings: channels-native SOCKS5 BIND
|
||||
|
||||
POC #5 from phase-0 §OQ-SK-07, run 2026-09-15. Crate:
|
||||
`alksocks-bind-poc` (standalone, `/workspace/alksocks-bind-poc/`), against
|
||||
alkcall 0.7.1 (crates.io), alktunnels 0.1.0 (crates.io, `local` feature —
|
||||
alkcall 0.7.1 (crates.io), alktunnels 0.1.0 (crates.io, `local` feature;
|
||||
the listen surface is complete in the published version),
|
||||
fast-socks5 1.0.0 (crates.io). 9 tests, all pass; `cargo clippy
|
||||
--all-targets -- -D warnings` and `cargo fmt --check` clean.
|
||||
@@ -19,7 +19,7 @@ exactly one inbound accept → reply#2 with the peer address → raw
|
||||
pass-through data plane); both accept variants behind the accept
|
||||
callback; the `socks5:bind` scope gate.** fast-socks5 parses the
|
||||
command but refuses it (`lib.rs:384-386`, `server.rs:813-816`); the
|
||||
wrapper drives the typestate directly and owns the whole BIND shape —
|
||||
wrapper drives the typestate directly and owns the whole BIND shape:
|
||||
**zero fast-socks5 upstream ask for BIND itself** (the typestate's
|
||||
`read_command()` returns `(cmd, target)` and the wrapper branches;
|
||||
`reply_success`'s `sock_addr` parameter is the reply#1 carrier;
|
||||
@@ -27,24 +27,24 @@ wrapper drives the typestate directly and owns the whole BIND shape —
|
||||
|
||||
## What was validated
|
||||
|
||||
1. **The BIND phase model — confirmed end to end.** The RFC
|
||||
1. **The BIND phase model was confirmed end to end.** The RFC
|
||||
conversation and the data plane ride the same channel `BiStream`:
|
||||
the typestate runs `accept_no_auth → read_command` (BIND arrives
|
||||
as `Socks5Command::TCPBind`), reply#1 carries `BND.ADDR`, the
|
||||
quiet wait selects the one inbound accept against the control
|
||||
stream, reply#2 carries the peer address, and `pump_bidi`
|
||||
(inline — R-02) two-pumps the accepted connection against the
|
||||
channel. Zero per-packet headers in-band (the CONNECT pass-through
|
||||
conclusion extends literally — BIND's post-reply#2 stage is a
|
||||
(inline, R-02) two-pumps the accepted connection against the
|
||||
channel. Zero per-packet headers in-band; the CONNECT pass-through
|
||||
conclusion extends literally (BIND's post-reply#2 stage is a
|
||||
plain byte stream).
|
||||
2. **The local accept variant (variant 1, the faithful RFC shape) —
|
||||
2. **The local accept variant (variant 1, the faithful RFC shape),
|
||||
confirmed.** A real `TcpListener` per BIND session: reply#1
|
||||
carries the listener's *actual* `local_addr` (a real network-B
|
||||
address); the "app server" dials the advertised address; reply#2
|
||||
carries the *real* peer address (`accept()`'s peer, byte-exact —
|
||||
carries the *real* peer address (`accept()`'s peer, byte-exact;
|
||||
the test asserts the app's ephemeral port); the data plane echoes
|
||||
both directions. Vanilla front-door clients work (test 6).
|
||||
3. **The composed accept variant (variant 2) — confirmed, with the
|
||||
3. **The composed accept variant (variant 2) confirmed, with the
|
||||
sequencing finding below.** The listener lives on an alktunnels
|
||||
listen-tunnel resource (`register_tunnel_listen_openable` +
|
||||
`AcceptQueue` + a real far-side `TcpListener`); the SOCKS5
|
||||
@@ -54,38 +54,38 @@ wrapper drives the typestate directly and owns the whole BIND shape —
|
||||
`alk/tunnel` listen channel → tunnel producer → real listener →
|
||||
the app server's connection → back through the pump. Per-listener
|
||||
ACL for free (the tunnel resource's ACL governs who can mint the
|
||||
listener), hub-relayable — the variant's whole point.
|
||||
4. **The scope gate — confirmed.** The pump handler reads the
|
||||
listener), hub-relayable, the variant's whole point.
|
||||
4. **The scope gate is confirmed.** The pump handler reads the
|
||||
open-op `AuthContext`'s identity and requires `socks5:bind` before
|
||||
serving a BIND; an identity without it gets the RFC-faithful
|
||||
`ConnectionNotAllowed` (0x02) refusal — while CONNECT still works
|
||||
`ConnectionNotAllowed` (0x02) refusal, while CONNECT still works
|
||||
for the same identity (the gate distinguishes ingress listeners
|
||||
from proxy egress; one identity having egress does not imply
|
||||
ingress). The gate is the open-op identity seam consumed at
|
||||
command-dispatch time — no new ops, no params change, no
|
||||
command-dispatch time; no new ops, no params change, no
|
||||
one-way-door exposure.
|
||||
5. **The quiet wait's semantics — confirmed.** A control byte after
|
||||
5. **The quiet wait's semantics are confirmed.** A control byte after
|
||||
reply#1 is protocol garbage (the ASSOCIATE post-reply semantic
|
||||
BIND shares; RFC 1928 §3's "BIND: no further control data"
|
||||
reading); the session is refused with no data plane. A client
|
||||
that BINDs and vanishes (EOF mid-wait) tears the listener down
|
||||
with its session — the accept future (which owns the listener)
|
||||
is dropped by the `select!` branch; a fresh dial to the
|
||||
advertised address fails with connection refused. The session's
|
||||
natural end (half-close post-data-plane) releases the listener
|
||||
the same way.
|
||||
6. **Failure mapping — confirmed RFC-faithful.** A factory failure
|
||||
with its session; the accept future (which owns the listener)
|
||||
is dropped by the `select!` branch; a fresh dial to the
|
||||
advertised address fails with connection refused. The session's
|
||||
natural end (half-close post-data-plane) releases the listener
|
||||
the same way.
|
||||
6. **Failure mapping is confirmed RFC-faithful.** A factory failure
|
||||
(pre-reply#1: an unbindable port, a held port) maps to reply#1
|
||||
with the factory error's reply code (shortage → GeneralFailure);
|
||||
an accept-stage failure (post-reply#1) maps to reply#2 with the
|
||||
mapped refusal code and no data plane. No phantom listeners, no
|
||||
phantom data planes; refusals are typed end to end.
|
||||
7. **One registration, all three commands — regression-guarded.**
|
||||
7. **One registration, all three commands, regression-guarded.**
|
||||
CONNECT (POC #1's data plane, unchanged) and UDP ASSOCIATE (the
|
||||
POC #2 skeleton) still serve on the same `channels/socks5/sub` op
|
||||
after the BIND branch lands. The RFC 1928 command surface is now
|
||||
complete in the crate's shape: CONNECT + BIND + ASSOCIATE.
|
||||
8. **The vanilla front door — confirmed for BIND.** The raw-pipe
|
||||
8. **The vanilla front door is confirmed for BIND.** The raw-pipe
|
||||
bridge (POC #1's CONNECT front-door shape, reused verbatim) carries
|
||||
a full vanilla BIND conversation: an in-process RFC 1928 client
|
||||
(greeting → BIND command → reply#1 → reply#2 → data plane) through
|
||||
@@ -96,23 +96,23 @@ wrapper drives the typestate directly and owns the whole BIND shape —
|
||||
|
||||
## Structural findings (the load-bearing discoveries)
|
||||
|
||||
**The address-first-vs-accept-first deadlock — the POC's one real
|
||||
**The address-first-vs-accept-first deadlock: the POC's one real
|
||||
discovery, and the composed variant's sequencing is now pinned.**
|
||||
SOCKS5 BIND is address-first (reply#1 must carry `BND.ADDR` *before*
|
||||
the app server can dial), while alktunnels' listen establisher is
|
||||
accept-first (its establishment phase pops an already-accepted
|
||||
connection — the open op blocks until one arrives). The first cut
|
||||
connection, so the open op blocks until one arrives). The first cut
|
||||
opened the listen-tunnel in the factory call (before reply#1) and
|
||||
deadlocked into the establishment timeout: the app server cannot dial
|
||||
until reply#1 tells it the address, and the open cannot complete until
|
||||
it dials. **The resolution: reply#1 first, the tunnel open rides the
|
||||
accept future** — the quiet wait is exactly where the establisher's
|
||||
accept future.** The quiet wait is exactly where the establisher's
|
||||
accept-pop block belongs. What remains upstream-blocked is *learning
|
||||
the bound address without consuming an accept* (ledger ask #1): the
|
||||
stand-in is the factory's carried `bind_addr` (the expected
|
||||
`local_addr` the far-side real listener reports).
|
||||
|
||||
Consequence for the ADR: variant 2 needs TWO upstream asks, not one —
|
||||
Consequence for the ADR: variant 2 needs TWO upstream asks, not one:
|
||||
(1) listen-addr inspection without consuming an accept (the address
|
||||
half), and (2) accepted handles carrying peer metadata (the reply#2
|
||||
half). Both are additive surface additions, not changes; the ask
|
||||
@@ -120,23 +120,23 @@ ledger below records both with the evidence.
|
||||
|
||||
**Establishment-bound interaction (the composed variant's wait
|
||||
bound).** The accept future's tunnel open runs under the listen
|
||||
establisher's default 10s establishment bound — a BIND wait longer
|
||||
establisher's default 10s establishment bound; a BIND wait longer
|
||||
than that resolves as a refusal (reply#2, GeneralFailure via the
|
||||
mapped open error). Correct-by-backstop, but a real BIND deployment
|
||||
(FTP control sessions idle far longer than 10s) needs either a raised
|
||||
bound (the open-op's `timeout` parameter — a registration choice, no
|
||||
bound (the open-op's `timeout` parameter; a registration choice, no
|
||||
wire change) or the accept future structured so the bound covers only
|
||||
the open, not the wait (POC note: the bound covered the whole accept
|
||||
here; the crate's shape should bound only the open-op round trip).
|
||||
Phase 1 pins this with the registration posture.
|
||||
|
||||
**The teardown-at-birth lesson repeats — and the fix is structural.**
|
||||
**The teardown-at-birth lesson repeats, and the fix is structural.**
|
||||
The composed variant's first working cut dropped the
|
||||
`TakenHalves::session` (the channel-entry owner) while destructuring
|
||||
it — the tunnel EOF'd at birth and the data plane read `early eof`
|
||||
it; the tunnel EOF'd at birth and the data plane read `early eof`
|
||||
(POC #2's findings predicted exactly this: "the session must live
|
||||
inside the table entry; the *compiler* should enforce it"). The fix:
|
||||
`BindTargetHandle` carries `_session: Option<TunnelSession>` — the
|
||||
`BindTargetHandle` carries `_session: Option<TunnelSession>`; the
|
||||
owner rides the handle and drops with it. This is now the third
|
||||
witness of the pattern (POC #2's egress table, alktunnels ADR-005's
|
||||
teardown contract, here); the crate's Phase 1 shape should make the
|
||||
@@ -144,7 +144,7 @@ plan payload own its channel handles structurally, never by let-binding.
|
||||
|
||||
**Peer-address fidelity is lost through the composed path** (ledger
|
||||
ask #2). The popped accept handle (`alktunnels::TargetHandle`) carries
|
||||
boxed halves only — the real peer address exists at the tunnel
|
||||
boxed halves only; the real peer address exists at the tunnel
|
||||
producer's listener; in-band the composed variant reports the sentinel
|
||||
(`0.0.0.0:0`) for reply#2 (the handle-swap reading applied to BIND:
|
||||
the reply is RFC-compatibility surface; the data plane is the
|
||||
@@ -154,7 +154,7 @@ item #2 is the fidelity ask.
|
||||
**The quiet wait is `select!`-single-shot, not a loop** (unlike
|
||||
ASSOCIATE's stage): the accept resolves at most once, and the control
|
||||
stream's first byte ends the session (garbage). A loop was
|
||||
unnecessary — clippy's `never_loop` caught the first cut's vestigial
|
||||
unnecessary; clippy's `never_loop` caught the first cut's vestigial
|
||||
`loop`; the phase model's clean statement is that BIND's quiet wait
|
||||
has exactly two exits (the accept, or the control stream's
|
||||
EOF/garbage/error). The data plane starts only from the accept
|
||||
@@ -164,48 +164,48 @@ branch.
|
||||
the egress's readiness contract.** The accept future owns the
|
||||
listener (local variant) or the tunnel-open + popped halves (composed
|
||||
variant); the quiet wait's `select!` drops it on the control-stream
|
||||
branch — that drop IS the client-gone teardown. The crate's Phase 1
|
||||
branch, and that drop IS the client-gone teardown. The crate's Phase 1
|
||||
equivalent: the accept callback's future must own its resources
|
||||
end-to-end (the same ownership lesson as POC #2's egress, mirrored).
|
||||
|
||||
## Wire/API notes for the wrapper
|
||||
|
||||
- fast-socks5's client `request()` reads exactly one reply — BIND's
|
||||
- fast-socks5's client `request()` reads exactly one reply: BIND's
|
||||
reply#2 is hand-read with fast-socks5's own public
|
||||
`read_address` codec (reply#1's codec applied again; no second
|
||||
codec hand-rolled). The consumer's reply#2 reader (`read_reply2`)
|
||||
is the Phase 1 client-session sketch.
|
||||
- `Socks5ServerProtocol::reply_success(sock_addr)` is reply#1's
|
||||
carrier — the `sock_addr` parameter carries `BND.ADDR` verbatim
|
||||
carrier: the `sock_addr` parameter carries `BND.ADDR` verbatim
|
||||
(the local variant's real listener; the composed variant's
|
||||
expected far-side address). No upstream change needed.
|
||||
- fast-socks5's `new_reply` helper is private; the BIND reply#2
|
||||
writer hand-rolls the 10-byte reply (VER, REP, RSV, ATYP, addr,
|
||||
port — 10 bytes v4 / 22 v6). Ledger ask #3 (fast-socks5, courtesy):
|
||||
port; 10 bytes v4 / 22 v6). Ledger ask #3 (fast-socks5, courtesy):
|
||||
publish `new_reply` (or a `reply(code, addr)` constructor) so
|
||||
embedders don't re-encode replies. (2026-09-20: rides the inline
|
||||
vendor — see §ledger statuses.)
|
||||
vendor; see §ledger statuses.)
|
||||
- `ToTargetAddr::to_target_addr()` (not `From`) is the
|
||||
`SocketAddr → TargetAddr` conversion — `TargetAddr::from` does not
|
||||
`SocketAddr → TargetAddr` conversion; `TargetAddr::from` does not
|
||||
exist (the POC hit this; the wrapper's accept layer will too).
|
||||
- The listen establisher's default 10s bound covers the composed
|
||||
variant's whole accept future in this POC's shape — see the
|
||||
variant's whole accept future in this POC's shape; see the
|
||||
establishment-bound finding above for the Phase 1 posture.
|
||||
- The scope gate is read from the `OpenHandler`'s `AuthContext`
|
||||
(CF-005/CF-006's per-call opener identity) — the same seam POC #1
|
||||
(CF-005/CF-006's per-call opener identity), the same seam POC #1
|
||||
validated for session-gating, now consumed for a *command-class*
|
||||
distinction. `socks5:bind` as a scope string follows the
|
||||
`socks5:open` convention (Phase 1 pins the exact convention).
|
||||
|
||||
## Upstream-ask ledger (the one-version alktunnels bump)
|
||||
|
||||
**Ledger statuses (2026-09-20 — the graduation landed).** The user's
|
||||
**Ledger statuses (2026-09-20, the graduation landed).** The user's
|
||||
standing note held: alktunnels 0.2.0 (ADRs 007/008, over alkcall
|
||||
0.8.0) shipped in one version bump — but adopted the ssh
|
||||
0.8.0) shipped in one version bump, but adopted the ssh
|
||||
`forwarded-tcpip`-shaped alternative over the per-ask carriers, so
|
||||
both asks are **superseded in shape**, not implemented as posed:
|
||||
|
||||
1. **Listen-addr inspection without consuming an accept — dissolved.**
|
||||
1. **Listen-addr inspection without consuming an accept: dissolved.**
|
||||
The bind-first establisher (`bindfirst_establisher`, ADR-008 §1)
|
||||
binds during establishment, so the open-op reply natively carries
|
||||
the real OS-chosen bound address (the `bound` reply field via
|
||||
@@ -214,24 +214,24 @@ both asks are **superseded in shape**, not implemented as posed:
|
||||
`TunnelListener::bound()`). There is no accept to avoid consuming;
|
||||
this ask's stand-in (the factory's carried `bind_addr`) is retired,
|
||||
as is the POC's "open rides the accept future" sequencing
|
||||
workaround — and the 10s establishment-bound finding dissolves with
|
||||
workaround, and the 10s establishment-bound finding dissolves with
|
||||
it (the accept wait lives in the pump phase now). The
|
||||
accept-first surface (`listen_establisher` + `AcceptQueue`) remains
|
||||
in 0.2.0 unchanged for the accept-as-open shape.
|
||||
2. **Accepted handles carrying peer metadata — landed at the wire's
|
||||
2. **Accepted handles carrying peer metadata: landed at the wire's
|
||||
native moment.** The forwarded op (`channels/tunnel/forwarded`,
|
||||
ADR-008 §3) carries `{listen, substrate, peer}` params per accept;
|
||||
`TargetHandle` gained no peer field — the graduation chose the
|
||||
`TargetHandle` gained no peer field; the graduation chose the
|
||||
forwarded-op shape over this item's option-i carrier. Correlation
|
||||
is a producer-minted u32 token (Amendment 1, 2026-09-18), not the
|
||||
originally-planned channel-ID key.
|
||||
3. **(fast-socks5, courtesy)** `new_reply` is private — publish it
|
||||
3. **(fast-socks5, courtesy)** `new_reply` is private; publish it
|
||||
(or a constructor) so embedders don't re-encode the reply wire
|
||||
form. Also still open from POC #4: the `net` feature gate and the
|
||||
`run_udp_proxy_custom` unconditional-bind seam. (Posture note,
|
||||
2026-09-20: fast-socks5 is third-party upstream and these are no
|
||||
longer asks — all three move into the inline vendor, OQ-SK-04
|
||||
resolved. 2026-09-21: under the extraction — ADR-011 §Amendment —
|
||||
longer asks; all three move into the inline vendor, OQ-SK-04
|
||||
resolved. 2026-09-21: under the extraction (ADR-011 §Amendment),
|
||||
`new_reply` is the one that rides in-tree; the `net` gate and the
|
||||
bind seam dissolve with the kernel-net region, which is not
|
||||
copied.)
|
||||
@@ -242,7 +242,7 @@ alktunnels 0.1.0; the Phase 1 shape composes variant 2 against 0.2.0
|
||||
|
||||
The user's standing note: collect alktunnels additions so one version
|
||||
bump ships them all. The POC ran against published alktunnels 0.1.0
|
||||
(no fork needed — the listen surface is complete there), and the asks
|
||||
(no fork needed; the listen surface is complete there), and the asks
|
||||
are additive surface, discovered in priority order:
|
||||
|
||||
1. **Listen-addr inspection without consuming an accept** (the
|
||||
@@ -259,13 +259,13 @@ are additive surface, discovered in priority order:
|
||||
the composed accept's reply#2 reports the sentinel; the RFC's
|
||||
"reply#2 carries the address of the incoming connection" is
|
||||
unfilled on the composed path. Shape: `TargetHandle` gaining an
|
||||
optional `peer: Option<SocketAddr>` (or a sibling
|
||||
`AcceptedHandle`) — additive, no wire change.
|
||||
3. **(fast-socks5, courtesy)** `new_reply` is private — publish it
|
||||
optional `peer: Option<SocketAddr>` (or a sibling
|
||||
`AcceptedHandle`); additive, no wire change.
|
||||
3. **(fast-socks5, courtesy)** `new_reply` is private; publish it
|
||||
(or a constructor) so embedders don't re-encode the reply wire
|
||||
form. Also still open from POC #4: the `net` feature gate and the
|
||||
`run_udp_proxy_custom` unconditional-bind seam. (2026-09-21: under
|
||||
the extraction — ADR-011 §Amendment — `new_reply` is the one that
|
||||
the extraction, ADR-011 §Amendment, `new_reply` is the one that
|
||||
rides in-tree; the `net` gate and the bind seam dissolve with the
|
||||
kernel-net region, which is not copied.)
|
||||
|
||||
@@ -275,7 +275,7 @@ reply#2) are honest interim shapes.
|
||||
|
||||
## OQ status after this POC
|
||||
|
||||
- **OQ-SK-08 (BIND)**: POC-validated end to end — the phase model
|
||||
- **OQ-SK-08 (BIND)**: POC-validated end to end: the phase model
|
||||
extends (reply#1 → quiet wait → accept → reply#2 → pass-through);
|
||||
both accept variants implemented (local faithful + composed
|
||||
listen-tunnel); the scope gate (`socks5:bind`) distinguishes
|
||||
@@ -283,20 +283,20 @@ reply#2) are honest interim shapes.
|
||||
*resolved in shape* (reply#1 first, the open rides the accept
|
||||
future) with the two upstream asks pinned as the remaining blockers
|
||||
for variant 2's full fidelity. Remaining for Phase 1: ship-in-base-
|
||||
crate + default-accept-variant ADR (the support is additive — old
|
||||
consumers still get the RFC refusal — so nothing here is a
|
||||
crate + default-accept-variant ADR (the support is additive, old
|
||||
consumers still get the RFC refusal, so nothing here is a
|
||||
one-way door); the establishment-bound posture; the exact
|
||||
scope-string convention.
|
||||
- **OQ-SK-07**: #1, #2, #4, #5 complete; #3 dropped (transferred with
|
||||
OQ-SK-06). The register is closed for this crate's Phase 0.
|
||||
- **OQ-SK-01 (dial policy)**: the accept seam is the dial-callback
|
||||
shape's mirror (`BindAcceptFactory` — function-not-trait, boxed
|
||||
shape's mirror (`BindAcceptFactory`: function-not-trait, boxed
|
||||
halves, one instance per session). The no-trait precedent now holds
|
||||
for the accept plane too.
|
||||
|
||||
## POC placement
|
||||
|
||||
Standalone crate (global workspace, `/workspace/alksocks-bind-poc/`)
|
||||
per the placement conventions — it composes three published crates
|
||||
per the placement conventions; it composes three published crates
|
||||
(alkcall 0.7.1, alktunnels 0.1.0, fast-socks5 1.0.0) and needs no code
|
||||
from this repo. Findings land here; the crate is the artifact.
|
||||
@@ -3,7 +3,7 @@ status: complete
|
||||
last_updated: 2026-09-13
|
||||
---
|
||||
|
||||
# alksocks POC findings — channels-native CONNECT + fast-socks5 wasm posture
|
||||
# alksocks POC findings: channels-native CONNECT + fast-socks5 wasm posture
|
||||
|
||||
POCs #1 and #4 from phase-0 §OQ-SK-07, run 2026-09-13.
|
||||
|
||||
@@ -13,37 +13,37 @@ POCs #1 and #4 from phase-0 §OQ-SK-07, run 2026-09-13.
|
||||
fast-socks5 1.0.0 (crates.io). 8 tests + 1 example
|
||||
(`local_front_door`, the curl front door). All pass.
|
||||
- **POC #4** (fast-socks5 wasm fork-minimality): `fs5-wasm-poc`
|
||||
(scratch crate, kept at `/workspace/alksocks-fs5-wasm-poc/` — not
|
||||
part of this repo; the finding is the artifact). fast-socks5's
|
||||
(scratch crate, kept at `/workspace/alksocks-fs5-wasm-poc/` (not
|
||||
part of this repo; the finding is the artifact)). fast-socks5's
|
||||
`src/` vendored, kernel-net pieces gated behind a `net` feature.
|
||||
|
||||
## POC #1 — channels-native SOCKS5 CONNECT
|
||||
## POC #1: channels-native SOCKS5 CONNECT
|
||||
|
||||
### What was validated
|
||||
|
||||
1. **`BiStream`-as-`T` genericity — confirmed.** The channels
|
||||
1. **`BiStream`-as-`T` genericity confirmed.** The channels
|
||||
`BiStream` (`alkcall::core::types`, `Box<dyn AsyncReadWrite +
|
||||
Unpin>`, impls `AsyncRead + AsyncWrite`, is `Unpin` + `Send`)
|
||||
satisfies fast-socks5's `Socks5ServerProtocol<T>` server typestate
|
||||
and `Socks5Stream<S>::use_stream` client path directly. No adapter,
|
||||
no wrapper stream, no boxing beyond what `BiStream` already is.
|
||||
2. **`register_openable_with_establisher` fit — confirmed.** The
|
||||
2. **`register_openable_with_establisher` fit confirmed.** The
|
||||
producer registers `channels/socks5/sub` (ALPN `alk/socks5`, scope
|
||||
`socks5:open`) with an establisher + pump handler exactly as
|
||||
alktunnels does for `channels/tunnel/sub`.
|
||||
3. **`pump_bidi` as the CONNECT data plane — confirmed.** Post-reply
|
||||
3. **`pump_bidi` as the CONNECT data plane confirmed.** Post-reply
|
||||
the handler awaits `pump_bidi(channel_bistream, target_read,
|
||||
target_write)` inline; echo round-trips clean through
|
||||
client → channel → pump → target → back.
|
||||
4. **Session refusal is a typed call error — confirmed.** An identity
|
||||
4. **Session refusal is a typed call error, confirmed.** An identity
|
||||
lacking the open scope gets `FORBIDDEN` at the registry ACL gate;
|
||||
`ChannelOpenError::CallFailed` carries it; no phantom channel.
|
||||
The establisher's per-call identity arrives as expected
|
||||
(witnessed `"consumer"`; CF-005/CF-006 seam works).
|
||||
5. **Dial failure → RFC reply code mapping — confirmed.** The dial
|
||||
5. **Dial failure → RFC reply code mapping confirmed.** The dial
|
||||
refusal surfaces to the RFC client as `ConnectionRefused` (0x05)
|
||||
in the CONNECT reply — the faithful per-RFC error mapping
|
||||
AGENTS.md convention 2 requires — with no phantom data plane.
|
||||
in the CONNECT reply, the faithful per-RFC error mapping
|
||||
AGENTS.md convention 2 requires, with no phantom data plane.
|
||||
6. **The front door works with a vanilla client (stretch goal).**
|
||||
The consumer-side local exposure shape: bind a real TCP listener,
|
||||
pipe each accepted connection raw onto a channel `BiStream`
|
||||
@@ -56,13 +56,13 @@ POCs #1 and #4 from phase-0 §OQ-SK-07, run 2026-09-13.
|
||||
|
||||
### Structural findings (the load-bearing discovery)
|
||||
|
||||
**The dial cannot live in the establisher — and that is correct, not
|
||||
**The dial cannot live in the establisher, and that is correct, not
|
||||
a limitation.** The SOCKS5 *target* is unknown at open time: RFC 1928
|
||||
method negotiation and the CONNECT command (with its target address)
|
||||
arrive in-stream, after channel establishment. The layering the POC
|
||||
validates:
|
||||
|
||||
- **The establisher is a session gate** — it validates/records the
|
||||
- **The establisher is a session gate**: it validates/records the
|
||||
opener identity (OQ-SK-02's seam) and does session-level policy. It
|
||||
never sees a target.
|
||||
- **The dial belongs at the command-read interception point**, inside
|
||||
@@ -81,32 +81,32 @@ natural dialer return type. The no-trait hunch is now evidence-backed.
|
||||
**Identity propagation (CF-005 (b)) has a producer-side step the
|
||||
harness had to do:** the accept-side install hook must call
|
||||
`channel0_conn.set_identity(resolved)` before building the
|
||||
`CallConnection` — the transport identity does not flow to the
|
||||
`CallConnection`; the transport identity does not flow to the
|
||||
channel-0 dispatch automatically. (alktunnels' harness does the same;
|
||||
modeled mTLS posture. A real mTLS transport would resolve the peer
|
||||
here.) Worth an upstream note: without it, scope-gated opens fail
|
||||
closed with `FORBIDDEN "authentication required"` — correct behavior,
|
||||
closed with `FORBIDDEN "authentication required"`, correct behavior,
|
||||
but the wiring requirement is implicit.
|
||||
|
||||
### Wire/API notes for the wrapper
|
||||
|
||||
- `TargetAddr::Ip(addr).to_string()` is `ip:port` — when handing the
|
||||
- `TargetAddr::Ip(addr).to_string()` is `ip:port`; when handing the
|
||||
dial closure a structured target, use `addr.ip().to_string()` +
|
||||
`addr.port()` separately (the POC hit this; the wrapper's target
|
||||
carrier will too).
|
||||
- `Socks5Stream::request(cmd, TargetAddr)` — the target must be
|
||||
- `Socks5Stream::request(cmd, TargetAddr)` requires that the target be
|
||||
constructed via `ToTargetAddr`; IP-string forms parse to IP targets
|
||||
(domain form passes through unresolved, per OQ-SK-05's DNS question).
|
||||
- `SocksServerError::AuthenticationRejected` is a unit variant (no
|
||||
message field) in 1.0.0.
|
||||
- `Establishment` with no plan (`Establishment::default()`) is the
|
||||
right shape for SOCKS5 — the dial happens post-establishment, so
|
||||
right shape for SOCKS5; the dial happens post-establishment, so
|
||||
there is nothing to thread establisher→handler.
|
||||
- The open-op params are trivially empty (`{}`); the spec's input
|
||||
schema is `{type: object}`. Params stay empty unless a later ADR
|
||||
adds per-session options.
|
||||
|
||||
## POC #4 — fast-socks5 wasm fork-minimality
|
||||
## POC #4: fast-socks5 wasm fork-minimality
|
||||
|
||||
### Baseline
|
||||
|
||||
@@ -117,7 +117,7 @@ but the wiring requirement is implicit.
|
||||
OQ-SK-04's premise. (alksocks-connect-poc itself re-confirms: the dep
|
||||
chain pulls `mio`/`socket2`.)
|
||||
|
||||
### The gating change — measured, not guessed
|
||||
### The gating change, measured not guessed
|
||||
|
||||
Vendored fast-socks5's `src/` into a scratch crate; feature-gated
|
||||
every kernel-net touch behind a `net` feature (default-on, exactly the
|
||||
@@ -133,20 +133,20 @@ alktty `local` pattern):
|
||||
| `Cargo.toml` | `net = ["tokio/net", "dep:socket2", "dep:tokio-stream"]`, default-on | tokio stays `default-features = false, io-util/time/sync/macros` |
|
||||
|
||||
Diff size vs upstream: **~120 changed lines across 5 files, all
|
||||
`#[cfg]` insertions** (client.rs 87 diff lines — inflated by the
|
||||
`#[cfg]` insertions** (client.rs 87 diff lines, inflated by the
|
||||
whitespace of moving the datagram block; server.rs 27; the rest ≤3).
|
||||
|
||||
### Results
|
||||
|
||||
- `cargo check --no-default-features` (native): clean.
|
||||
- `cargo check --target wasm32-unknown-unknown --no-default-features`:
|
||||
**clean** — no `mio`, no `socket2`, no `tokio::net` in the graph.
|
||||
**clean**: no `mio`, no `socket2`, no `tokio::net` in the graph.
|
||||
- `cargo test --no-default-features`: a full RFC 1928 CONNECT
|
||||
conversation (typestate server over a duplex ↔ `use_stream` client)
|
||||
passes — the wasm-clean subset is the *functional* protocol subset,
|
||||
passes; the wasm-clean subset is the *functional* protocol subset,
|
||||
not a husk.
|
||||
- `cargo check --target wasm32-unknown-unknown` with `net` on:
|
||||
fails in `mio` (expected — the target gate for wasm users is
|
||||
fails in `mio` (expected; the target gate for wasm users is
|
||||
`default-features = false`).
|
||||
|
||||
### Verdict for OQ-SK-04
|
||||
@@ -156,24 +156,24 @@ whitespace of moving the datagram block; server.rs 27; the rest ≤3).
|
||||
insertions and zero code rewrites; the wasm-clean subset still
|
||||
implements the whole RFC conversation. tokio itself needs
|
||||
`default-features = false` with an explicit `net` opt-in feature
|
||||
(`net = ["tokio/net", ...]`) — that is the entire shape of the change.
|
||||
(`net = ["tokio/net", ...]`); that is the entire shape of the change.
|
||||
|
||||
The one structural discovery: **`tokio_stream` (used for the legacy
|
||||
`Incoming` stream) and `socket2` must be optional deps keyed to the
|
||||
same feature**, and `try_join!` requires tokio's `macros` feature —
|
||||
same feature**, and `try_join!` requires tokio's `macros` feature;
|
||||
all mechanical.
|
||||
|
||||
Remaining for Case 1: the fork posture is vendoring *into this crate*
|
||||
(per phase-0: vendor the `T`-generic subset, gate the rest), keeping
|
||||
`/workspace/fast-socks5` as the differential reference; the upstream
|
||||
PR (feature-gate `net` behind a default-on feature) is small enough
|
||||
to offer as-is. **(2026-09-20: resolved — the inline vendor is the
|
||||
to offer as-is. **(2026-09-20: resolved; the inline vendor is the
|
||||
decision; the PR is an optional courtesy, never a wait. See phase-0
|
||||
OQ-SK-04.)**
|
||||
|
||||
### Follow-on changes (posture corrected 2026-09-20: not upstream asks)
|
||||
|
||||
**Ownership correction:** fast-socks5 is third-party upstream — we do
|
||||
**Ownership correction:** fast-socks5 is third-party upstream; we do
|
||||
not own it, and it has not been forked. These are therefore not asks;
|
||||
they are changes the inline vendor carries in-tree (OQ-SK-04, resolved
|
||||
2026-09-20). The wasm-gate change below is POC #4's; the other two
|
||||
@@ -182,7 +182,7 @@ into the vendor with it.
|
||||
|
||||
1. **Feature-gate kernel-net behind a default-on `net` feature**
|
||||
(POC #4's change, ~120 lines of `#[cfg]`). (2026-09-21: dissolved
|
||||
under the extraction — ADR-011 §Amendment; the kernel-net region
|
||||
under the extraction (ADR-011 §Amendment); the kernel-net region
|
||||
is not copied, and POC #4's gate map survives as the extraction
|
||||
recipe, not in-tree cfg.)
|
||||
2. **`run_udp_proxy_custom` binds a kernel socket unconditionally**
|
||||
@@ -190,24 +190,24 @@ into the vendor with it.
|
||||
customizable seam covers the relay closure and reply IP only; the
|
||||
channels path must drive the typestate directly. Make the
|
||||
`udp_bind_random_port` step optional / accept a caller-supplied
|
||||
socket. (Both #2 and the POC #5 courtesy item — publish `new_reply`
|
||||
— ride the vendor; see `poc-bind-findings.md` §ledger statuses.)
|
||||
socket. (Both #2 and the POC #5 courtesy item, publish `new_reply`,
|
||||
ride the vendor; see `poc-bind-findings.md` §ledger statuses.)
|
||||
|
||||
## OQ status after these POCs
|
||||
|
||||
- **OQ-SK-01 (dial policy)**: shape resolved empirically — an injected
|
||||
- **OQ-SK-01 (dial policy)**: shape resolved empirically: an injected
|
||||
dial callback invoked at the command-read interception point;
|
||||
boxed-halves return type; no trait (alktunnels' no-trait precedent
|
||||
holds). Where the callback *comes from* (alktunnels composition vs
|
||||
local TCP vs further hops) stays an assembly-layer question.
|
||||
- **OQ-SK-02 (auth mapping)**: the seam works — per-call opener
|
||||
- **OQ-SK-02 (auth mapping)**: the seam works; per-call opener
|
||||
identity arrives at the establisher, and channel-0 identity
|
||||
propagation is a wiring requirement the producer harness performs.
|
||||
In-band SOCKS auth was not exercised (NoAuth only); the RFC-facing
|
||||
username/password path for the local backend remains Phase 1.
|
||||
- **OQ-SK-04 (wasm)**: fork-minimality verified; the decision reduced
|
||||
to "does wasm make sense for alksocks" (the adapter story), with
|
||||
the mechanism de-risked. **(2026-09-20: resolved — inline vendor,
|
||||
the mechanism de-risked. **(2026-09-20: resolved, inline vendor,
|
||||
on the corrected ownership premise + the BIND/seam needs. See
|
||||
phase-0 OQ-SK-04.)**
|
||||
- **OQ-SK-07**: #1 and #4 complete; #2 (UDP ASSOCIATE) and #3 (noq)
|
||||
|
||||
@@ -3,7 +3,7 @@ status: complete
|
||||
last_updated: 2026-09-14
|
||||
---
|
||||
|
||||
# alksocks POC findings — channels-native UDP ASSOCIATE
|
||||
# alksocks POC findings: channels-native UDP ASSOCIATE
|
||||
|
||||
POC #2 from phase-0 §OQ-SK-07, run 2026-09-14. Crate:
|
||||
`alksocks-udp-poc` (standalone, `/workspace/alksocks-udp-poc/`), against
|
||||
@@ -20,47 +20,47 @@ composed variant's per-destination open cost is measured.
|
||||
|
||||
## What was validated
|
||||
|
||||
1. **The handle-swap data plane — confirmed end to end.** The
|
||||
1. **The handle-swap data plane was confirmed end to end.** The
|
||||
ASSOCIATE conversation and the datagram stage ride the same
|
||||
channel `BiStream`. The producer drives the typestate directly
|
||||
(`accept_no_auth → read_command → reply_success(sentinel)`) —
|
||||
(`accept_no_auth → read_command → reply_success(sentinel)`);
|
||||
`run_udp_proxy_custom` is not used (its unconditional
|
||||
`udp_bind_random_port` makes it unusable on channels, per the
|
||||
corrected §Prior art caveat; driving the typestate directly is the
|
||||
workaround the upstream ask would retire).
|
||||
2. **The in-band frame codec — confirmed.** The wire is alktunnels'
|
||||
boundary-preserving length codec (`frame_datagram`/`DatagramReader`
|
||||
— empty payload legal, never EOF; frames split/batch across channel
|
||||
2. **The in-band frame codec is confirmed.** The wire is alktunnels'
|
||||
boundary-preserving length codec (`frame_datagram`/`DatagramReader`;
|
||||
empty payload legal, never EOF; frames split/batch across channel
|
||||
chunks transparently) over the RFC address encoding
|
||||
(`TargetAddr::to_be_bytes()` — ATYP + addr + port). RSV/FRAG do not
|
||||
(`TargetAddr::to_be_bytes()`: ATYP + addr + port). RSV/FRAG do not
|
||||
ride the tunnel wire: fragmentation cannot even be expressed
|
||||
in-band; it is refused at front doors (FRAG ≠ 0 → refuse, the
|
||||
iroh-socks5 v1 posture). The RFC-header codecs
|
||||
(`new_udp_header`/`parse_udp_request`) are used only at the
|
||||
front-door boundary, exactly as the baseline specifies.
|
||||
3. **Per-datagram addressing, no flow table — confirmed.** One relay
|
||||
3. **Per-datagram addressing without a flow table was confirmed.** One relay
|
||||
loop per association; each frame carries its destination; the
|
||||
local-egress variant relays through one real `UdpSocket` with
|
||||
per-datagram `send_to`/`recv_from` (iroh-socks5's cut). Five
|
||||
in-order datagrams round-trip over one association
|
||||
(`udp_multiple_datagrams_one_association`).
|
||||
4. **Association lifetime = channel lifetime — confirmed.** The
|
||||
4. **Association lifetime = channel lifetime, confirmed.** The
|
||||
consumer's `shutdown()` half-closes the stream; the mux's
|
||||
implicit-EOF sentinel (REQ-CH-01) reaches the producer's stage read
|
||||
as EOF; the stage exits and the connection stays healthy (a fresh
|
||||
associate succeeds immediately after). No extra control vocabulary
|
||||
needed — the channels-level EOF is the association's end-of-life
|
||||
signal, as the iroh-socks5 eval predicted.
|
||||
5. **Control silence is structurally enforced — confirmed.** The
|
||||
as EOF; the stage exits and the connection stays healthy (a fresh
|
||||
associate succeeds immediately after). No extra control vocabulary
|
||||
needed; the channels-level EOF is the association's end-of-life
|
||||
signal, as the iroh-socks5 eval predicted.
|
||||
5. **Control silence is structurally enforced, confirmed.** The
|
||||
frame's first byte is ATYP, so any post-reply control byte parses
|
||||
as a garbage ATYP and fails the association loud
|
||||
(`udp_undecodable_frame_ends_association`, with the unassigned ATYP
|
||||
0x7F). The phase model needs no explicit mode byte: the reply
|
||||
itself synchronizes the mode switch.
|
||||
6. **Both egress variants — implemented and passing.**
|
||||
6. **Both egress variants: implemented and passing.**
|
||||
- **Local egress** (`LocalEgress`): one real UDP socket per
|
||||
association; per-datagram addressing; no flow table. Domain-form
|
||||
destinations resolve at the egress (producer-side DNS — the ssh
|
||||
destinations resolve at the egress (producer-side DNS, the ssh
|
||||
`-D` semantic, POC-resolved as "the egress owns resolution").
|
||||
- **Composed egress** (`ComposedEgress`): a lazy
|
||||
destination→alktunnels-udp-tunnel table (each new destination
|
||||
@@ -80,7 +80,7 @@ composed variant's per-destination open cost is measured.
|
||||
destination fan-out; the wire space itself is `u32`. The LRU cap +
|
||||
the dedup assertion (`open_count == 1` for repeated same-resource
|
||||
destinations) are pinned by `udp_composed_egress_lazy_table_lru_and_open_cost`.
|
||||
8. **The front door — RFC codec composition at the vanilla boundary,
|
||||
8. **The front door: RFC codec composition at the vanilla boundary,
|
||||
confirmed.** An in-process bridge exposes the association on a real
|
||||
UDP socket; the door's pump decodes RFC-header'd datagrams
|
||||
(`parse_udp_request`), sends `{addr, data}` frames in-band, and
|
||||
@@ -91,14 +91,14 @@ composed variant's per-destination open cost is measured.
|
||||
shape (the CONNECT front door in POC #1 already validated that
|
||||
pipe shape); this test pins the UDP-specific claim: RFC codec at
|
||||
the door, frames in-band, zero payload translation.
|
||||
9. **CONNECT on the same registration — regression-guarded.** One ALPN
|
||||
9. **CONNECT on the same registration, regression-guarded.** One ALPN
|
||||
(`alk/socks5`), one op (`channels/socks5/sub`), trivial params; the
|
||||
CONNECT path is the POC #1 data plane unchanged
|
||||
(`connect_still_served_on_the_same_registration`).
|
||||
10. **The identity-seam wiring requirement re-confirmed** for the
|
||||
composed path: the tunnel producer's channel-0 accept hook must
|
||||
set the opener identity (`set_identity`), or scope-gated tunnel
|
||||
opens fail closed with `FORBIDDEN "authentication required"` —
|
||||
opens fail closed with `FORBIDDEN "authentication required"`;
|
||||
the same POC #1 finding, now witnessed from the consumer side of
|
||||
the composed stack.
|
||||
|
||||
@@ -107,22 +107,22 @@ composed variant's per-destination open cost is measured.
|
||||
**The datagram stage is `select!`-driven, and the egress reply
|
||||
future's readiness contract is load-bearing.** The stage runs channel
|
||||
reads and the egress's reply stream concurrently (`tokio::select!`).
|
||||
Three readiness hazards were hit and fixed in this POC — all of them
|
||||
Three readiness hazards were hit and fixed in this POC, all of them
|
||||
generalizable API-design notes for the crate's Phase 1:
|
||||
|
||||
1. **The poll-then-block window loses replies.** The first iteration
|
||||
interleaved "drain egress replies (non-blocking), then block on the
|
||||
channel read" — the echo's reply arriving during the channel block
|
||||
channel read"; the echo's reply arriving during the channel block
|
||||
was never picked up. The fix is the concurrent select shape, which
|
||||
is also the honest full-duplex shape (an egress may produce
|
||||
unsolicited inbound datagrams at any time).
|
||||
2. **An instantly-ready `Ok(None)` starves the other branch with
|
||||
`biased`.** A `next_reply` that returns `Ok(None)` immediately
|
||||
(drained) makes the biased select take it every iteration — a
|
||||
(drained) makes the biased select take it every iteration, a
|
||||
livelock that starves the mux/demux on a current-thread runtime.
|
||||
The egress contract is therefore: *`next_reply` must be `Pending`
|
||||
when no reply is ready* (`Ok(None)` is a spurious wakeup, not a hot
|
||||
loop). The `LocalEgress` parks on `readable()` — note `writable()`
|
||||
loop). The `LocalEgress` parks on `readable()`; note `writable()`
|
||||
is the wrong readiness for UDP (always ready, the send-readiness,
|
||||
spins forever); `readable()` is the datagram-arrival readiness.
|
||||
The `ComposedEgress` parks on the shared inbound queue's `recv()`;
|
||||
@@ -131,7 +131,7 @@ generalizable API-design notes for the crate's Phase 1:
|
||||
3. **The length framing is applied exactly once, by the wire layer.**
|
||||
`Egress::next_reply` returns the *inner* bytes (`addr + data`);
|
||||
the producer's `write_frame` adds the single length prefix. (The
|
||||
first iteration framed in the egress and prefixed in the producer —
|
||||
first iteration framed in the egress and prefixed in the producer,
|
||||
double framing; the client parsed the length field as ATYP 0x00.)
|
||||
|
||||
**Double framing is the one wire bug the POC caught**, and it
|
||||
@@ -139,7 +139,7 @@ localizes a Phase 1 API decision: the egress (dial-callback) layer
|
||||
speaks *inner* bytes; the length codec is the wire layer's. The
|
||||
consumer side mirrors it: `UdpAssociation::send_datagram` frames once,
|
||||
its `DatagramReader` state (mid-frame position + batched-datagram
|
||||
pending queue) persists across `recv_datagram` calls — a datagram
|
||||
pending queue) persists across `recv_datagram` calls; a datagram
|
||||
split across channel chunks survives a caller-driven receive loop
|
||||
(the first cut dropped the reader state per call; the echo test's
|
||||
2-byte-prefix chunks exposed it).
|
||||
@@ -150,7 +150,7 @@ drops it (composed tunnels' write halves EOF their channels; the
|
||||
adopted channel entries are reaped by the sessions' `Drop`, alktunnels
|
||||
ADR-005). One structural bug here: the first cut bound the opened
|
||||
session to a local `let _session = ...` binding, which dropped at the
|
||||
end of the insert block — tearing the channel down at birth (the
|
||||
end of the insert block, tearing the channel down at birth (the
|
||||
relay task saw EOF instantly). The session must live inside the table
|
||||
entry. The crate's Phase 1 equivalent: the egress owns its tunnels
|
||||
end-to-end, and the *compiler* should enforce it (no `_` bindings).
|
||||
@@ -158,13 +158,13 @@ end-to-end, and the *compiler* should enforce it (no `_` bindings).
|
||||
## Wire/API notes for the wrapper
|
||||
|
||||
- `TargetAddr::to_be_bytes()` is the in-band address encoding (ATYP +
|
||||
addr + port) — the RFC UDP header minus RSV/FRAG. `unframe` decodes
|
||||
addr + port), the RFC UDP header minus RSV/FRAG. `unframe` decodes
|
||||
it by hand (synchronously over a borrowed slice);
|
||||
`fast_socks5::util::target_addr::read_address` is the async in-stream
|
||||
equivalent. `frame`/`inner`/`unframe` in the POC's `datagram.rs` are
|
||||
the Phase 1 codec's direct sketch.
|
||||
- `alktunnels::wire::frame_datagram`/`DatagramReader` are the length
|
||||
codec, reused unchanged — the shared framing story with
|
||||
codec, reused unchanged, the shared framing story with
|
||||
alktunnels' udp substrate (per the user's codec decision: depend on
|
||||
alktunnels, do not hand-roll).
|
||||
- The ASSOCIATE reply's `BND.ADDR` is the sentinel `127.0.0.1:0` on
|
||||
@@ -173,14 +173,14 @@ end-to-end, and the *compiler* should enforce it (no `_` bindings).
|
||||
(`relay_addr`) and never uses it for addressing. Only the front
|
||||
door has a real address to report.
|
||||
- `LocalEgress::next_reply` frames replies with the *sender's* address
|
||||
(the RFC's per-datagram source semantics, `recv_from`'s `from`) —
|
||||
(the RFC's per-datagram source semantics, `recv_from`'s `from`),
|
||||
not the original destination. For echo targets the two coincide;
|
||||
a real producer would attribute by egress socket (the composed
|
||||
variant's table entry carries the destination explicitly — its
|
||||
variant's table entry carries the destination explicitly; its
|
||||
replies are attributed correctly by construction).
|
||||
- The composed egress's per-destination attribution: the table's
|
||||
`reply_addr` is recorded at tunnel-open time (the datagram's
|
||||
original destination) — the tunnel IS the per-destination
|
||||
original destination); the tunnel IS the per-destination
|
||||
association, the eval's noted advantage of the composed shape.
|
||||
- `parse_udp_request` is async (though it completes immediately over a
|
||||
borrowed slice); a sync mirror (`unframe`) is used where the decode
|
||||
@@ -191,19 +191,19 @@ end-to-end, and the *compiler* should enforce it (no `_` bindings).
|
||||
## OQ status after this POC
|
||||
|
||||
- **OQ-SK-03 (UDP ASSOCIATE)**: the handle-swap baseline is
|
||||
POC-validated end to end — one channel per association, `{addr,
|
||||
POC-validated end to end: one channel per association, `{addr,
|
||||
data}` frames in-band (RSV/FRAG only at front doors), channel EOF =
|
||||
association lifetime, per-datagram addressing without a producer
|
||||
flow table. The datagram framing is wire-stable once published —
|
||||
flow table. The datagram framing is wire-stable once published, and
|
||||
the Phase 1 ADR has empirical ground under it. The demux-vs-phase
|
||||
question (the one-way-door extensibility argument) stays an ADR
|
||||
decision for Phase 1 with the first consumer in sight, but the POC
|
||||
adds evidence for the phase model: no in-band control vocabulary was
|
||||
needed, and post-reply garbage is refused structurally.
|
||||
- **OQ-SK-01 (dial policy)**: the egress seam is the dial-callback
|
||||
shape extended to the datagram stage (`EgressFactory` — the `DialFn`
|
||||
shape extended to the datagram stage (`EgressFactory`, the `DialFn`
|
||||
analogue). Both egress variants behind the callback, the protocol
|
||||
layer sees neither — the no-trait, boxed-halves precedent now holds
|
||||
layer sees neither; the no-trait, boxed-halves precedent now holds
|
||||
for the datagram plane too.
|
||||
- **OQ-SK-05 (target policy)**: the composed egress's per-target-ACL-
|
||||
for-free claim is validated (each destination rides its own tunnel
|
||||
@@ -214,6 +214,6 @@ end-to-end, and the *compiler* should enforce it (no `_` bindings).
|
||||
## POC placement
|
||||
|
||||
Standalone crate (global workspace, `/workspace/alksocks-udp-poc/`)
|
||||
per the placement conventions — it composes three published crates
|
||||
per the placement conventions; it composes three published crates
|
||||
(alkcall 0.7.1, alktunnels 0.1.0, fast-socks5 1.0.0) and needs no code
|
||||
from this repo. Findings land here; the crate is the artifact.
|
||||
+31
-31
@@ -18,7 +18,7 @@ leverages:
|
||||
4. **Task-Driven**: Structured task graphs with dependency analysis
|
||||
5. **Safe Exit**: Always have a way to unblock progress when stuck
|
||||
6. **Categorical Estimates**: Use risk/scope/impact categories, not time
|
||||
estimates. These are structurally important — upstream failures multiply
|
||||
estimates. These are structurally important: upstream failures multiply
|
||||
downstream damage regardless of developer type (human or LLM). See the
|
||||
cost-benefit framework in taskgraph's framework docs.
|
||||
|
||||
@@ -47,11 +47,11 @@ approaches
|
||||
**Process**:
|
||||
|
||||
1. Architect creates the architecture documentation structure:
|
||||
- `docs/architecture/README.md` — index with doc table, ADR table, lifecycle
|
||||
- `docs/architecture/overview.md` — package purpose, exports, dependencies
|
||||
- `docs/architecture/<component>.md` — one focused doc per component/area
|
||||
- `docs/architecture/decisions/` — numbered ADR files (ADR-001, ADR-002, ...)
|
||||
- `docs/architecture/open-questions.md` — centralized tracker with OQ-IDs
|
||||
- `docs/architecture/README.md` (index with doc table, ADR table, lifecycle)
|
||||
- `docs/architecture/overview.md` (package purpose, exports, dependencies)
|
||||
- `docs/architecture/<component>.md` (one focused doc per component/area)
|
||||
- `docs/architecture/decisions/` (numbered ADR files: ADR-001, ADR-002, ...)
|
||||
- `docs/architecture/open-questions.md` (centralized tracker with OQ-IDs)
|
||||
2. All docs start in `Draft` status. Spec docs reference ADRs by number (not
|
||||
inline rationale) and OQs by number (not inline questions).
|
||||
3. Architecture Review validates for ambiguities, risks, and structural issues
|
||||
@@ -134,16 +134,16 @@ WHAT, ADRs explain WHY, open questions track what's unresolved.
|
||||
- Extract decisions into numbered ADRs in `decisions/` directory
|
||||
- Centralize open questions in `open-questions.md` with OQ-IDs
|
||||
- Maintain `README.md` as the architecture index
|
||||
- Keep spec documents focused (~500 lines) — reference ADRs and OQs, don't inline them
|
||||
- Keep spec documents focused (~500 lines); reference ADRs and OQs, don't inline them
|
||||
- Redirect exploration work to Research Specialist
|
||||
- Iterate based on review feedback
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- `docs/architecture/README.md` — index with doc table, ADR table, lifecycle
|
||||
- `docs/architecture/<component>.md` — focused spec documents
|
||||
- `docs/architecture/decisions/` — numbered ADR files
|
||||
- `docs/architecture/open-questions.md` — centralized OQ tracker
|
||||
- `docs/architecture/README.md`: index with doc table, ADR table, lifecycle
|
||||
- `docs/architecture/<component>.md`: focused spec documents
|
||||
- `docs/architecture/decisions/`: numbered ADR files
|
||||
- `docs/architecture/open-questions.md`: centralized OQ tracker
|
||||
|
||||
---
|
||||
|
||||
@@ -179,17 +179,17 @@ sessions.
|
||||
**Mode**: Primary (manages worktrees and agent sessions)
|
||||
|
||||
**Uses**: The `worktree` tool from the **open-coordinator** opencode plugin.
|
||||
Single tool with `{action, args}` dispatch. Role is auto-detected — coordinator
|
||||
Single tool with `{action, args}` dispatch. Role is auto-detected: coordinator
|
||||
sessions get the full operation set, spawned implementation sessions get a
|
||||
limited set (current, notify, status). No mode toggle required.
|
||||
|
||||
**Tools**:
|
||||
|
||||
- `worktree({action, args})` — spawn, sessions, dashboard, message, abort,
|
||||
- `worktree({action, args})`, which dispatches spawn, sessions, dashboard, message, abort,
|
||||
cleanup
|
||||
- Bash (opencode CLI for session interaction)
|
||||
- Read (monitor task files)
|
||||
- `memory` / `memory_compact` — context management and session history (via
|
||||
- `memory` / `memory_compact` for context management and session history (via
|
||||
@alkdev/open-memory, when available)
|
||||
|
||||
**Key Behaviors**:
|
||||
@@ -218,10 +218,10 @@ limited set (current, notify, status). No mode toggle required.
|
||||
**Tools**:
|
||||
|
||||
- Read, Write, Edit, Glob, Grep, Bash
|
||||
- `worktree({action: "notify", ...})` — report progress/blockers to coordinator
|
||||
- `worktree({action: "current"})` — verify worktree assignment
|
||||
- `worktree({action: "notify", ...})` to report progress/blockers to coordinator
|
||||
- `worktree({action: "current"})` to verify worktree assignment
|
||||
- webSearch (documentation lookup)
|
||||
- `memory` / `memory_compact` — context management (via @alkdev/open-memory,
|
||||
- `memory` / `memory_compact` for context management (via @alkdev/open-memory,
|
||||
when available)
|
||||
|
||||
**Key Behaviors**:
|
||||
@@ -388,7 +388,7 @@ Implement OAuth2 authentication with provider abstraction.
|
||||
### Categorical Estimates
|
||||
|
||||
These fields are structurally important, not optional metadata. They power
|
||||
`taskgraph decompose`, `risk-path`, `critical`, and `bottleneck` — commands that
|
||||
`taskgraph decompose`, `risk-path`, `critical`, and `bottleneck`, commands that
|
||||
reveal structural problems in the task graph. A task missing `scope`, `risk`,
|
||||
`impact`, or `level` is a red flag indicating incomplete decomposition. See the
|
||||
cost-benefit framework in taskgraph's framework docs for the reasoning.
|
||||
@@ -422,15 +422,15 @@ cost-benefit framework in taskgraph's framework docs for the reasoning.
|
||||
|
||||
### Architecture work and the `level` field
|
||||
|
||||
Architecture work uses the same taskgraph format as implementation work —
|
||||
Architecture work uses the same taskgraph format as implementation work;
|
||||
there is no separate `architecture` level. The `level` enum
|
||||
(`planning, decomposition, implementation, review, research`) already covers
|
||||
architecture activities:
|
||||
|
||||
| Architecture activity | `level` | Rationale |
|
||||
|---|---|---|
|
||||
| Writing an ADR (the decision) | `implementation` | The ADR *is* the deliverable — the implementation of an architecture decision. Calling it `planning` would mislabel the artifact. |
|
||||
| Decomposing a spec into ADRs + OQs | `decomposition` | Direct fit — breaking a large architecture area into decideable units. |
|
||||
| Writing an ADR (the decision) | `implementation` | The ADR *is* the deliverable: the implementation of an architecture decision. Calling it `planning` would mislabel the artifact. |
|
||||
| Decomposing a spec into ADRs + OQs | `decomposition` | Direct fit (breaking a large architecture area into decideable units). |
|
||||
| Architecture review pass | `review` | Direct fit. |
|
||||
| Research / POC before a decision | `research` | Direct fit. |
|
||||
| Spec writing (overview, component spec) | `implementation` | The spec is the deliverable. |
|
||||
@@ -480,7 +480,7 @@ the deferral has two halves that must stay in sync:
|
||||
2. **The machine-readable half** (in `tasks/architecture/`): an
|
||||
external-trigger tracker task represents the unblocking condition. It is
|
||||
tagged `[external-trigger, deferred-oq]`, has `risk: trivial` and
|
||||
`level: research` (it is not actionable work — it tracks whether the
|
||||
`level: research` (it is not actionable work; it tracks whether the
|
||||
external condition has arrived), and its `id` is referenced from the OQ's
|
||||
`Blocked on:` text. This is the surface for the task graph: `taskgraph`
|
||||
tools can reason about it, and downstream work that depends on the
|
||||
@@ -490,7 +490,7 @@ When the unblocking condition arrives (a use case materializes, a crate is
|
||||
specced), the tracker task is marked `completed` and the OQ transitions from
|
||||
`deferred(scope)` to `open` (or directly to `resolved` if no architecture
|
||||
decision remains). The two halves serve different audiences and use one edge
|
||||
type (`depends_on`) — the reverse lookup uses `taskgraph dependents`. Do not
|
||||
type (`depends_on`); the reverse lookup uses `taskgraph dependents`. Do not
|
||||
add a `blocks:` field; it duplicates the edge and creates a sync hazard.
|
||||
|
||||
## Review Injection
|
||||
@@ -509,7 +509,7 @@ Use graph analysis to determine where reviews should happen:
|
||||
### Current (open-coordinator plugin)
|
||||
|
||||
The Coordinator uses the `worktree` tool from the open-coordinator opencode
|
||||
plugin. It's a single tool with `{action, args}` dispatch — no separate
|
||||
plugin. It's a single tool with `{action, args}` dispatch; there are no separate
|
||||
enable/toggle steps. Role is auto-detected from session state.
|
||||
|
||||
```
|
||||
@@ -563,13 +563,13 @@ a worktree.
|
||||
When the open-memory plugin is available alongside open-coordinator, the
|
||||
coordinator gains:
|
||||
|
||||
- `memory({tool: "children", args: {sessionId: "..."}})` — view sub-agent
|
||||
- `memory({tool: "children", args: {sessionId: "..."}})`: view sub-agent
|
||||
sessions spawned from the coordinator
|
||||
- `memory({tool: "messages", args: {sessionId: "..."}})` — read a spawned
|
||||
- `memory({tool: "messages", args: {sessionId: "..."}})` to read a spawned
|
||||
session's conversation for debugging
|
||||
- `memory({tool: "context"})` — check context window usage before long
|
||||
- `memory({tool: "context"})` to check context window usage before long
|
||||
monitoring sessions
|
||||
- `memory_compact()` — proactively compact at natural breakpoints
|
||||
- `memory_compact()` to compact proactively at natural breakpoints
|
||||
|
||||
Implementation agents can also use `memory({tool: "context"})` and
|
||||
`memory_compact()` to manage their context during long tasks.
|
||||
@@ -593,7 +593,7 @@ Once the hub is operational, coordination uses native operations:
|
||||
```
|
||||
|
||||
State moves from in-process tracking to Postgres `mappings` table. The
|
||||
open-coordinator plugin becomes unnecessary — the hub provides the same
|
||||
open-coordinator plugin becomes unnecessary; the hub provides the same
|
||||
capabilities as server-side operations accessible from any environment.
|
||||
|
||||
## Document Structure
|
||||
@@ -660,7 +660,7 @@ Accepted | Proposed | Deprecated | Superseded
|
||||
(Links to related specs and ADRs)
|
||||
```
|
||||
|
||||
Numbering starts at 001 within each project. ADRs are stable — once Accepted,
|
||||
Numbering starts at 001 within each project. ADRs are stable: once Accepted,
|
||||
they don't revert. If superseded, mark the old one and create a new one.
|
||||
|
||||
### Open Questions Format
|
||||
@@ -676,7 +676,7 @@ they don't revert. If superseded, mark the old one and create a new one.
|
||||
- **Status**: open | resolved | deferred(scope) | partially resolved
|
||||
- **Priority**: high | medium | low
|
||||
- **Resolution**: (when resolved)
|
||||
- **Blocked on**: (when deferred — concrete dependency that would unblock)
|
||||
- **Blocked on**: (when deferred; the concrete dependency that would unblock)
|
||||
- **Cross-references**: OQ-NN, ADR-NNN
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user