Files
alksocks/AGENTS.md
T
glm-5.3-flash 08a3694464 fuzz: adopt fuzzing (wave 1) + alkcall 0.8.1 / alktunnels 0.2.1 bumps
Adopt the alk*/fuzzing posture (alkcall 0.8.1 + alktunnels 0.2.1
precedent) before the v0.1 publish — the cheapest moment this crate
will ever have for it:

- alkcall 0.8.1 + alktunnels 0.2.1: the duplicate-adopt/open fix is
  directly relevant to this crate's consumer open/adopt paths
- fuzz/ workspace (the sibling layout): nightly-pinned subtree, stable
  fuzz/shared library (corpus replay = the fuzz gate), detached runner,
  JSON dict, deterministic seed generator; 129 committed seeds
- three wave-1 targets over already-pub APIs: datagram_codec (the
  ASSOCIATE frame codec — the alkcall-fuzz-doc-assigned downstream
  target), rfc1928_codec (the forked RFC 1928 core's handshake state
  machines, both protocol directions), params_parse (the wire-stable
  open-op surface)
- one diagnostic: DatagramReader::retained_bytes() (the decoder-state
  bound's assertion target, the alktunnels retained_bytes() posture)
- docs/research/fuzzing.md: the scoped plan + implementation log;
  AGENTS.md gains the corpus-replay verification line
- campaigns: 3 × 10-min detached runs exited 0 (datagram ~1.9M execs,
  rfc1928 ~2.0M, params ~3.1M; oom/timeout/crash 0/0/0 on all 33 fork
  jobs per target; coverage saturated on both codec targets). No crate
  bugs; three harness-model defects found + fixed (2 pre-campaign by
  corpus replay, 2 campaign-found, crash inputs promoted as seeds 900/
  901): the len==0 well-framed decode partition, the fieldless-struct
  serde-seq form, the BadAtyp re-frame arm, the short-domain count-walk
  gate
- wave 2 (session_pumps stateful target) deferred to post-0.1 — the
  byte-level targets carry the attacker-reachable load for the publish

Verification: cargo test (15 suites ok, default + all-features), clippy
(all-targets + wasm + fuzz/shared), fmt, cargo doc, publish --dry-run,
corpus replay 129 seeds green, cargo fuzz build clean.

Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alktunnels
docs/research/fuzzing.md; docs/research/fuzzing.md (this crate's copy)
2026-09-28 11:18:08 +00:00

22 KiB

AGENTS.md

Operating instructions for opencode agents working in this repo. opencode auto-loads this file as instructions, overriding the built-in defaults for this project. Custom agents in .opencode/agents/ inherit these rules unless their own prompts say otherwise.

Git Workflow

Commit and push when reasonable. When a change is complete and verified (build + lint + tests pass), commit and push to origin/main without asking. This overrides the built-in default of "only commit when explicitly asked."

The workflow:

  1. Make the change
  2. Verify: cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo doc --no-deps if docs changed
  3. Inspect git status and git diff before staging — stage only the intended files, never secrets
  4. Write a concise commit message matching the repo style (see git log --oneline -10). For multi-point changes, use a summary line plus a body with bullet points and a verification block.
  5. git push origin main
  6. Report the commit hash and the verification summary

Exceptions — do not commit or push without asking:

  • The change is exploratory / speculative (you're not sure the user wants it kept)
  • The user is actively reviewing the diff and may ask for changes
  • The change touches the wire format or a trait shape that backends or consumers implement (one-way doors — see "Wire formats are stable" and "Backend trait shapes are one-way doors" below; once consumers exist, those signatures are wire-stable contracts)
  • You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks

Never commit secrets, keys, or credentials. If a commit fails or hooks reject it, fix the issue and create a new commit — do not amend the failed one.

Git identity is preconfigured (glm-5.3-flash <glm-5.3-flash@alk.dev>). Do not change git config, skip hooks, or use git commit -i.

Project Conventions (Rust / SOCKS5 protocol crate)

This is the SOCKS5 protocol crate — a wrapper around fast-socks5 that offers the SSH -D (dynamic / SOCKS5 proxy) capability alongside alktunnels' -R and -L, composed with alkcall channels ("ALPN as a service"). It sits in the alk* family: alkcall (call + channels — the substrate), alktty (terminal sessions), alktunnels (arbitrary -L/-R/ tunnels). The distinguishing posture of this crate: the SOCKS5 server never binds a port unless explicitly configured to — the protocol is served inside alkcall channels data channels (a produced, ACL-scoped resource), not on a kernel socket. A local bind is one optional assembly-layer shape, not the protocol's home. The conventions below apply to all work in src/ and tests/. They mirror .opencode/agents/implementation-specialist.md §Project Conventions and are repeated here so they apply to every session, not just spawned implementation agents.

  1. No comments in code unless the user explicitly asks. This is a project-wide convention. Doc comments (///, //!) are fine and expected on public API. Inline // comments only when the user asks or when a non-obvious safety/correctness constraint would otherwise be missed (e.g., "a two-pump tunnel must shut down the opposite sink on pump completion — try_join! alone deadlocks; see alkcall ADR-050's pump_bidi / alknet ADR-078").

  2. Error handling — thiserror for library error types; map SOCKS5 reply codes (fast_socks5::ReplyError) faithfully per RFC 1928. No panics in library code. No unwrap() or expect() outside tests. If you reach for unwrap, the error path wasn't specified — stop and decide what should actually happen. For poisoned RwLock/Mutex, use unwrap_or_else(|e| e.into_inner()) so a panic in one operation does not cascade to other operations.

  3. tokio is the async runtime — all I/O is async. fast-socks5 is tokio-native; the wrapper's protocol layer, pumps, and client session types are all async. Use tokio::sync primitives (oneshot, mpsc) for lifecycle correlation. Any non-wasm backend module (local TCP listener for a "real" socks5:// endpoint) lives behind a feature flag like alktty/alktunnels' local.

  4. WASM target is load-bearing (by default) — the default crate (protocol-only) should compile to wasm32-unknown-unknown, following the alktty/alktunnels precedent. Use the wasm-clean tokio subset (rt, sync, io-util, macros, time) — do NOT use features = ["full"] — and keep socket/platform I/O feature-gated. Note: fast-socks5 uses tokio::net and socket2 unconditionally as published; the posture is the targeted fork (OQ-SK-04, resolved 2026-09-20 — convention 17; extraction pinned 2026-09-21, ADR-011 §Amendment; fork 2026-09-22, ADR-013): the T-generic subset was extracted in-tree — the kernel-net handlers are not copied — so the RFC 1928 core (src/rfc1928/) is wasm-clean by construction with no net-shaped feature; socket I/O rides local alone. Run the wasm check whenever a non-backend module changes; it is a hard gate (no dependency-graph failure is expected — the core contains no tokio::net).

  5. Wire format is stable — the SOCKS5 protocol itself is RFC 1928 (fixed); this crate's wire surface is the ALPN + the channel open-op params that carry it. Those params are a one-way door once consumers exist. Follow the alktunnels precedent: params are a self-contained JSON object identifying the produced resource (alkcall ADR-039 — params is ALPN-specific, interpreted by the open handler). Any params-format ADR must be written before the first consumer exists; after that, changes are additive-only.

  6. Producer/consumer, not server/client — both sides of a channels connection can initiate. A producer exposes the SOCKS5 service (registers openable channels via ChannelCore::register_openable); a consumer opens SOCKS5 channels and speaks RFC 1928 inside them. Both sides can be both simultaneously — connection direction (who opened it) is independent of service direction (who dials targets, who serves). Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect side" for the connection-establishment half specifically. (SOCKS5's own client/server roles are RFC vocabulary and keep their RFC names inside the protocol layer.) See alkcall ADR-022, ADR-037.

  7. Substrate-agnostic by construction — the protocol layer must not know whether the far side of a SOCKS5 connection is a kernel TCP socket, a channels BiStream, a unix socket, or an in-process pipe. fast-socks5's explicit server API is generic over T: AsyncRead + AsyncWrite + Unpin (Socks5ServerProtocol<T, ...>, transfer(inbound, outbound)); the wrapper must preserve that genericity — the channels adapter feeds the SOCKS5 state machine a BiStream (which is AsyncRead + AsyncWrite), the local backend feeds it a TcpStream. Substrate-specific types are confined to feature-gated backend modules, injected at the assembly layer — the same inversion-point pattern as alktty's TtyBackend and alktunnels' pump halves.

  8. Two-pump shutdown-on-completion is a contract — the SOCKS5 CONNECT data plane is the canonical two-pump shape (one pump per direction). Each pump MUST shut down the opposite sink when it completes; tokio::try_join! alone deadlocks. The helper is pinned upstream as alkcall channels::pump_bidi (ADR-050, review 007 R-03) — use it; do not hand-roll the two-pump shape. The channels layer drops all per-channel senders on transport EOF — rely on that for teardown, and emit EOF sentinels (zero-length chunks) on clean sink shutdown. Await pump_bidi inline inside the OpenHandler's task — the returned JoinHandle must track the data-plane lifetime (R-02; early return = teardown-at-birth).

  9. No forced local binding — the defining requirement of this crate. The SOCKS5 server must run without binding any port: the RFC 1928 conversation is carried inside a channels data channel, and target dials happen on the producing side (or hop further through alktunnels/alksocks). Local binds exist only as explicit, optional assembly-layer shapes on either side: the producer may serve from a real listener (a feature-gated local backend), and the consumer may expose the resource on a local port (the ssh -D front door that vanilla clients — curl, browsers, tun2proxy — point at). The protocol itself never binds. UDP ASSOCIATE is the hard case: RFC 1928 replies with a UDP relay address and the client sends datagrams to it, which seems to demand a bind — how the relay address is virtualized (or the associate shape adapted) is a Phase 0 question (OQ-SK-03). Binding decisions belong to the caller (assembly layer), never the protocol crate.

  10. Backpressure and limits are inherited, not redefined — bounded per-channel buffers, the 256-channel default caps (per-connection memory bound, ADR-040; per-identity DoS defense, ADR-041 — policy knobs, configurable without a wire change; the u32 wire space itself is ~4 billion), monotonic channel IDs, and the zero-length-sentinel EOF convention are alkcall channels invariants (see alkcall ADR-040/041/034). The crate consumes them via ChannelCore/ChannelClient; do not build a second demux/mux or re-derive limits. SOCKS5 CONNECT is a single stream per open (no sub-demux inside the channel) — one channel per SOCKS5 session, no protocol-level flow key.

  11. Vendored core types come from alkcall — Connection, ProtocolHandler, BiStream, BidiStreamSource, AuthContext, Identity, IdentityProvider, AccessControl, OwnershipProvider, HandlerError, StreamError come from alkcall::core. Do not vendor copies into this crate. alkcall is v0.8.x — breaking changes are expected at this major-zero stage; this is an early consumer, so we find and fix issues upstream rather than working around them. Pin alkcall = "0.8.0" and bump deliberately. The establishment surface (alkcall ADR-049 + amendments) is load-bearing for this crate: SOCKS5 session opens use register_openable_with_establisher so a refused/auth-failed session is a typed channel:open_failed call error, never a phantom channel; the establisher returns the negotiated handle via Establishment::new(plan) (typed-opaque ChannelPlan — payloads must be Send + Sync; 0.8.0 adds the optional reply projection — with_reply_field(s), ADR-049 amendment 3). The 0.7.0 identity surface (ledger CF-005/CF-006) is load-bearing for auth mapping: the per-call opener identity arrives on the open-op hooks — map it to SOCKS5 credentials/ACL there, not via any in-band SOCKS auth unless clients need RFC-facing username/password.

  12. Access control — the producer's openable channels get authorization for free via ChannelCore::register_openable, which wires AccessControl into the operation spec (the registry runs the ACL before the open handler). Scope-gate SOCKS5 opens (e.g. SOCKS5_OPEN_SCOPE, shape following alktty's TTY_OPEN_SCOPE) and optionally consult OwnershipProvider for resource ownership. SOCKS5 is arbitrary-egress by nature — treat the open gate and the target policy as the security boundary; per-target allowlists (if any) are a producer-side policy question for Phase 1 (OQ-SK-05). See alknet ADR-024 (registry layering, alkcall ADR-019), alknet ADR-050 (ownership, alkcall ADR-011), and alktunnels' resolved posture (target selection lives in the SOCKS5 protocol at the producing side; whatever ACL governs the socks5 resource governs everything reachable through it).

  13. Feature flags — substrate backends are feature-gated if the need arises. The base crate should compile lean (no socket/platform deps unless the feature is on). Verify both cargo test (default) and cargo test --all-features pass if features are added.

  14. Naming — Rust standard: snake_case for functions/variables/ modules, PascalCase for types/traits, SCREAMING_SNAKE_CASE for constants.

  15. Module structure — one module per file under src/, re-exported from src/lib.rs. Public API surface is lib.rs re-exports. The expected shape (pending Phase 1 pinning): the SOCKS5 protocol wrapper (server state machine + client session), the channels adapter (producer half — open handler for this crate's ALPN), the consumer half (a typed client that opens SOCKS5 channels and speaks RFC 1928 through them), and feature-gated backend modules (the optional local-bind listener). Backend modules are feature-gated and never imported from the protocol/adapter/client modules.

  16. ALPN naming — this crate owns a alk/-prefixed ALPN (provisional alk/socks5; final naming per alkcall ADR-004/ADR-006 convention). One ALPN per protocol. The ALPN string is wire-stable once published — decide via ADR before the first consumer.

  17. Upstream: alkcall is ours to shape; fast-socks5 is forked in-tree. alkcall is ours (the actively co-developed substrate): file asks early and land them there rather than working around them locally — the alktunnels precedent (the E-01/E-02 sweep, filed from its Phase 0 and landed in alkcall 0.5.0 within a day; the graduation sweep landed in alkcall 0.8.0 / alktunnels 0.2.0). fast-socks5 is NOT ours and never will be — it is third-party upstream (/workspace/fast-socks5 is a read-only reference checkout, v1.0.0, MIT), but the T-generic subset we depend on is now ours, as a targeted fork (OQ-SK-04 resolved 2026-09-20 as the inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 §Amendment; fork posture pinned 2026-09-22, ADR-013): the subset lives in src/rfc1928/ as fully owned code — project conventions apply to it unconditionally (no vendor carve-outs, no lint-allow posture, no verbatim preservation). The extraction cut (ADR-011's table) is the hand-port map: if a future need surfaces (e.g. the kernel-net region for a standalone vanilla-server example), port the piece from the reference checkout, adapt it to the conventions, and let the differential suite guard it. A future upstream fix that matters is a hand-port too — nothing is absorbed silently, and nothing waits on upstream. Keep /workspace/fast-socks5 for differential testing of RFC edge cases. Provenance is permanent: the module docs and the bundled license notice (src/rfc1928/LICENSE) always name the origin (v1.0.0, the extraction range, the fork point) — extend, never replace, the notice.

Verification Commands

Run these before committing. All must pass.

cargo test                                    # full suite
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps                           # if docs changed
cargo test --all-features                     # if features are added
cargo test --manifest-path fuzz/shared/Cargo.toml   # corpus replay (the fuzz gate)
cargo check --target wasm32-unknown-unknown   # default crate stays wasm-clean
cargo clippy --target wasm32-unknown-unknown -- -D warnings
cargo publish --dry-run --allow-dirty         # before a release

The corpus replay is the fuzz gate (the alkcall §7.9 posture): it replays every committed seed through the same invariant functions the fuzz targets run, on stable, without nightly. Campaigns (nightly, cargo-fuzz) run manually via fuzz/run-detached.sh when src/wire.rs, src/rfc1928/, or src/params.rs change — never as a foreground child of an agent session (docs/research/fuzzing.md §5).

The wasm check is the structural guard for the "default crate is wasm-clean" invariant (same as alktty/alktunnels) — run it whenever a non-backend module changes. It is a hard gate: the RFC 1928 core is an extraction with no kernel-net code (ADR-011 §Amendment), so no dependency-graph failure is expected.

Architecture Context

  • docs/research/phase-0.md — the Phase 0 (Exploration) document and the current state of this repo: vision, prior art, open questions (OQ-SK-01..NN), and POC candidates. Read it before non-trivial work. The SDD process lives in docs/sdd_process.md (Phase 0 in progress; docs/architecture/ does not exist yet).
  • The prior art for this crate:
    • alktunnels — /workspace/@alkdev/alktunnels (v0.1, the closest sibling): the producer/consumer tunnel crate on alkcall channels. Its Phase 0 findings (docs/research/phase-0-findings.md) settled the hub-owns-the-connection model, the two-pump shape, the establishment story (alkcall ADR-049), and the -D-composes-at-the- assembly-layer conclusion — this crate is that conclusion's realization. Its Phase 1 ADRs (params shape, ALPN strategy, access-control posture) are the template for this crate's Phase 1.
    • alktty — /workspace/@alkdev/alktty (v0.4): the first producer/consumer protocol crate on alkcall channels; the backend inversion point, wasm-clean default crate, and feature-gated local backend precedents.
    • alkcall — /workspace/@alkdev/alkcall (v0.8.0, crates.io). The substrate: call protocol + channels multiplexing. This crate consumes alkcall::core types and the channels ChannelCore/ChannelClient/register_openable_with_establisher surface, same as alktty/alktunnels. 0.8.0 adds the establisher reply projection (with_reply_field, ADR-049 amendment 3), flavor-form open-op ids in discovery (ADR-047 amendment 3), and the in-tree ChannelRelay/HubLegTemplate (ADR-051) — the hub re-produce machinery the SOCKS5 hub story composes on.
    • alktunnels — v0.2.0 (ADRs 007/008): the direct op (channels/tunnel/direct — dynamic-target egress; OQ-SK-01 Option A's vehicle), the bind-first establisher + forwarded op (the composed BIND substrate; OQ-SK-08 variant 2), and TunnelListener.
    • fast-socks5 — /workspace/fast-socks5 (v1.0.0, MIT, third-party upstream — NOT ours, never will be). The SOCKS5 implementation the RFC 1928 core forked: explicit typestate server API (Socks5ServerProtocol<T, states::*>), generic over T: AsyncRead + AsyncWrite + Unpin; run_tcp_proxy / run_udp_proxy_custom / transfer are the interception points; client side has Socks5Stream (generic over the backing socket) and Socks5Datagram (UDP associate). The posture is the targeted fork (OQ-SK-04 resolved 2026-09-20 as the inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 §Amendment; fork 2026-09-22, ADR-013 — convention 17): the T-generic subset lives in src/rfc1928/ as owned code, no kernel-net code and no net feature; BIND and the new_reply publication are the two carried changes; the reference checkout stays for differential testing of RFC edge cases.
    • alknet's SOCKS5 client — /workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs (ADR-090): the client-side example need — Socks5UdpSocket implements quinn::AsyncUdpSocket so QUIC rides SOCKS5 UDP ASSOCIATE. Validated by the quinn-proxy POC (/workspace/@alkdev/alknet/docs/research/quinn-quic-proxy/ findings.md). The open question for this crate's client story: the same trait impl against noq (iroh's quinn fork, /workspace/noq — noq::AsyncUdpSocket, Endpoint:: new_with_abstract_socket exist but with a different surface shape; see OQ-SK-06).
  • Key upstream ADRs that inform this crate's design (alknet numbers; alkcall's ports live in /workspace/@alkdev/alkcall/docs/architecture/ decisions/):
    • alknet ADR-093 / alkcall ADR-035 — channels pure channel multiplexing (8-byte header, no stream_type); the SOCKS5 session rides inside the channel's BiStream
    • alknet ADR-078 / alkcall ADR-050 — two-pump shutdown-on-completion; the helper is pinned upstream as alkcall::channels::pump_bidi — use it, do not hand-roll
    • alkcall ADR-049 (amendment 2) — the establishment phase; the establisher returns the negotiated handle via Establishment::new (typed-opaque ChannelPlan, Send + Sync payload constraint)
    • alknet ADR-074 / alkcall ADR-038 — ChannelConnection as a BidiStreamSource; every handler receives a Connection
    • alknet ADR-075 / alkcall ADR-039 — ChannelsAdapter / ChannelManager (substrate-agnostic demux; params is ALPN-specific — for this crate, the SOCKS5 service identity/policy)
    • alknet ADR-071 / alkcall ADR-034 — channels wire format (one-way door); alkcall ADR-036 — channel 0 is pre-negotiated alk/call; alkcall ADR-037 — channel lifecycle operations (channel/open on channel 0)
    • alkcall ADR-042 — hub relay; alktunnels' hub-proxy refinement — the hub terminates and re-produces per-hop ACL (a SOCKS5 resource is the same shape, "further downstream" — alktunnels phase-0-findings §Prior art)
    • alkcall ledger CF-005/CF-006 (0.7.0) — the connect-side serving identity seam: caller identity precedence (token > ServingConfig.identity > transport), per-call opener identity on the open-op hooks
  • The suite decomposition context: alknet was the massive POC ("vpn-like without being a vpn" + transport agnosticism) being rewritten; this crate extracts and improves its SOCKS5 story (client side: ADR-090's proxy support; server side: the -D capability alktunnels deferred to composition). alknet-client/src/socks5.rs and the quinn-proxy POC findings are the direct ancestors of this crate's client work.
  • If a TODO references a design direction that a later ADR has decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design.