Files
IndPjsk/core_vpn/src/mitm.rs
T

788 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::mut_qstring::MutQString;
use crate::tun::{BorrowedTunIo, RewindStream};
use crate::{error, info, warn};
use bytes::Bytes;
use http::Uri;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper_util::rt::TokioIo;
use ipstack::IpStackStream;
use regex::Regex;
use rustls::RootCertStore;
use std::io;
use std::net::SocketAddr;
use std::os::fd::RawFd;
use std::sync::{Arc, Mutex as StdMutex, OnceLock};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::UdpSocket;
use tokio::task::JoinSet;
use tokio_rustls::StartHandshake;
use tokio_util::sync::CancellationToken;
use tun_rs::BorrowedAsyncDevice;
use crate::acceptor::build_tls_acceptor;
static ROOT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
const BLOCKED_HOSTS: &[&str] = &["submit.backtrace.io"];
fn is_blocked_host(host: &str) -> bool {
let host = host.trim_end_matches('.');
BLOCKED_HOSTS
.iter()
.any(|blocked| host.eq_ignore_ascii_case(blocked))
}
const SNIFF_TIMEOUT: Duration = Duration::from_secs(5);
const TEARDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(3);
const TEARDOWN_SETTLE: Duration = Duration::from_millis(50);
const TEARDOWN_FIN_TIMEOUT: Duration = Duration::from_millis(300);
const TEARDOWN_GRACE: Duration = Duration::from_secs(4);
fn root_store() -> Arc<RootCertStore> {
ROOT_STORE
.get_or_init(|| {
let mut root_store = RootCertStore::empty();
let native_certs = rustls_native_certs::load_native_certs();
for err in native_certs.errors {
warn!("加载证书警告: {err}");
}
for cert in native_certs.certs {
let _ = root_store.add(cert);
}
load_system_ca_dirs(&mut root_store);
if root_store.is_empty() {
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
Arc::new(root_store)
})
.clone()
}
fn load_system_ca_dirs(store: &mut RootCertStore) {
use std::io::BufReader;
const DIRS: &[&str] = &[
"/system/etc/security/cacerts", // Android 13-
"/apex/com.android.conscrypt/cacerts", // Android 14+
];
for dir in DIRS {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let Ok(file) = std::fs::File::open(entry.path()) else {
continue;
};
for cert in rustls_pemfile::certs(&mut BufReader::new(file)).flatten() {
let _ = store.add(cert);
}
}
}
}
async fn sniff_client_hello<S>(
io: &mut RewindStream<S>,
) -> io::Result<Option<(Option<String>, rustls::server::Accepted)>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut acceptor = rustls::server::Acceptor::default();
let mut buf = vec![0u8; 16 * 1024];
loop {
let n = match tokio::time::timeout(SNIFF_TIMEOUT, io.read(&mut buf)).await {
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e),
Err(_) => return Ok(None), // 对端迟迟不发数据,当作非 TLS
};
if n == 0 {
return Ok(None);
}
if acceptor.read_tls(&mut &buf[..n]).is_err() {
return Ok(None);
}
match acceptor.accept() {
Ok(Some(accepted)) => {
let sni = accepted.client_hello().server_name().map(|s| s.to_string());
return Ok(Some((sni, accepted)));
}
Ok(None) => continue, // 还没凑齐
Err(_) => return Ok(None), // 不是合法 TLS
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_tls_connection<S>(
tcp: S,
dst: SocketAddr,
upstream: Upstream,
target_hosts: Vec<String>,
waiters: Waiters,
server_config: Arc<rustls::ServerConfig>,
cancel: CancellationToken,
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let mut io = RewindStream::new(tcp);
let sniffed = match sniff_client_hello(&mut io).await {
Ok(v) => v,
Err(e) => {
warn!("client hello sniff error on {dst:?}: {e:?}");
return Ok(());
}
};
let Some((sni, accepted)) = sniffed else {
io.rewind();
io.stop_recording();
return passthrough_cancel_wrapped(io, dst, None, upstream, &cancel).await;
};
if let Some(host) = sni.as_deref()
&& is_blocked_host(host)
{
warn!("blocked host {host} ({dst:?}), connection rejected");
return Ok(());
}
let host_matched = sni
.as_deref()
.map(|s| target_hosts.iter().any(|h| s.eq_ignore_ascii_case(h)))
.unwrap_or(false);
if !host_matched {
io.rewind();
io.stop_recording();
let r = passthrough_cancel_wrapped(io, dst, sni.as_deref(), upstream, &cancel).await;
if r.is_err() {
warn!("tcp_passthrough error on {sni:?} {dst:?}");
}
return r;
}
io.stop_recording();
let client_tls = StartHandshake::from_parts(accepted, io)
.into_stream(server_config)
.await?;
let svc_host = sni.clone().unwrap_or_default();
let svc = hyper::service::service_fn(move |req: hyper::Request<Incoming>| {
let host = svc_host.clone();
let waiters = waiters.clone();
let upstream = upstream.clone();
async move {
Ok::<_, std::convert::Infallible>(
forward_request(req, dst, host, waiters, upstream).await,
)
}
});
let conn =
hyper::server::conn::http1::Builder::new().serve_connection(TokioIo::new(client_tls), svc);
tokio::pin!(conn);
let mut graceful = false;
loop {
tokio::select! {
res = conn.as_mut() => {
match res {
Ok(()) => {}
Err(e) => warn!("downstream http1 connection error: {e:?}"),
}
break;
}
_ = cancel.cancelled(), if !graceful => {
info!("teardown: graceful shutdown of downstream connection");
conn.as_mut().graceful_shutdown();
graceful = true;
}
}
}
Ok(())
}
async fn passthrough_cancel_wrapped<IO>(
io: IO,
dst: SocketAddr,
sni: Option<&str>,
upstream: Upstream,
cancel: &CancellationToken,
) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
tokio::select! {
_ = cancel.cancelled() => Ok(()),
r = tcp_passthrough(io, dst, sni, upstream) => r,
}
}
#[allow(clippy::too_many_arguments)]
async fn forward_request(
mut req: hyper::Request<Incoming>,
dst: SocketAddr,
url_host: String,
waiters: Waiters,
upstream: Upstream,
) -> hyper::Response<Full<Bytes>> {
if let Some(rewritten) = force_full_mysekai(req.uri()) {
warn!("rewrite mysekai flag: {} -> {}", req.uri(), rewritten);
*req.uri_mut() = rewritten;
}
let uri = req.uri().clone();
let url = build_url(&url_host, &uri);
let path_and_query = uri
.path_and_query()
.map(|p| p.as_str().to_string())
.unwrap_or_else(|| "/".to_string());
let waiter = take_matching_waiter(&waiters, &path_and_query);
if let Some(w) = &waiter {
info!("step '{}' matched", w.id);
}
let want_body = waiter.is_some();
match forward_request_inner(req, dst, &url_host, upstream, want_body).await {
Ok((response, saved_body)) => {
if let (Some(w), Some(body)) = (waiter, saved_body) {
save_and_notify(w, url, body);
}
response
}
Err(e) => {
error!("forward to {dst} failed: {e:?}");
if let Some(w) = waiter {
crate::notify_step_result(&w.id, None);
}
hyper::Response::builder()
.status(hyper::StatusCode::BAD_GATEWAY)
.header("content-type", "text/plain; charset=utf-8")
.body(Full::new(Bytes::from(format!("mitm forward error: {e}"))))
.unwrap()
}
}
}
fn save_and_notify(waiter: Waiter, url: String, body: Bytes) {
tokio::spawn(async move {
let path = std::path::Path::new(&waiter.save_path);
if !path.is_absolute() {
error!("save_path 不是绝对路径({})", waiter.save_path);
}
if let Some(parent) = path.parent() {
let _ = tokio::fs::create_dir_all(parent).await;
}
match tokio::fs::write(path, &body).await {
Ok(()) => {
info!("step '{}' saved ({} bytes)", waiter.id, body.len());
crate::notify_step_result(&waiter.id, Some(&url));
}
Err(e) => {
error!("write {} failed: {e:?}", waiter.save_path);
crate::notify_step_result(&waiter.id, None);
}
}
});
}
async fn forward_request_inner(
req: hyper::Request<Incoming>,
dst: SocketAddr,
url_host: &str,
upstream: Upstream,
want_body: bool,
) -> anyhow::Result<(hyper::Response<Full<Bytes>>, Option<Bytes>)> {
let (mut parts, body) = req.into_parts();
let body_bytes = body.collect().await?.to_bytes();
// 拒绝长链接
for h in [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
"te",
"trailer",
"content-length",
] {
parts.headers.remove(h);
}
let mut client_config = rustls::ClientConfig::builder()
.with_root_certificates(root_store())
.with_no_client_auth();
client_config.alpn_protocols = vec![b"http/1.1".to_vec()];
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
let server_name = rustls::pki_types::ServerName::try_from(url_host.to_string())?;
let tcp_to_server = upstream.connect(dst, Some(url_host)).await?;
let server_tls = connector.connect(server_name, tcp_to_server).await?;
let (mut sender, conn) =
hyper::client::conn::http1::handshake::<_, Full<Bytes>>(TokioIo::new(server_tls)).await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
warn!("upstream http1 connection error: {e:?}");
}
});
let upstream_req = hyper::Request::from_parts(parts, Full::new(body_bytes));
let resp = sender.send_request(upstream_req).await?;
let (mut res_parts, res_body) = resp.into_parts();
let res_bytes = res_body.collect().await?.to_bytes();
for h in [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
"te",
"trailer",
] {
res_parts.headers.remove(h);
}
let saved = if want_body {
Some(res_bytes.clone())
} else {
None
};
Ok((
hyper::Response::from_parts(res_parts, Full::new(res_bytes)),
saved,
))
}
fn build_url(host: &str, uri: &Uri) -> String {
if uri.scheme().is_some() && uri.authority().is_some() {
return uri.to_string();
}
let pq = uri.path_and_query().map(|p| p.as_str()).unwrap_or("/");
format!("https://{host}{pq}")
}
fn force_full_mysekai(uri: &Uri) -> Option<Uri> {
const KEY: &str = "isForceAllReloadOnlyMysekai";
let mut query = qstring::QString::from(uri.query()?);
if query.get(KEY) != Some("False") {
return None;
}
query.set(KEY, "True");
Uri::try_from(format!("{}?{}", uri.path(), query).as_str()).ok()
}
#[derive(serde::Deserialize, Debug, Clone)]
pub struct StepSpec {
pub id: String,
#[serde(default)]
pub hosts: Vec<String>,
pub path: String,
#[serde(rename = "savePath")]
pub save_path: String,
/// 持续运行:命中后不摘除 waiter,同类型的包可以反复抓
#[serde(default)]
pub repeat: bool,
}
#[derive(Clone)]
pub struct Waiter {
pub id: String,
pub path: Regex,
pub save_path: String,
pub repeat: bool,
}
pub type Waiters = Arc<StdMutex<Vec<Waiter>>>;
fn take_matching_waiter(waiters: &StdMutex<Vec<Waiter>>, path_and_query: &str) -> Option<Waiter> {
let mut list = waiters.lock().unwrap();
let idx = list.iter().position(|w| w.path.is_match(path_and_query))?;
if list[idx].repeat {
// 持续运行:保留 waiter,让后续同类型的请求继续命中
Some(list[idx].clone())
} else {
Some(list.remove(idx))
}
}
#[derive(Clone, Debug)]
pub struct ProxyConfig {
pub tls: bool,
pub host: String,
pub port: u16,
pub auth: Option<String>,
}
impl ProxyConfig {
pub fn parse(input: &str) -> anyhow::Result<Option<Self>> {
let raw = input.trim();
if raw.is_empty() {
return Ok(None);
}
let url = url::Url::parse(raw)
.map_err(|e| anyhow::anyhow!("代理地址不是合法 URL: {raw} ({e})"))?;
let tls = match url.scheme() {
"http" => false,
"https" => true,
other => anyhow::bail!("代理只支持 http/https,当前是 {other}://"),
};
let host = url
.host_str()
.filter(|h| !h.is_empty())
.ok_or_else(|| anyhow::anyhow!("代理地址缺少 host: {raw}"))?
.to_string();
let port = url
.port_or_known_default()
.ok_or_else(|| anyhow::anyhow!("代理地址缺少端口: {raw}"))?;
let auth = if url.username().is_empty() {
None
} else {
let pass = url.password().unwrap_or("");
Some(base64_encode(
format!("{}:{}", url.username(), pass).as_bytes(),
))
};
Ok(Some(Self {
tls,
host,
port,
auth,
}))
}
}
fn base64_encode(data: &[u8]) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(data)
}
type UpstreamIo = Box<dyn AsyncReadWrite + Send + Unpin>;
trait AsyncReadWrite: AsyncRead + AsyncWrite {}
impl<T: AsyncRead + AsyncWrite + ?Sized> AsyncReadWrite for T {}
#[derive(Clone)]
struct Upstream {
proxy: Option<Arc<ProxyConfig>>,
}
impl Upstream {
fn new(proxy: Option<ProxyConfig>) -> Self {
Self {
proxy: proxy.map(Arc::new),
}
}
async fn connect(&self, dst: SocketAddr, host: Option<&str>) -> anyhow::Result<UpstreamIo> {
match &self.proxy {
None => {
let tcp = tokio::net::TcpStream::connect(dst).await?;
tcp.set_nodelay(true).ok();
Ok(Box::new(tcp))
}
Some(proxy) => connect_via_proxy(proxy, dst, host).await,
}
}
}
async fn connect_via_proxy(
proxy: &ProxyConfig,
dst: SocketAddr,
host: Option<&str>,
) -> anyhow::Result<UpstreamIo> {
let tcp = tokio::net::TcpStream::connect((proxy.host.as_str(), proxy.port))
.await
.map_err(|e| anyhow::anyhow!("连接代理 {}:{} 失败: {e}", proxy.host, proxy.port))?;
tcp.set_nodelay(true).ok();
let mut io: UpstreamIo = if proxy.tls {
let mut cfg = rustls::ClientConfig::builder()
.with_root_certificates(root_store())
.with_no_client_auth();
cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
let connector = tokio_rustls::TlsConnector::from(Arc::new(cfg));
let name = rustls::pki_types::ServerName::try_from(proxy.host.clone())?;
Box::new(
connector
.connect(name, tcp)
.await
.map_err(|e| anyhow::anyhow!("与代理建立 TLS 失败: {e}"))?,
)
} else {
Box::new(tcp)
};
let authority = match host {
Some(h) if !h.is_empty() => format!("{h}:{}", dst.port()),
_ => dst.to_string(),
};
let mut req = format!("CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\n");
if let Some(auth) = &proxy.auth {
req.push_str(&format!("Proxy-Authorization: Basic {auth}\r\n"));
}
req.push_str("\r\n");
io.write_all(req.as_bytes()).await?;
io.flush().await?;
let mut head = Vec::with_capacity(256);
let mut byte = [0u8; 1];
loop {
let n = io.read(&mut byte).await?;
if n == 0 {
anyhow::bail!("代理在 CONNECT 阶段直接断开");
}
head.push(byte[0]);
if head.ends_with(b"\r\n\r\n") {
break;
}
if head.len() > 16 * 1024 {
anyhow::bail!("代理 CONNECT 响应头过长");
}
}
let head_str = String::from_utf8_lossy(&head);
let status_line = head_str.lines().next().unwrap_or("").trim().to_string();
let ok = status_line
.split_whitespace()
.nth(1)
.map(|code| code == "200")
.unwrap_or(false);
if !ok {
anyhow::bail!("代理 CONNECT 失败: {status_line}");
}
Ok(io)
}
pub async fn run_session(
tun_fd: i32,
target_hosts: Vec<String>,
proxy: Option<ProxyConfig>,
waiters: Waiters,
cancel: CancellationToken,
) -> anyhow::Result<()> {
let device = unsafe { BorrowedAsyncDevice::borrow_raw(tun_fd as RawFd)? };
let mut ipstack_config = ipstack::IpStackConfig::default();
ipstack_config.mtu(1500)?;
let device_ref: &BorrowedAsyncDevice<'static> = unsafe { std::mem::transmute(&device) };
let tun_io = BorrowedTunIo::new(device_ref);
let mut ip_stack = ipstack::IpStack::new(ipstack_config, tun_io);
let mut bridge_tasks = JoinSet::new();
let tls_acceptor = build_tls_acceptor(target_hosts.clone());
let upstream = Upstream::new(proxy);
loop {
let token = cancel.clone();
tokio::select! {
_ = cancel.cancelled() => {
break;
}
_ = bridge_tasks.join_next(), if !bridge_tasks.is_empty() => {}
res = ip_stack.accept() => {
let stream = match res {
Ok(s) => s,
Err(e) => {
error!("ip_stack accept error (device might be dropped): {e:?}");
break;
}
};
match stream {
IpStackStream::Udp(udp) => {
let dst = udp.peer_addr();
if dst.port() == 443 {
// drop quic roughly
drop(udp);
} else {
bridge_tasks.spawn(async move {
if let Err(e) = udp_passthrough(udp, dst, token).await {
error!("udp passthrough error: {e:?}");
}
});
}
}
IpStackStream::Tcp(mut tcp) => {
let dst = tcp.peer_addr();
if dst.port() == 443 {
let (app_io, mut tun_io) = tokio::io::duplex(256 * 1024);
let pump_token = cancel.clone();
bridge_tasks.spawn(async move {
tokio::select! {
_ = pump_token.cancelled() => {
let _ = tokio::time::timeout(
TEARDOWN_DRAIN_TIMEOUT,
tokio::io::copy(&mut tun_io, &mut tcp),
)
.await;
}
_ = tokio::io::copy_bidirectional(&mut tun_io, &mut tcp) => {}
}
tokio::time::sleep(TEARDOWN_SETTLE).await;
let _ = tokio::time::timeout(
TEARDOWN_FIN_TIMEOUT,
tcp.shutdown(),
)
.await;
});
let hosts = target_hosts.clone();
let server_config = tls_acceptor.clone();
let token = cancel.clone();
let waiters = waiters.clone();
let upstream = upstream.clone();
tokio::spawn(async move {
match handle_tls_connection(
app_io,
dst,
upstream,
hosts,
waiters,
server_config,
token,
).await {
Ok(()) => {}
Err(e) => error!("handle tls conn error: {e:?}"),
}
});
} else {
let pump_token = cancel.clone();
let upstream = upstream.clone();
bridge_tasks.spawn(async move {
let _ =
tcp_passthrough_cancellable(tcp, dst, None, upstream, pump_token)
.await;
});
}
}
_ => {}
}
}
}
}
let _ = tokio::time::timeout(TEARDOWN_GRACE, async {
while bridge_tasks.join_next().await.is_some() {}
})
.await;
bridge_tasks.abort_all();
while bridge_tasks.join_next().await.is_some() {}
info!("session stopped");
Ok(())
}
async fn tcp_passthrough<IO>(
mut client: IO,
dst: SocketAddr,
sni: Option<&str>,
upstream: Upstream,
) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let mut server = upstream.connect(dst, sni).await?;
tokio::io::copy_bidirectional(&mut client, &mut server).await?;
Ok(())
}
async fn tcp_passthrough_cancellable<IO>(
mut client: IO,
dst: SocketAddr,
sni: Option<&str>,
upstream: Upstream,
cancel: CancellationToken,
) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let mut server = upstream.connect(dst, sni).await?;
tokio::select! {
_ = cancel.cancelled() => {
let _ = tokio::time::timeout(
TEARDOWN_DRAIN_TIMEOUT,
tokio::io::copy(&mut client, &mut server),
)
.await;
}
res = tokio::io::copy_bidirectional(&mut client, &mut server) => {
res?;
}
}
tokio::time::sleep(TEARDOWN_SETTLE).await;
let _ = tokio::time::timeout(TEARDOWN_FIN_TIMEOUT, client.shutdown()).await;
let _ = tokio::time::timeout(TEARDOWN_FIN_TIMEOUT, server.shutdown()).await;
Ok(())
}
async fn udp_passthrough<IO>(
client: IO,
dst: SocketAddr,
cancel: CancellationToken,
) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let bind_addr: SocketAddr = if dst.is_ipv4() {
"0.0.0.0:0".parse()?
} else {
"[::]:0".parse()?
};
let socket = UdpSocket::bind(bind_addr).await?;
socket.connect(dst).await?;
let socket = Arc::new(socket);
let (mut cr, mut cw) = tokio::io::split(client);
let mut client_buf = vec![0u8; 65535];
let mut server_buf = vec![0u8; 65535];
let idle_timeout = Duration::from_secs(12);
loop {
tokio::select! {
_ = cancel.cancelled() => break,
// c2s
res = tokio::time::timeout(idle_timeout, cr.read(&mut client_buf)) => {
match res {
Ok(Ok(0)) | Err(_) => break,
Ok(Ok(n)) => {
if socket.send(&client_buf[..n]).await.is_err() {
break;
}
}
Ok(Err(_)) => break,
}
}
// s2c
res = tokio::time::timeout(idle_timeout, socket.recv(&mut server_buf)) => {
match res {
Ok(Ok(n)) if n > 0 => {
if cw.write_all(&server_buf[..n]).await.is_err() {
break;
}
}
_ => break,
}
}
}
}
Ok(())
}