vendor: the server typestate — Socks5ServerProtocol + new_reply publication
Extract the explicit typestate from fast-socks5 v1.0.0's server.rs into src/vendor/server.rs (ADR-011 §Amendment extraction, vendor.md cut table row 1): the states module, Socks5ServerProtocol<T, S>, the auth machinery (AuthMethod/AuthMethodSuccessState, the NoAuthentication/PasswordAuthentication impls, auth_method_enums! + StandardAuthentication), negotiate_auth/read_command/reply_success/ reply_error, transfer/wait_on_tcp, try_notify!, ErrorContext<T>, SocksServerError (landed early by the wire-vocabulary task). - carried change #2: new_reply is pub (byte-identity tested — v4 10-byte / v6 22-byte forms + the reply-code table) - extraction adjustments: log calls dropped (unused bindings renamed), reply_error's parse().unwrap() replaced with a compile-time const, auth_method_enums! de-exported to pub(crate) use, no anyhow/async-trait - cut (not copied): the legacy Socks5Server/Incoming/Socks5Socket, Config<A>/Authentication layer, DnsResolveHelper, the run_* + udp kernel-net handlers, the net-only test module - 12 first-party duplex tests: full no-auth round-trip, version mismatch, 0xff method refusal, 0x07 unknown-command reply, both reply forms byte-exact, password accept/reject Verification: cargo test (25 pass), --all-features, clippy --all-targets -D warnings (native + wasm32), cargo check --target wasm32-unknown-unknown, cargo fmt --check, cargo doc.
This commit is contained in:
1 parent
7497b79c05
commit
9ba63fd239
2 files changed
+945
-19
No files matched your search
Vendored
+828
-7
@@ -1,18 +1,63 @@
|
||||
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
||||
//! in `mod.rs`; filled by the server-typestate extraction task).
|
||||
//!
|
||||
//! This file currently carries only the `SocksServerError` enum — the
|
||||
//! server-side wire error vocabulary, which the vendored `SocksError`
|
||||
//! (`SocksError::ServerError`) references (`#[from]`), so the wire
|
||||
//! vocabulary extraction needs it before the typestate itself lands.
|
||||
//! Upstream defines it at the top of `server.rs`; when the typestate
|
||||
//! extraction lands, the rest of the file's inventory joins it.
|
||||
//! Upstream file: `src/server.rs`. 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
|
||||
//! 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`.
|
||||
//!
|
||||
//! 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!`);
|
||||
//! `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
|
||||
//! `SocksServerError::AuthenticationRejected` are cut; the variant
|
||||
//! stays (the typestate's `accept_password_auth` produces it).
|
||||
//!
|
||||
//! The BIND note (ADR-007): BIND's driving is producer-side code —
|
||||
//! the typestate already returns `TCPBind` from `read_command()`,
|
||||
//! `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 std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::string::FromUtf8Error;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SocksServerError {
|
||||
@@ -62,4 +107,780 @@ impl SocksServerError {
|
||||
}
|
||||
}
|
||||
|
||||
use std::io;
|
||||
pub trait ErrorContext<T> {
|
||||
fn err_when(self, context: &'static str) -> Result<T, SocksServerError>;
|
||||
}
|
||||
|
||||
impl<T> ErrorContext<T> for Result<T, io::Error> {
|
||||
fn err_when(self, context: &'static str) -> Result<T, SocksServerError> {
|
||||
self.map_err(|source| SocksServerError::Io { source, context })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ErrorContext<T> for Result<T, FromUtf8Error> {
|
||||
fn err_when(self, context: &'static str) -> Result<T, SocksServerError> {
|
||||
self.map_err(|source| SocksServerError::FromUtf8 { source, context })
|
||||
}
|
||||
}
|
||||
pub mod states {
|
||||
pub struct Opened;
|
||||
pub struct Authenticated;
|
||||
pub struct CommandRead;
|
||||
}
|
||||
|
||||
pub struct Socks5ServerProtocol<T, S> {
|
||||
inner: T,
|
||||
_state: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<T, S> Socks5ServerProtocol<T, S> {
|
||||
fn new(inner: T) -> Self {
|
||||
Socks5ServerProtocol {
|
||||
inner,
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Socks5ServerProtocol<T, states::Opened> {
|
||||
/// Start handling the SOCKS5 protocol flow, wrapping a client socket.
|
||||
pub fn start(inner: T) -> Self {
|
||||
Self::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CheckResult {
|
||||
fn is_good(&self) -> bool;
|
||||
}
|
||||
|
||||
impl CheckResult for bool {
|
||||
fn is_good(&self) -> bool {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CheckResult for Option<T> {
|
||||
fn is_good(&self) -> bool {
|
||||
self.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> CheckResult for Result<T, E> {
|
||||
fn is_good(&self) -> bool {
|
||||
self.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Socks5ServerProtocol<T, states::Authenticated> {
|
||||
/// Finish handling the authentication method-specific part of the protocol,
|
||||
/// returning back to the overall SOCKS5 flow.
|
||||
pub fn finish_auth<A: AuthMethodSuccessState<T>>(auth: A) -> Self {
|
||||
Self::new(auth.into_inner())
|
||||
}
|
||||
|
||||
/// Wrap a socket in a SOCKS5 flow handler that's already marked as authenticated.
|
||||
///
|
||||
/// This is not actually part of the official SOCKS5 protocol, but allows you to
|
||||
/// only use the post-authentication subset of it.
|
||||
pub fn skip_auth_this_is_not_rfc_compliant(inner: T) -> Self {
|
||||
Self::new(inner)
|
||||
}
|
||||
|
||||
/// Handle the SOCKS5 auth negotiation supporting only the `NoAuthentication` method.
|
||||
pub async fn accept_no_auth(inner: T) -> Result<Self, SocksServerError>
|
||||
where
|
||||
T: AsyncWrite + AsyncRead + Unpin,
|
||||
{
|
||||
Ok(Socks5ServerProtocol::start(inner)
|
||||
.negotiate_auth(&[NoAuthentication])
|
||||
.await?
|
||||
.finish_auth())
|
||||
}
|
||||
|
||||
/// Handle the SOCKS5 auth negotiation supporting only the `PasswordAuthentication` method,
|
||||
/// and verify the provided username and password using the provided closure.
|
||||
///
|
||||
/// The closure can mutate state variables and/or return a result as `Option`/`Result`.
|
||||
pub async fn accept_password_auth<F, R>(
|
||||
inner: T,
|
||||
mut check: F,
|
||||
) -> Result<(Self, R), SocksServerError>
|
||||
where
|
||||
T: AsyncWrite + AsyncRead + Unpin,
|
||||
F: FnMut(String, String) -> R,
|
||||
R: CheckResult,
|
||||
{
|
||||
let (user, pass, auth) = Socks5ServerProtocol::start(inner)
|
||||
.negotiate_auth(&[PasswordAuthentication])
|
||||
.await?
|
||||
.read_username_password()
|
||||
.await?;
|
||||
let check_result = check(user, pass);
|
||||
if check_result.is_good() {
|
||||
Ok((auth.accept().await?.finish_auth(), check_result))
|
||||
} else {
|
||||
auth.reject().await?;
|
||||
Err(SocksServerError::AuthenticationRejected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for the final successful state of an authentication method's implementation.
|
||||
///
|
||||
/// This allows `Socks5ServerProtocol<T, states::Authenticated>::finish_authentication` to
|
||||
/// let the user continue with the protocol after the socket has been handed off to the
|
||||
/// authentication method.
|
||||
pub trait AuthMethodSuccessState<T> {
|
||||
fn into_inner(self) -> T;
|
||||
|
||||
fn finish_auth(self) -> Socks5ServerProtocol<T, states::Authenticated>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Socks5ServerProtocol::finish_auth(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A metadata trait for authentication methods, essentially binding an ID value
|
||||
/// (as used in the method negotiation) to an actual implementation of the method.
|
||||
///
|
||||
/// Use blank structs for individual protocol implementations and
|
||||
/// enums for sets of supported protocols (you'll need a matching enum for the `Impl`).
|
||||
pub trait AuthMethod<T>: Copy {
|
||||
type StartingState;
|
||||
fn method_id(self) -> u8;
|
||||
fn new(self, inner: T) -> Self::StartingState;
|
||||
}
|
||||
|
||||
pub struct NoAuthenticationImpl<T>(T);
|
||||
|
||||
impl<T> AuthMethodSuccessState<T> for NoAuthenticationImpl<T> {
|
||||
fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The "NO AUTHENTICATION REQUIRED" auth method, ID 00h as specifed by RFC 1928.
|
||||
///
|
||||
/// As the dummy no-auth method, it only has one state. Once it's been negotiated,
|
||||
/// you can immediately continue with `finish_authentication`.
|
||||
///
|
||||
/// Or not so immediately: if you want to use no-authentication with e.g. IP address
|
||||
/// allowlisting or TLS client certificate auth for TLS-wrapped SOCKS5, this is your
|
||||
/// opportunity to reject the no-authentication by dropping the connection!
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NoAuthentication;
|
||||
|
||||
impl<T> AuthMethod<T> for NoAuthentication {
|
||||
type StartingState = NoAuthenticationImpl<T>;
|
||||
|
||||
fn method_id(self) -> u8 {
|
||||
0x00
|
||||
}
|
||||
|
||||
fn new(self, inner: T) -> Self::StartingState {
|
||||
NoAuthenticationImpl(inner)
|
||||
}
|
||||
}
|
||||
|
||||
mod password_states {
|
||||
pub struct Started;
|
||||
pub struct Received;
|
||||
pub struct Finished;
|
||||
}
|
||||
|
||||
pub struct PasswordAuthenticationImpl<T, S> {
|
||||
inner: T,
|
||||
_state: PhantomData<S>,
|
||||
}
|
||||
|
||||
pub type PasswordAuthenticationStarted<T> = PasswordAuthenticationImpl<T, password_states::Started>;
|
||||
|
||||
impl<T, S> PasswordAuthenticationImpl<T, S> {
|
||||
fn new(inner: T) -> Self {
|
||||
PasswordAuthenticationImpl {
|
||||
inner,
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + Unpin> PasswordAuthenticationImpl<T, password_states::Started> {
|
||||
/// Handle the username and password sent by the client.
|
||||
pub async fn read_username_password(
|
||||
self,
|
||||
) -> Result<
|
||||
(
|
||||
String,
|
||||
String,
|
||||
PasswordAuthenticationImpl<T, password_states::Received>,
|
||||
),
|
||||
SocksServerError,
|
||||
> {
|
||||
let mut socket = self.inner;
|
||||
let [_version, user_len] = read_exact!(socket, [0u8; 2]).err_when("reading user len")?;
|
||||
|
||||
if user_len < 1 {
|
||||
return Err(SocksServerError::EmptyUsername);
|
||||
}
|
||||
|
||||
let username =
|
||||
read_exact!(socket, vec![0u8; user_len as usize]).err_when("reading username")?;
|
||||
|
||||
let [pass_len] = read_exact!(socket, [0u8; 1]).err_when("reading password len")?;
|
||||
|
||||
if pass_len < 1 {
|
||||
return Err(SocksServerError::EmptyPassword);
|
||||
}
|
||||
|
||||
let password =
|
||||
read_exact!(socket, vec![0u8; pass_len as usize]).err_when("reading password")?;
|
||||
|
||||
let username = String::from_utf8(username).err_when("converting username")?;
|
||||
let password = String::from_utf8(password).err_when("converting password")?;
|
||||
|
||||
Ok((username, password, PasswordAuthenticationImpl::new(socket)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + Unpin> PasswordAuthenticationImpl<T, password_states::Received> {
|
||||
/// Notify the client with a "SUCCEEDED" reply and proceed to finish the authentication.
|
||||
pub async fn accept(
|
||||
mut self,
|
||||
) -> Result<PasswordAuthenticationImpl<T, password_states::Finished>, SocksServerError> {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED])
|
||||
.await
|
||||
.err_when("replying auth success")?;
|
||||
|
||||
Ok(PasswordAuthenticationImpl::new(self.inner))
|
||||
}
|
||||
|
||||
/// Notify the client with a "NOT_ACCEPTABLE" reply and drop the socket.
|
||||
pub async fn reject(mut self) -> Result<(), SocksServerError> {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE])
|
||||
.await
|
||||
.err_when("replying with auth method not acceptable")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AuthMethodSuccessState<T> for PasswordAuthenticationImpl<T, password_states::Finished> {
|
||||
fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// The "USERNAME/PASSWORD" auth method, ID 02h as specified by RFC 1928.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PasswordAuthentication;
|
||||
|
||||
impl<T> AuthMethod<T> for PasswordAuthentication {
|
||||
type StartingState = PasswordAuthenticationImpl<T, password_states::Started>;
|
||||
|
||||
fn method_id(self) -> u8 {
|
||||
0x02
|
||||
}
|
||||
|
||||
fn new(self, inner: T) -> Self::StartingState {
|
||||
PasswordAuthenticationImpl::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! try_notify {
|
||||
($proto:expr, $e:expr) => {
|
||||
match $e {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
let _ = $proto.reply_error(&err.to_reply_error()).await;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auth_method_enums {
|
||||
(
|
||||
$(#[$enum_meta:meta])*
|
||||
$vis:vis enum $enum:ident / $(#[$state_enum_meta:meta])* $state_enum:ident<$state_enum_par:ident> {
|
||||
$($method:ident($state:ty)),+ $(,)?
|
||||
}
|
||||
) => {
|
||||
$(#[$state_enum_meta])*
|
||||
$vis enum $state_enum<$state_enum_par> {
|
||||
$($method($state)),+
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
$(#[$enum_meta])*
|
||||
$vis enum $enum {
|
||||
$($method($method)),+
|
||||
}
|
||||
|
||||
impl<T> AuthMethod<T> for $enum {
|
||||
type StartingState = $state_enum<T>;
|
||||
|
||||
fn method_id(self) -> u8 {
|
||||
match self {
|
||||
$($enum::$method(auth) => AuthMethod::<T>::method_id(auth)),+
|
||||
}
|
||||
}
|
||||
|
||||
fn new(self, inner: T) -> Self::StartingState {
|
||||
match self {
|
||||
$($enum::$method(auth) => $state_enum::$method(auth.new(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.
|
||||
///
|
||||
/// If you want to add your own custom methods, you can generate a similar enum using the `auth_method_enums` macro.
|
||||
pub enum StandardAuthentication / StandardAuthenticationStarted<T> {
|
||||
NoAuthentication(NoAuthenticationImpl<T>),
|
||||
PasswordAuthentication(PasswordAuthenticationImpl<T, password_states::Started>),
|
||||
}
|
||||
}
|
||||
|
||||
impl StandardAuthentication {
|
||||
/// Return a slice containing either both supported methods or only `PasswordAuthentication`.
|
||||
pub fn allow_no_auth(allow: bool) -> &'static [StandardAuthentication] {
|
||||
if allow {
|
||||
&[
|
||||
// The order of authentication methods can be tested by clients in sequence,
|
||||
// so list more secure or preferred methods first
|
||||
StandardAuthentication::PasswordAuthentication(PasswordAuthentication),
|
||||
StandardAuthentication::NoAuthentication(NoAuthentication),
|
||||
]
|
||||
} else {
|
||||
&[StandardAuthentication::PasswordAuthentication(
|
||||
PasswordAuthentication,
|
||||
)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::Opened> {
|
||||
/// Negotiate an authentication method from a list of supported ones and initialize it.
|
||||
///
|
||||
/// Internally, this reads the list of authentication methods provided by the client, and
|
||||
/// picks the first one for which there exists an implementation in `server_methods`.
|
||||
///
|
||||
/// If none of the auth methods requested by the client are in `server_methods`,
|
||||
/// returns a `SocksServerError::AuthMethodUnacceptable`.
|
||||
pub async fn negotiate_auth<M: AuthMethod<T>>(
|
||||
mut self,
|
||||
server_methods: &[M],
|
||||
) -> Result<M::StartingState, SocksServerError> {
|
||||
let [version, methods_len] =
|
||||
read_exact!(self.inner, [0u8; 2]).err_when("reading methods")?;
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksServerError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
// {METHODS available from the client}
|
||||
// eg. (non-auth) {0, 1}
|
||||
// eg. (auth) {0, 1, 2}
|
||||
let methods =
|
||||
read_exact!(self.inner, vec![0u8; methods_len as usize]).err_when("reading methods")?;
|
||||
|
||||
// server_methods order matter!
|
||||
// the server could choose to prioritize methods
|
||||
for server_method in server_methods {
|
||||
for client_method_id in methods.iter() {
|
||||
if server_method.method_id() == *client_method_id {
|
||||
self.inner
|
||||
.write_all(&[consts::SOCKS5_VERSION, *client_method_id])
|
||||
.await
|
||||
.err_when("replying with auth method")?;
|
||||
return Ok(server_method.new(self.inner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inner
|
||||
.write_all(&[
|
||||
consts::SOCKS5_VERSION,
|
||||
consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE,
|
||||
])
|
||||
.await
|
||||
.err_when("replying with method not acceptable")?;
|
||||
Err(SocksServerError::AuthMethodUnacceptable(methods))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::CommandRead> {
|
||||
/// Reply success to the client according to the RFC.
|
||||
/// This consumes the wrapper as after this message actual proxying should begin.
|
||||
pub async fn reply_success(mut self, sock_addr: SocketAddr) -> Result<T, SocksServerError> {
|
||||
self.inner
|
||||
.write(&new_reply(&ReplyError::Succeeded, sock_addr))
|
||||
.await
|
||||
.err_when("writing successful reply")?;
|
||||
|
||||
self.inner.flush().await.err_when("flushing auth reply")?;
|
||||
|
||||
Ok(self.inner)
|
||||
}
|
||||
|
||||
/// Reply error to the client with the reply code according to the RFC.
|
||||
pub async fn reply_error(mut self, error: &ReplyError) -> Result<(), SocksServerError> {
|
||||
let reply = new_reply(error, V4_UNSPECIFIED_ZERO);
|
||||
self.inner
|
||||
.write(&reply)
|
||||
.await
|
||||
.err_when("writing unsuccessful reply")?;
|
||||
|
||||
self.inner.flush().await.err_when("flushing auth reply")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::Authenticated> {
|
||||
/// Decide to whether or not, accept the authentication method.
|
||||
/// Don't forget that the methods list sent by the client, contains one or more methods.
|
||||
///
|
||||
/// # Request
|
||||
/// ```text
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// | 1 | 1 | 1 | 1 | Variable | 2 |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// ```
|
||||
///
|
||||
/// It the request is correct, it should returns a ['SocketAddr'].
|
||||
///
|
||||
pub async fn read_command(
|
||||
mut self,
|
||||
) -> Result<
|
||||
(
|
||||
Socks5ServerProtocol<T, states::CommandRead>,
|
||||
Socks5Command,
|
||||
TargetAddr,
|
||||
),
|
||||
SocksServerError,
|
||||
> {
|
||||
let [version, cmd, _rsv, address_type] =
|
||||
read_exact!(self.inner, [0u8; 4]).err_when("reading command")?;
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksServerError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
let mut proto = Socks5ServerProtocol::new(self.inner);
|
||||
|
||||
// Guess address type
|
||||
let target_addr = try_notify!(proto, read_address(&mut proto.inner, address_type).await);
|
||||
|
||||
let cmd = try_notify!(
|
||||
proto,
|
||||
Socks5Command::from_u8(cmd).ok_or(SocksServerError::UnknownCommand(cmd))
|
||||
);
|
||||
|
||||
Ok((proto, cmd, target_addr))
|
||||
}
|
||||
}
|
||||
|
||||
/// The RFC reply form's placeholder BND.ADDR for error replies
|
||||
/// (statically constructed — upstream parsed `"0.0.0.0:0"` at runtime).
|
||||
const V4_UNSPECIFIED_ZERO: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));
|
||||
|
||||
/// Generate reply code according to the RFC.
|
||||
///
|
||||
/// Carried change #2 (ADR-011): upstream keeps this helper private
|
||||
/// (fast-socks5 `server.rs:1380`); in-tree it is `pub` so the BIND
|
||||
/// reply#2 writer and the consumer's reply readers reuse the
|
||||
/// 10-byte (v4) / 22-byte (v6) reply form instead of re-encoding it.
|
||||
pub fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec<u8> {
|
||||
let (addr_type, mut ip_oct, mut port) = match sock_addr {
|
||||
SocketAddr::V4(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
SocketAddr::V6(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut reply = vec![
|
||||
consts::SOCKS5_VERSION,
|
||||
error.as_u8(), // transform the error into byte code
|
||||
0x00, // reserved
|
||||
addr_type, // address type (ipv4, v6, domain)
|
||||
];
|
||||
reply.append(&mut ip_oct);
|
||||
reply.append(&mut port);
|
||||
|
||||
reply
|
||||
}
|
||||
|
||||
/// Wait until a TCP stream (that's not supposed to receive anything) closes.
|
||||
///
|
||||
/// This is intended for cancelling the `transfer_udp` task.
|
||||
pub async fn wait_on_tcp<I>(stream: &mut I) -> Result<(), SocksServerError>
|
||||
where
|
||||
I: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buf = [0; 1];
|
||||
match stream.read(&mut buf).await {
|
||||
Ok(0) => Err(SocksServerError::EOF),
|
||||
Ok(_) => Err(SocksServerError::UnexpectedUdpControlGarbage(buf[0])),
|
||||
Err(err) => Err(err).err_when("waiting on UDP control stream"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a bidirectional proxy between two streams.
|
||||
/// Using 2 different generators, because they could be different structs with same traits.
|
||||
pub async fn transfer<I, O>(mut inbound: I, mut outbound: O)
|
||||
where
|
||||
I: AsyncRead + AsyncWrite + Unpin,
|
||||
O: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let _ = tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::new_reply;
|
||||
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 std::net::SocketAddr;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[test]
|
||||
fn new_reply_v4_form() {
|
||||
let addr: SocketAddr = "127.0.0.1:1080".parse().unwrap();
|
||||
let reply = new_reply(&ReplyError::Succeeded, addr);
|
||||
assert_eq!(reply, vec![0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 4, 56]);
|
||||
assert_eq!(reply.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_reply_v6_form() {
|
||||
let addr: SocketAddr = "[2001:db8::1]:80".parse().unwrap();
|
||||
let reply = new_reply(&ReplyError::CommandNotSupported, addr);
|
||||
assert_eq!(reply.len(), 22);
|
||||
assert_eq!(reply[0], 0x05);
|
||||
assert_eq!(reply[1], 0x07);
|
||||
assert_eq!(reply[2], 0x00);
|
||||
assert_eq!(reply[3], 0x04);
|
||||
assert_eq!(
|
||||
&reply[4..20],
|
||||
&0x2001_0db8_0000_0000_0000_0000_0000_0001u128.to_be_bytes()
|
||||
);
|
||||
assert_eq!(&reply[20..22], &80u16.to_be_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_reply_error_codes_are_carried() {
|
||||
let addr: SocketAddr = "10.0.0.1:1".parse().unwrap();
|
||||
for (code, byte) in [
|
||||
(ReplyError::Succeeded, 0x00),
|
||||
(ReplyError::GeneralFailure, 0x01),
|
||||
(ReplyError::ConnectionNotAllowed, 0x02),
|
||||
(ReplyError::NetworkUnreachable, 0x03),
|
||||
(ReplyError::HostUnreachable, 0x04),
|
||||
(ReplyError::ConnectionRefused, 0x05),
|
||||
(ReplyError::ConnectionTimeout, 0x06),
|
||||
(ReplyError::TtlExpired, 0x06),
|
||||
(ReplyError::CommandNotSupported, 0x07),
|
||||
(ReplyError::AddressTypeNotSupported, 0x08),
|
||||
] {
|
||||
assert_eq!(new_reply(&code, addr)[1], byte);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn version_mismatch_is_refused() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_no_auth(server)
|
||||
.await
|
||||
.map(|_| ())
|
||||
});
|
||||
client.write_all(&[0x04, 0x01, 0x00]).await.unwrap();
|
||||
let err = handle.await.unwrap().unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SocksServerError::UnsupportedSocksVersion(0x04)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_acceptable_method_replies_0xff() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_no_auth(server)
|
||||
.await
|
||||
.map(|_| ())
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x02]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0xff]);
|
||||
let err = handle.await.unwrap().unwrap_err();
|
||||
assert!(matches!(err, SocksServerError::AuthMethodUnacceptable(ref m) if m == &vec![0x02]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_command_replies_0x07() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
let proto =
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_no_auth(server)
|
||||
.await
|
||||
.unwrap();
|
||||
proto.read_command().await.map(|_| ())
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0x00]);
|
||||
client
|
||||
.write_all(&[0x05, 0x7f, 0x00, 0x01, 192, 168, 0, 1, 0, 80])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut reply = [0u8; 10];
|
||||
client.read_exact(&mut reply).await.unwrap();
|
||||
assert_eq!(reply, [0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
|
||||
let err = handle.await.unwrap().unwrap_err();
|
||||
assert!(matches!(err, SocksServerError::UnknownCommand(0x7f)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_reply_form_round_trip() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(Socks5Command, TargetAddr), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
let proto =
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_no_auth(server)
|
||||
.await
|
||||
.unwrap();
|
||||
let (proto, cmd, target) = proto.read_command().await.unwrap();
|
||||
let bound: SocketAddr = "198.51.100.7:443".parse().unwrap();
|
||||
let mut inner = proto.reply_success(bound).await.unwrap();
|
||||
inner.write_all(b"ok").await.unwrap();
|
||||
Ok((cmd, target))
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0x00]);
|
||||
client
|
||||
.write_all(&[0x05, 0x01, 0x00, 0x01, 192, 0, 2, 1, 0, 80])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut reply = [0u8; 10];
|
||||
client.read_exact(&mut reply).await.unwrap();
|
||||
assert_eq!(reply, [0x05, 0x00, 0x00, 0x01, 198, 51, 100, 7, 1, 187]);
|
||||
let mut data = [0u8; 2];
|
||||
client.read_exact(&mut data).await.unwrap();
|
||||
assert_eq!(&data, b"ok");
|
||||
let (cmd, target) = handle.await.unwrap().unwrap();
|
||||
assert_eq!(cmd, Socks5Command::TCPConnect);
|
||||
assert_eq!(target, TargetAddr::Ip("192.0.2.1:80".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn error_reply_form_round_trip() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
let proto =
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_no_auth(server)
|
||||
.await
|
||||
.unwrap();
|
||||
let (proto, _cmd, _target) = proto.read_command().await.unwrap();
|
||||
proto.reply_error(&ReplyError::ConnectionNotAllowed).await
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0x00]);
|
||||
client
|
||||
.write_all(&[0x05, 0x01, 0x00, 0x03, 3, b'i', b'p', b'v', b'6', 0, 80])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut reply = [0u8; 10];
|
||||
client.read_exact(&mut reply).await.unwrap();
|
||||
assert_eq!(reply, [0x05, 0x02, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
|
||||
handle.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_auth_accepts_and_rejects() {
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
let (proto, _ok) =
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_password_auth(
|
||||
server,
|
||||
|user, pass| user == "alice" && pass == "secret",
|
||||
)
|
||||
.await?;
|
||||
drop(proto);
|
||||
Ok(())
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x02]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0x02]);
|
||||
client
|
||||
.write_all(&[
|
||||
0x01, 0x05, b'a', b'l', b'i', b'c', b'e', 0x06, b's', b'e', b'c', b'r', b'e', b't',
|
||||
])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut status = [0u8; 2];
|
||||
client.read_exact(&mut status).await.unwrap();
|
||||
assert_eq!(status, [0x01, 0x00]);
|
||||
handle.await.unwrap().unwrap();
|
||||
|
||||
let (mut client, server) = tokio::io::duplex(64);
|
||||
let handle: tokio::task::JoinHandle<Result<(), SocksServerError>> =
|
||||
tokio::spawn(async move {
|
||||
Socks5ServerProtocol::<_, states::Authenticated>::accept_password_auth(
|
||||
server,
|
||||
|_user, _pass| false,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
});
|
||||
client.write_all(&[0x05, 0x01, 0x02]).await.unwrap();
|
||||
let mut method_reply = [0u8; 2];
|
||||
client.read_exact(&mut method_reply).await.unwrap();
|
||||
assert_eq!(method_reply, [0x05, 0x02]);
|
||||
client
|
||||
.write_all(&[0x01, 0x03, b'b', b'o', b'b', 0x01, b'x'])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut status = [0u8; 2];
|
||||
client.read_exact(&mut status).await.unwrap();
|
||||
assert_eq!(status, [0x01, 0xff]);
|
||||
let err = handle.await.unwrap().unwrap_err();
|
||||
assert!(matches!(err, SocksServerError::AuthenticationRejected));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: implementation/vendor-server-typestate
|
||||
name: Vendor — the server typestate (Socks5ServerProtocol + carried changes)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [implementation/vendor-wire-vocabulary]
|
||||
scope: narrow
|
||||
risk: medium
|
||||
@@ -109,26 +109,29 @@ surface BIND needs is present and public.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `src/vendor/server.rs` exists with the inventory above;
|
||||
- [x] `src/vendor/server.rs` exists with the inventory above;
|
||||
`cargo test` compiles
|
||||
- [ ] `cargo check --target wasm32-unknown-unknown` clean
|
||||
- [ ] No `run_tcp_proxy`/`run_udp_proxy*`/`udp_bind_random_port`/
|
||||
- [x] `cargo check --target wasm32-unknown-unknown` clean
|
||||
- [x] No `run_tcp_proxy`/`run_udp_proxy*`/`udp_bind_random_port`/
|
||||
`handle_udp_*`/`transfer_udp`/`Socks5Server`/`Incoming`/
|
||||
`Socks5Socket`/`DnsResolveHelper`/`Config`/`Authentication`/
|
||||
`authenticate_callback` anywhere under `src/vendor/`
|
||||
- [ ] No `anyhow`, `log`, `tokio::net`, `socket2`, `tokio-stream`,
|
||||
(only doc-comment provenance mentions, matching `mod.rs`'s
|
||||
precedent — no definitions)
|
||||
- [x] No `anyhow`, `log`, `tokio::net`, `socket2`, `tokio-stream`,
|
||||
`async-trait` anywhere under `src/vendor/`
|
||||
- [ ] `new_reply` (or `reply`) is `pub` and produces byte-identical
|
||||
- [x] `new_reply` (or `reply`) is `pub` and produces byte-identical
|
||||
output to upstream's private encoder (10 bytes v4 / 22 bytes v6,
|
||||
VER/REP/RSV/ATYP/addr/port) — unit-tested
|
||||
- [ ] `reply_error` contains no `unwrap`/`expect` (convention 2)
|
||||
- [ ] `accept_no_auth` over an in-memory duplex completes a full
|
||||
VER/REP/RSV/ATYP/addr/port) — unit-tested (kept upstream's
|
||||
`new_reply` name; the task allowed either)
|
||||
- [x] `reply_error` contains no `unwrap`/`expect` (convention 2)
|
||||
- [x] `accept_no_auth` over an in-memory duplex completes a full
|
||||
negotiation + `read_command` + `reply_success`/`reply_error`
|
||||
round-trip against a first-party test client (tokio duplex
|
||||
streams — no sockets), covering: version mismatch refusal,
|
||||
no acceptable method (0xff reply), unknown command (0x07 reply),
|
||||
success reply form, error reply form
|
||||
- [ ] `cargo clippy --all-targets -- -D warnings` and
|
||||
- [x] `cargo clippy --all-targets -- -D warnings` and
|
||||
`cargo fmt --check` clean
|
||||
|
||||
## References
|
||||
@@ -148,8 +151,110 @@ surface BIND needs is present and public.
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- **Verbatim-code lint posture**: upstream style trips two
|
||||
newer-clippy lints in the copied region —
|
||||
`clippy::wrong_self_convention` (`AuthMethod::new(self, inner)`
|
||||
— the typestate's method-initializer idiom) and
|
||||
`clippy::doc_lazy_continuation` (upstream doc-comment style).
|
||||
`wrong_self_convention` is allowed at module level with rationale
|
||||
(churning vendored code would break the mechanical per-file diff
|
||||
posture; the same precedent as the wire-vocabulary task's
|
||||
module-level allows); the doc-continuation lint was fixed by
|
||||
rewording this file's own module doc (the offending pattern was in
|
||||
the new module text, not copied code).
|
||||
- **Dropped `log` calls leave two bindings unused**: the
|
||||
`debug!`/`trace!` drops in `read_username_password` and
|
||||
`read_command` leave the destructured `version`/`rsv` variables
|
||||
unconsumed; renamed to `_version`/`_rsv` (mechanical adjustment
|
||||
for the dropped logs, not a behavior change).
|
||||
- **`try_notify!`'s `error!` call dropped**: the macro's failure
|
||||
branch logged the secondary reply failure via `log::error!`;
|
||||
dropped per the adjustments. The branch still awaits
|
||||
`reply_error` (behavior preserved — the error reply still gets a
|
||||
best-effort attempt; its failure is now silently ignored, which
|
||||
matches the dropped log's only role).
|
||||
- **`transfer`'s result handling**: upstream logs the
|
||||
`copy_bidirectional` outcome (`debug!`/`error!`); in-tree the
|
||||
result is explicitly discarded (`let _ =`). The function's
|
||||
contract (run both pumps to completion, absorb the outcome) is
|
||||
unchanged; the crate's data plane is `pump_bidi` (ADR-050) —
|
||||
`transfer` rides for fidelity as the interception-point shape.
|
||||
- **`reply_error`'s unwrap replacement**: upstream parses
|
||||
`"0.0.0.0:0"` at runtime (infallible but `unwrap`);
|
||||
in-tree it is a `const V4_UNSPECIFIED_ZERO: SocketAddr` built from
|
||||
`Ipv4Addr::UNSPECIFIED` — statically infallible, byte-identical
|
||||
output (the success-reply form test pins the error-reply
|
||||
BND.ADDR as `0.0.0.0:0`).
|
||||
- **`auth_method_enums!` scoping**: converted from `#[macro_export]`
|
||||
to `macro_rules!` + `pub(crate) use` (same adjustment as
|
||||
`read_exact!`). As with `ready!` in stream.rs, the re-export has no
|
||||
in-crate consumer yet (the macro's only use is in this file), so
|
||||
it carries `#[allow(unused_imports)]` with the same rationale —
|
||||
kept verbatim so the util/vendor diff stays complete for the
|
||||
client-session task, which may consume it.
|
||||
- **Tests are first-party**: upstream's `server.rs` test module is
|
||||
the net-only `test_bind` (cut); the test module here is written
|
||||
fresh against the crate's test layout — tokio duplex streams, no
|
||||
sockets, no `env_logger`. The round-trip suite covers the
|
||||
acceptance list: version mismatch, 0xff method refusal (with the
|
||||
2-byte wire reply pinned), unknown command (0x07 reply + the
|
||||
`try_notify!` error-reply path), success/error reply forms
|
||||
(byte-exact), and the password-auth accept/reject paths.
|
||||
`new_reply` byte-identity is pinned v4 (10 bytes) + v6 (22 bytes)
|
||||
+ the full reply-code table (0x00–0x08, TtlExpired/ConnectionTimeout
|
||||
both mapping to 0x06).
|
||||
- **`Socks5Command::from_u8` + `ok_or` stays** as upstream — the
|
||||
unknown-command path maps through
|
||||
`SocksServerError::UnknownCommand` → `CommandNotSupported` (pinned
|
||||
by the unknown-command test's 0x07 reply byte).
|
||||
- **`ErrorContext<T>` stays `pub`**: the trait is in the task's
|
||||
extract inventory (it is the typestate's pervasive error-context
|
||||
idiom); upstream publishes it; kept `pub` verbatim.
|
||||
- **Client session note**: `wait_on_tcp`/`transfer` have no in-crate
|
||||
consumer yet (the producer task group drives the typestate
|
||||
directly; the data plane is `pump_bidi`). They ride the extraction
|
||||
per vendor.md's extraction table; if dead-code warnings had
|
||||
appeared they would have been handled like the vocabulary task's
|
||||
`ready!` — none did (pub items in a lib crate are reachable).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Extracted the server typestate from fast-socks5 v1.0.0 into
|
||||
`src/vendor/server.rs` per ADR-011 §Amendment + vendor.md (cut table
|
||||
row 1): the `states` module, `Socks5ServerProtocol<T, S>` +
|
||||
`new`/`start`, `CheckResult`, `accept_no_auth`/`accept_password_auth`/
|
||||
`finish_auth`/`skip_auth_this_is_not_rfc_compliant`,
|
||||
`AuthMethodSuccessState<T>`/`AuthMethod<T>`,
|
||||
`NoAuthentication`/`NoAuthenticationImpl`,
|
||||
`PasswordAuthentication`/`PasswordAuthenticationImpl` (+ the
|
||||
`password_states` module and the `PasswordAuthenticationStarted`
|
||||
alias), the `auth_method_enums!` macro (de-exported to
|
||||
`pub(crate) use`, as `read_exact!`) + `StandardAuthentication`,
|
||||
`negotiate_auth`/`read_command`/`reply_success`/`reply_error`,
|
||||
`transfer`/`wait_on_tcp`, the `try_notify!` macro, `ErrorContext<T>`
|
||||
+ impls, and `SocksServerError` (already landed verbatim by the
|
||||
wire-vocabulary task's early `#[from]` need — now joined by the rest
|
||||
of the inventory). The cut regions (the legacy
|
||||
`Socks5Server`/`Incoming`/`Socks5Socket`, `Config<A>` +
|
||||
`Authentication` + the example auth impls + `authenticate_callback`,
|
||||
`DnsResolveHelper`, the `run_*` kernel-net handlers +
|
||||
`udp_bind_random_port` + `handle_udp_*`/`transfer_udp`, the net-only
|
||||
`test_bind`) were not copied — verified by sweep. Carried change #2
|
||||
landed: `new_reply` is `pub` (upstream name kept), with byte-identity
|
||||
tests (v4 10-byte / v6 22-byte forms + the full reply-code table).
|
||||
Extraction-time adjustments applied: `log` calls dropped (with the
|
||||
resulting unused bindings renamed), `anyhow`/`async-trait` absent,
|
||||
`reply_error`'s runtime `parse().unwrap()` replaced with a
|
||||
compile-time `const` (`V4_UNSPECIFIED_ZERO`), `#[allow(deprecated)]`
|
||||
gone with the legacy region, and no `#[allow(async_fn_in_trait)]` is
|
||||
needed (`DnsResolveHelper` is cut; `AuthMethod` is not async). 12
|
||||
first-party tests cover the typestate over tokio duplex streams (no
|
||||
sockets): full no-auth negotiation + command read + both reply
|
||||
forms, version mismatch, 0xff method refusal, 0x07 unknown-command
|
||||
reply (the `try_notify!` path), password accept/reject, and the
|
||||
`new_reply` byte forms. Verified: `cargo test` (25 pass),
|
||||
`--all-features`, clippy `--all-targets -- -D warnings` native +
|
||||
wasm32, `cargo check --target wasm32-unknown-unknown`, `cargo fmt
|
||||
--check`, `cargo doc`, and the negative sweeps (no cut-region names,
|
||||
no forbidden deps in vendor code; only doc-comment provenance
|
||||
mentions).
|
||||
Reference in new issue
Block a user