mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 23:54:48 +03:00
add SNI-rewrite MITM tunnels for YouTube/googlevideo + fix gzip decode
SNI-rewrite tunnels (src/proxy_server.rs): - CONNECT to youtube.com / googlevideo.com / doubleclick / etc. now bypasses the Apps Script relay entirely and goes direct to the Google edge IP with SNI=front_domain. - Accepts browser TLS with our MITM cert, opens outbound TLS to config.google_ip with SNI=config.front_domain, bridges decrypted bytes. - Matches Python's _do_sni_rewrite_tunnel behavior. Faster than relay for large streams (video). - Also respects config.hosts override map (custom IP per suffix). gzip decode fix (src/domain_fronter.rs): - Apps Script outer response is gzipped. Previous stub always failed, causing 'non-utf8 json' errors. Swapped in flate2::GzDecoder. - Verified end-to-end: HTTP and HTTPS requests through apps_script relay succeed and return real Google IPs.
This commit is contained in:
Generated
+42
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -123,6 +129,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
@@ -191,6 +206,16 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -344,6 +369,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"flate2",
|
||||
"h2",
|
||||
"http",
|
||||
"httparse",
|
||||
@@ -369,6 +395,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
@@ -701,6 +737,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
|
||||
@@ -29,6 +29,7 @@ httparse = "1"
|
||||
rand = "0.8"
|
||||
h2 = "0.4"
|
||||
http = "1"
|
||||
flate2 = "1"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
@@ -674,15 +674,10 @@ where
|
||||
}
|
||||
|
||||
fn decode_gzip(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
|
||||
// Minimal gzip decode — we don't pull in flate2 to keep deps small.
|
||||
// Apps Script typically doesn't emit gzip to us (we disable brotli, but
|
||||
// Google's frontend may still use gzip). On decode failure we just pass
|
||||
// the raw bytes through; the caller ignores errors.
|
||||
let _ = data;
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"gzip decode not implemented",
|
||||
))
|
||||
use std::io::Read;
|
||||
let mut out = Vec::with_capacity(data.len() * 2);
|
||||
flate2::read::GzDecoder::new(data).read_to_end(&mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn find_double_crlf(buf: &[u8]) -> Option<usize> {
|
||||
|
||||
+225
-3
@@ -3,12 +3,60 @@ use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_rustls::rustls::client::danger::{
|
||||
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
|
||||
};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use tokio_rustls::{TlsAcceptor, TlsConnector};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::domain_fronter::DomainFronter;
|
||||
use crate::mitm::MitmCertManager;
|
||||
|
||||
const SNI_REWRITE_SUFFIXES: &[&str] = &[
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
"youtube-nocookie.com",
|
||||
"youtubeeducation.com",
|
||||
"googlevideo.com",
|
||||
"ytimg.com",
|
||||
"ggpht.com",
|
||||
"gvt1.com",
|
||||
"gvt2.com",
|
||||
"doubleclick.net",
|
||||
"googlesyndication.com",
|
||||
"googleadservices.com",
|
||||
"google-analytics.com",
|
||||
"googletagmanager.com",
|
||||
"googletagservices.com",
|
||||
"fonts.googleapis.com",
|
||||
];
|
||||
|
||||
fn matches_sni_rewrite(host: &str) -> bool {
|
||||
let h = host.to_ascii_lowercase();
|
||||
let h = h.trim_end_matches('.');
|
||||
SNI_REWRITE_SUFFIXES
|
||||
.iter()
|
||||
.any(|s| h == *s || h.ends_with(&format!(".{}", s)))
|
||||
}
|
||||
|
||||
fn hosts_override<'a>(hosts: &'a std::collections::HashMap<String, String>, host: &str) -> Option<&'a str> {
|
||||
let h = host.to_ascii_lowercase();
|
||||
let h = h.trim_end_matches('.');
|
||||
if let Some(ip) = hosts.get(h) {
|
||||
return Some(ip.as_str());
|
||||
}
|
||||
let parts: Vec<&str> = h.split('.').collect();
|
||||
for i in 1..parts.len() {
|
||||
let parent = parts[i..].join(".");
|
||||
if let Some(ip) = hosts.get(&parent) {
|
||||
return Some(ip.as_str());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProxyError {
|
||||
#[error("io: {0}")]
|
||||
@@ -20,17 +68,48 @@ pub struct ProxyServer {
|
||||
port: u16,
|
||||
fronter: Arc<DomainFronter>,
|
||||
mitm: Arc<Mutex<MitmCertManager>>,
|
||||
rewrite_ctx: Arc<RewriteCtx>,
|
||||
}
|
||||
|
||||
pub struct RewriteCtx {
|
||||
pub google_ip: String,
|
||||
pub front_domain: String,
|
||||
pub hosts: std::collections::HashMap<String, String>,
|
||||
pub tls_connector: TlsConnector,
|
||||
}
|
||||
|
||||
impl ProxyServer {
|
||||
pub fn new(config: &Config, mitm: Arc<Mutex<MitmCertManager>>) -> Result<Self, ProxyError> {
|
||||
let fronter = DomainFronter::new(config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{e}")))?;
|
||||
|
||||
let tls_config = if config.verify_ssl {
|
||||
let mut roots = tokio_rustls::rustls::RootCertStore::empty();
|
||||
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
ClientConfig::builder()
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth()
|
||||
} else {
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerify))
|
||||
.with_no_client_auth()
|
||||
};
|
||||
let tls_connector = TlsConnector::from(Arc::new(tls_config));
|
||||
|
||||
let rewrite_ctx = Arc::new(RewriteCtx {
|
||||
google_ip: config.google_ip.clone(),
|
||||
front_domain: config.front_domain.clone(),
|
||||
hosts: config.hosts.clone(),
|
||||
tls_connector,
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
host: config.listen_host.clone(),
|
||||
port: config.listen_port,
|
||||
fronter: Arc::new(fronter),
|
||||
mitm,
|
||||
rewrite_ctx,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,8 +132,9 @@ impl ProxyServer {
|
||||
let _ = sock.set_nodelay(true);
|
||||
let fronter = self.fronter.clone();
|
||||
let mitm = self.mitm.clone();
|
||||
let rewrite_ctx = self.rewrite_ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_client(sock, fronter, mitm).await {
|
||||
if let Err(e) = handle_client(sock, fronter, mitm, rewrite_ctx).await {
|
||||
tracing::debug!("client {} closed: {}", peer, e);
|
||||
}
|
||||
});
|
||||
@@ -66,6 +146,7 @@ async fn handle_client(
|
||||
mut sock: TcpStream,
|
||||
fronter: Arc<DomainFronter>,
|
||||
mitm: Arc<Mutex<MitmCertManager>>,
|
||||
rewrite_ctx: Arc<RewriteCtx>,
|
||||
) -> std::io::Result<()> {
|
||||
// Read the first request (head only).
|
||||
let (head, leftover) = match read_http_head(&mut sock).await? {
|
||||
@@ -77,7 +158,12 @@ async fn handle_client(
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "bad request"))?;
|
||||
|
||||
if method.eq_ignore_ascii_case("CONNECT") {
|
||||
do_connect(sock, &target, fronter, mitm).await
|
||||
let (host, port) = parse_host_port(&target);
|
||||
if matches_sni_rewrite(&host) || hosts_override(&rewrite_ctx.hosts, &host).is_some() {
|
||||
do_sni_rewrite_connect(sock, &host, port, mitm, rewrite_ctx).await
|
||||
} else {
|
||||
do_connect(sock, &target, fronter, mitm).await
|
||||
}
|
||||
} else {
|
||||
do_plain_http(sock, &head, &leftover, fronter).await
|
||||
}
|
||||
@@ -189,6 +275,142 @@ async fn do_connect(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_sni_rewrite_connect(
|
||||
mut sock: TcpStream,
|
||||
host: &str,
|
||||
port: u16,
|
||||
mitm: Arc<Mutex<MitmCertManager>>,
|
||||
rewrite_ctx: Arc<RewriteCtx>,
|
||||
) -> std::io::Result<()> {
|
||||
sock.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;
|
||||
sock.flush().await?;
|
||||
|
||||
let target_ip = hosts_override(&rewrite_ctx.hosts, host)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| rewrite_ctx.google_ip.clone());
|
||||
|
||||
tracing::info!(
|
||||
"SNI-rewrite tunnel -> {}:{} via {} (outbound SNI={})",
|
||||
host, port, target_ip, rewrite_ctx.front_domain
|
||||
);
|
||||
|
||||
// Accept browser TLS with a cert we sign for `host`.
|
||||
let server_config = {
|
||||
let mut m = mitm.lock().await;
|
||||
match m.get_server_config(host) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("cert gen failed for {}: {}", host, e);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let inbound = match TlsAcceptor::from(server_config).accept(sock).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
tracing::debug!("inbound TLS accept failed for {}: {}", host, e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Open outbound TLS to google_ip with SNI=front_domain.
|
||||
let upstream_tcp = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
TcpStream::connect((target_ip.as_str(), port)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
tracing::debug!("upstream connect failed for {}: {}", host, e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("upstream connect timeout for {}", host);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let _ = upstream_tcp.set_nodelay(true);
|
||||
|
||||
let server_name = match ServerName::try_from(rewrite_ctx.front_domain.clone()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tracing::error!("invalid front_domain '{}': {}", rewrite_ctx.front_domain, e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let outbound = match rewrite_ctx
|
||||
.tls_connector
|
||||
.connect(server_name, upstream_tcp)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
tracing::debug!("outbound TLS connect failed for {}: {}", host, e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Bridge decrypted bytes between the two TLS streams.
|
||||
let (mut ir, mut iw) = tokio::io::split(inbound);
|
||||
let (mut or, mut ow) = tokio::io::split(outbound);
|
||||
let client_to_server = async { tokio::io::copy(&mut ir, &mut ow).await };
|
||||
let server_to_client = async { tokio::io::copy(&mut or, &mut iw).await };
|
||||
tokio::select! {
|
||||
_ = client_to_server => {}
|
||||
_ = server_to_client => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoVerify;
|
||||
|
||||
impl ServerCertVerifier for NoVerify {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_host_port(target: &str) -> (String, u16) {
|
||||
if let Some((h, p)) = target.rsplit_once(':') {
|
||||
let port: u16 = p.parse().unwrap_or(443);
|
||||
|
||||
Reference in New Issue
Block a user