glm-5.3-flash 8af23eeee0 docs: OQ-SK2-09 (the UDP door's peer-pin posture) + OQ-SK2-06's trigger-ownership check
- OQ-SK2-09 recorded: option 2 (the same-IP lock + reply-port
  refresh + the RFC drop rule) landed in 0.1.0 (31fb11c); option 3
  (the control-conn-derived expected peer — the complete fix)
  deferred on a concrete wildcard-bind deployment with address-level
  client mobility (QUIC migration is the likely driver); the
  deferred-index row added
- OQ-SK2-06: the trigger ownership checked (2026-09-28) — genuinely
  external by verified default (no suite-internal surface names a
  sandboxed alksocks consumer; the substrate is complete and
  gate-verified, nothing waits on nothing); the Schrödinger-bind
  check recorded in the OQ + the tracker task
- overview.md: the OQ index updated (SK2-09 added, SK2-06's
  ownership noted); the door bullet in the CHANGELOG carries the
  peer-pin posture + the async resolver note

Verification: cargo test + --all-features (15 suites each), clippy
--all-targets --all-features -D warnings clean, fmt clean, wasm check
clean, doc 0 warnings.
2026-09-28 09:17:04 +00:00
2026-09-12 07:01:39 +00:00

alksocks

SOCKS5 (RFC 1928) as a produced resource on alkcall channels: the ssh -D (dynamic / SOCKS5 proxy) flavor as a first-class protocol crate — the sibling of alktty (terminals) and alktunnels (-L/-R tunnels). The RFC 1928 conversation rides inside a channels data channel like any other produced resource, and target dials happen on the producing side.

The defining posture: the protocol never binds a port unless explicitly configured to. Kernel binds exist only as explicit, feature-gated assembly shapes on either side — the consumer may expose the resource on a local port (the front door vanilla clients — curl, browsers, tun2proxy — point at), and the producer may serve from a real listener. A producer/consumer protocol crate, no transport dependencies; the transport is whatever alkcall runs over.

Quick start

Producer — register the SOCKS5 open op

use std::sync::Arc;

use alkcall::channels::ChannelCore;
use alkcall::registry::registration::OperationRegistry;
use alkcall::core::auth::AuthContext;
use alksocks::producer::Socks5Producer;

// The three seams (ADR-004): dial (CONNECT), egress (UDP ASSOCIATE),
// accept (BIND). Missing seams = those commands refuse
// RFC-faithfully; behind `local`, alksocks::local::{tcp_dial,
// udp_egress, bind_accept} are the reference socket-backed
// constructors, and the composed providers (feature `composed`) ride
// alktunnels' direct-op/bind-first resources instead.
let producer = Socks5Producer::builder()
    .dial(alksocks::local::tcp_dial())
    .egress(alksocks::local::udp_egress())
    .accept(alksocks::local::bind_accept("127.0.0.1".parse()?))
    .build();

let core = ChannelCore::new(manager, default_policy());
let op_registry = Arc::new(OperationRegistry::new());

// Registers `channels/socks5/sub` on the `alk/socks5` ALPN: the
// registry's ACL (scope `socks5:open`) gates the open op, the
// session-gate establisher checks the opener identity, and the pump
// handler drives the RFC 1928 state machine in-stream over the
// channel's `BiStream`. Wire `core` + `op_registry` into your
// channels serving stack — this call is the socks5-specific step:
producer.register(&core, &op_registry, AuthContext::anonymous(b"alk/socks5"), None)?;

Consumer — open a SOCKS5 channel (CONNECT)

use alkcall::channels::client::ChannelClient;
use alksocks::rfc1928::util::target_addr::TargetAddr;
use alksocks::Socks5ChannelSession;

let client: ChannelClient = /* your alk/channels connection */;

// One session = one channel = one SOCKS5 session. The RFC handshake
// runs in-stream over the channel; a refused target surfaces the RFC
// reply code (typed errors, no phantom channel — the adopted channel
// is torn down before the error returns).
let mut session = Socks5ChannelSession::connect(
    &client,
    TargetAddr::Domain { addr: "example.internal".into(), port: 80 },
).await?;

let (mut read, mut write) = session.split().expect("pipe");
// ... raw pass-through: the channel IS the target connection ...

Consumer — UDP ASSOCIATE (the framed datagram stage)

use alksocks::AssociateSession;
use alksocks::rfc1928::util::target_addr::TargetAddr;

// One channel per association; datagrams ride `[len: u16 BE]
// [addr + data]` frames in-band (ADR-003) — the RFC codec exists at
// the front doors only.
let mut assoc = AssociateSession::associate(&client).await?;
let dest = TargetAddr::Domain { addr: "dns.internal".into(), port: 53 };
assoc.send_datagram(&dest, b"query").await?;
let (src, payload) = assoc.recv_datagram().await?.expect("a datagram");

Consumer — BIND (the reply#1/reply#2 reader)

use alksocks::BindSession;
use alksocks::rfc1928::util::target_addr::TargetAddr;

// Requires the `socks5:bind` grant on the producing side (a separate
// capability from `socks5:open` — never implied, ADR-006). The
// session drives the phase model: reply#1 → quiet wait → exactly one
// accept → reply#2 → raw pass-through (ADR-007).
let mut bind = BindSession::bind(&client, TargetAddr::Ip("0.0.0.0:0".parse()?)).await?;
let reply1 = bind.reply1().await?; // the BND.ADDR the producer bound
// ... hand reply1's address to the peer, await the accept + reply#2 ...

Consumer — the front door (vanilla clients)

use std::sync::Arc;

use alkcall::channels::client::ChannelClient;
use alksocks::{CredentialTable, FrontDoor, UdpFrontDoor};

let client: Arc<ChannelClient> = /* your alk/channels connection */;

// The ssh `-D` local-exposure shape: a real `socks5://127.0.0.1:port`
// endpoint relaying each accepted connection into a channel. NoAuth
// raw pipe by default; a `CredentialTable` turns the door into an
// RFC 1929 cred-bearing endpoint whose credentials map to producer
// identities (ADR-005). `UdpFrontDoor` serves one association per
// bound relay socket.
let door = FrontDoor::builder()
    .bind("127.0.0.1:1080".parse()?)
    .build()?;
door.serve(client).await?;

Producer — the local listener shape (the other door)

use alksocks::producer::Socks5Producer;

// Feature `local`: serve a produced SOCKS5 resource from a real
// listener — vanilla clients dial it, each accepted connection is
// served via `serve_socks5_over` (the NoAuth in-stream RFC
// conversation; the seams dial locally). The bind is explicit — the
// builder requires it; the protocol layer never binds.
let producer = Socks5Producer::builder()
    .dial(alksocks::local::tcp_dial())
    .build();
let door = alksocks::ProducerDoor::builder()
    .producer(producer)
    .bind("127.0.0.1:1081".parse()?)
    .build()?;
let guard = door.serve().await?;

Architecture

Two roles, symmetric per the alk convention (avoid "server"/"client": both sides of a connection can be either; role follows the resource):

Role Via alk/channels
Producer (dials targets, serves the RFC conversation) Socks5Producer / register_socks5_openable — the open op (channels/socks5/sub) with the session-gate establisher and the command-dispatch pump; the RFC 1928 typestate core drives negotiation, command read, and replies in-stream over the channel's BiStream; CONNECT/BIND data planes ride alkcall::channels::pump_bidi
Consumer (wants the proxy) Socks5ChannelSession (CONNECT) / AssociateSession (ASSOCIATE) / BindSession (BIND) — typed channels-native sessions, each owning its channel's teardown

One connection hosts many SOCKS5 sessions (one per channel); sessions are independent. Both roles are callable from either connection side (producer and consumer are roles, not sides); RFC 1928's own client/server roles keep their RFC names inside the protocol layer.

The RFC conversation is the data plane

The open op's params are deliberately trivial ({}, ADR-001) — there is nothing to negotiate at open time (the SOCKS5 target is unknown until the RFC command arrives). Everything else is RFC 1928, in-stream:

  • CONNECT: raw pass-through, 0 B overhead — the channel's halves are the target connection (the two-pump shape).
  • ASSOCIATE: the reply's BND.ADDR is a pure RFC handle (the sentinel in-band); datagrams ride [len: u16 BE][addr + data] frames in-band (ADR-003, the handle swap) to the egress seam. One channel per association; the association lifetime is the channel's.
  • BIND: reply#1 → quiet wait → exactly one accept (the accept seam) → reply#2 → raw pass-through (ADR-007).

Authorization is two grants, never implied (ADR-006): the open gate (socks5:open) at the registry, the command gate (socks5:bind) at BIND dispatch. The target policy is the dialer (ADR-012): the producer's dial/egress/accept closures are the security boundary — whatever they allow is what the resource reaches.

The seams

The protocol layer is substrate-agnostic by construction (ADR-004): DialFn / EgressFactory / BindAcceptFactory are functions, not traits at the three I/O planes; ownership rides the handle structurally. A channels BiStream, a TcpStream, or an in-process pipe drops in behind the same seams — no socket type leaks into the protocol layer.

WASM target

The default crate (no features) compiles to wasm32-unknown-unknown — the protocol layer for a sandboxed TS/Python adapter. The RFC 1928 core is a targeted fork of fast-socks5 v1.0.0's T-generic subset, extracted in-tree as fully owned code (src/rfc1928/, MIT notice bundled; ADR-011/013): no kernel-net code, no net feature — the socket I/O rides local alone, and the default build is wasm-clean by construction.

Features

Feature Contents wasm
(default) the RFC 1928 core (src/rfc1928/ — codecs, typestate, client use_stream), the ASSOCIATE wire codec, the open-op spec, the three seams, the producer + consumer session types — protocol only yes
local the socket-backed seam providers (tcp_dial / udp_egress / bind_accept), the front doors (FrontDoor + UdpFrontDoor, the consumer-side exposure; ProducerDoor, the producer-side listener) no
composed the alktunnels-composed providers: direct_egress (the direct-op egress, a lazy destination→tunnel table) + bindfirst_accept (the BIND accept whose listener lives on a bind-first listen resource) yes

Version 0.1

The first release rides alkcall 0.8.0 (the establisher reply projection is not consumed yet — the open op's params are trivial) and alktunnels 0.2.0 (the direct op + the bind-first/forwarded flow power the composed providers). Wire-stable surface: the alk/socks5 ALPN, the channels/socks5/sub open op with {} params, and the two scope constants — all pinned by ADRs 001–014.

Verification

cargo test                                    # default crate (wasm-clean)
cargo test --features local                   # + real-socket suites
cargo test --all-features                     # + the composed suites
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo check --target wasm32-unknown-unknown   # the wasm-clean guard

Documentation

  • Architecture docs — the authoritative spec set (in the repository, not the published package): ADRs (001–014), the wire format, the producer/consumer contracts, and the fork provenance.
  • API docs — full crate documentation on docs.rs.

License

MIT OR Apache-2.0

S
Description
No description provided
Readme
1.7 MiB
0 Stars 6 Watchers 0 Forks
Languages
Rust 99%
Python 0.9%
Shell 0.1%