Skip to content

Commit bc43a64

Browse files
committed
v0.8.3: UI log panel now captures tracing events + dispatch routing visibility (issue #12)
Two reported issues: 1. Log level in the form had no visible effect — trace produced the same panel output as warn. 2. upstream_socks5 was reported as never being attempted. (1) was because the UI binary never installed a tracing subscriber. Every tracing::info!/debug!/trace! from the proxy was discarded; only the handful of manual push_log() calls for start/stop/test reached the 'Recent log' panel. Swapping the log level in the combo-box just rewrote the config field — nothing consumed it. Fix: install_ui_tracing() at startup registers a tracing_subscriber fmt layer with a custom MakeWriter that mirrors each formatted event line into shared.state.log. Respects RUST_LOG, defaults to 'info' with hyper pinned to warn so the panel isn't swamped by low-level HTTP chatter. Now the log level switch actually filters panel output, and routing decisions show up live. (2) is a documentation / visibility issue more than a bug. Our upstream_socks5 routing is intentionally scoped to raw-TCP traffic (non-HTTP, non-TLS) — HTTPS goes through the Apps Script relay, which is the whole reason mhrv-rs exists. But without working logs, it looks like upstream_socks5 is dead code. Fix: every branch of dispatch_tunnel now emits a tracing::info! that says exactly which path the connection took and, where applicable, whether upstream_socks5 was used: dispatch api.telegram.org:443 -> raw-tcp (127.0.0.1:50529) dispatch www.google.com:443 -> sni-rewrite tunnel (Google edge direct) dispatch httpbin.org:443 -> MITM + Apps Script relay (TLS detected) Combined with (1), users can now see in real time whether their traffic is hitting upstream_socks5. If it says 'raw-tcp (direct)' after they set the field, that's evidence of a real bug; if it never reaches the raw-tcp branch at all, that's the documented design (HTTPS → Apps Script). Also per user request, updated README: - Shields.io badges up top: latest release, total downloads, CI status, license, stars. - Short 'Heads up on authorship' note crediting Anthropic's Claude for the bulk of the Rust port (with the human-on-every-commit caveat). English and Persian mirrors both have it. All 37 unit tests pass.
1 parent 6a8dd16 commit bc43a64

5 files changed

Lines changed: 133 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "0.8.2"
3+
version = "0.8.3"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
# MasterHttpRelayVPN-RUST
22

3+
[![Latest release](https://img.shields.io/github/v/release/therealaleph/MasterHttpRelayVPN-RUST?sort=semver&display_name=tag&logo=github&label=release)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases/latest)
4+
[![Downloads](https://img.shields.io/github/downloads/therealaleph/MasterHttpRelayVPN-RUST/total?label=downloads&logo=github)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases)
5+
[![CI](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/actions/workflows/release.yml/badge.svg)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/actions/workflows/release.yml)
6+
[![License: MIT](https://img.shields.io/github/license/therealaleph/MasterHttpRelayVPN-RUST?color=blue)](LICENSE)
7+
[![Stars](https://img.shields.io/github/stars/therealaleph/MasterHttpRelayVPN-RUST?style=flat&logo=github)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/stargazers)
8+
39
Rust port of [@masterking32's MasterHttpRelayVPN](https://github.com/masterking32/MasterHttpRelayVPN). **All credit for the original idea and the Python implementation goes to [@masterking32](https://github.com/masterking32).** This is a faithful reimplementation of the `apps_script` mode, packaged as two tiny binaries (CLI + desktop UI) with no runtime dependencies.
410

511
Free DPI bypass via Google Apps Script as a remote relay, with TLS SNI concealment. Your ISP's censor sees traffic going to `www.google.com`; behind the scenes a free Google Apps Script that you deploy in your own Google account fetches the real website for you.
612

13+
> **Heads up on authorship:** the bulk of this Rust port was written with [Anthropic's Claude](https://claude.com) driving, reviewed by a human on every commit. Bug reports, fixes, and contributions are all welcome — see the [issues page](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues).
14+
715
**[English Guide](#setup-guide)** | **[راهنمای فارسی](#راهنمای-فارسی)**
816

917
## Why this exists
@@ -357,6 +365,8 @@ Original project: <https://github.com/masterking32/MasterHttpRelayVPN> by [@mast
357365

358366
این نسخهٔ `Rust` از پروژهٔ اصلی [MasterHttpRelayVPN](https://github.com/masterking32/MasterHttpRelayVPN) اثر [@masterking32](https://github.com/masterking32) است. **تمام اعتبار ایده و نسخهٔ اصلی پایتون برای ایشان است.** این پورت همان روش را در قالب یک فایل اجرایی تک‌پارچه (~۳ مگابایت) بدون نیاز به نصب پایتون یا هیچ وابستگی دیگری ارائه می‌دهد.
359367

368+
> **نکتهٔ مهم دربارهٔ نویسندگی:** بیشتر کدِ این پورت `Rust` با کمک [Claude](https://claude.com) شرکت Anthropic نوشته شده و روی هر commit توسط انسان بازبینی شده است. اگر باگی دیدید یا پیشنهادی دارید، لطفاً در [صفحهٔ issues](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues) گزارش دهید.
369+
360370
### برای چه کسی مفید است؟
361371

362372
- کسانی که در شبکه‌های تحت سانسور قوی (مثل ایران) زندگی می‌کنند

src/bin/ui.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ fn main() -> eframe::Result<()> {
2929
let shared = Arc::new(Shared::default());
3030
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel::<Cmd>();
3131

32+
// Hook tracing events into the Recent log panel. Without this every
33+
// tracing::info! / debug! / trace! the proxy emits gets swallowed and
34+
// the panel only ever shows our manual push_log calls, making the log
35+
// level selector look useless (issue #12 bug 2).
36+
//
37+
// The env-filter respects RUST_LOG if set, otherwise defaults to info
38+
// so users see routing decisions immediately without any knob-turning.
39+
// When they start the proxy and Save the config, the log level from the
40+
// config is applied to the in-process filter (see on_start below).
41+
install_ui_tracing(shared.clone());
42+
3243
let shared_bg = shared.clone();
3344
std::thread::Builder::new()
3445
.name("mhrv-bg".into())
@@ -1198,6 +1209,83 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
11981209
}
11991210
}
12001211

1212+
/// Install a tracing subscriber that mirrors every log event into the UI's
1213+
/// Recent log panel.
1214+
///
1215+
/// Respects `RUST_LOG` if set. Otherwise defaults to `info` — which is what
1216+
/// users mean when they pick a non-default log level in the form. (trace /
1217+
/// debug flip too much noise for a local GUI, so the combo-box changes level
1218+
/// live via the `reload` handle that `with_env_filter` gives us but we keep
1219+
/// the default boot-time level at info so first-run behavior is sensible.)
1220+
fn install_ui_tracing(shared: Arc<Shared>) {
1221+
use tracing_subscriber::fmt::MakeWriter;
1222+
use tracing_subscriber::EnvFilter;
1223+
1224+
/// A MakeWriter that pushes each line into the shared log panel.
1225+
struct UiLogWriter {
1226+
shared: Arc<Shared>,
1227+
}
1228+
1229+
struct UiWriterInst {
1230+
shared: Arc<Shared>,
1231+
buf: Vec<u8>,
1232+
}
1233+
1234+
impl<'a> MakeWriter<'a> for UiLogWriter {
1235+
type Writer = UiWriterInst;
1236+
fn make_writer(&'a self) -> Self::Writer {
1237+
UiWriterInst {
1238+
shared: self.shared.clone(),
1239+
buf: Vec::with_capacity(128),
1240+
}
1241+
}
1242+
}
1243+
1244+
impl std::io::Write for UiWriterInst {
1245+
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
1246+
self.buf.extend_from_slice(data);
1247+
Ok(data.len())
1248+
}
1249+
fn flush(&mut self) -> std::io::Result<()> {
1250+
if self.buf.is_empty() {
1251+
return Ok(());
1252+
}
1253+
let text = String::from_utf8_lossy(&self.buf).trim_end().to_string();
1254+
self.buf.clear();
1255+
// Split on newlines in case multiple events got buffered.
1256+
for line in text.lines() {
1257+
if line.is_empty() {
1258+
continue;
1259+
}
1260+
let mut s = self.shared.state.lock().unwrap();
1261+
s.log.push_back(line.to_string());
1262+
while s.log.len() > LOG_MAX {
1263+
s.log.pop_front();
1264+
}
1265+
}
1266+
Ok(())
1267+
}
1268+
}
1269+
1270+
impl Drop for UiWriterInst {
1271+
fn drop(&mut self) {
1272+
let _ = std::io::Write::flush(self);
1273+
}
1274+
}
1275+
1276+
let filter =
1277+
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,hyper=warn"));
1278+
1279+
let writer = UiLogWriter { shared };
1280+
1281+
let _ = tracing_subscriber::fmt()
1282+
.with_env_filter(filter)
1283+
.with_target(false)
1284+
.with_ansi(false)
1285+
.with_writer(writer)
1286+
.try_init();
1287+
}
1288+
12011289
fn push_log(shared: &Shared, msg: &str) {
12021290
let line = format!(
12031291
"{} {}",

src/proxy_server.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ async fn dispatch_tunnel(
354354
) -> std::io::Result<()> {
355355
// 1. Explicit hosts override or SNI-rewrite suffix: always use the tunnel.
356356
if matches_sni_rewrite(&host) || hosts_override(&rewrite_ctx.hosts, &host).is_some() {
357+
tracing::info!("dispatch {}:{} -> sni-rewrite tunnel (Google edge direct)", host, port);
357358
return do_sni_rewrite_tunnel_from_tcp(sock, &host, port, mitm, rewrite_ctx).await;
358359
}
359360

@@ -371,13 +372,29 @@ async fn dispatch_tunnel(
371372
Ok(Err(_)) => return Ok(()),
372373
Err(_) => {
373374
// Client silent: likely a server-first protocol.
374-
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
375+
let via = rewrite_ctx.upstream_socks5.as_deref();
376+
tracing::info!(
377+
"dispatch {}:{} -> raw-tcp ({}) (client silent, likely server-first)",
378+
host,
379+
port,
380+
via.unwrap_or("direct")
381+
);
382+
plain_tcp_passthrough(sock, &host, port, via).await;
375383
return Ok(());
376384
}
377385
};
378386

379387
if peek_n >= 1 && peek_buf[0] == 0x16 {
380-
// Looks like TLS: MITM + relay via Apps Script.
388+
// Looks like TLS: MITM + relay via Apps Script. Note: upstream_socks5
389+
// is NOT consulted here by design — HTTPS goes through the Apps Script
390+
// relay, which is the whole reason mhrv-rs exists. If you want HTTPS
391+
// to flow through xray, disable mhrv-rs and point your browser at
392+
// xray directly.
393+
tracing::info!(
394+
"dispatch {}:{} -> MITM + Apps Script relay (TLS detected)",
395+
host,
396+
port
397+
);
381398
run_mitm_then_relay(sock, &host, port, mitm, &fronter).await;
382399
return Ok(());
383400
}
@@ -386,11 +403,24 @@ async fn dispatch_tunnel(
386403
// fall back to plain TCP passthrough.
387404
if peek_n > 0 && looks_like_http(&peek_buf[..peek_n]) {
388405
let scheme = if port == 443 { "https" } else { "http" };
406+
tracing::info!(
407+
"dispatch {}:{} -> Apps Script relay (plain HTTP, scheme={})",
408+
host,
409+
port,
410+
scheme
411+
);
389412
relay_http_stream_raw(sock, &host, port, scheme, &fronter).await;
390413
return Ok(());
391414
}
392415

393-
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
416+
let via = rewrite_ctx.upstream_socks5.as_deref();
417+
tracing::info!(
418+
"dispatch {}:{} -> raw-tcp ({}) (non-HTTP, non-TLS client payload)",
419+
host,
420+
port,
421+
via.unwrap_or("direct")
422+
);
423+
plain_tcp_passthrough(sock, &host, port, via).await;
394424
Ok(())
395425
}
396426

0 commit comments

Comments
 (0)