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
+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.
|
||||
Reference in new issue
Block a user