mirror of
https://github.com/EasyTier/EasyTier.git
synced 2024-11-16 03:32:43 +08:00
support socks5 proxy
Some checks are pending
EasyTier Core / pre_job (push) Waiting to run
EasyTier Core / build (linux-aarch64, ubuntu-latest, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-arm, ubuntu-latest, arm-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armhf, ubuntu-latest, arm-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7, ubuntu-latest, armv7-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7hf, ubuntu-latest, armv7-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-mips, ubuntu-latest, mips-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-mipsel, ubuntu-latest, mipsel-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-x86_64, ubuntu-latest, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier Core / core-result (push) Blocked by required conditions
EasyTier GUI / pre_job (push) Waiting to run
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-latest, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-latest, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier GUI / gui-result (push) Blocked by required conditions
EasyTier Mobile / pre_job (push) Waiting to run
EasyTier Mobile / build-mobile (android, ubuntu-latest, android) (push) Blocked by required conditions
EasyTier Mobile / mobile-result (push) Blocked by required conditions
EasyTier Test / pre_job (push) Waiting to run
EasyTier Test / test (push) Blocked by required conditions
Some checks are pending
EasyTier Core / pre_job (push) Waiting to run
EasyTier Core / build (linux-aarch64, ubuntu-latest, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-arm, ubuntu-latest, arm-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armhf, ubuntu-latest, arm-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7, ubuntu-latest, armv7-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7hf, ubuntu-latest, armv7-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-mips, ubuntu-latest, mips-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-mipsel, ubuntu-latest, mipsel-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-x86_64, ubuntu-latest, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier Core / core-result (push) Blocked by required conditions
EasyTier GUI / pre_job (push) Waiting to run
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-latest, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-latest, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier GUI / gui-result (push) Blocked by required conditions
EasyTier Mobile / pre_job (push) Waiting to run
EasyTier Mobile / build-mobile (android, ubuntu-latest, android) (push) Blocked by required conditions
EasyTier Mobile / mobile-result (push) Blocked by required conditions
EasyTier Test / pre_job (push) Waiting to run
EasyTier Test / test (push) Blocked by required conditions
usage: --socks5 12345 create an socks5 server on port 12345, can be used by socks5 client to access virtual network.
This commit is contained in:
parent
2aa686f7ad
commit
ae54a872ce
|
@ -206,7 +206,7 @@ defguard_wireguard_rs = "0.4.2"
|
|||
|
||||
|
||||
[features]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun"]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"]
|
||||
full = [
|
||||
"quic",
|
||||
"websocket",
|
||||
|
@ -215,9 +215,10 @@ full = [
|
|||
"aes-gcm",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"socks5",
|
||||
]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp"]
|
||||
bsd = ["aes-gcm", "mimalloc", "smoltcp"]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
|
||||
bsd = ["aes-gcm", "mimalloc", "smoltcp", "socks5"]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
|
||||
mimalloc = ["dep:mimalloc-rust"]
|
||||
|
@ -231,3 +232,4 @@ websocket = [
|
|||
"dep:rcgen",
|
||||
]
|
||||
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
|
||||
socks5 = ["dep:smoltcp"]
|
||||
|
|
|
@ -110,4 +110,7 @@ core_clap:
|
|||
zh-CN: "禁用P2P通信,只通过--peers指定的节点转发数据包"
|
||||
relay_all_peer_rpc:
|
||||
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
socks5:
|
||||
en: "enable socks5 server, allow socks5 client to access virtual network. format: <port>, e.g.: 1080"
|
||||
zh-CN: "启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080"
|
|
@ -64,6 +64,9 @@ pub trait ConfigLoader: Send + Sync {
|
|||
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>>;
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>);
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url>;
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
||||
|
||||
fn dump(&self) -> String;
|
||||
}
|
||||
|
||||
|
@ -201,6 +204,8 @@ struct Config {
|
|||
|
||||
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
||||
|
||||
socks5_proxy: Option<url::Url>,
|
||||
|
||||
flags: Option<Flags>,
|
||||
}
|
||||
|
||||
|
@ -500,6 +505,14 @@ impl ConfigLoader for TomlConfigLoader {
|
|||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
||||
self.config.lock().unwrap().routes = routes;
|
||||
}
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url> {
|
||||
self.config.lock().unwrap().socks5_proxy.clone()
|
||||
}
|
||||
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
||||
self.config.lock().unwrap().socks5_proxy = addr;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
mod tests;
|
||||
|
||||
use std::{
|
||||
backtrace,
|
||||
io::Write as _,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
@ -274,6 +272,13 @@ struct Cli {
|
|||
default_value = "false"
|
||||
)]
|
||||
relay_all_peer_rpc: bool,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.socks5").to_string()
|
||||
)]
|
||||
socks5: Option<u16>,
|
||||
}
|
||||
|
||||
rust_i18n::i18n!("locales");
|
||||
|
@ -492,6 +497,15 @@ impl From<Cli> for TomlConfigLoader {
|
|||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
if let Some(socks5_proxy) = cli.socks5 {
|
||||
cfg.set_socks5_portal(Some(
|
||||
format!("socks5://0.0.0.0:{}", socks5_proxy)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut f = cfg.get_flags();
|
||||
if cli.default_protocol.is_some() {
|
||||
f.default_protocol = cli.default_protocol.as_ref().unwrap().clone();
|
||||
|
|
21
easytier/src/gateway/fast_socks5/LICENSE
Normal file
21
easytier/src/gateway/fast_socks5/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jonathan Dizdarevic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
easytier/src/gateway/fast_socks5/README.md
Normal file
1
easytier/src/gateway/fast_socks5/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Code is modified from https://github.com/dizda/fast-socks5
|
318
easytier/src/gateway/fast_socks5/mod.rs
Normal file
318
easytier/src/gateway/fast_socks5/mod.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
//! Fast SOCKS5 client/server implementation written in Rust async/.await (with tokio).
|
||||
//!
|
||||
//! This library is maintained by [anyip.io](https://anyip.io/) a residential and mobile socks5 proxy provider.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - An `async`/`.await` [SOCKS5](https://tools.ietf.org/html/rfc1928) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4 Client](https://www.openssh.com/txt/socks4.protocol) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4a Client](https://www.openssh.com/txt/socks4a.protocol) implementation.
|
||||
//! - No **unsafe** code
|
||||
//! - Built on-top of `tokio` library
|
||||
//! - Ultra lightweight and scalable
|
||||
//! - No system dependencies
|
||||
//! - Cross-platform
|
||||
//! - Authentication methods:
|
||||
//! - No-Auth method
|
||||
//! - Username/Password auth method
|
||||
//! - Custom auth methods can be implemented via the Authentication Trait
|
||||
//! - Credentials returned on authentication success
|
||||
//! - All SOCKS5 RFC errors (replies) should be mapped
|
||||
//! - `AsyncRead + AsyncWrite` traits are implemented on Socks5Stream & Socks5Socket
|
||||
//! - `IPv4`, `IPv6`, and `Domains` types are supported
|
||||
//! - Config helper for Socks5Server
|
||||
//! - Helpers to run a Socks5Server à la *"std's TcpStream"* via `incoming.next().await`
|
||||
//! - Examples come with real cases commands scenarios
|
||||
//! - Can disable `DNS resolving`
|
||||
//! - Can skip the authentication/handshake process, which will directly handle command's request (useful to save useless round-trips in a current authenticated environment)
|
||||
//! - Can disable command execution (useful if you just want to forward the request to a different server)
|
||||
//!
|
||||
//!
|
||||
//! ## Install
|
||||
//!
|
||||
//! Open in [crates.io](https://crates.io/crates/fast-socks5).
|
||||
//!
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Please check [`examples`](https://github.com/dizda/fast-socks5/tree/master/examples) directory.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod server;
|
||||
pub mod util;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use util::target_addr::read_address;
|
||||
use util::target_addr::TargetAddr;
|
||||
use util::target_addr::ToTargetAddr;
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::read_exact;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Socks5Command {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
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]
|
||||
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 },
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//impl Vec<AuthenticationMethod> {
|
||||
// pub fn as_bytes(&self) -> &[u8] {
|
||||
// self.iter().map(|l| l.as_u8()).collect()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<&[AuthenticationMethod]> for &[u8] {
|
||||
// fn from(_: Vec<AuthenticationMethod>) -> Self {
|
||||
// &[0x00]
|
||||
// }
|
||||
//}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SocksError {
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("the data for key `{0}` is not available")]
|
||||
Redaction(String),
|
||||
#[error("invalid header (expected {expected:?}, found {found:?})")]
|
||||
InvalidHeader { expected: String, found: String },
|
||||
|
||||
#[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 failed `{0}`")]
|
||||
AuthenticationFailed(String),
|
||||
#[error("Authentication rejected `{0}`")]
|
||||
AuthenticationRejected(String),
|
||||
|
||||
#[error("Error with reply: {0}.")]
|
||||
ReplyError(#[from] ReplyError),
|
||||
|
||||
#[cfg(feature = "socks4")]
|
||||
#[error("Error with reply: {0}.")]
|
||||
ReplySocks4Error(#[from] socks4::ReplyError),
|
||||
|
||||
#[error("Argument input error: `{0}`.")]
|
||||
ArgumentInputError(&'static str),
|
||||
|
||||
// #[error("Other: `{0}`.")]
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
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,
|
||||
// OtherReply(u8),
|
||||
}
|
||||
|
||||
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,
|
||||
// ReplyError::OtherReply(c) => c,
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
// _ => ReplyError::OtherReply(code),
|
||||
_ => unreachable!("ReplyError code unsupported."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
let mut header = vec![
|
||||
0, 0, // RSV
|
||||
0, // FRAG
|
||||
];
|
||||
header.append(&mut target_addr.to_target_addr()?.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])> {
|
||||
let rsv = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
if !rsv.eq(&[0u8; 2]) {
|
||||
return Err(ReplyError::GeneralFailure.into());
|
||||
}
|
||||
|
||||
let [frag, atyp] = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
let target_addr = read_address(&mut req, atyp).await.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
Ok((frag, target_addr, req))
|
||||
}
|
842
easytier/src/gateway/fast_socks5/server.rs
Normal file
842
easytier/src/gateway/fast_socks5/server.rs
Normal file
|
@ -0,0 +1,842 @@
|
|||
use super::new_udp_header;
|
||||
use super::parse_udp_request;
|
||||
use super::read_exact;
|
||||
use super::util::stream::tcp_connect_with_timeout;
|
||||
use super::util::target_addr::{read_address, TargetAddr};
|
||||
use super::Socks5Command;
|
||||
use super::{consts, AuthenticationMethod, ReplyError, Result, SocksError};
|
||||
use anyhow::Context;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::try_join;
|
||||
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config<A: Authentication = DenyAuthentication> {
|
||||
/// Timeout of the command request
|
||||
request_timeout: u64,
|
||||
/// Avoid useless roundtrips if we don't need the Authentication layer
|
||||
skip_auth: bool,
|
||||
/// Enable dns-resolving
|
||||
dns_resolve: bool,
|
||||
/// Enable command execution
|
||||
execute_command: bool,
|
||||
/// Enable UDP support
|
||||
allow_udp: bool,
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 1-2 auth methods (no auth) rather than 3 (with auth)
|
||||
allow_no_auth: bool,
|
||||
/// Contains the authentication trait to use the user against with
|
||||
auth: Option<Arc<A>>,
|
||||
}
|
||||
|
||||
impl<A: Authentication> Default for Config<A> {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
request_timeout: 10,
|
||||
skip_auth: false,
|
||||
dns_resolve: true,
|
||||
execute_command: true,
|
||||
allow_udp: false,
|
||||
allow_no_auth: false,
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this trait to handle a custom authentication on your end.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Authentication: Send + Sync {
|
||||
type Item;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item>;
|
||||
}
|
||||
|
||||
/// Basic user/pass auth method provided.
|
||||
pub struct SimpleUserPassword {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The struct returned when the user has successfully authenticated
|
||||
pub struct AuthSucceeded {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// This is an example to auth via simple credentials.
|
||||
/// If the auth succeed, we return the username authenticated with, for further uses.
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for SimpleUserPassword {
|
||||
type Item = AuthSucceeded;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
if let Some((username, password)) = credentials {
|
||||
// Client has supplied credentials
|
||||
if username == self.username && password == self.password {
|
||||
// Some() will allow the authentication and the credentials
|
||||
// will be forwarded to the socket
|
||||
Some(AuthSucceeded { username })
|
||||
} else {
|
||||
// Credentials incorrect, we deny the auth
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// The client hasn't supplied any credentials, which only happens
|
||||
// when `Config::allow_no_auth()` is set as `true`
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will simply return Option::None, which denies the authentication
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct DenyAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for DenyAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// While this one will always allow the user in.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct AcceptAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for AcceptAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Authentication> Config<A> {
|
||||
/// How much time it should wait until the request timeout.
|
||||
pub fn set_request_timeout(&mut self, n: u64) -> &mut Self {
|
||||
self.request_timeout = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Skip the entire auth/handshake part, which means the server will directly wait for
|
||||
/// the command request.
|
||||
pub fn set_skip_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.skip_auth = value;
|
||||
self.auth = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable authentication
|
||||
/// 'static lifetime for Authentication avoid us to use `dyn Authentication`
|
||||
/// and set the Arc before calling the function.
|
||||
pub fn with_authentication<T: Authentication + 'static>(self, authentication: T) -> Config<T> {
|
||||
Config {
|
||||
request_timeout: self.request_timeout,
|
||||
skip_auth: self.skip_auth,
|
||||
dns_resolve: self.dns_resolve,
|
||||
execute_command: self.execute_command,
|
||||
allow_udp: self.allow_udp,
|
||||
allow_no_auth: self.allow_no_auth,
|
||||
auth: Some(Arc::new(authentication)),
|
||||
}
|
||||
}
|
||||
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 2 auth methods rather than 3 (with auth)
|
||||
pub fn set_allow_no_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_no_auth = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to execute commands
|
||||
pub fn set_execute_command(&mut self, value: bool) -> &mut Self {
|
||||
self.execute_command = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Will the server perform dns resolve
|
||||
pub fn set_dns_resolve(&mut self, value: bool) -> &mut Self {
|
||||
self.dns_resolve = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to allow udp traffic
|
||||
pub fn set_udp_support(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_udp = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AsyncTcpConnector {
|
||||
type S: AsyncRead + AsyncWrite + Unpin + Send + Sync;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<Self::S>;
|
||||
}
|
||||
|
||||
pub struct DefaultTcpConnector {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for DefaultTcpConnector {
|
||||
type S = TcpStream;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<TcpStream> {
|
||||
tcp_connect_with_timeout(addr, timeout_s).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap TcpStream and contains Socks5 protocol implementation.
|
||||
pub struct Socks5Socket<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
{
|
||||
inner: T,
|
||||
config: Arc<Config<A>>,
|
||||
auth: AuthenticationMethod,
|
||||
target_addr: Option<TargetAddr>,
|
||||
cmd: Option<Socks5Command>,
|
||||
/// Socket address which will be used in the reply message.
|
||||
reply_ip: Option<IpAddr>,
|
||||
/// If the client has been authenticated, that's where we store his credentials
|
||||
/// to be accessed from the socket
|
||||
credentials: Option<A::Item>,
|
||||
tcp_connector: C,
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
Socks5Socket<T, A, C>
|
||||
{
|
||||
pub fn new(socket: T, config: Arc<Config<A>>, tcp_connector: C) -> Self {
|
||||
Socks5Socket {
|
||||
inner: socket,
|
||||
config,
|
||||
auth: AuthenticationMethod::None,
|
||||
target_addr: None,
|
||||
cmd: None,
|
||||
reply_ip: None,
|
||||
credentials: None,
|
||||
tcp_connector,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the bind IP address in Socks5Reply.
|
||||
///
|
||||
/// Only the inner socket owner knows the correct reply bind addr, so leave this field to be
|
||||
/// populated. For those strict clients, users can use this function to set the correct IP
|
||||
/// address.
|
||||
///
|
||||
/// Most popular SOCKS5 clients [1] [2] ignore BND.ADDR and BND.PORT the reply of command
|
||||
/// CONNECT, but this field could be useful in some other command, such as UDP ASSOCIATE.
|
||||
///
|
||||
/// [1]: https://github.com/chromium/chromium/blob/bd2c7a8b65ec42d806277dd30f138a673dec233a/net/socket/socks5_client_socket.cc#L481
|
||||
/// [2]: https://github.com/curl/curl/blob/d15692ebbad5e9cfb871b0f7f51a73e43762cee2/lib/socks.c#L978
|
||||
pub fn set_reply_ip(&mut self, addr: IpAddr) {
|
||||
self.reply_ip = Some(addr);
|
||||
}
|
||||
|
||||
/// Process clients SOCKS requests
|
||||
/// This is the entry point where a whole request is processed.
|
||||
pub async fn upgrade_to_socks5(mut self) -> Result<Socks5Socket<T, A, C>> {
|
||||
trace!("upgrading to socks5...");
|
||||
|
||||
// Handshake
|
||||
if !self.config.skip_auth {
|
||||
let methods = self.get_methods().await?;
|
||||
|
||||
let auth_method = self.can_accept_method(methods).await?;
|
||||
|
||||
if self.config.auth.is_some() {
|
||||
let credentials = self.authenticate(auth_method).await?;
|
||||
self.credentials = Some(credentials);
|
||||
}
|
||||
} else {
|
||||
debug!("skipping auth");
|
||||
}
|
||||
|
||||
match self.request().await {
|
||||
Ok(_) => {}
|
||||
Err(SocksError::ReplyError(e)) => {
|
||||
// If a reply error has been returned, we send it to the client
|
||||
self.reply_error(&e).await?;
|
||||
return Err(e.into()); // propagate the error to end this connection's task
|
||||
}
|
||||
// if any other errors has been detected, we simply end connection's task
|
||||
Err(d) => return Err(d),
|
||||
};
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Consumes the `Socks5Socket`, returning the wrapped stream.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Read the authentication method provided by the client.
|
||||
/// A client send a list of methods that he supports, he could send
|
||||
///
|
||||
/// - 0: Non auth
|
||||
/// - 2: Auth with username/password
|
||||
///
|
||||
/// Altogether, then the server choose to use of of these,
|
||||
/// or deny the handshake (thus the connection).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```text
|
||||
/// {SOCKS Version, methods-length}
|
||||
/// eg. (non-auth) {5, 2}
|
||||
/// eg. (auth) {5, 3}
|
||||
/// ```
|
||||
///
|
||||
async fn get_methods(&mut self) -> Result<Vec<u8>> {
|
||||
trace!("Socks5Socket: get_methods()");
|
||||
// read the first 2 bytes which contains the SOCKS version and the methods len()
|
||||
let [version, methods_len] =
|
||||
read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?;
|
||||
debug!(
|
||||
"Handshake headers: [version: {version}, methods len: {len}]",
|
||||
version = version,
|
||||
len = methods_len,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::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])
|
||||
.context("Can't get methods.")?;
|
||||
debug!("methods supported sent by the client: {:?}", &methods);
|
||||
|
||||
// Return methods available
|
||||
Ok(methods)
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// Client send an array of 3 entries: [0, 1, 2]
|
||||
/// ```text
|
||||
/// {SOCKS Version, Authentication chosen}
|
||||
/// eg. (non-auth) {5, 0}
|
||||
/// eg. (GSSAPI) {5, 1}
|
||||
/// eg. (auth) {5, 2}
|
||||
/// ```
|
||||
///
|
||||
/// # Response
|
||||
/// ```text
|
||||
/// eg. (accept non-auth) {5, 0x00}
|
||||
/// eg. (non-acceptable) {5, 0xff}
|
||||
/// ```
|
||||
///
|
||||
async fn can_accept_method(&mut self, client_methods: Vec<u8>) -> Result<u8> {
|
||||
let method_supported;
|
||||
|
||||
if let Some(_auth) = self.config.auth.as_ref() {
|
||||
if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) {
|
||||
// can auth with password
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD;
|
||||
} else {
|
||||
// client hasn't provided a password
|
||||
if self.config.allow_no_auth {
|
||||
// but we allow no auth, for ip whitelisting
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
} else {
|
||||
// we don't allow no auth, so we deny the entry
|
||||
debug!("Don't support this auth method, reply with (0xff)");
|
||||
self.inner
|
||||
.write_all(&[
|
||||
consts::SOCKS5_VERSION,
|
||||
consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE,
|
||||
])
|
||||
.await
|
||||
.context("Can't reply with method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthMethodUnacceptable(client_methods));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Reply with method {} ({})",
|
||||
AuthenticationMethod::from_u8(method_supported).context("Method not supported")?,
|
||||
method_supported
|
||||
);
|
||||
self.inner
|
||||
.write(&[consts::SOCKS5_VERSION, method_supported])
|
||||
.await
|
||||
.context("Can't reply with method auth-none")?;
|
||||
Ok(method_supported)
|
||||
}
|
||||
|
||||
async fn read_username_password(socket: &mut T) -> Result<(String, String)> {
|
||||
trace!("Socks5Socket: authenticate()");
|
||||
let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?;
|
||||
debug!(
|
||||
"Auth: [version: {version}, user len: {len}]",
|
||||
version = version,
|
||||
len = user_len,
|
||||
);
|
||||
|
||||
if user_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Username malformed ({} chars)",
|
||||
user_len
|
||||
)));
|
||||
}
|
||||
|
||||
let username =
|
||||
read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?;
|
||||
debug!("username bytes: {:?}", &username);
|
||||
|
||||
let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?;
|
||||
debug!("Auth: [pass len: {len}]", len = pass_len,);
|
||||
|
||||
if pass_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Password malformed ({} chars)",
|
||||
pass_len
|
||||
)));
|
||||
}
|
||||
|
||||
let password =
|
||||
read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?;
|
||||
debug!("password bytes: {:?}", &password);
|
||||
|
||||
let username = String::from_utf8(username).context("Failed to convert username")?;
|
||||
let password = String::from_utf8(password).context("Failed to convert password")?;
|
||||
|
||||
Ok((username, password))
|
||||
}
|
||||
|
||||
/// Only called if
|
||||
/// - this server has `Authentication` trait implemented.
|
||||
/// - and the client supports authentication via username/password
|
||||
/// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true`
|
||||
async fn authenticate(&mut self, auth_method: u8) -> Result<A::Item> {
|
||||
let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
let credentials = Self::read_username_password(&mut self.inner).await?;
|
||||
Some(credentials)
|
||||
} else {
|
||||
// the client hasn't provided any credentials, the function auth.authenticate()
|
||||
// will then check None, according to other parameters provided by the trait
|
||||
// such as IP, etc.
|
||||
None
|
||||
};
|
||||
|
||||
let auth = self.config.auth.as_ref().context("No auth module")?;
|
||||
|
||||
if let Some(credentials) = auth.authenticate(credentials).await {
|
||||
if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
// only the password way expect to write a response at this moment
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED])
|
||||
.await
|
||||
.context("Can't reply auth success")?;
|
||||
}
|
||||
|
||||
info!("User logged successfully.");
|
||||
|
||||
return Ok(credentials);
|
||||
} else {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE])
|
||||
.await
|
||||
.context("Can't reply with auth method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthenticationRejected(format!(
|
||||
"Authentication, rejected."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to principally cover ReplyError types for both functions read & execute request.
|
||||
async fn request(&mut self) -> Result<()> {
|
||||
self.read_command().await?;
|
||||
|
||||
if self.config.dns_resolve {
|
||||
self.resolve_dns().await?;
|
||||
} else {
|
||||
debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.")
|
||||
}
|
||||
|
||||
if self.config.execute_command {
|
||||
self.execute_command().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reply error to the client with the reply code according to the RFC.
|
||||
async fn reply_error(&mut self, error: &ReplyError) -> Result<()> {
|
||||
let reply = new_reply(error, "0.0.0.0:0".parse().unwrap());
|
||||
debug!("reply error to be written: {:?}", &reply);
|
||||
|
||||
self.inner
|
||||
.write(&reply)
|
||||
.await
|
||||
.context("Can't write the reply!")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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'].
|
||||
///
|
||||
async fn read_command(&mut self) -> Result<()> {
|
||||
let [version, cmd, rsv, address_type] =
|
||||
read_exact!(self.inner, [0u8; 4]).context("Malformed request")?;
|
||||
debug!(
|
||||
"Request: [version: {version}, command: {cmd}, rev: {rsv}, address_type: {address_type}]",
|
||||
version = version,
|
||||
cmd = cmd,
|
||||
rsv = rsv,
|
||||
address_type = address_type,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
match Socks5Command::from_u8(cmd) {
|
||||
None => return Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPConnect => {
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::UDPAssociate => {
|
||||
if !self.config.allow_udp {
|
||||
return Err(ReplyError::CommandNotSupported.into());
|
||||
}
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::TCPBind => return Err(ReplyError::CommandNotSupported.into()),
|
||||
},
|
||||
}
|
||||
|
||||
// Guess address type
|
||||
let target_addr = read_address(&mut self.inner, address_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
self.target_addr = Some(target_addr);
|
||||
|
||||
debug!("Request target is {}", self.target_addr.as_ref().unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is public, it can be call manually on your own-willing
|
||||
/// if config flag has been turned off: `Config::dns_resolve == false`.
|
||||
pub async fn resolve_dns(&mut self) -> Result<()> {
|
||||
trace!("resolving dns");
|
||||
if let Some(target_addr) = self.target_addr.take() {
|
||||
// decide whether we have to resolve DNS or not
|
||||
self.target_addr = match target_addr {
|
||||
TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?),
|
||||
TargetAddr::Ip(_) => Some(target_addr),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the socks5 command that the client wants.
|
||||
async fn execute_command(&mut self) -> Result<()> {
|
||||
match &self.cmd {
|
||||
None => Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPBind => Err(ReplyError::CommandNotSupported.into()),
|
||||
Socks5Command::TCPConnect => return self.execute_command_connect().await,
|
||||
Socks5Command::UDPAssociate => {
|
||||
if self.config.allow_udp {
|
||||
return self.execute_command_udp_assoc().await;
|
||||
} else {
|
||||
Err(ReplyError::CommandNotSupported.into())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the target address that the client wants,
|
||||
/// then forward the data between them (client <=> target address).
|
||||
async fn execute_command_connect(&mut self) -> Result<()> {
|
||||
// async-std's ToSocketAddrs doesn't supports external trait implementation
|
||||
// @see https://github.com/async-rs/async-std/issues/539
|
||||
let addr = self
|
||||
.target_addr
|
||||
.as_ref()
|
||||
.context("target_addr empty")?
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
// TCP connect with timeout, to avoid memory leak for connection that takes forever
|
||||
let outbound = self
|
||||
.tcp_connector
|
||||
.tcp_connect(addr, self.config.request_timeout)
|
||||
.await?;
|
||||
|
||||
debug!("Connected to remote destination");
|
||||
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer(&mut self.inner, outbound).await
|
||||
}
|
||||
|
||||
/// Bind to a random UDP port, wait for the traffic from
|
||||
/// the client, and then forward the data to the remote addr.
|
||||
async fn execute_command_udp_assoc(&mut self) -> Result<()> {
|
||||
// The DST.ADDR and DST.PORT fields contain the address and port that
|
||||
// the client expects to use to send UDP datagrams on for the
|
||||
// association. The server MAY use this information to limit access
|
||||
// to the association.
|
||||
// @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928.
|
||||
//
|
||||
// We do NOT limit the access from the client currently in this implementation.
|
||||
let _not_used = self.target_addr.as_ref();
|
||||
|
||||
// Listen with UDP6 socket, so the client can connect to it with either
|
||||
// IPv4 or IPv6.
|
||||
let peer_sock = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
// Respect the pre-populated reply IP address.
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(
|
||||
self.reply_ip.context("invalid reply ip")?,
|
||||
peer_sock.local_addr()?.port(),
|
||||
),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer_udp(peer_sock).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn target_addr(&self) -> Option<&TargetAddr> {
|
||||
self.target_addr.as_ref()
|
||||
}
|
||||
|
||||
pub fn auth(&self) -> &AuthenticationMethod {
|
||||
&self.auth
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> &Option<Socks5Command> {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
/// Borrow the credentials of the user has authenticated with
|
||||
pub fn get_credentials(&self) -> Option<&<<A as Authentication>::Item as Deref>::Target>
|
||||
where
|
||||
<A as Authentication>::Item: Deref,
|
||||
{
|
||||
self.credentials.as_deref()
|
||||
}
|
||||
|
||||
/// Get the credentials of the user has authenticated with
|
||||
pub fn take_credentials(&mut self) -> Option<A::Item> {
|
||||
self.credentials.take()
|
||||
}
|
||||
|
||||
pub fn tcp_connector(&self) -> &C {
|
||||
&self.tcp_connector
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy data between two peers
|
||||
/// Using 2 different generators, because they could be different structs with same traits.
|
||||
async fn transfer<I, O>(mut inbound: I, mut outbound: O) -> Result<()>
|
||||
where
|
||||
I: AsyncRead + AsyncWrite + Unpin,
|
||||
O: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await {
|
||||
Ok(res) => info!("transfer closed ({}, {})", res.0, res.1),
|
||||
Err(err) => error!("transfer error: {:?}", err),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, client_addr) = inbound.recv_from(&mut buf).await?;
|
||||
debug!("Server recieve udp from {}", client_addr);
|
||||
inbound.connect(client_addr).await?;
|
||||
|
||||
let (frag, target_addr, data) = parse_udp_request(&buf[..size]).await?;
|
||||
|
||||
if frag != 0 {
|
||||
debug!("Discard UDP frag packets sliently.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Server forward to packet to {}", target_addr);
|
||||
let mut target_addr = target_addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
target_addr.set_ip(match target_addr.ip() {
|
||||
std::net::IpAddr::V4(v4) => std::net::IpAddr::V6(v4.to_ipv6_mapped()),
|
||||
v6 @ std::net::IpAddr::V6(_) => v6,
|
||||
});
|
||||
outbound.send_to(data, target_addr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, remote_addr) = outbound.recv_from(&mut buf).await?;
|
||||
debug!("Recieve packet from {}", remote_addr);
|
||||
|
||||
let mut data = new_udp_header(remote_addr)?;
|
||||
data.extend_from_slice(&buf[..size]);
|
||||
inbound.send(&data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_udp(inbound: UdpSocket) -> Result<()> {
|
||||
let outbound = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
let req_fut = handle_udp_request(&inbound, &outbound);
|
||||
let res_fut = handle_udp_response(&inbound, &outbound);
|
||||
match try_join!(req_fut, res_fut) {
|
||||
Ok(_) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Fixes the issue "cannot borrow data in dereference of `Pin<&mut >` as mutable"
|
||||
//
|
||||
// cf. https://users.rust-lang.org/t/take-in-impl-future-cannot-borrow-data-in-a-dereference-of-pin/52042
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> Unpin for Socks5Socket<T, A, S> where
|
||||
T: AsyncRead + AsyncWrite + Unpin
|
||||
{
|
||||
}
|
||||
|
||||
/// Allow us to read directly from the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncRead for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(context, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow us to write directly into the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncWrite for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(context, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(context)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate reply code according to the RFC.
|
||||
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
|
||||
}
|
2
easytier/src/gateway/fast_socks5/util/mod.rs
Normal file
2
easytier/src/gateway/fast_socks5/util/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod stream;
|
||||
pub mod target_addr;
|
65
easytier/src/gateway/fast_socks5/util/stream.rs
Normal file
65
easytier/src/gateway/fast_socks5/util/stream.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::time::Duration;
|
||||
use tokio::io::ErrorKind as IOErrorKind;
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::gateway::fast_socks5::{ReplyError, Result};
|
||||
|
||||
/// 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_export]
|
||||
macro_rules! read_exact {
|
||||
($stream: expr, $array: expr) => {{
|
||||
let mut x = $array;
|
||||
// $stream
|
||||
// .read_exact(&mut x)
|
||||
// .await
|
||||
// .map_err(|_| io_err("lol"))?;
|
||||
$stream.read_exact(&mut x).await.map(|_| x)
|
||||
}};
|
||||
}
|
||||
|
||||
pub async fn tcp_connect_with_timeout<T>(addr: T, request_timeout_s: u64) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
let fut = tcp_connect(addr);
|
||||
match timeout(Duration::from_secs(request_timeout_s), fut).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ReplyError::ConnectionTimeout.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tcp_connect<T>(addr: T) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
match TcpStream::connect(addr).await {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => match e.kind() {
|
||||
// Match other TCP errors with ReplyError
|
||||
IOErrorKind::ConnectionRefused => Err(ReplyError::ConnectionRefused.into()),
|
||||
IOErrorKind::ConnectionAborted => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::ConnectionReset => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::NotConnected => Err(ReplyError::NetworkUnreachable.into()),
|
||||
_ => Err(e.into()), // #[error("General failure")] ?
|
||||
},
|
||||
}
|
||||
}
|
244
easytier/src/gateway/fast_socks5/util/target_addr.rs
Normal file
244
easytier/src/gateway/fast_socks5/util/target_addr.rs
Normal file
|
@ -0,0 +1,244 @@
|
|||
use crate::gateway::fast_socks5::consts;
|
||||
use crate::gateway::fast_socks5::consts::SOCKS5_ADDR_TYPE_IPV4;
|
||||
use crate::gateway::fast_socks5::SocksError;
|
||||
use crate::read_exact;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::vec::IntoIter;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// SOCKS5 reply code
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AddrError {
|
||||
#[error("DNS Resolution failed")]
|
||||
DNSResolutionFailed,
|
||||
#[error("Can't read IPv4")]
|
||||
IPv4Unreadable,
|
||||
#[error("Can't read IPv6")]
|
||||
IPv6Unreadable,
|
||||
#[error("Can't read port number")]
|
||||
PortNumberUnreadable,
|
||||
#[error("Can't read domain len")]
|
||||
DomainLenUnreadable,
|
||||
#[error("Can't read Domain content")]
|
||||
DomainContentUnreadable,
|
||||
#[error("Malformed UTF-8")]
|
||||
Utf8,
|
||||
#[error("Unknown address type")]
|
||||
IncorrectAddressType,
|
||||
#[error("{0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// 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 async fn resolve_dns(self) -> anyhow::Result<TargetAddr> {
|
||||
match self {
|
||||
TargetAddr::Ip(ip) => Ok(TargetAddr::Ip(ip)),
|
||||
TargetAddr::Domain(domain, port) => {
|
||||
debug!("Attempt to DNS resolve the domain {}...", &domain);
|
||||
|
||||
let socket_addr = lookup_host((&domain[..], port))
|
||||
.await
|
||||
.context(AddrError::DNSResolutionFailed)?
|
||||
.next()
|
||||
.ok_or(AddrError::Custom(
|
||||
"Can't fetch DNS to the domain.".to_string(),
|
||||
))?;
|
||||
debug!("domain name resolved to {}", socket_addr);
|
||||
|
||||
// has been converted to an ip
|
||||
Ok(TargetAddr::Ip(socket_addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) -> anyhow::Result<Vec<u8>> {
|
||||
let mut buf = vec![];
|
||||
match self {
|
||||
TargetAddr::Ip(SocketAddr::V4(addr)) => {
|
||||
debug!("TargetAddr::IpV4");
|
||||
|
||||
buf.extend_from_slice(&[SOCKS5_ADDR_TYPE_IPV4]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Ip(SocketAddr::V6(addr)) => {
|
||||
debug!("TargetAddr::IpV6");
|
||||
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_IPV6]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Domain(ref domain, port) => {
|
||||
debug!("TargetAddr::Domain");
|
||||
if domain.len() > u8::max_value() as usize {
|
||||
return Err(SocksError::ExceededMaxDomainLen(domain.len()).into());
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (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()
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
) -> anyhow::Result<TargetAddr> {
|
||||
let addr = match atyp {
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4 => {
|
||||
debug!("Address type `IPv4`");
|
||||
Addr::V4(read_exact!(stream, [0u8; 4]).context(AddrError::IPv4Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6 => {
|
||||
debug!("Address type `IPv6`");
|
||||
Addr::V6(read_exact!(stream, [0u8; 16]).context(AddrError::IPv6Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
|
||||
debug!("Address type `domain`");
|
||||
let len = read_exact!(stream, [0]).context(AddrError::DomainLenUnreadable)?[0];
|
||||
let domain = read_exact!(stream, vec![0u8; len as usize])
|
||||
.context(AddrError::DomainContentUnreadable)?;
|
||||
// make sure the bytes are correct utf8 string
|
||||
let domain = String::from_utf8(domain).context(AddrError::Utf8)?;
|
||||
|
||||
Addr::Domain(domain)
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!(AddrError::IncorrectAddressType)),
|
||||
};
|
||||
|
||||
// Find port number
|
||||
let port = read_exact!(stream, [0u8; 2]).context(AddrError::PortNumberUnreadable)?;
|
||||
// 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()?,
|
||||
Addr::V6(x) => (Ipv6Addr::from(x), port).to_target_addr()?,
|
||||
Addr::Domain(domain) => TargetAddr::Domain(domain, port),
|
||||
};
|
||||
|
||||
Ok(addr)
|
||||
}
|
|
@ -9,6 +9,12 @@ pub mod tcp_proxy;
|
|||
#[cfg(feature = "smoltcp")]
|
||||
pub mod tokio_smoltcp;
|
||||
pub mod udp_proxy;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod fast_socks5;
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod socks5;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CidrSet {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
|
330
easytier/src/gateway/socks5.rs
Normal file
330
easytier/src/gateway/socks5.rs
Normal file
|
@ -0,0 +1,330 @@
|
|||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gateway::{
|
||||
fast_socks5::server::{
|
||||
AcceptAuthentication, AsyncTcpConnector, Config, SimpleUserPassword, Socks5Socket,
|
||||
},
|
||||
tokio_smoltcp::TcpStream,
|
||||
},
|
||||
tunnel::packet_def::PacketType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use pnet::packet::{ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, Packet};
|
||||
use tokio::select;
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{mpsc, Mutex},
|
||||
task::JoinSet,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::GlobalCtx},
|
||||
gateway::tokio_smoltcp::{channel_device, Net, NetConfig},
|
||||
peers::{peer_manager::PeerManager, PeerPacketFilter},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||
struct Socks5Entry {
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
}
|
||||
|
||||
type Socks5EntrySet = Arc<DashSet<Socks5Entry>>;
|
||||
|
||||
struct Socks5ServerNet {
|
||||
ipv4_addr: Ipv4Addr,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
smoltcp_net: Arc<Net>,
|
||||
forward_tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
impl Socks5ServerNet {
|
||||
pub fn new(
|
||||
ipv4_addr: Ipv4Addr,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
entries: Socks5EntrySet,
|
||||
) -> Self {
|
||||
let mut forward_tasks = JoinSet::new();
|
||||
let mut cap = smoltcp::phy::DeviceCapabilities::default();
|
||||
cap.max_transmission_unit = 1280;
|
||||
cap.medium = smoltcp::phy::Medium::Ip;
|
||||
let (dev, stack_sink, mut stack_stream) = channel_device::ChannelDevice::new(cap);
|
||||
|
||||
let packet_recv = packet_recv.clone();
|
||||
forward_tasks.spawn(async move {
|
||||
let mut smoltcp_stack_receiver = packet_recv.lock().await;
|
||||
while let Some(packet) = smoltcp_stack_receiver.recv().await {
|
||||
tracing::trace!(?packet, "receive from peer send to smoltcp packet");
|
||||
if let Err(e) = stack_sink.send(Ok(packet.payload().to_vec())).await {
|
||||
tracing::error!("send to smoltcp stack failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack sink exited");
|
||||
panic!("smoltcp stack sink exited");
|
||||
});
|
||||
|
||||
forward_tasks.spawn(async move {
|
||||
while let Some(data) = stack_stream.recv().await {
|
||||
tracing::trace!(
|
||||
?data,
|
||||
"receive from smoltcp stack and send to peer mgr packet"
|
||||
);
|
||||
let Some(ipv4) = Ipv4Packet::new(&data) else {
|
||||
tracing::error!(?data, "smoltcp stack stream get non ipv4 packet");
|
||||
continue;
|
||||
};
|
||||
|
||||
let dst = ipv4.get_destination();
|
||||
let packet = ZCPacket::new_with_payload(&data);
|
||||
if let Err(e) = peer_manager.send_msg_ipv4(packet, dst).await {
|
||||
tracing::error!("send to peer failed in smoltcp sender: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack stream exited");
|
||||
panic!("smoltcp stack stream exited");
|
||||
});
|
||||
|
||||
let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip);
|
||||
let net = Net::new(
|
||||
dev,
|
||||
NetConfig::new(
|
||||
interface_config,
|
||||
format!("{}/24", ipv4_addr).parse().unwrap(),
|
||||
vec![format!("{}", ipv4_addr).parse().unwrap()],
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
ipv4_addr,
|
||||
auth,
|
||||
|
||||
smoltcp_net: Arc::new(net),
|
||||
forward_tasks: Arc::new(std::sync::Mutex::new(forward_tasks)),
|
||||
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tcp_stream(&self, stream: tokio::net::TcpStream) {
|
||||
let mut config = Config::<AcceptAuthentication>::default();
|
||||
config.set_request_timeout(10);
|
||||
config.set_skip_auth(false);
|
||||
config.set_allow_no_auth(true);
|
||||
|
||||
struct SmolTcpConnector(
|
||||
Arc<Net>,
|
||||
Socks5EntrySet,
|
||||
std::sync::Mutex<Option<Socks5Entry>>,
|
||||
);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for SmolTcpConnector {
|
||||
type S = TcpStream;
|
||||
|
||||
async fn tcp_connect(
|
||||
&self,
|
||||
addr: SocketAddr,
|
||||
timeout_s: u64,
|
||||
) -> crate::gateway::fast_socks5::Result<TcpStream> {
|
||||
let port = self.0.get_port();
|
||||
|
||||
let entry = Socks5Entry {
|
||||
src: SocketAddr::new(self.0.get_address(), port),
|
||||
dst: addr,
|
||||
};
|
||||
*self.2.lock().unwrap() = Some(entry.clone());
|
||||
self.1.insert(entry);
|
||||
|
||||
let remote_socket = timeout(
|
||||
Duration::from_secs(timeout_s),
|
||||
self.0.tcp_connect(addr, port),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "connect to remote timeout")?;
|
||||
|
||||
remote_socket.map_err(|e| super::fast_socks5::SocksError::Other(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmolTcpConnector {
|
||||
fn drop(&mut self) {
|
||||
if let Some(entry) = self.2.lock().unwrap().take() {
|
||||
self.1.remove(&entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let socket = Socks5Socket::new(
|
||||
stream,
|
||||
Arc::new(config),
|
||||
SmolTcpConnector(
|
||||
self.smoltcp_net.clone(),
|
||||
self.entries.clone(),
|
||||
std::sync::Mutex::new(None),
|
||||
),
|
||||
);
|
||||
|
||||
self.forward_tasks.lock().unwrap().spawn(async move {
|
||||
match socket.upgrade_to_socks5().await {
|
||||
Ok(_) => {
|
||||
tracing::info!("socks5 handle success");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("socks5 handshake failed: {:?}", e);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Socks5Server {
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
packet_sender: mpsc::Sender<ZCPacket>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
|
||||
net: Arc<Mutex<Option<Socks5ServerNet>>>,
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerPacketFilter for Socks5Server {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
let hdr = packet.peer_manager_header().unwrap();
|
||||
if hdr.packet_type != PacketType::Data as u8 {
|
||||
return Some(packet);
|
||||
};
|
||||
|
||||
let payload_bytes = packet.payload();
|
||||
|
||||
let ipv4 = Ipv4Packet::new(payload_bytes).unwrap();
|
||||
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap();
|
||||
let entry = Socks5Entry {
|
||||
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
|
||||
src: SocketAddr::new(ipv4.get_destination().into(), tcp_packet.get_destination()),
|
||||
};
|
||||
|
||||
if !self.entries.contains(&entry) {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let _ = self.packet_sender.try_send(packet).ok();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Socks5Server {
|
||||
pub fn new(
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
) -> Arc<Self> {
|
||||
let (packet_sender, packet_recv) = mpsc::channel(1024);
|
||||
Arc::new(Self {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
auth,
|
||||
|
||||
tasks: Arc::new(Mutex::new(JoinSet::new())),
|
||||
packet_recv: Arc::new(Mutex::new(packet_recv)),
|
||||
packet_sender,
|
||||
|
||||
net: Arc::new(Mutex::new(None)),
|
||||
entries: Arc::new(DashSet::new()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_net_update_task(self: &Arc<Self>) {
|
||||
let net = self.net.clone();
|
||||
let global_ctx = self.global_ctx.clone();
|
||||
let peer_manager = self.peer_manager.clone();
|
||||
let packet_recv = self.packet_recv.clone();
|
||||
let entries = self.entries.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
let mut prev_ipv4 = None;
|
||||
loop {
|
||||
let mut event_recv = global_ctx.subscribe();
|
||||
|
||||
let cur_ipv4 = global_ctx.get_ipv4();
|
||||
if prev_ipv4 != cur_ipv4 {
|
||||
prev_ipv4 = cur_ipv4;
|
||||
entries.clear();
|
||||
|
||||
if cur_ipv4.is_none() {
|
||||
let _ = net.lock().await.take();
|
||||
} else {
|
||||
net.lock().await.replace(Socks5ServerNet::new(
|
||||
cur_ipv4.unwrap(),
|
||||
None,
|
||||
peer_manager.clone(),
|
||||
packet_recv.clone(),
|
||||
entries.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
select! {
|
||||
_ = event_recv.recv() => {}
|
||||
_ = tokio::time::sleep(Duration::from_secs(120)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn run(self: &Arc<Self>) -> Result<(), Error> {
|
||||
let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bind_addr = format!(
|
||||
"{}:{}",
|
||||
proxy_url.host_str().unwrap(),
|
||||
proxy_url.port().unwrap()
|
||||
);
|
||||
|
||||
let listener = TcpListener::bind(bind_addr.parse::<SocketAddr>().unwrap()).await?;
|
||||
|
||||
self.peer_manager
|
||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||
.await;
|
||||
|
||||
self.run_net_update_task().await;
|
||||
|
||||
let net = self.net.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((socket, _addr)) => {
|
||||
tracing::info!("accept a new connection, {:?}", socket);
|
||||
if let Some(net) = net.lock().await.as_ref() {
|
||||
net.handle_tcp_stream(socket);
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::error!("accept error = {:?}", err),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicU16, Ordering},
|
||||
Arc,
|
||||
|
@ -134,7 +134,10 @@ impl Net {
|
|||
fut,
|
||||
)
|
||||
}
|
||||
fn get_port(&self) -> u16 {
|
||||
pub fn get_address(&self) -> IpAddr {
|
||||
self.ip_addr.address().into()
|
||||
}
|
||||
pub fn get_port(&self) -> u16 {
|
||||
self.from_port
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
|
||||
Some(if x > 60000 { 10000 } else { x + 1 })
|
||||
|
@ -147,10 +150,10 @@ impl Net {
|
|||
TcpListener::new(self.reactor.clone(), addr.into()).await
|
||||
}
|
||||
/// Opens a TCP connection to a remote host.
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr) -> io::Result<TcpStream> {
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr, local_port: u16) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(
|
||||
self.reactor.clone(),
|
||||
(self.ip_addr.address(), self.get_port()).into(),
|
||||
(self.ip_addr.address(), local_port).into(),
|
||||
addr.into(),
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -32,6 +32,9 @@ use crate::vpn_portal::{self, VpnPortal};
|
|||
|
||||
use super::listeners::ListenerManager;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
use crate::gateway::socks5::Socks5Server;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IpProxy {
|
||||
tcp_proxy: Arc<TcpProxy>,
|
||||
|
@ -116,6 +119,9 @@ pub struct Instance {
|
|||
|
||||
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server: Arc<Socks5Server>,
|
||||
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
|
@ -161,6 +167,9 @@ impl Instance {
|
|||
#[cfg(not(feature = "wireguard"))]
|
||||
let vpn_portal_inst = vpn_portal::NullVpnPortal;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None);
|
||||
|
||||
Instance {
|
||||
inst_name: global_ctx.inst_name.clone(),
|
||||
id,
|
||||
|
@ -181,6 +190,9 @@ impl Instance {
|
|||
|
||||
vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))),
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server,
|
||||
|
||||
global_ctx,
|
||||
}
|
||||
}
|
||||
|
@ -387,6 +399,9 @@ impl Instance {
|
|||
self.run_vpn_portal().await?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
self.socks5_server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user