Files
alksocks/README.md
T
glm-5.3-flash f56b44589c release: v0.3.0 — the dependency-major (alkcall 0.10.0 / alktunnels 0.4.0)
- alkcall 0.9.0 -> 0.10.0 (the gateway ownership-seam release): the
  GatewayDispatch ownership-provider threading + the loud unwired-spine
  default are not on this crate's paths (no gateway module use; registry
  dispatch on the native wire untouched). Zero API/wire change here.
- alktunnels 0.3.0 -> 0.4.0 (its dependency-major ride): no alktunnels
  API/wire change; the composed providers see none of it.
- fuzz/shared: the harness's own alkcall pin bumped in lockstep (its
  ChannelOpenError construction makes a drifting pin a duplicate-type
  compile failure, not silent drift) — the only bump contact.
- Docs: CHANGELOG 0.3.0 entry, README version blurb (alktunnels style),
  AGENTS.md pin update, deps tree in overview.md.
- Minor-not-patch for the established reason: semver-incompatible
  dependency bumps + this crate's public ChannelOpenError re-exports
  (SocksOpenError::Open/open_ref) are semver-visible downstream.

Verification: cargo test (default 145 unit + suites green), --features
local, --all-features (171 unit + all suites), clippy --all-targets
--all-features -D warnings, fmt --check, wasm32-unknown-unknown check +
clippy, fuzz corpus replay 3/3, publish --dry-run clean.
2026-10-19 03:53:47 +00:00

271 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, 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<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)
```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 |
## Version 0.1
The first release rides alkcall 0.8.1 (the establisher reply
projection is not consumed yet — the open op's params are trivial;
the 0.8.1 duplicate-adopt fix is directly relevant to the consumer
open/adopt paths) and alktunnels 0.2.1 (the direct op + the
bind-first/forwarded flow power the composed providers). The
`fuzz/` workspace (excluded from the published package) fuzzes the
two attacker-reachable parser classes — the ASSOCIATE datagram codec
and the RFC 1928 handshake state machine. 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.
0.2.0 rides the alkcall bump 0.8.1 → 0.9.0 (the stub-sweep release —
the four registered-but-always-failing stubs became real
implementations; none were on this crate's paths) and the alktunnels
bump 0.2.1 → 0.3.0. No alksocks API change, no wire change, zero code
contact with the 0.9.0 breaking surface; minor rather than patch
contact with the 0.9.0 breaking surface; minor rather than patch
because the semver-incompatible dependency bumps + this crate's
alkcall type re-exports make it semver-visible downstream.
0.3.0 rides the alkcall bump 0.9.0 → 0.10.0 (the gateway
ownership-seam release — `GatewayDispatch` ownership-provider
threading and the loud unwired-spine default; the gateway module
isn't on this crate's paths, registry dispatch on the native wire is
untouched) and the alktunnels bump 0.3.0 → 0.4.0 (the dependency-major
ride on alkcall 0.10.0). No alksocks API change, no wire change, zero
code contact with the 0.10.0 breaking surface (the only contact is the
fuzz/shared harness's own alkcall pin — a mechanical lockstep bump);
minor rather than patch for the same semver-visible-dependency reason
as 0.2.0.
## 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