Add: socks5 support. It may have problems with DoT, will see.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user