rfc1928: the targeted fork — rename + convention pass (ADR-013)

The mechanical half of the posture change: src/vendor/ ->
src/rfc1928/ (module alksocks::rfc1928), tests/vendor-differential.rs
-> tests/rfc1928-differential.rs (git mv — history preserved).

- Fork point pinned: the tree as of 5b18d4d (the extraction review's
  gate commit); provenance permanent in the module docs and the
  extended LICENSE notice (upstream MIT preserved, fork-point +
  post-fork modification statement added — extend, never replace)
- The lint-allow posture dissolved: all module-level #[allow]s
  removed and the suppressed smells fixed (u8::max_value() ->
  u8::MAX, needless lifetimes elided, match_like_matches_macro
  applied, io::Error::new(Other) -> io::Error::other, the
  AuthMethod::new self-taking signature renamed
  into_starting_state, the F-4 ToSocketAddrs string re-worded — it
  no longer names the cut resolve_dns(), the unused ready! macro
  and dead AuthenticationMethod::from_u8 removed, the upstream
  commented-out async-std note folded into the impl doc)
- Module docs rewritten: ownership statement (project conventions
  apply unconditionally), extraction provenance (ADR-011) +
  ownership (ADR-013) + maintenance posture (behavioral reference,
  hand-port path)
- Differential suite survives the rename untouched (32 tests pin
  bytes, not upstream text)

Verification: cargo test (38+32 green), cargo clippy --all-targets
-D warnings, cargo fmt --check, cargo check/clippy
--target wasm32-unknown-unknown, cargo test --all-features, cargo
doc --no-deps — all pass.
This commit is contained in:
glm-5.3-flash committed 2026-09-22 12:41:30 +00:00
1 parent 001aac1fd5
commit f8c34444fb
10 files changed
+150 -191

No files matched your search

@@ -96,7 +96,7 @@ vendor.md, and the review task. "The tree as of the review commit
fork-point statement + the modification note.
- **The reference checkout survives as a behavioral reference.**
`/workspace/fast-socks5` stays for differential testing of RFC
edge cases; the differential suite (tests/vendor-differential.rs)
edge cases; the differential suite (tests/rfc1928-differential.rs)
pins bytes, not upstream text, so it survives the rename
untouched. Textual-diff discipline is retired; behavioral
differential testing is retained.
+4 -4
View File
@@ -10,14 +10,14 @@
//! assembly shapes (`local`), never in the protocol layer.
//!
//! The RFC wire vocabulary and the explicit typestate live in
//! [`vendor`]: an extraction of fast-socks5 v1.0.0's `T`-generic
//! [`rfc1928`]: a targeted fork of fast-socks5 v1.0.0's `T`-generic
//! subset (MIT, third-party upstream) carrying two deliberate changes
//! (BIND support, `new_reply` publication) — see
//! `docs/architecture/vendor.md` and ADR-011.
//! `docs/architecture/rfc1928.md`, ADR-013, and ADR-011.
//!
//! Architecture: `docs/architecture/overview.md` (spec set:
//! wire.md, producer.md, consumer.md, vendor.md).
//! wire.md, producer.md, consumer.md, rfc1928.md).
#![forbid(unsafe_code)]
pub mod vendor;
pub mod rfc1928;
+17 -7
View File
@@ -4,13 +4,23 @@ https://github.com/dizda/fast-socks5
Copyright (c) 2025 Jonathan Dizdarevic
The files under this `src/vendor/` directory are an extraction of
fast-socks5 v1.0.0 (https://github.com/dizda/fast-socks5), carried
in-tree by alksocks under ADR-011 (docs/architecture/decisions/
011-inline-vendor.md). Upstream has no per-file license headers; this
bundled notice is the license obligation's carrier. The extraction
cut and the carried changes are recorded in
docs/architecture/vendor.md.
The files under this `src/rfc1928/` directory were extracted from
fast-socks5 v1.0.0 (https://github.com/dizda/fast-socks5) and are
carried in-tree by alksocks (extraction per ADR-011,
docs/architecture/decisions/011-inline-vendor.md; extraction range
git 57b107b..5b18d4d; fork point = the tree as of commit 5b18d4d,
the extraction review's gate commit — ownership per ADR-013,
docs/architecture/decisions/013-targeted-fork.md). Upstream has no
per-file license headers; this bundled notice is the license
obligation's carrier. The extraction cut and the fork's deliberate
modifications are recorded in docs/architecture/rfc1928.md.
Modifications after the fork point are Copyright (c) 2026 the
alksocks developers, licensed under the crate license
(MIT OR Apache-2.0, per alksocks' Cargo.toml); the upstream MIT
notice above is preserved per its terms. The upstream project is
third-party and unmodified in place — see the ADRs for what was
changed after the fork point.
---
+9 -14
View File
@@ -1,7 +1,7 @@
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
//! in `mod.rs`; filled by the client-session extraction task).
//! Forked from fast-socks5 v1.0.0 (upstream `src/client.rs` —
//! extraction; ownership per ADR-013, see the crate root's `mod.rs`).
//!
//! Upstream file: `src/client.rs`. Extracted region: the `T`-generic
//! Extracted region: the `T`-generic
//! client session — [`Socks5Stream<S>`] with the `use_stream` path
//! (the version/method greeting, the method-choice read, the
//! password-auth sub-handshake, the request encoder `request_header`,
@@ -12,7 +12,8 @@
//! `pump_bidi`-shaped consumers and the front doors can pipe through
//! it.
//!
//! Not extracted (vendor.md's cut table row 2): `Socks5Datagram`
//! Not forked (rfc1928.md's not-forked inventory row 2):
//! `Socks5Datagram`
//! (struct + impl — the kernel `UdpSocket` associate client; the
//! crate's associate session is first-party per ADR-003's handle
//! swap) and the `Socks5Stream<TcpStream>` convenience impl
@@ -41,17 +42,11 @@
//! Tests are first-party (upstream's client test module rides the cut
//! net region): raw-byte mock servers over tokio duplex streams, the
//! pinned byte sequences written directly — keeping the client's wire
//! expectations uncorrelated from the vendored server typestate.
//!
//! Verbatim-code lint posture: the copied regions are upstream
//! v1.0.0 code (minus the cut + adjustments), so newer-clippy style
//! lints on upstream style are allowed at module level rather than
//! churned per-line — the diff vs the reference checkout stays
//! mechanical (review task).
//! expectations uncorrelated from the forked server typestate.
use crate::vendor::util::stream::read_exact;
use crate::vendor::util::target_addr::{read_address, TargetAddr};
use crate::vendor::{consts, AuthenticationMethod, ReplyError, Result, Socks5Command, SocksError};
use crate::rfc1928::util::stream::read_exact;
use crate::rfc1928::util::target_addr::{read_address, TargetAddr};
use crate::rfc1928::{consts, AuthenticationMethod, ReplyError, Result, Socks5Command, SocksError};
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
+30 -42
View File
@@ -1,44 +1,45 @@
//! The inline vendor: an extraction of fast-socks5 v1.0.0's
//! The RFC 1928 core: a targeted fork of fast-socks5 v1.0.0's
//! `T`-generic subset (MIT, third-party upstream —
//! github.com/dizda/fast-socks5; `/workspace/fast-socks5` is the
//! read-only reference checkout, kept for differential testing of RFC
//! edge cases).
//! read-only reference checkout, kept for differential testing of
//! RFC edge cases).
//!
//! Extracted per ADR-011 §Amendment (docs/architecture/decisions/
//! 011-inline-vendor.md): only the code already generic over
//! `T: AsyncRead + AsyncWrite + Unpin` rides in-tree — the kernel-net
//! handlers, the `socks4` module, and the legacy `Config`/
//! `Authentication` layer are **not** copied. The extraction cut, the
//! two carried changes (BIND support, `new_reply` publication), and
//! the extraction-time adjustments (no `anyhow`, no `log`, no
//! `async-trait`) are recorded in `docs/architecture/vendor.md`.
//! Arrived by extraction (ADR-011 §Amendment, docs/architecture/
//! decisions/011-inline-vendor.md): only the code already generic
//! over `T: AsyncRead + AsyncWrite + Unpin` was copied in-tree — the
//! kernel-net handlers, the `socks4` module, and the legacy `Config`/
//! `Authentication` layer were **not** copied. The extraction cut,
//! the fork's two carried changes (BIND support, `new_reply`
//! publication), and the extraction-time adjustments (no `anyhow`,
//! no `log`, no `async-trait`) are recorded in
//! `docs/architecture/rfc1928.md`.
//!
//! Ownership (ADR-013, docs/architecture/decisions/013-targeted-fork.md):
//! this is fully owned code — project conventions apply
//! unconditionally (no vendor carve-outs, no verbatim preservation,
//! no lint-allow posture). Fork point: the tree as of the
//! extraction-review commit (`5b18d4d`); extraction provenance in
//! git history (`57b107b..5b18d4d`). A future upstream fix that
//! matters is hand-ported against the reference checkout and guarded
//! by the differential suite.
//!
//! This file is upstream's `src/lib.rs` vocabulary region (the
//! wire-vocabulary extraction task): the `consts`, the command/auth
//! enums, the reply-code codec, the UDP datagram codec, and the error
//! vocabulary pruned to the extracted region's consumers. The
//! typestate (upstream `server.rs`, minus the cut regions) lives in
//! vocabulary pruned to the region's consumers. The typestate
//! (upstream `server.rs`, minus the cut regions) lives in
//! [`server`]; the client session (upstream `client.rs`, the
//! `use_stream` path + the `AsyncRead`/`AsyncWrite` impls) lives in
//! [`client`].
//!
//! License: upstream is MIT with no per-file headers — the bundled
//! notice in `this module's LICENSE file` (`src/vendor/LICENSE`, v1.0.0
//! attribution) is the license obligation's carrier.
//! notice (`LICENSE`, this module's directory, v1.0.0 attribution +
//! the fork-point statement) is the license obligation's carrier.
//!
//! Maintenance posture: the vendored code is first-class, co-evolves
//! with the wrapper, and is the crate's only RFC 1928 wire vocabulary
//! ("no second codec, ever" — ADR-011). A future upstream release is
//! re-based against deliberately, never absorbed silently.
//!
//! Verbatim-code lint posture: the copied regions are upstream
//! v1.0.0 code (minus the cut + adjustments), so newer-clippy style
//! lints on upstream style are allowed at module level rather than
//! churned per-line — the diff vs the reference checkout stays
//! mechanical (review task).
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::redundant_closure)]
//! Maintenance posture: this code co-evolves with the wrapper and is
//! the crate's only RFC 1928 wire vocabulary ("no second codec,
//! ever" — ADR-011's rationale, carried). Nothing is absorbed
//! silently from upstream; nothing waits on upstream.
pub mod client;
pub mod server;
@@ -119,7 +120,6 @@ pub enum AuthenticationMethod {
Password { username: String, password: String },
}
#[allow(dead_code)]
impl AuthenticationMethod {
#[inline]
#[rustfmt::skip]
@@ -130,16 +130,6 @@ impl AuthenticationMethod {
consts::SOCKS5_AUTH_METHOD_PASSWORD
}
}
#[inline]
#[rustfmt::skip]
fn from_u8(code: u8) -> Option<AuthenticationMethod> {
match code {
consts::SOCKS5_AUTH_METHOD_NONE => Some(AuthenticationMethod::None),
consts::SOCKS5_AUTH_METHOD_PASSWORD => Some(AuthenticationMethod::Password { username: "test".to_string(), password: "test".to_string()}),
_ => None,
}
}
}
impl fmt::Display for AuthenticationMethod {
@@ -299,9 +289,7 @@ pub fn new_udp_header<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>, UdpHea
}
/// Parse data from UDP client on raw buffer, return (frag, target_addr, payload).
pub async fn parse_udp_request<'a>(
mut req: &'a [u8],
) -> Result<(u8, TargetAddr, &'a [u8]), UdpHeaderError> {
pub async fn parse_udp_request(mut req: &[u8]) -> Result<(u8, TargetAddr, &[u8]), UdpHeaderError> {
let rsv = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?;
if !rsv.eq(&[0u8; 2]) {
+26 -37
View File
@@ -1,24 +1,24 @@
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
//! in `mod.rs`; filled by the server-typestate extraction task).
//! Forked from fast-socks5 v1.0.0 (upstream `src/server.rs` —
//! extraction; ownership per ADR-013, see the crate root's `mod.rs`).
//!
//! Upstream file: `src/server.rs`. Extracted region: the explicit
//! Extracted region: the explicit
//! typestate (`states`, `Socks5ServerProtocol<T, S>`, auth
//! negotiation with the `AuthMethod<T>` machinery, `read_command`,
//! `reply_success`, `reply_error`, `transfer`, `wait_on_tcp`,
//! `ErrorContext<T>`), `SocksServerError`, and the published reply
//! encoder `new_reply` (carried change #2 — upstream's is private,
//! `fn new_reply` at `server.rs:1380`; in-tree it is `pub` so the
//! `fn new_reply` at `server.rs:1380`; here it is `pub` so the
//! BIND reply#2 writer and consumer reply readers reuse the
//! 10/22-byte reply form instead of re-encoding it). The cut regions
//! (the legacy `Socks5Server`/`Socks5Socket`/`Config`/`Authentication`
//! layer, the `run_*` kernel-net handlers, `DnsResolveHelper`, the
//! net-only tests) stay in `/workspace/fast-socks5` — the cut table
//! row 1 in `docs/architecture/vendor.md`.
//! net-only tests) stay in `/workspace/fast-socks5` — the not-forked
//! inventory row 1 in `docs/architecture/rfc1928.md`.
//!
//! Extraction-time adjustments (mechanical, ADR-011 §Amendment):
//! `log` calls dropped; `anyhow::Context` dropped; the
//! `#[macro_export]` on `auth_method_enums!` converted to
//! `pub(crate) use` scoping (same adjustment as `read_exact!`);
//! `#[macro_export]` on `auth_method_enums!` converted to `pub`
//! scoping within this module (same adjustment as `read_exact!`);
//! `reply_error`'s `"0.0.0.0:0".parse().unwrap()` replaced with a
//! `const` parsed at compile time (no `unwrap` outside tests).
//! The upstream `Config<A>`/`Authentication` impls that consumed
@@ -30,24 +30,16 @@
//! `reply_success(sock_addr)` carries reply#1's BND.ADDR, and
//! reply#2 is written with the published `new_reply`. No BIND code
//! lives here; this file only guarantees the surface is present.
//!
//! Verbatim-code lint posture: the copied regions are upstream
//! v1.0.0 code (minus the cut + adjustments), so newer-clippy style
//! lints on upstream style are allowed at module level rather than
//! churned per-line — the diff vs the reference checkout stays
//! mechanical (review task).
#![allow(clippy::wrong_self_convention)] // AuthMethod::new(self, ..) is upstream
use crate::vendor::consts;
use crate::vendor::util::stream::read_exact;
use crate::vendor::util::stream::ConnectError;
use crate::vendor::util::target_addr::read_address;
use crate::vendor::util::target_addr::TargetAddr;
use crate::vendor::AddrError;
use crate::vendor::ReplyError;
use crate::vendor::Socks5Command;
use crate::vendor::UdpHeaderError;
use crate::rfc1928::consts;
use crate::rfc1928::util::stream::read_exact;
use crate::rfc1928::util::stream::ConnectError;
use crate::rfc1928::util::target_addr::read_address;
use crate::rfc1928::util::target_addr::TargetAddr;
use crate::rfc1928::AddrError;
use crate::rfc1928::ReplyError;
use crate::rfc1928::Socks5Command;
use crate::rfc1928::UdpHeaderError;
use std::io;
use std::marker::PhantomData;
use std::net::Ipv4Addr;
@@ -249,7 +241,7 @@ pub trait AuthMethodSuccessState<T> {
pub trait AuthMethod<T>: Copy {
type StartingState;
fn method_id(self) -> u8;
fn new(self, inner: T) -> Self::StartingState;
fn into_starting_state(self, inner: T) -> Self::StartingState;
}
pub struct NoAuthenticationImpl<T>(T);
@@ -278,7 +270,7 @@ impl<T> AuthMethod<T> for NoAuthentication {
0x00
}
fn new(self, inner: T) -> Self::StartingState {
fn into_starting_state(self, inner: T) -> Self::StartingState {
NoAuthenticationImpl(inner)
}
}
@@ -384,7 +376,7 @@ impl<T> AuthMethod<T> for PasswordAuthentication {
0x02
}
fn new(self, inner: T) -> Self::StartingState {
fn into_starting_state(self, inner: T) -> Self::StartingState {
PasswordAuthenticationImpl::new(inner)
}
}
@@ -428,18 +420,15 @@ macro_rules! auth_method_enums {
}
}
fn new(self, inner: T) -> Self::StartingState {
fn into_starting_state(self, inner: T) -> Self::StartingState {
match self {
$($enum::$method(auth) => $state_enum::$method(auth.new(inner))),+
$($enum::$method(auth) => $state_enum::$method(auth.into_starting_state(inner))),+
}
}
}
};
}
#[allow(unused_imports)]
pub(crate) use auth_method_enums;
auth_method_enums! {
/// The combination of all authentication methods supported by this crate out of the box,
/// as an enum appropriate for static dispatch.
@@ -503,7 +492,7 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::Opened>
.write_all(&[consts::SOCKS5_VERSION, *client_method_id])
.await
.err_when("replying with auth method")?;
return Ok(server_method.new(self.inner));
return Ok(server_method.into_starting_state(self.inner));
}
}
}
@@ -660,9 +649,9 @@ mod tests {
use super::states;
use super::Socks5ServerProtocol;
use super::SocksServerError;
use crate::vendor::util::target_addr::TargetAddr;
use crate::vendor::ReplyError;
use crate::vendor::Socks5Command;
use crate::rfc1928::util::target_addr::TargetAddr;
use crate::rfc1928::ReplyError;
use crate::rfc1928::Socks5Command;
use std::net::SocketAddr;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
@@ -1,5 +1,6 @@
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
//! in the vendor root's `mod.rs`).
//! The RFC 1928 core's utility modules (upstream `src/util/` —
//! extraction; see the fork statement in the crate root's `mod.rs`
//! and ADR-013 for ownership).
//!
//! Upstream layout: `stream.rs` (the `read_exact!`/`ready!` macros +
//! `ConnectError`) and `target_addr.rs` (`TargetAddr` + the address
@@ -1,14 +1,16 @@
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
//! in the vendor root's `mod.rs`).
//! Forked from fast-socks5 v1.0.0 (upstream `src/util/stream.rs` —
//! extraction; ownership per ADR-013, see the crate root's `mod.rs`).
//!
//! Upstream carries the `read_exact!`/`ready!` macros, `ConnectError`,
//! and the `tcp_connect*` dialers; only the macros + `ConnectError`
//! ride the extraction (the dialers are the cut kernel-net region —
//! the crate's local backend is first-party tokio code, ADR-008).
//! Extraction-time adjustment: upstream exports both macros at the
//! crate root via `#[macro_export]`; in-tree they are plain
//! `macro_rules!` scoped by `pub(crate) use` so they stay internal to
//! the vendor (implementation detail, not alksocks public API).
//! and the `tcp_connect*` dialers; only the `read_exact!` macro +
//! `ConnectError` rode the extraction (the dialers and the `ready!`
//! macro are the cut kernel-net region — the crate's local backend is
//! first-party tokio code, ADR-008, and nothing in the extracted
//! region used `ready!`). Extraction-time adjustment: upstream
//! exports the macro at the crate root via `#[macro_export]`; here it
//! is a plain `macro_rules!` scoped by `pub(crate) use` so it stays
//! internal to the RFC 1928 core (implementation detail, not alksocks
//! public API).
/// Easy to destructure bytes buffers by naming each fields:
///
@@ -36,21 +38,9 @@ macro_rules! read_exact {
}};
}
#[allow(unused_macros)]
macro_rules! ready {
($e:expr $(,)?) => {
match $e {
std::task::Poll::Ready(t) => t,
std::task::Poll::Pending => return std::task::Poll::Pending,
}
};
}
pub(crate) use read_exact;
#[allow(unused_imports)]
pub(crate) use ready;
use crate::vendor::ReplyError;
use crate::rfc1928::ReplyError;
use std::io;
#[derive(thiserror::Error, Debug)]
@@ -86,7 +76,7 @@ impl ConnectError {
#[cfg(test)]
mod tests {
use super::ConnectError;
use crate::vendor::ReplyError;
use crate::rfc1928::ReplyError;
use std::io::ErrorKind;
#[test]
@@ -1,29 +1,18 @@
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
//! in the vendor root's `mod.rs`).
//! Forked from fast-socks5 v1.0.0 (upstream `src/util/target_addr.rs`
//! — extraction; ownership per ADR-013, see the crate root's
//! `mod.rs`).
//!
//! Upstream carries `TargetAddr`, the address codec (`read_address`),
//! and the `resolve_dns` helper; only the codec rides the extraction
//! and the `resolve_dns` helper; only the codec rode the extraction
//! (DNS resolution is the cut net region — `tokio::net::lookup_host`
//! is not in the wasm-clean graph). The `resolve_dns` method is cut
//! with its two `AddrError` variants (`DNSResolutionFailed`,
//! `NoDNSRecords`); the `ToSocketAddrs` impl's error text referencing
//! `resolve_dns` is preserved verbatim for fidelity.
//!
//! Verbatim-code lint posture: this file is upstream v1.0.0 code
//! (minus the cut region), so newer-clippy style lints on upstream
//! style are allowed at module level rather than churned per-line —
//! the diff vs the reference checkout stays mechanical (review task).
//! `NoDNSRecords`).
#![allow(clippy::match_like_matches_macro)] // upstream style
#![allow(clippy::legacy_numeric_constants)] // u8::max_value() is upstream
#![allow(clippy::io_other_error)] // io::Error::new(Other) is upstream
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::redundant_closure)]
use crate::vendor::consts;
use crate::vendor::consts::SOCKS5_ADDR_TYPE_IPV4;
use crate::vendor::util::stream::read_exact;
use crate::vendor::ReplyError;
use crate::rfc1928::consts;
use crate::rfc1928::consts::SOCKS5_ADDR_TYPE_IPV4;
use crate::rfc1928::util::stream::read_exact;
use crate::rfc1928::ReplyError;
use std::fmt;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
@@ -76,10 +65,7 @@ pub enum TargetAddr {
impl TargetAddr {
pub fn is_ip(&self) -> bool {
match self {
TargetAddr::Ip(_) => true,
_ => false,
}
matches!(self, TargetAddr::Ip(_))
}
pub fn is_domain(&self) -> bool {
@@ -102,7 +88,7 @@ impl TargetAddr {
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
}
TargetAddr::Domain(ref domain, port) => {
if domain.len() > u8::max_value() as usize {
if domain.len() > u8::MAX as usize {
return Err(AddrError::DomainLenTooLong(domain.len()));
}
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]);
@@ -122,17 +108,17 @@ impl TargetAddr {
}
}
// async-std ToSocketAddrs doesn't supports external trait implementation
// @see https://github.com/async-rs/async-std/issues/539
/// The blocking `ToSocketAddrs` form: IP targets resolve directly;
/// domain targets cannot be resolved here (domain resolution is the
/// egress's job — ADR-012) and error.
impl std::net::ToSocketAddrs for TargetAddr {
type Iter = IntoIter<SocketAddr>;
fn to_socket_addrs(&self) -> io::Result<IntoIter<SocketAddr>> {
match *self {
TargetAddr::Ip(addr) => Ok(vec![addr].into_iter()),
TargetAddr::Domain(_, _) => Err(io::Error::new(
io::ErrorKind::Other,
"Domain name has to be explicitly resolved, please use TargetAddr::resolve_dns().",
TargetAddr::Domain(_, _) => Err(io::Error::other(
"Domain name cannot be resolved by the blocking ToSocketAddrs interface; resolve it before constructing the target.",
)),
}
}
@@ -154,7 +140,7 @@ pub trait ToTargetAddr {
fn to_target_addr(&self) -> io::Result<TargetAddr>;
}
impl<'a> ToTargetAddr for (&'a str, u16) {
impl ToTargetAddr for (&str, u16) {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
// try to parse as an IP first
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
@@ -228,18 +214,17 @@ pub async fn read_address<T: AsyncRead + Unpin>(
) -> Result<TargetAddr, AddrError> {
let addr = match atyp {
consts::SOCKS5_ADDR_TYPE_IPV4 => {
Addr::V4(read_exact!(stream, [0u8; 4]).map_err(|err| AddrError::IPv4Unreadable(err))?)
Addr::V4(read_exact!(stream, [0u8; 4]).map_err(AddrError::IPv4Unreadable)?)
}
consts::SOCKS5_ADDR_TYPE_IPV6 => {
Addr::V6(read_exact!(stream, [0u8; 16]).map_err(|err| AddrError::IPv6Unreadable(err))?)
Addr::V6(read_exact!(stream, [0u8; 16]).map_err(AddrError::IPv6Unreadable)?)
}
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
let len =
read_exact!(stream, [0]).map_err(|err| AddrError::DomainLenUnreadable(err))?[0];
let len = read_exact!(stream, [0]).map_err(AddrError::DomainLenUnreadable)?[0];
let domain = read_exact!(stream, vec![0u8; len as usize])
.map_err(|err| AddrError::DomainContentUnreadable(err))?;
.map_err(AddrError::DomainContentUnreadable)?;
// make sure the bytes are correct utf8 string
let domain = String::from_utf8(domain).map_err(|err| AddrError::Utf8(err))?;
let domain = String::from_utf8(domain).map_err(AddrError::Utf8)?;
Addr::Domain(domain)
}
@@ -247,7 +232,7 @@ pub async fn read_address<T: AsyncRead + Unpin>(
};
// Find port number
let port = read_exact!(stream, [0u8; 2]).map_err(|err| AddrError::PortNumberUnreadable(err))?;
let port = read_exact!(stream, [0u8; 2]).map_err(AddrError::PortNumberUnreadable)?;
// Convert (u8 * 2) into u16
let port = (port[0] as u16) << 8 | port[1] as u16;
@@ -255,10 +240,10 @@ pub async fn read_address<T: AsyncRead + Unpin>(
let addr: TargetAddr = match addr {
Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port)
.to_target_addr()
.map_err(|err| AddrError::AddrConversionFailed(err))?,
.map_err(AddrError::AddrConversionFailed)?,
Addr::V6(x) => (Ipv6Addr::from(x), port)
.to_target_addr()
.map_err(|err| AddrError::AddrConversionFailed(err))?,
.map_err(AddrError::AddrConversionFailed)?,
Addr::Domain(domain) => TargetAddr::Domain(domain, port),
};
@@ -268,7 +253,7 @@ pub async fn read_address<T: AsyncRead + Unpin>(
#[cfg(test)]
mod tests {
use super::*;
use crate::vendor::consts;
use crate::rfc1928::consts;
use tokio::io::AsyncWriteExt;
async fn read_target_addr(bytes: &[u8]) -> Result<TargetAddr, AddrError> {
@@ -1,13 +1,14 @@
//! The vendor's differential RFC edge-case suite (the wire form is
//! the point — expectations are pinned as explicit bytes).
//! The RFC 1928 core's differential RFC edge-case suite (the wire
//! form is the point — expectations are pinned as explicit bytes).
//!
//! Posture (docs/architecture/vendor.md §Maintenance Posture):
//! `/workspace/fast-socks5` (v1.0.0) is the behavioral reference —
//! consulted while writing these tests; the assertions pin the
//! expected bytes, never the reference checkout itself (build-time
//! dependency-free). Where the extraction deliberately diverges (the
//! pruned error variants, the published `new_reply`), the test pins
//! the in-tree contract and the divergence is recorded in vendor.md.
//! Posture (docs/architecture/rfc1928.md §Maintenance Posture,
//! ADR-013): `/workspace/fast-socks5` (v1.0.0) is the behavioral
//! reference — consulted while writing these tests; the assertions
//! pin the expected bytes, never the reference checkout itself
//! (build-time dependency-free). Where the fork deliberately
//! diverges (the pruned error variants, the published `new_reply`),
//! the test pins the in-tree contract and the divergence is recorded
//! in rfc1928.md.
//!
//! Differential convention: every non-obvious expectation names the
//! upstream behavior it pins (file + line in the v1.0.0 checkout).
@@ -16,19 +17,19 @@
//! `local` feature; the suite runs in the default wasm-clean crate's
//! native test build.
use alksocks::vendor::client::{Config, Socks5Stream};
use alksocks::vendor::consts;
use alksocks::vendor::server::{
use alksocks::rfc1928::client::{Config, Socks5Stream};
use alksocks::rfc1928::consts;
use alksocks::rfc1928::server::{
new_reply, transfer, wait_on_tcp, Socks5ServerProtocol, SocksServerError,
};
use alksocks::vendor::util::target_addr::{AddrError, TargetAddr};
use alksocks::vendor::{new_udp_header, parse_udp_request, ReplyError, Socks5Command, SocksError};
use alksocks::rfc1928::util::target_addr::{AddrError, TargetAddr};
use alksocks::rfc1928::{new_udp_header, parse_udp_request, ReplyError, Socks5Command, SocksError};
use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
type ServerTask<T> = tokio::task::JoinHandle<Result<T, SocksServerError>>;
/// Drive the vendored server through `accept_no_auth` (negotiation +
/// Drive the forked server through `accept_no_auth` (negotiation +
/// the auth-choice write) on a spawned task, mapping the success
/// value to `()` so the handle unwraps without a `Debug` bound.
fn spawn_no_auth(server: tokio::io::DuplexStream) -> ServerTask<()> {
@@ -652,20 +653,20 @@ async fn client_domain_request_encoding_is_len_prefixed() {
/// for non-ASSOCIATE commands was
/// `anyhow::Error::msg("target addr should be present")`
/// (`src/client.rs:285`); in-tree it is the typed
/// `SocksError::MissingTargetAddr` (vendor.md's adjustment list) —
/// `SocksError::MissingTargetAddr` (rfc1928.md's adjustment list) —
/// pinned by the wire-vocabulary module's own test
/// (`mod.rs` tests: `socks_error_from_variants` keeps the variant
/// reachable; the private-API form of this test lives in
/// `src/vendor/client.rs`).
/// `src/rfc1928/client.rs`).
#[tokio::test]
async fn client_udp_associate_zero_target_fallback_is_10_zero_bytes() {
// The vendored client's `target_addr: None` + UDPAssociate arm
// The forked client's `target_addr: None` + UDPAssociate arm
// emits exactly this 10-byte form; reproduced byte-for-byte here
// (integration surface) and covered against the real code path in
// the client module's in-module test (privacy-scoped).
let expected: &[u8] = &[0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0];
let mut addr_field = &expected[4..10];
let parsed = alksocks::vendor::util::target_addr::read_address(&mut addr_field, 0x01)
let parsed = alksocks::rfc1928::util::target_addr::read_address(&mut addr_field, 0x01)
.await
.unwrap();
// The zero form decodes to the unspecified IPv4 addr, port 0 —
@@ -716,7 +717,7 @@ async fn udp_codec_garbage_reserved_is_rejected() {
let err = parse_udp_request(&frame).await.unwrap_err();
assert!(matches!(
err,
alksocks::vendor::UdpHeaderError::GarbageInReserved
alksocks::rfc1928::UdpHeaderError::GarbageInReserved
));
}