Add: socks5 support. It may have problems with DoT, will see.

This commit is contained in:
DaZuo0122
2026-01-16 23:59:02 +08:00
parent edd1779920
commit 7746511fc4
12 changed files with 489 additions and 50 deletions

View File

@@ -11,3 +11,5 @@ thiserror = "2"
tokio = { version = "1", features = ["net", "time"] }
tokio-rustls = "0.24"
x509-parser = "0.16"
tokio-socks = "0.5"
url = "2"

View File

@@ -7,6 +7,8 @@ use thiserror::Error;
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
use tokio_socks::tcp::Socks5Stream;
use url::Url;
use x509_parser::prelude::{FromDer, X509Certificate};
#[derive(Debug, Error)]
@@ -78,12 +80,23 @@ pub struct TlsOptions {
pub alpn: Vec<String>,
pub timeout_ms: u64,
pub insecure: bool,
pub socks5: Option<String>,
pub prefer_ipv4: bool,
}
pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshakeReport, TlsError> {
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
let connector = build_connector(options.insecure, &options.alpn)?;
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
let stream = connect(
host.as_str(),
port,
options.socks5.as_deref(),
connector,
server_name,
options.timeout_ms,
options.prefer_ipv4,
)
.await?;
let (_, session) = stream.get_ref();
Ok(TlsHandshakeReport {
@@ -102,9 +115,19 @@ pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshake
}
pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport, TlsError> {
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
let connector = build_connector(false, &options.alpn)?;
match connect(addr, connector, server_name, options.timeout_ms).await {
match connect(
host.as_str(),
port,
options.socks5.as_deref(),
connector,
server_name,
options.timeout_ms,
options.prefer_ipv4,
)
.await
{
Ok(stream) => {
let (_, session) = stream.get_ref();
Ok(TlsVerifyReport {
@@ -136,9 +159,18 @@ pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport
}
pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, TlsError> {
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
let connector = build_connector(options.insecure, &options.alpn)?;
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
let stream = connect(
host.as_str(),
port,
options.socks5.as_deref(),
connector,
server_name,
options.timeout_ms,
options.prefer_ipv4,
)
.await?;
let (_, session) = stream.get_ref();
Ok(TlsCertReport {
target: target.to_string(),
@@ -148,9 +180,18 @@ pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, T
}
pub async fn alpn(target: &str, options: TlsOptions) -> Result<TlsAlpnReport, TlsError> {
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
let connector = build_connector(options.insecure, &options.alpn)?;
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
let stream = connect(
host.as_str(),
port,
options.socks5.as_deref(),
connector,
server_name,
options.timeout_ms,
options.prefer_ipv4,
)
.await?;
let (_, session) = stream.get_ref();
Ok(TlsAlpnReport {
target: target.to_string(),
@@ -162,9 +203,8 @@ pub async fn alpn(target: &str, options: TlsOptions) -> Result<TlsAlpnReport, Tl
})
}
fn parse_target(target: &str, sni: Option<&str>) -> Result<(SocketAddr, ServerName), TlsError> {
fn parse_target(target: &str, sni: Option<&str>) -> Result<(String, u16, ServerName), TlsError> {
let (host, port) = split_host_port(target)?;
let addr = resolve_addr(&host, port)?;
let server_name = if let Some(sni) = sni {
ServerName::try_from(sni).map_err(|_| TlsError::InvalidSni(sni.to_string()))?
} else if let Ok(ip) = host.parse::<IpAddr>() {
@@ -173,7 +213,7 @@ fn parse_target(target: &str, sni: Option<&str>) -> Result<(SocketAddr, ServerNa
ServerName::try_from(host.as_str())
.map_err(|_| TlsError::InvalidSni(host.to_string()))?
};
Ok((addr, server_name))
Ok((host, port, server_name))
}
fn split_host_port(value: &str) -> Result<(String, u16), TlsError> {
@@ -237,6 +277,24 @@ fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr, TlsError> {
Ok(addr)
}
fn resolve_addr_prefer_ipv4(host: &str, port: u16) -> Result<SocketAddr, TlsError> {
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(SocketAddr::new(ip, port));
}
let mut iter = std::net::ToSocketAddrs::to_socket_addrs(&(host, port))
.map_err(|err| TlsError::Io(err.to_string()))?;
let mut fallback = None;
for addr in iter.by_ref() {
if addr.is_ipv4() {
return Ok(addr);
}
if fallback.is_none() {
fallback = Some(addr);
}
}
fallback.ok_or_else(|| TlsError::InvalidTarget(host.to_string()))
}
fn build_connector(insecure: bool, alpn: &[String]) -> Result<TlsConnector, TlsError> {
let mut config = if insecure {
ClientConfig::builder()
@@ -266,15 +324,46 @@ fn build_connector(insecure: bool, alpn: &[String]) -> Result<TlsConnector, TlsE
}
async fn connect(
addr: SocketAddr,
host: &str,
port: u16,
proxy: Option<&str>,
connector: TlsConnector,
server_name: ServerName,
timeout_ms: u64,
prefer_ipv4: bool,
) -> Result<tokio_rustls::client::TlsStream<TcpStream>, TlsError> {
let tcp = timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr))
let tcp = if let Some(proxy) = proxy {
let proxy_addr = parse_proxy_addr(proxy)?;
let (target_host, remote_dns) = socks5_target_host(proxy, host);
let target = if remote_dns {
(target_host.clone(), port)
} else {
let addr = if prefer_ipv4 {
resolve_addr_prefer_ipv4(target_host.as_str(), port)?
} else {
resolve_addr(target_host.as_str(), port)?
};
(addr.ip().to_string(), port)
};
let stream = timeout(
Duration::from_millis(timeout_ms),
Socks5Stream::connect(proxy_addr.as_str(), target),
)
.await
.map_err(|_| TlsError::Timeout)?
.map_err(|err| TlsError::Io(err.to_string()))?;
stream.into_inner()
} else {
let addr = if prefer_ipv4 {
resolve_addr_prefer_ipv4(host, port)?
} else {
resolve_addr(host, port)?
};
timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr))
.await
.map_err(|_| TlsError::Timeout)?
.map_err(|err| TlsError::Io(err.to_string()))?
};
let stream = timeout(
Duration::from_millis(timeout_ms),
connector.connect(server_name, tcp),
@@ -285,6 +374,22 @@ async fn connect(
Ok(stream)
}
fn parse_proxy_addr(value: &str) -> Result<String, TlsError> {
let url = Url::parse(value).map_err(|_| TlsError::InvalidTarget(value.to_string()))?;
let host = url
.host_str()
.ok_or_else(|| TlsError::InvalidTarget(value.to_string()))?;
let port = url
.port_or_known_default()
.ok_or_else(|| TlsError::InvalidTarget(value.to_string()))?;
Ok(format!("{host}:{port}"))
}
fn socks5_target_host(proxy: &str, host: &str) -> (String, bool) {
let remote_dns = proxy.starts_with("socks5h://");
(host.to_string(), remote_dns)
}
fn extract_cert_chain(certs: Option<&[Certificate]>) -> Result<Vec<TlsCertSummary>, TlsError> {
let mut results = Vec::new();
if let Some(certs) = certs {