mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
v0.3.0: SOCKS5 listener + smart TLS/HTTP/plain-TCP dispatch
Ports the SOCKS5 + fallback-chain design from @masterking32's
MasterHTTP-WithSOCKS branch so xray / Telegram / app-level TCP
clients work through this proxy.
Changes:
- New SOCKS5 listener on listen_port+1 (configurable via socks5_port)
- RFC 1928 CONNECT handshake (v5, no-auth, ATYP IPv4/domain/IPv6)
- Shared smart dispatch with the HTTP-CONNECT path
- Unified dispatch_tunnel() used by both CONNECT entry points:
1. If host matches SNI-rewrite suffix or hosts override: go direct
to google_ip via the MITM+TLS tunnel (fast path for google.com,
youtube, etc.)
2. Peek the first byte (300ms timeout for server-first protocols):
- 0x16: TLS client hello -> MITM + relay via Apps Script (scheme=https)
- HTTP method signature: HTTP relay via Apps Script (scheme=http)
- Anything else or timeout: plain TCP passthrough to the target
- handle_mitm_request() now takes a scheme arg (http/https) so the
same code path handles both MITM'd HTTPS and port-80 plain HTTP
- New plain_tcp_passthrough helper: bidirectional TCP bridge used as
the final fallback (covers MTProto / raw TCP / server-first protos)
Config:
- Added optional socks5_port field; defaults to listen_port+1
README:
- Added browser vs xray/Telegram instructions under 'Step 6'
Live-tested: HTTP proxy, HTTP proxy -> HTTPS, SOCKS5 -> HTTP,
SOCKS5 -> HTTPS, Google search via SNI-tunnel (now returns full
JS page) all pass.
This commit is contained in:
Generated
+1
-1
@@ -365,7 +365,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "0.2.2"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "0.2.2"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -98,14 +98,21 @@ mhrv-rs.exe --config config.json # Windows
|
|||||||
- **`mhrv-rs test`** — send one request through the relay and report success/timing. Useful when setting up or debugging. Does not need the proxy to be running.
|
- **`mhrv-rs test`** — send one request through the relay and report success/timing. Useful when setting up or debugging. Does not need the proxy to be running.
|
||||||
- **`mhrv-rs scan-ips`** — parallel TLS probe of known Google frontend IPs, sorted by latency. Swap the winning IP into your `google_ip` config field for best performance.
|
- **`mhrv-rs scan-ips`** — parallel TLS probe of known Google frontend IPs, sorted by latency. Swap the winning IP into your `google_ip` config field for best performance.
|
||||||
|
|
||||||
### Step 6: Point your browser at the proxy
|
### Step 6: Point your client at the proxy
|
||||||
|
|
||||||
Configure your browser to use HTTP proxy `127.0.0.1:8085`.
|
The tool listens on **two** ports:
|
||||||
|
- **HTTP proxy** on `listen_port` (default `8085`) — for browsers / any HTTP-aware client
|
||||||
|
- **SOCKS5 proxy** on `socks5_port` (default `listen_port + 1`, i.e. `8086`) — for xray / Telegram / app-level clients
|
||||||
|
|
||||||
- **Firefox**: Settings → Network Settings → Manual proxy → enter for HTTP, check "Also use this proxy for HTTPS"
|
**Browser (HTTP proxy):**
|
||||||
- **Chrome/Edge**: System proxy settings, or use SwitchyOmega extension
|
- **Firefox**: Settings → Network Settings → Manual proxy → HTTP `127.0.0.1:8085`, check "Also use this proxy for HTTPS"
|
||||||
|
- **Chrome/Edge**: System proxy settings, or SwitchyOmega
|
||||||
- **macOS system-wide**: System Settings → Network → Wi-Fi → Details → Proxies → Web + Secure Web Proxy
|
- **macOS system-wide**: System Settings → Network → Wi-Fi → Details → Proxies → Web + Secure Web Proxy
|
||||||
|
|
||||||
|
**xray / Telegram (SOCKS5):**
|
||||||
|
- Point the SOCKS5 setting at `127.0.0.1:8086`, no auth.
|
||||||
|
- Non-HTTP protocols (MTProto, raw TCP) fall back to plain-TCP passthrough automatically.
|
||||||
|
|
||||||
## What's implemented vs not
|
## What's implemented vs not
|
||||||
|
|
||||||
This port focuses on the **`apps_script` mode** which is the only one that reliably works in 2026. Implemented:
|
This port focuses on the **`apps_script` mode** which is the only one that reliably works in 2026. Implemented:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"auth_key": "CHANGE_ME_TO_A_STRONG_SECRET",
|
"auth_key": "CHANGE_ME_TO_A_STRONG_SECRET",
|
||||||
"listen_host": "127.0.0.1",
|
"listen_host": "127.0.0.1",
|
||||||
"listen_port": 8085,
|
"listen_port": 8085,
|
||||||
|
"socks5_port": 8086,
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"verify_ssl": true,
|
"verify_ssl": true,
|
||||||
"hosts": {}
|
"hosts": {}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ pub struct Config {
|
|||||||
pub listen_host: String,
|
pub listen_host: String,
|
||||||
#[serde(default = "default_listen_port")]
|
#[serde(default = "default_listen_port")]
|
||||||
pub listen_port: u16,
|
pub listen_port: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub socks5_port: Option<u16>,
|
||||||
#[serde(default = "default_log_level")]
|
#[serde(default = "default_log_level")]
|
||||||
pub log_level: String,
|
pub log_level: String,
|
||||||
#[serde(default = "default_verify_ssl")]
|
#[serde(default = "default_verify_ssl")]
|
||||||
|
|||||||
@@ -176,7 +176,10 @@ async fn main() -> ExitCode {
|
|||||||
Command::Serve => {}
|
Command::Serve => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
|
||||||
tracing::warn!("mhrv-rs {} starting (mode: apps_script)", VERSION);
|
tracing::warn!("mhrv-rs {} starting (mode: apps_script)", VERSION);
|
||||||
|
tracing::info!("HTTP proxy : {}:{}", config.listen_host, config.listen_port);
|
||||||
|
tracing::info!("SOCKS5 proxy : {}:{}", config.listen_host, socks5_port);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Apps Script relay: SNI={} -> script.google.com (via {})",
|
"Apps Script relay: SNI={} -> script.google.com (via {})",
|
||||||
config.front_domain,
|
config.front_domain,
|
||||||
|
|||||||
+290
-55
@@ -63,6 +63,7 @@ pub enum ProxyError {
|
|||||||
pub struct ProxyServer {
|
pub struct ProxyServer {
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
|
socks5_port: u16,
|
||||||
fronter: Arc<DomainFronter>,
|
fronter: Arc<DomainFronter>,
|
||||||
mitm: Arc<Mutex<MitmCertManager>>,
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
rewrite_ctx: Arc<RewriteCtx>,
|
rewrite_ctx: Arc<RewriteCtx>,
|
||||||
@@ -101,9 +102,12 @@ impl ProxyServer {
|
|||||||
tls_connector,
|
tls_connector,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
host: config.listen_host.clone(),
|
host: config.listen_host.clone(),
|
||||||
port: config.listen_port,
|
port: config.listen_port,
|
||||||
|
socks5_port,
|
||||||
fronter: Arc::new(fronter),
|
fronter: Arc::new(fronter),
|
||||||
mitm,
|
mitm,
|
||||||
rewrite_ctx,
|
rewrite_ctx,
|
||||||
@@ -111,19 +115,24 @@ impl ProxyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self) -> Result<(), ProxyError> {
|
pub async fn run(self) -> Result<(), ProxyError> {
|
||||||
let addr = format!("{}:{}", self.host, self.port);
|
let http_addr = format!("{}:{}", self.host, self.port);
|
||||||
let listener = TcpListener::bind(&addr).await?;
|
let socks_addr = format!("{}:{}", self.host, self.socks5_port);
|
||||||
|
let http_listener = TcpListener::bind(&http_addr).await?;
|
||||||
|
let socks_listener = TcpListener::bind(&socks_addr).await?;
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Listening on {} — set your browser HTTP proxy to this address.",
|
"Listening HTTP on {} — set your browser HTTP proxy to this address.",
|
||||||
addr
|
http_addr
|
||||||
|
);
|
||||||
|
tracing::warn!(
|
||||||
|
"Listening SOCKS5 on {} — xray / Telegram / app-level SOCKS5 clients use this.",
|
||||||
|
socks_addr
|
||||||
);
|
);
|
||||||
|
|
||||||
// Periodic stats log (every 60s at info level).
|
|
||||||
let stats_fronter = self.fronter.clone();
|
let stats_fronter = self.fronter.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(60));
|
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(60));
|
||||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||||
ticker.tick().await; // drop the immediate first tick
|
ticker.tick().await;
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
let s = stats_fronter.snapshot_stats();
|
let s = stats_fronter.snapshot_stats();
|
||||||
@@ -133,34 +142,65 @@ impl ProxyServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loop {
|
let http_fronter = self.fronter.clone();
|
||||||
let (sock, peer) = match listener.accept().await {
|
let http_mitm = self.mitm.clone();
|
||||||
Ok(x) => x,
|
let http_ctx = self.rewrite_ctx.clone();
|
||||||
Err(e) => {
|
let http_task = tokio::spawn(async move {
|
||||||
tracing::error!("accept error: {}", e);
|
loop {
|
||||||
continue;
|
let (sock, peer) = match http_listener.accept().await {
|
||||||
}
|
Ok(x) => x,
|
||||||
};
|
Err(e) => {
|
||||||
let _ = sock.set_nodelay(true);
|
tracing::error!("accept (http): {}", e);
|
||||||
let fronter = self.fronter.clone();
|
continue;
|
||||||
let mitm = self.mitm.clone();
|
}
|
||||||
let rewrite_ctx = self.rewrite_ctx.clone();
|
};
|
||||||
tokio::spawn(async move {
|
let _ = sock.set_nodelay(true);
|
||||||
if let Err(e) = handle_client(sock, fronter, mitm, rewrite_ctx).await {
|
let fronter = http_fronter.clone();
|
||||||
tracing::debug!("client {} closed: {}", peer, e);
|
let mitm = http_mitm.clone();
|
||||||
}
|
let rewrite_ctx = http_ctx.clone();
|
||||||
});
|
tokio::spawn(async move {
|
||||||
}
|
if let Err(e) = handle_http_client(sock, fronter, mitm, rewrite_ctx).await {
|
||||||
|
tracing::debug!("http client {} closed: {}", peer, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let socks_fronter = self.fronter.clone();
|
||||||
|
let socks_mitm = self.mitm.clone();
|
||||||
|
let socks_ctx = self.rewrite_ctx.clone();
|
||||||
|
let socks_task = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (sock, peer) = match socks_listener.accept().await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("accept (socks): {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = sock.set_nodelay(true);
|
||||||
|
let fronter = socks_fronter.clone();
|
||||||
|
let mitm = socks_mitm.clone();
|
||||||
|
let rewrite_ctx = socks_ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_socks5_client(sock, fronter, mitm, rewrite_ctx).await {
|
||||||
|
tracing::debug!("socks client {} closed: {}", peer, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = tokio::join!(http_task, socks_task);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_client(
|
async fn handle_http_client(
|
||||||
mut sock: TcpStream,
|
mut sock: TcpStream,
|
||||||
fronter: Arc<DomainFronter>,
|
fronter: Arc<DomainFronter>,
|
||||||
mitm: Arc<Mutex<MitmCertManager>>,
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
rewrite_ctx: Arc<RewriteCtx>,
|
rewrite_ctx: Arc<RewriteCtx>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
// Read the first request (head only).
|
|
||||||
let (head, leftover) = match read_http_head(&mut sock).await? {
|
let (head, leftover) = match read_http_head(&mut sock).await? {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
@@ -171,16 +211,195 @@ async fn handle_client(
|
|||||||
|
|
||||||
if method.eq_ignore_ascii_case("CONNECT") {
|
if method.eq_ignore_ascii_case("CONNECT") {
|
||||||
let (host, port) = parse_host_port(&target);
|
let (host, port) = parse_host_port(&target);
|
||||||
if matches_sni_rewrite(&host) || hosts_override(&rewrite_ctx.hosts, &host).is_some() {
|
sock.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;
|
||||||
do_sni_rewrite_connect(sock, &host, port, mitm, rewrite_ctx).await
|
sock.flush().await?;
|
||||||
} else {
|
dispatch_tunnel(sock, host, port, fronter, mitm, rewrite_ctx).await
|
||||||
do_connect(sock, &target, fronter, mitm).await
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
do_plain_http(sock, &head, &leftover, fronter).await
|
do_plain_http(sock, &head, &leftover, fronter).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- SOCKS5 ----------
|
||||||
|
|
||||||
|
async fn handle_socks5_client(
|
||||||
|
mut sock: TcpStream,
|
||||||
|
fronter: Arc<DomainFronter>,
|
||||||
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
|
rewrite_ctx: Arc<RewriteCtx>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
// RFC 1928 handshake: VER=5, NMETHODS, METHODS...
|
||||||
|
let mut hdr = [0u8; 2];
|
||||||
|
sock.read_exact(&mut hdr).await?;
|
||||||
|
if hdr[0] != 0x05 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let nmethods = hdr[1] as usize;
|
||||||
|
let mut methods = vec![0u8; nmethods];
|
||||||
|
sock.read_exact(&mut methods).await?;
|
||||||
|
// Only "no auth" (0x00) is supported.
|
||||||
|
if !methods.contains(&0x00) {
|
||||||
|
sock.write_all(&[0x05, 0xff]).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
sock.write_all(&[0x05, 0x00]).await?;
|
||||||
|
|
||||||
|
// Request: VER=5, CMD, RSV=0, ATYP, DST.ADDR, DST.PORT
|
||||||
|
let mut req = [0u8; 4];
|
||||||
|
sock.read_exact(&mut req).await?;
|
||||||
|
if req[0] != 0x05 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let cmd = req[1];
|
||||||
|
if cmd != 0x01 {
|
||||||
|
// CONNECT only.
|
||||||
|
sock.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let atyp = req[3];
|
||||||
|
let host: String = match atyp {
|
||||||
|
0x01 => {
|
||||||
|
let mut ip = [0u8; 4];
|
||||||
|
sock.read_exact(&mut ip).await?;
|
||||||
|
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
let mut len = [0u8; 1];
|
||||||
|
sock.read_exact(&mut len).await?;
|
||||||
|
let mut name = vec![0u8; len[0] as usize];
|
||||||
|
sock.read_exact(&mut name).await?;
|
||||||
|
String::from_utf8_lossy(&name).into_owned()
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
let mut ip = [0u8; 16];
|
||||||
|
sock.read_exact(&mut ip).await?;
|
||||||
|
let addr = std::net::Ipv6Addr::from(ip);
|
||||||
|
addr.to_string()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
sock.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut port_buf = [0u8; 2];
|
||||||
|
sock.read_exact(&mut port_buf).await?;
|
||||||
|
let port = u16::from_be_bytes(port_buf);
|
||||||
|
|
||||||
|
tracing::info!("SOCKS5 CONNECT -> {}:{}", host, port);
|
||||||
|
|
||||||
|
// Success reply with zeroed BND.
|
||||||
|
sock.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
sock.flush().await?;
|
||||||
|
|
||||||
|
dispatch_tunnel(sock, host, port, fronter, mitm, rewrite_ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Smart dispatch (used by both HTTP CONNECT and SOCKS5) ----------
|
||||||
|
|
||||||
|
async fn dispatch_tunnel(
|
||||||
|
sock: TcpStream,
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
fronter: Arc<DomainFronter>,
|
||||||
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
|
rewrite_ctx: Arc<RewriteCtx>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
// 1. Explicit hosts override or SNI-rewrite suffix: always use the tunnel.
|
||||||
|
if matches_sni_rewrite(&host) || hosts_override(&rewrite_ctx.hosts, &host).is_some() {
|
||||||
|
return do_sni_rewrite_tunnel_from_tcp(sock, &host, port, mitm, rewrite_ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Peek at the first byte to detect TLS vs plain. Time-bounded — if the
|
||||||
|
// client doesn't send anything within 300ms, assume server-first
|
||||||
|
// protocol (SMTP, POP3, FTP banner) and jump straight to plain TCP.
|
||||||
|
let mut peek_buf = [0u8; 8];
|
||||||
|
let peek_n = match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(300),
|
||||||
|
sock.peek(&mut peek_buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(n)) => n,
|
||||||
|
Ok(Err(_)) => return Ok(()),
|
||||||
|
Err(_) => {
|
||||||
|
// Client silent: likely a server-first protocol.
|
||||||
|
plain_tcp_passthrough(sock, &host, port).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if peek_n >= 1 && peek_buf[0] == 0x16 {
|
||||||
|
// Looks like TLS: MITM + relay via Apps Script.
|
||||||
|
run_mitm_then_relay(sock, &host, port, mitm, &fronter).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Not TLS. If bytes look like HTTP, relay on scheme=http. Otherwise
|
||||||
|
// fall back to plain TCP passthrough.
|
||||||
|
if peek_n > 0 && looks_like_http(&peek_buf[..peek_n]) {
|
||||||
|
let scheme = if port == 443 { "https" } else { "http" };
|
||||||
|
relay_http_stream_raw(sock, &host, port, scheme, &fronter).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
plain_tcp_passthrough(sock, &host, port).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Plain TCP passthrough ----------
|
||||||
|
|
||||||
|
async fn plain_tcp_passthrough(mut sock: TcpStream, host: &str, port: u16) {
|
||||||
|
let target_host = host.trim_start_matches('[').trim_end_matches(']');
|
||||||
|
let upstream = match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
TcpStream::connect((target_host, port)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(s)) => s,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
tracing::debug!("plain-tcp connect {}:{} failed: {}", host, port, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::debug!("plain-tcp connect {}:{} timeout", host, port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = upstream.set_nodelay(true);
|
||||||
|
tracing::info!("plain-tcp passthrough -> {}:{}", host, port);
|
||||||
|
let (mut ar, mut aw) = sock.split();
|
||||||
|
let (mut br, mut bw) = {
|
||||||
|
let (r, w) = upstream.into_split();
|
||||||
|
(r, w)
|
||||||
|
};
|
||||||
|
let t1 = tokio::io::copy(&mut ar, &mut bw);
|
||||||
|
let t2 = tokio::io::copy(&mut br, &mut aw);
|
||||||
|
tokio::select! {
|
||||||
|
_ = t1 => {}
|
||||||
|
_ = t2 => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_http(first_bytes: &[u8]) -> bool {
|
||||||
|
// Cheap sniff: must start with an ASCII HTTP method token followed by a space.
|
||||||
|
for m in [
|
||||||
|
"GET ",
|
||||||
|
"POST ",
|
||||||
|
"PUT ",
|
||||||
|
"HEAD ",
|
||||||
|
"DELETE ",
|
||||||
|
"PATCH ",
|
||||||
|
"OPTIONS ",
|
||||||
|
"CONNECT ",
|
||||||
|
"TRACE ",
|
||||||
|
] {
|
||||||
|
if first_bytes.starts_with(m.as_bytes()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Read an HTTP head (request line + headers) up to the first \r\n\r\n.
|
/// Read an HTTP head (request line + headers) up to the first \r\n\r\n.
|
||||||
/// Returns (head_bytes, leftover_after_head). The leftover may contain part
|
/// Returns (head_bytes, leftover_after_head). The leftover may contain part
|
||||||
/// of the request body already received.
|
/// of the request body already received.
|
||||||
@@ -260,26 +479,22 @@ fn is_valid_http_method(m: &str) -> bool {
|
|||||||
|
|
||||||
// ---------- CONNECT handling ----------
|
// ---------- CONNECT handling ----------
|
||||||
|
|
||||||
async fn do_connect(
|
async fn run_mitm_then_relay(
|
||||||
mut sock: TcpStream,
|
sock: TcpStream,
|
||||||
target: &str,
|
host: &str,
|
||||||
fronter: Arc<DomainFronter>,
|
port: u16,
|
||||||
mitm: Arc<Mutex<MitmCertManager>>,
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
) -> std::io::Result<()> {
|
fronter: &DomainFronter,
|
||||||
let (host, port) = parse_host_port(target);
|
) {
|
||||||
tracing::info!("CONNECT -> {}:{}", host, port);
|
tracing::info!("MITM TLS -> {}:{}", host, port);
|
||||||
|
|
||||||
sock.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;
|
|
||||||
sock.flush().await?;
|
|
||||||
|
|
||||||
// MITM: build a server config for this domain and accept TLS.
|
|
||||||
let server_config = {
|
let server_config = {
|
||||||
let mut m = mitm.lock().await;
|
let mut m = mitm.lock().await;
|
||||||
match m.get_server_config(&host) {
|
match m.get_server_config(host) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("cert gen failed for {}: {}", host, e);
|
tracing::error!("cert gen failed for {}: {}", host, e);
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -289,13 +504,14 @@ async fn do_connect(
|
|||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::debug!("TLS accept failed for {}: {}", host, e);
|
tracing::debug!("TLS accept failed for {}: {}", host, e);
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep-alive loop: read HTTP requests from the decrypted stream.
|
// Keep-alive loop: read HTTP requests from the decrypted stream.
|
||||||
|
// scheme=https because we MITM-terminated TLS.
|
||||||
loop {
|
loop {
|
||||||
match handle_mitm_request(&mut tls, &host, port, &fronter).await {
|
match handle_mitm_request(&mut tls, host, port, fronter, "https").await {
|
||||||
Ok(true) => continue,
|
Ok(true) => continue,
|
||||||
Ok(false) => break,
|
Ok(false) => break,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -304,19 +520,36 @@ async fn do_connect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_sni_rewrite_connect(
|
// ---------- Plain HTTP relay on a raw TCP stream (port 80 targets) ----------
|
||||||
|
|
||||||
|
async fn relay_http_stream_raw(
|
||||||
mut sock: TcpStream,
|
mut sock: TcpStream,
|
||||||
host: &str,
|
host: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
|
scheme: &str,
|
||||||
|
fronter: &DomainFronter,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
match handle_mitm_request(&mut sock, host, port, fronter, scheme).await {
|
||||||
|
Ok(true) => continue,
|
||||||
|
Ok(false) => break,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::debug!("http relay error for {}: {}", host, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_sni_rewrite_tunnel_from_tcp(
|
||||||
|
sock: TcpStream,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
mitm: Arc<Mutex<MitmCertManager>>,
|
mitm: Arc<Mutex<MitmCertManager>>,
|
||||||
rewrite_ctx: Arc<RewriteCtx>,
|
rewrite_ctx: Arc<RewriteCtx>,
|
||||||
) -> std::io::Result<()> {
|
) -> 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)
|
let target_ip = hosts_override(&rewrite_ctx.hosts, host)
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_else(|| rewrite_ctx.google_ip.clone());
|
.unwrap_or_else(|| rewrite_ctx.google_ip.clone());
|
||||||
@@ -457,6 +690,7 @@ async fn handle_mitm_request<S>(
|
|||||||
host: &str,
|
host: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
fronter: &DomainFronter,
|
fronter: &DomainFronter,
|
||||||
|
scheme: &str,
|
||||||
) -> std::io::Result<bool>
|
) -> std::io::Result<bool>
|
||||||
where
|
where
|
||||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||||
@@ -474,13 +708,14 @@ where
|
|||||||
// Read body if content-length is set.
|
// Read body if content-length is set.
|
||||||
let body = read_body(stream, &leftover, &headers).await?;
|
let body = read_body(stream, &leftover, &headers).await?;
|
||||||
|
|
||||||
let url = if port == 443 {
|
let default_port = if scheme == "https" { 443 } else { 80 };
|
||||||
format!("https://{}{}", host, path)
|
let url = if port == default_port {
|
||||||
|
format!("{}://{}{}", scheme, host, path)
|
||||||
} else {
|
} else {
|
||||||
format!("https://{}:{}{}", host, port, path)
|
format!("{}://{}:{}{}", scheme, host, port, path)
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("MITM {} {}", method, url);
|
tracing::info!("relay {} {}", method, url);
|
||||||
|
|
||||||
let response = fronter.relay(&method, &url, &headers, &body).await;
|
let response = fronter.relay(&method, &url, &headers, &body).await;
|
||||||
stream.write_all(&response).await?;
|
stream.write_all(&response).await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user