mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 06:24:35 +03:00
v0.5.0: optional upstream SOCKS5 for non-HTTP traffic (Telegram et al.)
The Apps Script relay is HTTP-only, and the SNI-rewrite tunnel only works for Google-hosted domains — so MTProto / IMAP / SSH / anything else used to drop to a direct-TCP passthrough, which provides zero circumvention. Users behind a DPI that blocks Telegram saw constant disconnect/reconnect loops because the raw TCP ran right into the block. Fix: add an optional 'upstream_socks5' config field. When set, the raw-TCP fallback chains the flow into that SOCKS5 proxy (typically a local xray / v2ray / sing-box with a VLESS / Trojan / Shadowsocks outbound to your own VPS) instead of connecting directly. The whole rest of the pipeline is unchanged: - HTTP / HTTPS still MITMs and relays via Apps Script - SNI-rewrite suffixes (google.com, youtube.com, …) still hit the direct Google-edge tunnel (so YouTube stays fast) - Only the raw-TCP bucket (Telegram MTProto, SSH, IMAP, …) gets the new upstream chain Changes: - config.rs: add Option<String> upstream_socks5 field - proxy_server.rs: thread it through RewriteCtx; rewrite plain_tcp_passthrough to call a new socks5_connect_via() helper when configured, with graceful fallback to direct - ui.rs: new 'Upstream SOCKS5' input with tooltip + placeholder, ConfigWire round-trip - README.md: new 'Pair with xray for Telegram' section (EN + FA) with the architecture diagram and example config Verified end-to-end in Docker: xray with the user's working VLESS Reality config, mhrv-rs with upstream_socks5 pointing at it. - HTTPS via mhrv-rs SOCKS5: origin = Google IP (Apps Script path) ✓ - Raw TCP to 3 Telegram DCs + api.telegram.org: all SOCKS5 rep=0, log shows 'tcp via upstream-socks5 127.0.0.1:50529 -> …' ✓ - youtube.com / google.com: 'SNI-rewrite tunnel' (unchanged) ✓ - Real Telegram Desktop stayed connected cleanly (user-confirmed).
This commit is contained in:
Generated
+1
-1
@@ -1360,7 +1360,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "0.4.4"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "0.4.4"
|
version = "0.5.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"
|
||||||
|
|||||||
@@ -187,7 +187,29 @@ The tool listens on **two** ports. Use whichever your client supports:
|
|||||||
|
|
||||||
**SOCKS5 proxy** (Telegram, xray, app-level clients) — `127.0.0.1:8086`, no auth.
|
**SOCKS5 proxy** (Telegram, xray, app-level clients) — `127.0.0.1:8086`, no auth.
|
||||||
|
|
||||||
- Works for HTTP, HTTPS, **and** non-HTTP protocols (Telegram's MTProto, raw TCP). The server auto-detects each connection and falls back to plain TCP passthrough when the payload isn't HTTP.
|
- Works for HTTP, HTTPS, **and** non-HTTP protocols (Telegram's MTProto, raw TCP). The server auto-detects each connection: HTTP/HTTPS go through the Apps Script relay, SNI-rewritable domains go through the direct Google-edge tunnel, and anything else falls through to raw TCP.
|
||||||
|
|
||||||
|
## Telegram, IMAP, SSH — pair with xray (optional)
|
||||||
|
|
||||||
|
The Apps Script relay only speaks HTTP request/response, so non-HTTP protocols (Telegram MTProto, IMAP, SSH, arbitrary raw TCP) can't travel through it. Without anything else, those flows hit the direct-TCP fallback — which means they're not actually tunneled, and an ISP that blocks Telegram will still block them.
|
||||||
|
|
||||||
|
Fix: run a local [xray](https://github.com/XTLS/Xray-core) (or v2ray / sing-box) with a VLESS/Trojan/Shadowsocks outbound that goes to a VPS of your own, and point mhrv-rs at xray's SOCKS5 inbound via the **Upstream SOCKS5** field (or the `upstream_socks5` config key). When set, raw-TCP flows coming through mhrv-rs's SOCKS5 listener get chained into xray → the real tunnel, instead of connecting directly.
|
||||||
|
|
||||||
|
```
|
||||||
|
Telegram ┐ ┌─ Apps Script ── HTTP/HTTPS
|
||||||
|
├─ SOCKS5 :8086 ─┤ mhrv-rs ├─ SNI rewrite ─── google.com, youtube.com, …
|
||||||
|
Browser ┘ └─ upstream SOCKS5 ─ xray ── VLESS ── your VPS (Telegram, IMAP, SSH, raw TCP)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example config fragment (both UI and JSON):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"upstream_socks5": "127.0.0.1:50529"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP/HTTPS continues to route through the Apps Script relay (no change), and the SNI-rewrite tunnel for `google.com` / `youtube.com` / etc. keeps bypassing both — so YouTube stays as fast as before while Telegram gets a real tunnel.
|
||||||
|
|
||||||
## Diagnostics
|
## Diagnostics
|
||||||
|
|
||||||
@@ -217,6 +239,7 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w
|
|||||||
- [x] `test` and `scan-ips` subcommands
|
- [x] `test` and `scan-ips` subcommands
|
||||||
- [x] Script IDs masked in logs (`prefix…suffix`) so `info` logs don't leak deployment IDs
|
- [x] Script IDs masked in logs (`prefix…suffix`) so `info` logs don't leak deployment IDs
|
||||||
- [x] Desktop UI (egui) — cross-platform, no bundler needed
|
- [x] Desktop UI (egui) — cross-platform, no bundler needed
|
||||||
|
- [x] Optional upstream SOCKS5 chaining for non-HTTP traffic (Telegram MTProto, IMAP, SSH…) so raw-TCP flows can be tunneled through xray / v2ray / sing-box instead of connecting directly. HTTP/HTTPS keeps going through the Apps Script relay.
|
||||||
|
|
||||||
Intentionally **not** implemented (rationale included so future contributors don't spend cycles on them):
|
Intentionally **not** implemented (rationale included so future contributors don't spend cycles on them):
|
||||||
|
|
||||||
@@ -382,7 +405,29 @@ Firefox cert store خودش را جدا دارد؛ installer تلاش میک
|
|||||||
|
|
||||||
**SOCKS5 proxy** (تلگرام، xray، کلاینتهای app-level) — `127.0.0.1:8086`، بدون auth.
|
**SOCKS5 proxy** (تلگرام، xray، کلاینتهای app-level) — `127.0.0.1:8086`، بدون auth.
|
||||||
|
|
||||||
برای HTTP و HTTPS و **هم** پروتکلهای غیر-HTTP (MTProto تلگرام، TCP خام) کار میکند. ابزار بهصورت هوشمند تشخیص میدهد و اگر ترافیک HTTP نبود، بهصورت plain TCP passthrough میفرستد.
|
برای HTTP و HTTPS و **هم** پروتکلهای غیر-HTTP (MTProto تلگرام، TCP خام) کار میکند. ابزار بهصورت هوشمند تشخیص میدهد: HTTP/HTTPS از رلهٔ Apps Script میرود، دامنههای قابل SNI-rewrite از تونل مستقیم لبهٔ گوگل، و بقیه به TCP خام میافتد.
|
||||||
|
|
||||||
|
### تلگرام، IMAP، SSH — با xray جفت کنید (اختیاری)
|
||||||
|
|
||||||
|
رلهٔ Apps Script فقط HTTP request/response میفهمد، پس پروتکلهای غیر-HTTP (MTProto تلگرام، IMAP، SSH، TCP خام) از داخلش عبور نمیکنند. بدون کار اضافه این جور ترافیک به مسیر TCP مستقیم میافتد — یعنی واقعاً tunnel نمیشود و اگر ISP تلگرام را بلاک کرده باشد، همچنان بلاک است.
|
||||||
|
|
||||||
|
راه حل: یک [xray](https://github.com/XTLS/Xray-core) (یا v2ray / sing-box) با outbound VLESS/Trojan/Shadowsocks به یک VPS شخصی خودتان بالا بیاورید، و mhrv-rs را از طریق فیلد **Upstream SOCKS5** در UI (یا کلید `upstream_socks5` در config) به SOCKS5 inbound آن وصل کنید. با این کار ترافیک TCP خامی که از SOCKS5 mhrv-rs رد میشود، بهجای اتصال مستقیم، از xray رد شده و به تونل واقعی میرسد.
|
||||||
|
|
||||||
|
```
|
||||||
|
تلگرام ┐ ┌─ Apps Script ── HTTP/HTTPS
|
||||||
|
├─ SOCKS5 :8086 ┤ mhrv-rs ├─ SNI rewrite ─── google.com, youtube.com, …
|
||||||
|
مرورگر ┘ └─ upstream SOCKS5 ─ xray ── VLESS ── VPS شما (تلگرام، IMAP، SSH، TCP خام)
|
||||||
|
```
|
||||||
|
|
||||||
|
قطعهای از config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"upstream_socks5": "127.0.0.1:50529"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP/HTTPS هیچ تغییری نمیکند (همچنان از Apps Script میرود) و تونل SNI-rewrite برای `google.com` / `youtube.com` / … هم سر جای خودش است — پس یوتوب مثل قبل سریع میماند و تلگرام بالاخره یک تونل واقعی میگیرد.
|
||||||
|
|
||||||
### محدودیتهای شناختهشده
|
### محدودیتهای شناختهشده
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ struct FormState {
|
|||||||
socks5_port: String,
|
socks5_port: String,
|
||||||
log_level: String,
|
log_level: String,
|
||||||
verify_ssl: bool,
|
verify_ssl: bool,
|
||||||
|
upstream_socks5: String,
|
||||||
show_auth_key: bool,
|
show_auth_key: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ fn load_form() -> FormState {
|
|||||||
socks5_port: c.socks5_port.map(|p| p.to_string()).unwrap_or_default(),
|
socks5_port: c.socks5_port.map(|p| p.to_string()).unwrap_or_default(),
|
||||||
log_level: c.log_level,
|
log_level: c.log_level,
|
||||||
verify_ssl: c.verify_ssl,
|
verify_ssl: c.verify_ssl,
|
||||||
|
upstream_socks5: c.upstream_socks5.unwrap_or_default(),
|
||||||
show_auth_key: false,
|
show_auth_key: false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -150,6 +152,7 @@ fn load_form() -> FormState {
|
|||||||
socks5_port: "8086".into(),
|
socks5_port: "8086".into(),
|
||||||
log_level: "info".into(),
|
log_level: "info".into(),
|
||||||
verify_ssl: true,
|
verify_ssl: true,
|
||||||
|
upstream_socks5: String::new(),
|
||||||
show_auth_key: false,
|
show_auth_key: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +204,10 @@ impl FormState {
|
|||||||
verify_ssl: self.verify_ssl,
|
verify_ssl: self.verify_ssl,
|
||||||
hosts: std::collections::HashMap::new(),
|
hosts: std::collections::HashMap::new(),
|
||||||
enable_batching: false,
|
enable_batching: false,
|
||||||
|
upstream_socks5: {
|
||||||
|
let v = self.upstream_socks5.trim();
|
||||||
|
if v.is_empty() { None } else { Some(v.to_string()) }
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +239,8 @@ struct ConfigWire<'a> {
|
|||||||
verify_ssl: bool,
|
verify_ssl: bool,
|
||||||
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
|
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
|
||||||
hosts: &'a std::collections::HashMap<String, String>,
|
hosts: &'a std::collections::HashMap<String, String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
upstream_socks5: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -259,6 +268,7 @@ impl<'a> From<&'a Config> for ConfigWire<'a> {
|
|||||||
log_level: c.log_level.as_str(),
|
log_level: c.log_level.as_str(),
|
||||||
verify_ssl: c.verify_ssl,
|
verify_ssl: c.verify_ssl,
|
||||||
hosts: &c.hosts,
|
hosts: &c.hosts,
|
||||||
|
upstream_socks5: c.upstream_socks5.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,6 +368,20 @@ impl eframe::App for App {
|
|||||||
ui.add(egui::TextEdit::singleline(&mut self.form.socks5_port).desired_width(80.0));
|
ui.add(egui::TextEdit::singleline(&mut self.form.socks5_port).desired_width(80.0));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Upstream SOCKS5")
|
||||||
|
.on_hover_text(
|
||||||
|
"Optional. host:port of an upstream SOCKS5 proxy (e.g. xray / v2ray / sing-box).\n\
|
||||||
|
When set, non-HTTP / raw-TCP traffic arriving on the SOCKS5 listener is\n\
|
||||||
|
chained through this proxy instead of connecting directly — this is what\n\
|
||||||
|
makes Telegram MTProto, IMAP, SSH etc. actually tunnel.\n\
|
||||||
|
HTTP/HTTPS traffic still routes through the Apps Script relay and the\n\
|
||||||
|
SNI-rewrite tunnel as before."
|
||||||
|
);
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut self.form.upstream_socks5)
|
||||||
|
.hint_text("empty = direct; 127.0.0.1:50529 for a local xray")
|
||||||
|
.desired_width(f32::INFINITY));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Log level");
|
ui.label("Log level");
|
||||||
egui::ComboBox::from_id_source("loglevel")
|
egui::ComboBox::from_id_source("loglevel")
|
||||||
.selected_text(&self.form.log_level)
|
.selected_text(&self.form.log_level)
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ pub struct Config {
|
|||||||
pub hosts: HashMap<String, String>,
|
pub hosts: HashMap<String, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enable_batching: bool,
|
pub enable_batching: bool,
|
||||||
|
/// Optional upstream SOCKS5 proxy for non-HTTP / raw-TCP traffic
|
||||||
|
/// (e.g. `"127.0.0.1:50529"` pointing at a local xray / v2ray instance).
|
||||||
|
/// When set, the SOCKS5 listener forwards raw-TCP flows through it
|
||||||
|
/// instead of connecting directly. HTTP/HTTPS traffic (which goes
|
||||||
|
/// through the Apps Script relay) and SNI-rewrite tunnels are
|
||||||
|
/// unaffected.
|
||||||
|
#[serde(default)]
|
||||||
|
pub upstream_socks5: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_google_ip() -> String {
|
fn default_google_ip() -> String {
|
||||||
|
|||||||
+137
-17
@@ -74,6 +74,7 @@ pub struct RewriteCtx {
|
|||||||
pub front_domain: String,
|
pub front_domain: String,
|
||||||
pub hosts: std::collections::HashMap<String, String>,
|
pub hosts: std::collections::HashMap<String, String>,
|
||||||
pub tls_connector: TlsConnector,
|
pub tls_connector: TlsConnector,
|
||||||
|
pub upstream_socks5: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxyServer {
|
impl ProxyServer {
|
||||||
@@ -100,6 +101,7 @@ impl ProxyServer {
|
|||||||
front_domain: config.front_domain.clone(),
|
front_domain: config.front_domain.clone(),
|
||||||
hosts: config.hosts.clone(),
|
hosts: config.hosts.clone(),
|
||||||
tls_connector,
|
tls_connector,
|
||||||
|
upstream_socks5: config.upstream_socks5.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
|
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
|
||||||
@@ -326,7 +328,7 @@ async fn dispatch_tunnel(
|
|||||||
Ok(Err(_)) => return Ok(()),
|
Ok(Err(_)) => return Ok(()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Client silent: likely a server-first protocol.
|
// Client silent: likely a server-first protocol.
|
||||||
plain_tcp_passthrough(sock, &host, port).await;
|
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -345,32 +347,63 @@ async fn dispatch_tunnel(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
plain_tcp_passthrough(sock, &host, port).await;
|
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Plain TCP passthrough ----------
|
// ---------- Plain TCP passthrough ----------
|
||||||
|
|
||||||
async fn plain_tcp_passthrough(mut sock: TcpStream, host: &str, port: u16) {
|
async fn plain_tcp_passthrough(
|
||||||
|
mut sock: TcpStream,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
upstream_socks5: Option<&str>,
|
||||||
|
) {
|
||||||
let target_host = host.trim_start_matches('[').trim_end_matches(']');
|
let target_host = host.trim_start_matches('[').trim_end_matches(']');
|
||||||
let upstream = match tokio::time::timeout(
|
let upstream = if let Some(proxy) = upstream_socks5 {
|
||||||
std::time::Duration::from_secs(10),
|
match socks5_connect_via(proxy, target_host, port).await {
|
||||||
TcpStream::connect((target_host, port)),
|
Ok(s) => {
|
||||||
)
|
tracing::info!("tcp via upstream-socks5 {} -> {}:{}", proxy, host, port);
|
||||||
.await
|
s
|
||||||
{
|
}
|
||||||
Ok(Ok(s)) => s,
|
Err(e) => {
|
||||||
Ok(Err(e)) => {
|
tracing::warn!(
|
||||||
tracing::debug!("plain-tcp connect {}:{} failed: {}", host, port, e);
|
"upstream-socks5 {} -> {}:{} failed: {} (falling back to direct)",
|
||||||
return;
|
proxy, host, port, e
|
||||||
|
);
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
TcpStream::connect((target_host, port)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(s)) => s,
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
} else {
|
||||||
tracing::debug!("plain-tcp connect {}:{} timeout", host, port);
|
match tokio::time::timeout(
|
||||||
return;
|
std::time::Duration::from_secs(10),
|
||||||
|
TcpStream::connect((target_host, port)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(s)) => {
|
||||||
|
tracing::info!("plain-tcp passthrough -> {}:{}", host, port);
|
||||||
|
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);
|
let _ = upstream.set_nodelay(true);
|
||||||
tracing::info!("plain-tcp passthrough -> {}:{}", host, port);
|
|
||||||
let (mut ar, mut aw) = sock.split();
|
let (mut ar, mut aw) = sock.split();
|
||||||
let (mut br, mut bw) = {
|
let (mut br, mut bw) = {
|
||||||
let (r, w) = upstream.into_split();
|
let (r, w) = upstream.into_split();
|
||||||
@@ -384,6 +417,93 @@ async fn plain_tcp_passthrough(mut sock: TcpStream, host: &str, port: u16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a TCP stream to `(host, port)` through an upstream SOCKS5 proxy
|
||||||
|
/// (no-auth only). Returns the connected stream after SOCKS5 negotiation.
|
||||||
|
async fn socks5_connect_via(
|
||||||
|
proxy: &str,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
) -> std::io::Result<TcpStream> {
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
let mut s = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
TcpStream::connect(proxy),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "connect timeout"))??;
|
||||||
|
let _ = s.set_nodelay(true);
|
||||||
|
|
||||||
|
// Greeting: VER=5, NMETHODS=1, METHOD=no-auth
|
||||||
|
s.write_all(&[0x05, 0x01, 0x00]).await?;
|
||||||
|
let mut reply = [0u8; 2];
|
||||||
|
s.read_exact(&mut reply).await?;
|
||||||
|
if reply[0] != 0x05 || reply[1] != 0x00 {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("socks5 greet rejected: {:?}", reply),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONNECT request: VER=5, CMD=1, RSV=0, ATYP=3 (domain) | 1 (IPv4) | 4 (IPv6)
|
||||||
|
let mut req: Vec<u8> = Vec::with_capacity(8 + host.len());
|
||||||
|
req.extend_from_slice(&[0x05, 0x01, 0x00]);
|
||||||
|
if let Ok(v4) = host.parse::<std::net::Ipv4Addr>() {
|
||||||
|
req.push(0x01);
|
||||||
|
req.extend_from_slice(&v4.octets());
|
||||||
|
} else if let Ok(v6) = host.parse::<std::net::Ipv6Addr>() {
|
||||||
|
req.push(0x04);
|
||||||
|
req.extend_from_slice(&v6.octets());
|
||||||
|
} else {
|
||||||
|
let hb = host.as_bytes();
|
||||||
|
if hb.len() > 255 {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"hostname > 255",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
req.push(0x03);
|
||||||
|
req.push(hb.len() as u8);
|
||||||
|
req.extend_from_slice(hb);
|
||||||
|
}
|
||||||
|
req.extend_from_slice(&port.to_be_bytes());
|
||||||
|
s.write_all(&req).await?;
|
||||||
|
|
||||||
|
// Reply header: VER, REP, RSV, ATYP, BND.ADDR, BND.PORT
|
||||||
|
let mut head = [0u8; 4];
|
||||||
|
s.read_exact(&mut head).await?;
|
||||||
|
if head[0] != 0x05 || head[1] != 0x00 {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("socks5 connect rejected rep=0x{:02x}", head[1]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Skip BND.ADDR + BND.PORT.
|
||||||
|
match head[3] {
|
||||||
|
0x01 => {
|
||||||
|
let mut b = [0u8; 4 + 2];
|
||||||
|
s.read_exact(&mut b).await?;
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
let mut b = [0u8; 16 + 2];
|
||||||
|
s.read_exact(&mut b).await?;
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
let mut len = [0u8; 1];
|
||||||
|
s.read_exact(&mut len).await?;
|
||||||
|
let mut name = vec![0u8; len[0] as usize + 2];
|
||||||
|
s.read_exact(&mut name).await?;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
format!("socks5 bad ATYP in reply: {}", other),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
fn looks_like_http(first_bytes: &[u8]) -> bool {
|
fn looks_like_http(first_bytes: &[u8]) -> bool {
|
||||||
// Cheap sniff: must start with an ASCII HTTP method token followed by a space.
|
// Cheap sniff: must start with an ASCII HTTP method token followed by a space.
|
||||||
for m in [
|
for m in [
|
||||||
|
|||||||
Reference in New Issue
Block a user