vendor: the wire vocabulary — consts, commands, codecs, errors, macros
Extract fast-socks5 v1.0.0's non-net lib.rs region into src/vendor/mod.rs (scaffolding-pinned root; the task's lib.rs target name corrected — a second root file would conflict, see task Notes): consts, Socks5Command (as_u8/from_u8 made pub), AuthenticationMethod, SocksError pruned to the extracted region's consumers (no anyhow/ Redaction/InvalidHeader/ReplySocks4Error/ArgumentInputError), the Result alias, ReplyError codec, UdpHeaderError + new_udp_header/ parse_udp_request. util/target_addr.rs gains TargetAddr/AddrError (minus resolve_dns + its two variants)/ToTargetAddr/Addr/read_address; util/stream.rs gains the read_exact!/ready! macros (de-exported to pub(crate) use) + ConnectError + to_reply_error. SocksServerError lands verbatim in server.rs early — SocksError's #[from] needs it before the typestate task. 16 first-party codec tests (round-trips, mapping tables, garbage-RSV/unknown-ATYP); no sockets, no env_logger. Newer-clippy style lints allowed at module level in vendored files (verbatim diff posture, rationale recorded). Verification: cargo test (16 pass) + --all-features; clippy --all-targets -D warnings (native + wasm32); cargo check --target wasm32-unknown-unknown; cargo fmt --check; cargo doc. Negative sweeps: no anyhow/log/tokio::net/socket2/tokio-stream/ async-trait under src/vendor/; no #[macro_export]; resolve_dns and the pruned error variants absent.
This commit is contained in:
1 parent
57b107bdf0
commit
7497b79c05
5 files changed
+1157
-20
No files matched your search
Vendored
+437
@@ -13,6 +13,14 @@
|
|||||||
//! the extraction-time adjustments (no `anyhow`, no `log`, no
|
//! the extraction-time adjustments (no `anyhow`, no `log`, no
|
||||||
//! `async-trait`) are recorded in `docs/architecture/vendor.md`.
|
//! `async-trait`) are recorded in `docs/architecture/vendor.md`.
|
||||||
//!
|
//!
|
||||||
|
//! 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
|
||||||
|
//! [`server`]; the client session (upstream `client.rs`, `use_stream`
|
||||||
|
//! path) will live in [`client`].
|
||||||
|
//!
|
||||||
//! License: upstream is MIT with no per-file headers — the bundled
|
//! License: upstream is MIT with no per-file headers — the bundled
|
||||||
//! notice in [`this module's LICENSE file`](LICENSE) (v1.0.0
|
//! notice in [`this module's LICENSE file`](LICENSE) (v1.0.0
|
||||||
//! attribution) is the license obligation's carrier.
|
//! attribution) is the license obligation's carrier.
|
||||||
@@ -21,7 +29,436 @@
|
|||||||
//! with the wrapper, and is the crate's only RFC 1928 wire vocabulary
|
//! with the wrapper, and is the crate's only RFC 1928 wire vocabulary
|
||||||
//! ("no second codec, ever" — ADR-011). A future upstream release is
|
//! ("no second codec, ever" — ADR-011). A future upstream release is
|
||||||
//! re-based against deliberately, never absorbed silently.
|
//! 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)]
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io;
|
||||||
|
use thiserror::Error;
|
||||||
|
use util::stream::read_exact;
|
||||||
|
use util::target_addr::read_address;
|
||||||
|
use util::target_addr::AddrError;
|
||||||
|
use util::target_addr::TargetAddr;
|
||||||
|
use util::target_addr::ToTargetAddr;
|
||||||
|
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub mod consts {
|
||||||
|
pub const SOCKS5_VERSION: u8 = 0x05;
|
||||||
|
|
||||||
|
pub const SOCKS5_AUTH_METHOD_NONE: u8 = 0x00;
|
||||||
|
pub const SOCKS5_AUTH_METHOD_GSSAPI: u8 = 0x01;
|
||||||
|
pub const SOCKS5_AUTH_METHOD_PASSWORD: u8 = 0x02;
|
||||||
|
pub const SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE: u8 = 0xff;
|
||||||
|
|
||||||
|
pub const SOCKS5_CMD_TCP_CONNECT: u8 = 0x01;
|
||||||
|
pub const SOCKS5_CMD_TCP_BIND: u8 = 0x02;
|
||||||
|
pub const SOCKS5_CMD_UDP_ASSOCIATE: u8 = 0x03;
|
||||||
|
|
||||||
|
pub const SOCKS5_ADDR_TYPE_IPV4: u8 = 0x01;
|
||||||
|
pub const SOCKS5_ADDR_TYPE_DOMAIN_NAME: u8 = 0x03;
|
||||||
|
pub const SOCKS5_ADDR_TYPE_IPV6: u8 = 0x04;
|
||||||
|
|
||||||
|
pub const SOCKS5_REPLY_SUCCEEDED: u8 = 0x00;
|
||||||
|
pub const SOCKS5_REPLY_GENERAL_FAILURE: u8 = 0x01;
|
||||||
|
pub const SOCKS5_REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02;
|
||||||
|
pub const SOCKS5_REPLY_NETWORK_UNREACHABLE: u8 = 0x03;
|
||||||
|
pub const SOCKS5_REPLY_HOST_UNREACHABLE: u8 = 0x04;
|
||||||
|
pub const SOCKS5_REPLY_CONNECTION_REFUSED: u8 = 0x05;
|
||||||
|
pub const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06;
|
||||||
|
pub const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
|
||||||
|
pub const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum Socks5Command {
|
||||||
|
TCPConnect,
|
||||||
|
TCPBind,
|
||||||
|
UDPAssociate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socks5Command {
|
||||||
|
#[inline]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn as_u8(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Socks5Command::TCPConnect => consts::SOCKS5_CMD_TCP_CONNECT,
|
||||||
|
Socks5Command::TCPBind => consts::SOCKS5_CMD_TCP_BIND,
|
||||||
|
Socks5Command::UDPAssociate => consts::SOCKS5_CMD_UDP_ASSOCIATE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn from_u8(code: u8) -> Option<Socks5Command> {
|
||||||
|
match code {
|
||||||
|
consts::SOCKS5_CMD_TCP_CONNECT => Some(Socks5Command::TCPConnect),
|
||||||
|
consts::SOCKS5_CMD_TCP_BIND => Some(Socks5Command::TCPBind),
|
||||||
|
consts::SOCKS5_CMD_UDP_ASSOCIATE => Some(Socks5Command::UDPAssociate),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum AuthenticationMethod {
|
||||||
|
None,
|
||||||
|
Password { username: String, password: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl AuthenticationMethod {
|
||||||
|
#[inline]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn as_u8(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
AuthenticationMethod::None => consts::SOCKS5_AUTH_METHOD_NONE,
|
||||||
|
AuthenticationMethod::Password {..} =>
|
||||||
|
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 {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
AuthenticationMethod::None => f.write_str("AuthenticationMethod::None"),
|
||||||
|
AuthenticationMethod::Password { .. } => f.write_str("AuthenticationMethod::Password"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SocksError {
|
||||||
|
#[error("i/o error: {0}")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error("Auth method unacceptable `{0:?}`.")]
|
||||||
|
AuthMethodUnacceptable(Vec<u8>),
|
||||||
|
#[error("Unsupported SOCKS version `{0}`.")]
|
||||||
|
UnsupportedSocksVersion(u8),
|
||||||
|
#[error("Domain exceeded max sequence length")]
|
||||||
|
ExceededMaxDomainLen(usize),
|
||||||
|
#[error("Authentication rejected `{0}`")]
|
||||||
|
AuthenticationRejected(String),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
ServerError(#[from] server::SocksServerError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
UdpHeaderError(#[from] UdpHeaderError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
AddrError(#[from] AddrError),
|
||||||
|
|
||||||
|
#[error("Error with reply: {0}.")]
|
||||||
|
ReplyError(#[from] ReplyError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T, E = SocksError> = core::result::Result<T, E>;
|
||||||
|
|
||||||
|
/// SOCKS5 reply code
|
||||||
|
#[derive(Error, Debug, Copy, Clone)]
|
||||||
|
pub enum ReplyError {
|
||||||
|
#[error("Succeeded")]
|
||||||
|
Succeeded,
|
||||||
|
#[error("General failure")]
|
||||||
|
GeneralFailure,
|
||||||
|
#[error("Connection not allowed by ruleset")]
|
||||||
|
ConnectionNotAllowed,
|
||||||
|
#[error("Network unreachable")]
|
||||||
|
NetworkUnreachable,
|
||||||
|
#[error("Host unreachable")]
|
||||||
|
HostUnreachable,
|
||||||
|
#[error("Connection refused")]
|
||||||
|
ConnectionRefused,
|
||||||
|
#[error("Connection timeout")]
|
||||||
|
ConnectionTimeout,
|
||||||
|
#[error("TTL expired")]
|
||||||
|
TtlExpired,
|
||||||
|
#[error("Command not supported")]
|
||||||
|
CommandNotSupported,
|
||||||
|
#[error("Address type not supported")]
|
||||||
|
AddressTypeNotSupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplyError {
|
||||||
|
#[inline]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn as_u8(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
ReplyError::Succeeded => consts::SOCKS5_REPLY_SUCCEEDED,
|
||||||
|
ReplyError::GeneralFailure => consts::SOCKS5_REPLY_GENERAL_FAILURE,
|
||||||
|
ReplyError::ConnectionNotAllowed => consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
|
||||||
|
ReplyError::NetworkUnreachable => consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
|
||||||
|
ReplyError::HostUnreachable => consts::SOCKS5_REPLY_HOST_UNREACHABLE,
|
||||||
|
ReplyError::ConnectionRefused => consts::SOCKS5_REPLY_CONNECTION_REFUSED,
|
||||||
|
ReplyError::ConnectionTimeout => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||||
|
ReplyError::TtlExpired => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||||
|
ReplyError::CommandNotSupported => consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
|
||||||
|
ReplyError::AddressTypeNotSupported => consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn from_u8(code: u8) -> ReplyError {
|
||||||
|
match code {
|
||||||
|
consts::SOCKS5_REPLY_SUCCEEDED => ReplyError::Succeeded,
|
||||||
|
consts::SOCKS5_REPLY_GENERAL_FAILURE => ReplyError::GeneralFailure,
|
||||||
|
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED => ReplyError::ConnectionNotAllowed,
|
||||||
|
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE => ReplyError::NetworkUnreachable,
|
||||||
|
consts::SOCKS5_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable,
|
||||||
|
consts::SOCKS5_REPLY_CONNECTION_REFUSED => ReplyError::ConnectionRefused,
|
||||||
|
consts::SOCKS5_REPLY_TTL_EXPIRED => ReplyError::TtlExpired,
|
||||||
|
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED => ReplyError::CommandNotSupported,
|
||||||
|
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED => ReplyError::AddressTypeNotSupported,
|
||||||
|
_ => unreachable!("ReplyError code unsupported."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum UdpHeaderError {
|
||||||
|
#[error(transparent)]
|
||||||
|
AddrError(#[from] AddrError),
|
||||||
|
#[error("could not convert target addr: {0}")]
|
||||||
|
ToTargetAddr(#[source] io::Error),
|
||||||
|
#[error("could not read UDP header: {0}")]
|
||||||
|
ReadingError(#[source] io::Error),
|
||||||
|
#[error("does not match the expected reserved field")]
|
||||||
|
GarbageInReserved,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate UDP header
|
||||||
|
///
|
||||||
|
/// # UDP Request header structure.
|
||||||
|
/// ```text
|
||||||
|
/// +----+------+------+----------+----------+----------+
|
||||||
|
/// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||||
|
/// +----+------+------+----------+----------+----------+
|
||||||
|
/// | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||||
|
/// +----+------+------+----------+----------+----------+
|
||||||
|
///
|
||||||
|
/// The fields in the UDP request header are:
|
||||||
|
///
|
||||||
|
/// o RSV Reserved X'0000'
|
||||||
|
/// o FRAG Current fragment number
|
||||||
|
/// o ATYP address type of following addresses:
|
||||||
|
/// o IP V4 address: X'01'
|
||||||
|
/// o DOMAINNAME: X'03'
|
||||||
|
/// o IP V6 address: X'04'
|
||||||
|
/// o DST.ADDR desired destination address
|
||||||
|
/// o DST.PORT desired destination port
|
||||||
|
/// o DATA user data
|
||||||
|
/// ```
|
||||||
|
pub fn new_udp_header<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>, UdpHeaderError> {
|
||||||
|
let mut header = vec![
|
||||||
|
0, 0, // RSV
|
||||||
|
0, // FRAG
|
||||||
|
];
|
||||||
|
header.append(
|
||||||
|
&mut target_addr
|
||||||
|
.to_target_addr()
|
||||||
|
.map_err(UdpHeaderError::ToTargetAddr)?
|
||||||
|
.to_be_bytes()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
let rsv = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?;
|
||||||
|
|
||||||
|
if !rsv.eq(&[0u8; 2]) {
|
||||||
|
return Err(UdpHeaderError::GarbageInReserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
let [frag, atyp] = read_exact!(req, [0u8; 2]).map_err(UdpHeaderError::ReadingError)?;
|
||||||
|
|
||||||
|
let target_addr = read_address(&mut req, atyp).await?;
|
||||||
|
|
||||||
|
Ok((frag, target_addr, req))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn socks5_command_mapping() {
|
||||||
|
assert_eq!(
|
||||||
|
Socks5Command::TCPConnect.as_u8(),
|
||||||
|
consts::SOCKS5_CMD_TCP_CONNECT
|
||||||
|
);
|
||||||
|
assert_eq!(Socks5Command::TCPBind.as_u8(), consts::SOCKS5_CMD_TCP_BIND);
|
||||||
|
assert_eq!(
|
||||||
|
Socks5Command::UDPAssociate.as_u8(),
|
||||||
|
consts::SOCKS5_CMD_UDP_ASSOCIATE
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Socks5Command::from_u8(consts::SOCKS5_CMD_TCP_CONNECT),
|
||||||
|
Some(Socks5Command::TCPConnect)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Socks5Command::from_u8(consts::SOCKS5_CMD_TCP_BIND),
|
||||||
|
Some(Socks5Command::TCPBind)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Socks5Command::from_u8(consts::SOCKS5_CMD_UDP_ASSOCIATE),
|
||||||
|
Some(Socks5Command::UDPAssociate)
|
||||||
|
);
|
||||||
|
assert_eq!(Socks5Command::from_u8(0x00), None);
|
||||||
|
assert_eq!(Socks5Command::from_u8(0x04), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_error_mapping_table() {
|
||||||
|
let table = [
|
||||||
|
(ReplyError::Succeeded, consts::SOCKS5_REPLY_SUCCEEDED),
|
||||||
|
(
|
||||||
|
ReplyError::GeneralFailure,
|
||||||
|
consts::SOCKS5_REPLY_GENERAL_FAILURE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::ConnectionNotAllowed,
|
||||||
|
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::NetworkUnreachable,
|
||||||
|
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::HostUnreachable,
|
||||||
|
consts::SOCKS5_REPLY_HOST_UNREACHABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::ConnectionRefused,
|
||||||
|
consts::SOCKS5_REPLY_CONNECTION_REFUSED,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::ConnectionTimeout,
|
||||||
|
consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||||
|
),
|
||||||
|
(ReplyError::TtlExpired, consts::SOCKS5_REPLY_TTL_EXPIRED),
|
||||||
|
(
|
||||||
|
ReplyError::CommandNotSupported,
|
||||||
|
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ReplyError::AddressTypeNotSupported,
|
||||||
|
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (reply, code) in table {
|
||||||
|
assert_eq!(reply.as_u8(), code);
|
||||||
|
assert!(matches!(ReplyError::from_u8(code), r if r.as_u8() == code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_header_round_trip_ip() {
|
||||||
|
let target = TargetAddr::Ip("192.168.0.1:80".parse().unwrap());
|
||||||
|
let header = new_udp_header(target.clone()).unwrap();
|
||||||
|
assert_eq!(header, vec![0, 0, 0, 1, 192, 168, 0, 1, 0, 80]);
|
||||||
|
|
||||||
|
let (frag, parsed_target, payload) = parse_udp_request(&header).await.unwrap();
|
||||||
|
assert_eq!(frag, 0);
|
||||||
|
assert_eq!(parsed_target, target);
|
||||||
|
assert!(payload.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_header_round_trip_domain() {
|
||||||
|
let target = TargetAddr::Domain("example.com".to_owned(), 53);
|
||||||
|
let header = new_udp_header(target.clone()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
header,
|
||||||
|
vec![
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
|
||||||
|
11,
|
||||||
|
b'e',
|
||||||
|
b'x',
|
||||||
|
b'a',
|
||||||
|
b'm',
|
||||||
|
b'p',
|
||||||
|
b'l',
|
||||||
|
b'e',
|
||||||
|
b'.',
|
||||||
|
b'c',
|
||||||
|
b'o',
|
||||||
|
b'm',
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (frag, parsed_target, payload) = parse_udp_request(&header).await.unwrap();
|
||||||
|
assert_eq!(frag, 0);
|
||||||
|
assert_eq!(parsed_target, target);
|
||||||
|
assert!(payload.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_header_rejects_garbage_reserved() {
|
||||||
|
let mut header = new_udp_header(TargetAddr::Domain("example.com".to_owned(), 53)).unwrap();
|
||||||
|
header[1] = 0xff;
|
||||||
|
let err = parse_udp_request(&header).await.unwrap_err();
|
||||||
|
assert!(matches!(err, UdpHeaderError::GarbageInReserved));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_header_carries_fragment_and_payload() {
|
||||||
|
let mut frame = new_udp_header(TargetAddr::Ip("10.0.0.1:53".parse().unwrap())).unwrap();
|
||||||
|
frame[2] = 3;
|
||||||
|
frame.extend_from_slice(b"hello");
|
||||||
|
let (frag, target, payload) = parse_udp_request(&frame).await.unwrap();
|
||||||
|
assert_eq!(frag, 3);
|
||||||
|
assert_eq!(target, TargetAddr::Ip("10.0.0.1:53".parse().unwrap()));
|
||||||
|
assert_eq!(payload, b"hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_header_rejects_unknown_address_type() {
|
||||||
|
let frame = vec![0, 0, 0, 0x02, 1, 2, 3, 4, 0, 0];
|
||||||
|
let err = parse_udp_request(&frame).await.unwrap_err();
|
||||||
|
assert!(matches!(err, UdpHeaderError::AddrError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn socks_error_from_variants() {
|
||||||
|
let err: SocksError = UdpHeaderError::GarbageInReserved.into();
|
||||||
|
assert!(matches!(err, SocksError::UdpHeaderError(_)));
|
||||||
|
let err: SocksError = AddrError::IncorrectAddressType.into();
|
||||||
|
assert!(matches!(err, SocksError::AddrError(_)));
|
||||||
|
let err: SocksError = ReplyError::CommandNotSupported.into();
|
||||||
|
assert!(matches!(err, SocksError::ReplyError(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+63
@@ -1,2 +1,65 @@
|
|||||||
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
||||||
//! in `mod.rs`; filled by the server-typestate extraction task).
|
//! 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.
|
||||||
|
|
||||||
|
use crate::vendor::util::stream::ConnectError;
|
||||||
|
use crate::vendor::AddrError;
|
||||||
|
use crate::vendor::ReplyError;
|
||||||
|
use crate::vendor::UdpHeaderError;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum SocksServerError {
|
||||||
|
#[error("i/o error when {context}: {source}")]
|
||||||
|
Io {
|
||||||
|
source: io::Error,
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
|
#[error("string error when {context}: {source}")]
|
||||||
|
FromUtf8 {
|
||||||
|
source: FromUtf8Error,
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
|
#[error(transparent)]
|
||||||
|
ConnectError(#[from] ConnectError),
|
||||||
|
#[error(transparent)]
|
||||||
|
UdpHeaderError(#[from] UdpHeaderError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AddrError(#[from] AddrError),
|
||||||
|
#[error("BUG: {0}")] // should be unreachable
|
||||||
|
Bug(&'static str),
|
||||||
|
#[error("Auth method unacceptable `{0:?}`.")]
|
||||||
|
AuthMethodUnacceptable(Vec<u8>),
|
||||||
|
#[error("Unsupported SOCKS version `{0}`.")]
|
||||||
|
UnsupportedSocksVersion(u8),
|
||||||
|
#[error("Unsupported SOCKS command `{0}`.")]
|
||||||
|
UnknownCommand(u8),
|
||||||
|
#[error("Unexpected garbage received on TCP stream used for UDP proxy keep-alive: `{0}`")]
|
||||||
|
UnexpectedUdpControlGarbage(u8),
|
||||||
|
#[error("Empty username received")]
|
||||||
|
EmptyUsername,
|
||||||
|
#[error("Empty password received")]
|
||||||
|
EmptyPassword,
|
||||||
|
#[error("Authentication rejected")]
|
||||||
|
AuthenticationRejected,
|
||||||
|
#[error("End of stream")]
|
||||||
|
EOF,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocksServerError {
|
||||||
|
pub fn to_reply_error(&self) -> ReplyError {
|
||||||
|
match self {
|
||||||
|
SocksServerError::UnknownCommand(_) => ReplyError::CommandNotSupported,
|
||||||
|
SocksServerError::AddrError(err) => err.to_reply_error(),
|
||||||
|
_ => ReplyError::GeneralFailure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::io;
|
||||||
Vendored
+116
-4
@@ -1,7 +1,119 @@
|
|||||||
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
||||||
//! in the vendor root's `mod.rs`; filled by the wire-vocabulary
|
//! in the vendor root's `mod.rs`).
|
||||||
//! extraction task).
|
|
||||||
//!
|
//!
|
||||||
//! Upstream carries the `read_exact!`/`ready!` macros, `ConnectError`,
|
//! Upstream carries the `read_exact!`/`ready!` macros, `ConnectError`,
|
||||||
//! and the `tcp_connect*` dialers; only the first two ride the
|
//! and the `tcp_connect*` dialers; only the macros + `ConnectError`
|
||||||
//! extraction (the dialers are the cut kernel-net region).
|
//! 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).
|
||||||
|
|
||||||
|
/// Easy to destructure bytes buffers by naming each fields:
|
||||||
|
///
|
||||||
|
/// # Examples (before)
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let mut buf = [0u8; 2];
|
||||||
|
/// stream.read_exact(&mut buf).await?;
|
||||||
|
/// let [version, method_len] = buf;
|
||||||
|
///
|
||||||
|
/// assert_eq!(version, 0x05);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Examples (after)
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let [version, method_len] = read_exact!(stream, [0u8; 2]);
|
||||||
|
///
|
||||||
|
/// assert_eq!(version, 0x05);
|
||||||
|
/// ```
|
||||||
|
macro_rules! read_exact {
|
||||||
|
($stream: expr, $array: expr) => {{
|
||||||
|
let mut x = $array;
|
||||||
|
$stream.read_exact(&mut x).await.map(|_| x)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 std::io;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ConnectError {
|
||||||
|
#[error("Connection timed out")]
|
||||||
|
ConnectionTimeout,
|
||||||
|
#[error("Connection refused: {0}")]
|
||||||
|
ConnectionRefused(#[source] io::Error),
|
||||||
|
#[error("Connection aborted: {0}")]
|
||||||
|
ConnectionAborted(#[source] io::Error),
|
||||||
|
#[error("Connection reset: {0}")]
|
||||||
|
ConnectionReset(#[source] io::Error),
|
||||||
|
#[error("Not connected: {0}")]
|
||||||
|
NotConnected(#[source] io::Error),
|
||||||
|
#[error("Other i/o error: {0}")]
|
||||||
|
Other(#[source] io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectError {
|
||||||
|
pub fn to_reply_error(&self) -> ReplyError {
|
||||||
|
match self {
|
||||||
|
ConnectError::ConnectionTimeout => ReplyError::ConnectionTimeout,
|
||||||
|
ConnectError::ConnectionRefused(_) => ReplyError::ConnectionRefused,
|
||||||
|
ConnectError::ConnectionAborted(_) | ConnectError::ConnectionReset(_) => {
|
||||||
|
ReplyError::ConnectionNotAllowed
|
||||||
|
}
|
||||||
|
ConnectError::NotConnected(_) => ReplyError::NetworkUnreachable,
|
||||||
|
ConnectError::Other(_) => ReplyError::GeneralFailure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ConnectError;
|
||||||
|
use crate::vendor::ReplyError;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_reply_error_mapping() {
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::ConnectionTimeout.to_reply_error(),
|
||||||
|
ReplyError::ConnectionTimeout
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::ConnectionRefused(ErrorKind::ConnectionRefused.into()).to_reply_error(),
|
||||||
|
ReplyError::ConnectionRefused
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::ConnectionAborted(ErrorKind::ConnectionAborted.into()).to_reply_error(),
|
||||||
|
ReplyError::ConnectionNotAllowed
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::ConnectionReset(ErrorKind::ConnectionReset.into()).to_reply_error(),
|
||||||
|
ReplyError::ConnectionNotAllowed
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::NotConnected(ErrorKind::NotConnected.into()).to_reply_error(),
|
||||||
|
ReplyError::NetworkUnreachable
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
ConnectError::Other(ErrorKind::Other.into()).to_reply_error(),
|
||||||
|
ReplyError::GeneralFailure
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+448
-3
@@ -1,7 +1,452 @@
|
|||||||
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
//! Vendored from fast-socks5 v1.0.0 (extraction — see the module doc
|
||||||
//! in the vendor root's `mod.rs`; filled by the wire-vocabulary
|
//! in the vendor root's `mod.rs`).
|
||||||
//! extraction task).
|
|
||||||
//!
|
//!
|
||||||
//! Upstream carries `TargetAddr`, the address codec (`read_address`),
|
//! 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 rides the extraction
|
||||||
//! (DNS resolution is the cut net region).
|
//! (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).
|
||||||
|
|
||||||
|
#![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 std::fmt;
|
||||||
|
use std::io;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||||
|
use std::vec::IntoIter;
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||||
|
|
||||||
|
/// SOCKS5 reply code
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum AddrError {
|
||||||
|
#[error("Domain length {0} exceeded maximum")]
|
||||||
|
DomainLenTooLong(usize),
|
||||||
|
#[error("Can't read IPv4: {0}")]
|
||||||
|
IPv4Unreadable(#[source] io::Error),
|
||||||
|
#[error("Can't read IPv6: {0}")]
|
||||||
|
IPv6Unreadable(#[source] io::Error),
|
||||||
|
#[error("Can't read port number: {0}")]
|
||||||
|
PortNumberUnreadable(#[source] io::Error),
|
||||||
|
#[error("Can't read domain len: {0}")]
|
||||||
|
DomainLenUnreadable(#[source] io::Error),
|
||||||
|
#[error("Can't read domain content: {0}")]
|
||||||
|
DomainContentUnreadable(#[source] io::Error),
|
||||||
|
#[error("Can't convert address: {0}")]
|
||||||
|
AddrConversionFailed(#[source] io::Error),
|
||||||
|
#[error("Malformed UTF-8")]
|
||||||
|
Utf8(#[source] std::string::FromUtf8Error),
|
||||||
|
#[error("Unknown address type")]
|
||||||
|
IncorrectAddressType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddrError {
|
||||||
|
pub fn to_reply_error(&self) -> ReplyError {
|
||||||
|
match self {
|
||||||
|
AddrError::IncorrectAddressType => ReplyError::AddressTypeNotSupported,
|
||||||
|
_ => ReplyError::ConnectionRefused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A description of a connection target.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum TargetAddr {
|
||||||
|
/// Connect to an IP address.
|
||||||
|
Ip(SocketAddr),
|
||||||
|
/// Connect to a fully qualified domain name.
|
||||||
|
///
|
||||||
|
/// The domain name will be passed along to the proxy server and DNS lookup
|
||||||
|
/// will happen there.
|
||||||
|
Domain(String, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetAddr {
|
||||||
|
pub fn is_ip(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
TargetAddr::Ip(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_domain(&self) -> bool {
|
||||||
|
!self.is_ip()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_be_bytes(&self) -> Result<Vec<u8>, AddrError> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
match self {
|
||||||
|
TargetAddr::Ip(SocketAddr::V4(addr)) => {
|
||||||
|
buf.extend_from_slice(&[SOCKS5_ADDR_TYPE_IPV4]);
|
||||||
|
|
||||||
|
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||||
|
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||||
|
}
|
||||||
|
TargetAddr::Ip(SocketAddr::V6(addr)) => {
|
||||||
|
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_IPV6]);
|
||||||
|
|
||||||
|
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||||
|
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||||
|
}
|
||||||
|
TargetAddr::Domain(ref domain, port) => {
|
||||||
|
if domain.len() > u8::max_value() as usize {
|
||||||
|
return Err(AddrError::DomainLenTooLong(domain.len()));
|
||||||
|
}
|
||||||
|
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]);
|
||||||
|
buf.extend_from_slice(domain.as_bytes()); // domain content
|
||||||
|
buf.extend_from_slice(&port.to_be_bytes());
|
||||||
|
// port content (.to_be_bytes() convert from u16 to u8 type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_string_and_port(self) -> (String, u16) {
|
||||||
|
match self {
|
||||||
|
TargetAddr::Ip(socket_addr) => (socket_addr.ip().to_string(), socket_addr.port()),
|
||||||
|
TargetAddr::Domain(domain, port) => (domain, port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async-std ToSocketAddrs doesn't supports external trait implementation
|
||||||
|
// @see https://github.com/async-rs/async-std/issues/539
|
||||||
|
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().",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TargetAddr {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
TargetAddr::Ip(ref addr) => write!(f, "{}", addr),
|
||||||
|
TargetAddr::Domain(ref addr, ref port) => write!(f, "{}:{}", addr, port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for objects that can be converted to `TargetAddr`.
|
||||||
|
pub trait ToTargetAddr {
|
||||||
|
/// Converts the value of `self` to a `TargetAddr`.
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToTargetAddr for (&'a 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>() {
|
||||||
|
return (addr, self.1).to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
|
||||||
|
return (addr, self.1).to_target_addr();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TargetAddr::Domain(self.0.to_owned(), self.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddr {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
Ok(TargetAddr::Ip(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddrV4 {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddr::V4(*self).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for SocketAddrV6 {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddr::V6(*self).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (IpAddr, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
match self.0 {
|
||||||
|
IpAddr::V4(ipv4_addr) => (ipv4_addr, self.1).to_target_addr(),
|
||||||
|
IpAddr::V6(ipv6_addr) => (ipv6_addr, self.1).to_target_addr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (Ipv4Addr, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddrV4::new(self.0, self.1).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTargetAddr for TargetAddr {
|
||||||
|
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Addr {
|
||||||
|
V4([u8; 4]),
|
||||||
|
V6([u8; 16]),
|
||||||
|
Domain(String), // Vec<[u8]> or Box<[u8]> or String ?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is used by the client & the server
|
||||||
|
pub async fn read_address<T: AsyncRead + Unpin>(
|
||||||
|
stream: &mut T,
|
||||||
|
atyp: u8,
|
||||||
|
) -> 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))?)
|
||||||
|
}
|
||||||
|
consts::SOCKS5_ADDR_TYPE_IPV6 => {
|
||||||
|
Addr::V6(read_exact!(stream, [0u8; 16]).map_err(|err| AddrError::IPv6Unreadable(err))?)
|
||||||
|
}
|
||||||
|
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
|
||||||
|
let len =
|
||||||
|
read_exact!(stream, [0]).map_err(|err| AddrError::DomainLenUnreadable(err))?[0];
|
||||||
|
let domain = read_exact!(stream, vec![0u8; len as usize])
|
||||||
|
.map_err(|err| AddrError::DomainContentUnreadable(err))?;
|
||||||
|
// make sure the bytes are correct utf8 string
|
||||||
|
let domain = String::from_utf8(domain).map_err(|err| AddrError::Utf8(err))?;
|
||||||
|
|
||||||
|
Addr::Domain(domain)
|
||||||
|
}
|
||||||
|
_ => return Err(AddrError::IncorrectAddressType),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find port number
|
||||||
|
let port = read_exact!(stream, [0u8; 2]).map_err(|err| AddrError::PortNumberUnreadable(err))?;
|
||||||
|
// Convert (u8 * 2) into u16
|
||||||
|
let port = (port[0] as u16) << 8 | port[1] as u16;
|
||||||
|
|
||||||
|
// Merge ADDRESS + PORT into a TargetAddr
|
||||||
|
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))?,
|
||||||
|
Addr::V6(x) => (Ipv6Addr::from(x), port)
|
||||||
|
.to_target_addr()
|
||||||
|
.map_err(|err| AddrError::AddrConversionFailed(err))?,
|
||||||
|
Addr::Domain(domain) => TargetAddr::Domain(domain, port),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::vendor::consts;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
async fn read_target_addr(bytes: &[u8]) -> Result<TargetAddr, AddrError> {
|
||||||
|
let (mut client, mut server) = tokio::io::duplex(64);
|
||||||
|
client.write_all(&bytes[1..]).await.unwrap();
|
||||||
|
drop(client);
|
||||||
|
read_address(&mut server, bytes[0]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_address_ipv4_round_trip() {
|
||||||
|
let addr = read_target_addr(&[consts::SOCKS5_ADDR_TYPE_IPV4, 192, 168, 0, 1, 0, 80])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(addr, TargetAddr::Ip("192.168.0.1:80".parse().unwrap()));
|
||||||
|
assert!(addr.is_ip());
|
||||||
|
let encoded = addr.to_be_bytes().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encoded,
|
||||||
|
vec![consts::SOCKS5_ADDR_TYPE_IPV4, 192, 168, 0, 1, 0, 80,]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_address_ipv6_round_trip() {
|
||||||
|
let addr = read_target_addr(&[
|
||||||
|
consts::SOCKS5_ADDR_TYPE_IPV6,
|
||||||
|
0x20,
|
||||||
|
0x01,
|
||||||
|
0x0d,
|
||||||
|
0xb8,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0x01,
|
||||||
|
0x01,
|
||||||
|
0xbb,
|
||||||
|
])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(addr, TargetAddr::Ip("[2001:db8::1]:443".parse().unwrap()));
|
||||||
|
let encoded = addr.to_be_bytes().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encoded,
|
||||||
|
vec![
|
||||||
|
consts::SOCKS5_ADDR_TYPE_IPV6,
|
||||||
|
0x20,
|
||||||
|
0x01,
|
||||||
|
0x0d,
|
||||||
|
0xb8,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0x01,
|
||||||
|
0x01,
|
||||||
|
0xbb,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_address_domain_round_trip() {
|
||||||
|
let addr = read_target_addr(&[
|
||||||
|
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
|
||||||
|
11,
|
||||||
|
b'e',
|
||||||
|
b'x',
|
||||||
|
b'a',
|
||||||
|
b'm',
|
||||||
|
b'p',
|
||||||
|
b'l',
|
||||||
|
b'e',
|
||||||
|
b'.',
|
||||||
|
b'c',
|
||||||
|
b'o',
|
||||||
|
b'm',
|
||||||
|
0,
|
||||||
|
80,
|
||||||
|
])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(addr, TargetAddr::Domain("example.com".to_owned(), 80));
|
||||||
|
assert!(addr.is_domain());
|
||||||
|
let encoded = addr.to_be_bytes().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encoded,
|
||||||
|
vec![
|
||||||
|
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
|
||||||
|
11,
|
||||||
|
b'e',
|
||||||
|
b'x',
|
||||||
|
b'a',
|
||||||
|
b'm',
|
||||||
|
b'p',
|
||||||
|
b'l',
|
||||||
|
b'e',
|
||||||
|
b'.',
|
||||||
|
b'c',
|
||||||
|
b'o',
|
||||||
|
b'm',
|
||||||
|
0,
|
||||||
|
80
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(addr.into_string_and_port(), ("example.com".to_owned(), 80));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_address_unknown_atyp_is_incorrect() {
|
||||||
|
let err = read_target_addr(&[0x02, 0, 0]).await.unwrap_err();
|
||||||
|
assert!(matches!(err, AddrError::IncorrectAddressType));
|
||||||
|
assert!(matches!(
|
||||||
|
err.to_reply_error(),
|
||||||
|
ReplyError::AddressTypeNotSupported
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_be_bytes_domain_too_long() {
|
||||||
|
let long_domain = "a".repeat(256);
|
||||||
|
let addr = TargetAddr::Domain(long_domain, 80);
|
||||||
|
assert!(matches!(
|
||||||
|
addr.to_be_bytes(),
|
||||||
|
Err(AddrError::DomainLenTooLong(256))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_socket_addrs_domain_is_explicit_resolution_only() {
|
||||||
|
let addr = TargetAddr::Domain("example.com".to_owned(), 80);
|
||||||
|
assert!(std::net::ToSocketAddrs::to_socket_addrs(&addr).is_err());
|
||||||
|
let ip: SocketAddr = "10.0.0.1:80".parse().unwrap();
|
||||||
|
let addr = TargetAddr::Ip(ip);
|
||||||
|
assert_eq!(
|
||||||
|
std::net::ToSocketAddrs::to_socket_addrs(&addr)
|
||||||
|
.unwrap()
|
||||||
|
.next(),
|
||||||
|
Some(ip)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_target_addr_conversions() {
|
||||||
|
assert_eq!(
|
||||||
|
("example.com", 80u16).to_target_addr().unwrap(),
|
||||||
|
TargetAddr::Domain("example.com".to_owned(), 80)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
("1.2.3.4", 80u16).to_target_addr().unwrap(),
|
||||||
|
TargetAddr::Ip("1.2.3.4:80".parse().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80u16)
|
||||||
|
.to_target_addr()
|
||||||
|
.unwrap(),
|
||||||
|
TargetAddr::Ip("1.2.3.4:80".parse().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(Ipv6Addr::LOCALHOST, 80u16).to_target_addr().unwrap(),
|
||||||
|
TargetAddr::Ip("[::1]:80".parse().unwrap())
|
||||||
|
);
|
||||||
|
let target = TargetAddr::Domain("example.com".to_owned(), 80);
|
||||||
|
assert_eq!(target.to_target_addr().unwrap(), target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: implementation/vendor-wire-vocabulary
|
id: implementation/vendor-wire-vocabulary
|
||||||
name: Vendor — the wire vocabulary (consts, commands, codecs, errors, macros)
|
name: Vendor — the wire vocabulary (consts, commands, codecs, errors, macros)
|
||||||
status: pending
|
status: completed
|
||||||
depends_on: [implementation/vendor-crate-scaffolding]
|
depends_on: [implementation/vendor-crate-scaffolding]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
risk: low
|
risk: low
|
||||||
@@ -88,24 +88,30 @@ behavior is byte-identical to upstream v1.0.0.
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [ ] `src/vendor/lib.rs`, `src/vendor/util/{mod,stream,target_addr}.rs`
|
- [x] `src/vendor/lib.rs`, `src/vendor/util/{mod,stream,target_addr}.rs`
|
||||||
exist with the inventory above; `cargo test` compiles
|
exist with the inventory above; `cargo test` compiles
|
||||||
- [ ] `cargo check --target wasm32-unknown-unknown` clean
|
(**boundary note**: the vocabulary lives in `src/vendor/mod.rs`,
|
||||||
- [ ] `cargo check` (native, default features) clean
|
not a second root file `src/vendor/lib.rs` — the completed
|
||||||
- [ ] No `anyhow`, `log`, `tokio::net`, `socket2`, `tokio-stream`,
|
scaffolding task pinned the vendor root as `mod.rs` with upstream
|
||||||
|
file names preserved; a second root file would be an E0758
|
||||||
|
conflict. Upstream's `lib.rs` content minus the cut regions is
|
||||||
|
what `mod.rs` now carries.)
|
||||||
|
- [x] `cargo check --target wasm32-unknown-unknown` clean
|
||||||
|
- [x] `cargo check` (native, default features) clean
|
||||||
|
- [x] No `anyhow`, `log`, `tokio::net`, `socket2`, `tokio-stream`,
|
||||||
`async-trait` anywhere under `src/vendor/`
|
`async-trait` anywhere under `src/vendor/`
|
||||||
- [ ] `Socks5Command::as_u8`/`from_u8` are `pub` (the wrapper needs
|
- [x] `Socks5Command::as_u8`/`from_u8` are `pub` (the wrapper needs
|
||||||
them; upstream had them private)
|
them; upstream had them private)
|
||||||
- [ ] `read_exact!`/`ready!` are not exported at the crate root (no
|
- [x] `read_exact!`/`ready!` are not exported at the crate root (no
|
||||||
`#[macro_export]`); they resolve inside the vendor module
|
`#[macro_export]`); they resolve inside the vendor module
|
||||||
- [ ] `TargetAddr` has no `resolve_dns` method; `AddrError` has no
|
- [x] `TargetAddr` has no `resolve_dns` method; `AddrError` has no
|
||||||
`DNSResolutionFailed`/`NoDNSRecords` variants
|
`DNSResolutionFailed`/`NoDNSRecords` variants
|
||||||
- [ ] `SocksError` has no `Other(anyhow::Error)`, no `Redaction`, no
|
- [x] `SocksError` has no `Other(anyhow::Error)`, no `Redaction`, no
|
||||||
`InvalidHeader`, no `ReplySocks4Error`, no `ArgumentInputError`
|
`InvalidHeader`, no `ReplySocks4Error`, no `ArgumentInputError`
|
||||||
variants
|
variants
|
||||||
- [ ] `cargo clippy --all-targets -- -D warnings` and
|
- [x] `cargo clippy --all-targets -- -D warnings` and
|
||||||
`cargo fmt --check` clean
|
`cargo fmt --check` clean
|
||||||
- [ ] Unit tests for the codecs pass: `to_be_bytes`/`read_address`
|
- [x] Unit tests for the codecs pass: `to_be_bytes`/`read_address`
|
||||||
round-trip (v4/v6/domain), `new_udp_header`/`parse_udp_request`
|
round-trip (v4/v6/domain), `new_udp_header`/`parse_udp_request`
|
||||||
round-trip, `ReplyError::as_u8`/`from_u8` mapping table — ported
|
round-trip, `ReplyError::as_u8`/`from_u8` mapping table — ported
|
||||||
from upstream's non-net test regions where extractable
|
from upstream's non-net test regions where extractable
|
||||||
@@ -127,8 +133,82 @@ behavior is byte-identical to upstream v1.0.0.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
> To be filled by implementation agent
|
- **Module placement correction**: the task text says
|
||||||
|
`src/vendor/lib.rs`, but the completed scaffolding task pinned the
|
||||||
|
vendor root as `src/vendor/mod.rs` (skeleton with `mod` +
|
||||||
|
submodule declarations already committed, 57b107b). A second root
|
||||||
|
file (`lib.rs` alongside `mod.rs`) is an E0758 conflict, so the
|
||||||
|
vocabulary landed in `mod.rs` — upstream's `lib.rs` content minus
|
||||||
|
the cut regions, appended after the scaffolding module doc. The
|
||||||
|
review task's file-by-file diff vs upstream is unaffected (the
|
||||||
|
upstream `lib.rs` region maps to `mod.rs`; noted here for the
|
||||||
|
reviewer).
|
||||||
|
- **`SocksServerError` lands early**: `SocksError` keeps
|
||||||
|
`ServerError(#[from] server::SocksServerError)` per the task's
|
||||||
|
keep-list, but `server.rs` was a stub — a `#[from]` needs the type
|
||||||
|
now. Upstream defines `SocksServerError` at the top of
|
||||||
|
`server.rs`; it landed there verbatim (enum + `to_reply_error`,
|
||||||
|
including `UnexpectedUdpControlGarbage`) as the first item of the
|
||||||
|
server-typestate task's inventory, with a module doc noting the
|
||||||
|
early landing. The server task's remaining inventory is unchanged.
|
||||||
|
- **Verbatim-code lint posture**: the copied upstream regions trip
|
||||||
|
newer-clippy style lints (`match_like_matches_macro`,
|
||||||
|
`legacy_numeric_constants` for `u8::max_value()`, `io_other_error`
|
||||||
|
for `io::Error::new(Other)`, `needless_lifetimes`,
|
||||||
|
`redundant_closure`). Churning vendored code to satisfy a newer
|
||||||
|
clippy would break the mechanical per-file diff posture (vendor.md
|
||||||
|
§Maintenance Posture; review task criterion 1), so the lints are
|
||||||
|
allowed at module level with the rationale recorded. Behavior is
|
||||||
|
untouched.
|
||||||
|
- **`AuthenticationMethod::as_u8`/`from_u8` stay private** with
|
||||||
|
upstream's `#[allow(dead_code)]`: the client session task's
|
||||||
|
`send_version_and_methods` consumes them when it lands. Making
|
||||||
|
them pub now would be ahead of need (the task pins pub only for
|
||||||
|
`Socks5Command`).
|
||||||
|
- **`ready!` macro**: upstream's server typestate consumes it (the
|
||||||
|
`transfer`/`wait_on_tcp` region); nothing in the vocabulary region
|
||||||
|
does. It rides with `#[allow(unused_macros)]`/`#[allow(unused_imports)]`
|
||||||
|
until the typestate lands — kept verbatim now so the util diff
|
||||||
|
stays complete.
|
||||||
|
- **Tests**: the vocabulary region's unit tests are first-party
|
||||||
|
(upstream's `lib.rs` test module is the cut net-heavy region —
|
||||||
|
none of its tests are extractable); the codec tests here follow
|
||||||
|
the crate's test layout (tokio duplex for `read_address`, plain
|
||||||
|
slice parsing for `parse_udp_request`, no `env_logger`, no
|
||||||
|
sockets). `ConnectError::to_reply_error` and `AddrError::
|
||||||
|
to_reply_error` mappings are pinned too (the dial seam + the
|
||||||
|
address-type reply mapping are wire-adjacent behavior). Vendored
|
||||||
|
enums keep upstream's derives (no `PartialEq` was added), so tests
|
||||||
|
compare via `as_u8()`/`matches!` rather than adding derives.
|
||||||
|
- **The `ToSocketAddrs` impl's error string** still names
|
||||||
|
`TargetAddr::resolve_dns()` (upstream text preserved verbatim) even
|
||||||
|
though the method is cut — noted in the module doc; a future
|
||||||
|
consumer hitting that path gets the explicit-resolution message
|
||||||
|
exactly as upstream produced it.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> To be filled on completion
|
Extracted the wire vocabulary from fast-socks5 v1.0.0 per ADR-011
|
||||||
|
§Amendment + vendor.md: upstream `lib.rs`'s non-net region (consts,
|
||||||
|
`Socks5Command` with pub `as_u8`/`from_u8`, `AuthenticationMethod`,
|
||||||
|
the pruned `SocksError` (no `anyhow`/`Redaction`/`InvalidHeader`/
|
||||||
|
`ConnectError`-from/`ReplySocks4Error`/`ArgumentInputError`), the
|
||||||
|
`Result` alias, `ReplyError` codec, `UdpHeaderError` +
|
||||||
|
`new_udp_header`/`parse_udp_request`) into `src/vendor/mod.rs` (the
|
||||||
|
scaffolding-pinned root; the task's `lib.rs` target name corrected —
|
||||||
|
see Notes), `AddrError`/`TargetAddr`/`ToTargetAddr`/`Addr`/
|
||||||
|
`read_address` minus `resolve_dns` into `util/target_addr.rs`, and
|
||||||
|
the `read_exact!`/`ready!` macros (de-exported to `pub(crate) use`)
|
||||||
|
+ `ConnectError` + `to_reply_error` into `util/stream.rs`.
|
||||||
|
`SocksServerError` (enum + `to_reply_error`) landed verbatim in
|
||||||
|
`server.rs` early — `SocksError`'s `#[from]` needs it before the
|
||||||
|
typestate task lands. 16 first-party unit tests cover the codecs
|
||||||
|
(v4/v6/domain round-trips, UDP header/parse incl. frag/payload/garbage
|
||||||
|
RSV/unknown ATYP, reply-code mapping table, command mapping,
|
||||||
|
`to_reply_error` mappings, `ToTargetAddr` conversions, domain-length
|
||||||
|
cap). Verified: `cargo test` (16 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 anyhow/log/tokio::net/socket2/
|
||||||
|
tokio-stream/async-trait in vendor code; no `#[macro_export]`;
|
||||||
|
resolve_dns + the five pruned error variants absent).
|
||||||
Reference in new issue
Block a user