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.
This commit is contained in:
therealaleph
2026-04-22 16:34:40 +03:00
parent 293e5714bb
commit 5371bfc7d5
5 changed files with 133 additions and 5 deletions
Generated
+1 -1
View File
@@ -1317,7 +1317,7 @@ dependencies = [
[[package]]
name = "mhrv-rs"
version = "0.8.2"
version = "0.8.3"
dependencies = [
"base64 0.22.1",
"bytes",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mhrv-rs"
version = "0.8.2"
version = "0.8.3"
edition = "2021"
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
license = "MIT"
+10
View File
@@ -1,9 +1,17 @@
# MasterHttpRelayVPN-RUST
[![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)
[![Downloads](https://img.shields.io/github/downloads/therealaleph/MasterHttpRelayVPN-RUST/total?label=downloads&logo=github)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/releases)
[![CI](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/actions/workflows/release.yml/badge.svg)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/actions/workflows/release.yml)
[![License: MIT](https://img.shields.io/github/license/therealaleph/MasterHttpRelayVPN-RUST?color=blue)](LICENSE)
[![Stars](https://img.shields.io/github/stars/therealaleph/MasterHttpRelayVPN-RUST?style=flat&logo=github)](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/stargazers)
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.
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.
> **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).
**[English Guide](#setup-guide)** | **[راهنمای فارسی](#راهنمای-فارسی)**
## Why this exists
@@ -357,6 +365,8 @@ Original project: <https://github.com/masterking32/MasterHttpRelayVPN> by [@mast
این نسخهٔ `Rust` از پروژهٔ اصلی [MasterHttpRelayVPN](https://github.com/masterking32/MasterHttpRelayVPN) اثر [@masterking32](https://github.com/masterking32) است. **تمام اعتبار ایده و نسخهٔ اصلی پایتون برای ایشان است.** این پورت همان روش را در قالب یک فایل اجرایی تک‌پارچه (~۳ مگابایت) بدون نیاز به نصب پایتون یا هیچ وابستگی دیگری ارائه می‌دهد.
> **نکتهٔ مهم دربارهٔ نویسندگی:** بیشتر کدِ این پورت `Rust` با کمک [Claude](https://claude.com) شرکت Anthropic نوشته شده و روی هر commit توسط انسان بازبینی شده است. اگر باگی دیدید یا پیشنهادی دارید، لطفاً در [صفحهٔ issues](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues) گزارش دهید.
### برای چه کسی مفید است؟
- کسانی که در شبکه‌های تحت سانسور قوی (مثل ایران) زندگی می‌کنند
+88
View File
@@ -29,6 +29,17 @@ fn main() -> eframe::Result<()> {
let shared = Arc::new(Shared::default());
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel::<Cmd>();
// Hook tracing events into the Recent log panel. Without this every
// tracing::info! / debug! / trace! the proxy emits gets swallowed and
// the panel only ever shows our manual push_log calls, making the log
// level selector look useless (issue #12 bug 2).
//
// The env-filter respects RUST_LOG if set, otherwise defaults to info
// so users see routing decisions immediately without any knob-turning.
// When they start the proxy and Save the config, the log level from the
// config is applied to the in-process filter (see on_start below).
install_ui_tracing(shared.clone());
let shared_bg = shared.clone();
std::thread::Builder::new()
.name("mhrv-bg".into())
@@ -1198,6 +1209,83 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
}
}
/// Install a tracing subscriber that mirrors every log event into the UI's
/// Recent log panel.
///
/// Respects `RUST_LOG` if set. Otherwise defaults to `info` — which is what
/// users mean when they pick a non-default log level in the form. (trace /
/// debug flip too much noise for a local GUI, so the combo-box changes level
/// live via the `reload` handle that `with_env_filter` gives us but we keep
/// the default boot-time level at info so first-run behavior is sensible.)
fn install_ui_tracing(shared: Arc<Shared>) {
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::EnvFilter;
/// A MakeWriter that pushes each line into the shared log panel.
struct UiLogWriter {
shared: Arc<Shared>,
}
struct UiWriterInst {
shared: Arc<Shared>,
buf: Vec<u8>,
}
impl<'a> MakeWriter<'a> for UiLogWriter {
type Writer = UiWriterInst;
fn make_writer(&'a self) -> Self::Writer {
UiWriterInst {
shared: self.shared.clone(),
buf: Vec::with_capacity(128),
}
}
}
impl std::io::Write for UiWriterInst {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.buf.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let text = String::from_utf8_lossy(&self.buf).trim_end().to_string();
self.buf.clear();
// Split on newlines in case multiple events got buffered.
for line in text.lines() {
if line.is_empty() {
continue;
}
let mut s = self.shared.state.lock().unwrap();
s.log.push_back(line.to_string());
while s.log.len() > LOG_MAX {
s.log.pop_front();
}
}
Ok(())
}
}
impl Drop for UiWriterInst {
fn drop(&mut self) {
let _ = std::io::Write::flush(self);
}
}
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,hyper=warn"));
let writer = UiLogWriter { shared };
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_ansi(false)
.with_writer(writer)
.try_init();
}
fn push_log(shared: &Shared, msg: &str) {
let line = format!(
"{} {}",
+33 -3
View File
@@ -354,6 +354,7 @@ async fn dispatch_tunnel(
) -> 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() {
tracing::info!("dispatch {}:{} -> sni-rewrite tunnel (Google edge direct)", host, port);
return do_sni_rewrite_tunnel_from_tcp(sock, &host, port, mitm, rewrite_ctx).await;
}
@@ -371,13 +372,29 @@ async fn dispatch_tunnel(
Ok(Err(_)) => return Ok(()),
Err(_) => {
// Client silent: likely a server-first protocol.
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
let via = rewrite_ctx.upstream_socks5.as_deref();
tracing::info!(
"dispatch {}:{} -> raw-tcp ({}) (client silent, likely server-first)",
host,
port,
via.unwrap_or("direct")
);
plain_tcp_passthrough(sock, &host, port, via).await;
return Ok(());
}
};
if peek_n >= 1 && peek_buf[0] == 0x16 {
// Looks like TLS: MITM + relay via Apps Script.
// Looks like TLS: MITM + relay via Apps Script. Note: upstream_socks5
// is NOT consulted here by design — HTTPS goes through the Apps Script
// relay, which is the whole reason mhrv-rs exists. If you want HTTPS
// to flow through xray, disable mhrv-rs and point your browser at
// xray directly.
tracing::info!(
"dispatch {}:{} -> MITM + Apps Script relay (TLS detected)",
host,
port
);
run_mitm_then_relay(sock, &host, port, mitm, &fronter).await;
return Ok(());
}
@@ -386,11 +403,24 @@ async fn dispatch_tunnel(
// 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" };
tracing::info!(
"dispatch {}:{} -> Apps Script relay (plain HTTP, scheme={})",
host,
port,
scheme
);
relay_http_stream_raw(sock, &host, port, scheme, &fronter).await;
return Ok(());
}
plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await;
let via = rewrite_ctx.upstream_socks5.as_deref();
tracing::info!(
"dispatch {}:{} -> raw-tcp ({}) (non-HTTP, non-TLS client payload)",
host,
port,
via.unwrap_or("direct")
);
plain_tcp_passthrough(sock, &host, port, via).await;
Ok(())
}