# alksocks SOCKS5 (RFC 1928) as a produced resource on [alkcall](https://crates.io/crates/alkcall) channels: the `ssh -D` (dynamic / SOCKS5 proxy) flavor as a first-class protocol crate; the sibling of [alktty](https://crates.io/crates/alktty) (terminals) and [alktunnels](https://crates.io/crates/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 ```rust 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) ```rust 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 with 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) ```rust 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) ```rust 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) ```rust use std::sync::Arc; use alkcall::channels::client::ChannelClient; use alksocks::{CredentialTable, FrontDoor, UdpFrontDoor}; let client: Arc = /* 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) ```rust 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`](https://docs.rs/alksocks/latest/alksocks/producer/struct.Socks5Producer.html) / [`register_socks5_openable`](https://docs.rs/alksocks/latest/alksocks/producer/fn.register_socks5_openable.html): 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`](https://docs.rs/alkcall/latest/alkcall/channels/pump/fn.pump_bidi.html) | | **Consumer** (wants the proxy) | [`Socks5ChannelSession`](https://docs.rs/alksocks/latest/alksocks/consumer/struct.Socks5ChannelSession.html) (CONNECT) / [`AssociateSession`](https://docs.rs/alksocks/latest/alksocks/consumer/struct.AssociateSession.html) (ASSOCIATE) / [`BindSession`](https://docs.rs/alksocks/latest/alksocks/consumer/struct.BindSession.html) (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](https://github.com/dizda/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`](https://docs.rs/alksocks/latest/alksocks/local/fn.tcp_dial.html) / [`udp_egress`](https://docs.rs/alksocks/latest/alksocks/local/fn.udp_egress.html) / [`bind_accept`](https://docs.rs/alksocks/latest/alksocks/local/fn.bind_accept.html)), the front doors ([`FrontDoor`](https://docs.rs/alksocks/latest/alksocks/door/struct.FrontDoor.html) + [`UdpFrontDoor`](https://docs.rs/alksocks/latest/alksocks/door/struct.UdpFrontDoor.html), the consumer-side exposure; [`ProducerDoor`](https://docs.rs/alksocks/latest/alksocks/local/producer_door/struct.ProducerDoor.html), the producer-side listener) | no | | `composed` | the alktunnels-composed providers: [`direct_egress`](https://docs.rs/alksocks/latest/alksocks/composed/fn.direct_egress.html) (the direct-op egress, a lazy destination→tunnel table) + [`bindfirst_accept`](https://docs.rs/alksocks/latest/alksocks/composed/fn.bindfirst_accept.html) (the BIND accept whose listener lives on a bind-first listen resource) | yes | ## Verification ```bash 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](docs/architecture/README.md): 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](https://docs.rs/alksocks): full crate documentation on docs.rs. ## License MIT OR Apache-2.0