Skip to content

Commit 11ac654

Browse files
feat(direct): use mhrv-rs as upstream proxy for Psiphon / xray
1 parent 598a890 commit 11ac654

8 files changed

Lines changed: 401 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ If something doesn't work:
160160

161161
**ISP blocks `script.google.com` itself.** mhrv-rs has a `direct` mode that uses only the SNI-rewrite tunnel (no Apps Script). Use it once to access `script.google.com` to deploy your script, then switch to apps_script mode. See [direct mode](docs/guide.md#direct-mode).
162162

163+
**I want to use mhrv-rs as Psiphon's (or xray's) upstream proxy.** Run mhrv-rs in `direct` mode and point Psiphon's *upstream proxy* setting at the host:port shown under the Connect button. Unfronted hosts pass through as raw TCP, so Psiphon's bootstrap traffic reaches Psiphon's servers untouched. See [docs/use-as-upstream.md](docs/use-as-upstream.md).
164+
163165
**My Google search shows up without JavaScript.** The Apps Script `User-Agent` is fixed to `Google-Apps-Script` (Google won't let scripts change it), so some sites serve a no-JS fallback. Workaround: add the affected domain to your `hosts` map so it goes through the SNI-rewrite tunnel with your real browser User-Agent. `google.com`, `youtube.com`, `fonts.googleapis.com` are already on this list by default.
164166

165167
**More questions:** [full FAQ in the long guide](docs/guide.md#faq).
@@ -330,6 +332,8 @@ System Settings → Network → Wi-Fi → Details → **Proxies** → هر دو
330332

331333
**ISP خود `script.google.com` را مسدود کرده.** mhrv-rs یک حالت `direct` دارد که فقط از تونل بازنویسی SNI استفاده می‌کند (بدون Apps Script). یک‌بار از این حالت استفاده کن تا به `script.google.com` برسی و اسکریپت را دیپلوی کنی، بعد به حالت apps_script سوئیچ کن. [حالت direct](docs/guide.fa.md#حالت-direct).
332334

335+
**می‌خواهم از mhrv-rs به‌عنوان پروکسی upstream برای Psiphon (یا xray) استفاده کنم.** mhrv-rs را در حالت `direct` اجرا کن و در تنظیمات Psiphon قسمت *upstream proxy* را روی host:port که زیر دکمهٔ Connect نمایش داده می‌شود تنظیم کن. هاست‌هایی که در لیست fronting قرار ندارند به‌صورت raw TCP عبور می‌کنند، پس ترافیک bootstrap سایفون به سرورهای سایفون دست‌نخورده می‌رسد. [docs/use-as-upstream.fa.md](docs/use-as-upstream.fa.md).
336+
333337
**جست‌وجوی گوگلم بدون JavaScript ظاهر می‌شود.** `User-Agent` Apps Script ثابت روی `Google-Apps-Script` است (گوگل نمی‌گذارد اسکریپت‌ها عوضش کنند)، پس بعضی سایت‌ها نسخهٔ بدون JS برمی‌گردانند. راه‌حل: دامنهٔ مورد نظر را به `hosts` اضافه کن تا از تونل بازنویسی SNI با User-Agent واقعی مرورگرت برود. `google.com`، `youtube.com`، `fonts.googleapis.com` به‌طور پیش‌فرض در این لیست‌اند.
334338

335339
**سؤالات بیشتر:** [FAQ کامل در راهنمای بلند](docs/guide.fa.md#سؤالات-رایج).

android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,77 @@ fun HomeScreen(
337337
)
338338
}
339339

340+
// Upstream-proxy hint: while running in Direct mode, surface the
341+
// local listen port with a one-tap copy so users wiring this up
342+
// as an upstream don't have to dig through config. Direct is the
343+
// only mode that makes sense here — apps_script and full try to
344+
// relay everything via Apps Script, which breaks binary protocols
345+
// like Psiphon's. See docs/use-as-upstream.md.
346+
//
347+
// Gating on PROXY_ONLY: Android allows only one active VPN at a
348+
// time. If mhrv-rs is running its own VpnService (VPN_TUN mode),
349+
// Psiphon cannot establish its own VPN and the upstream address
350+
// here is not actionable. Show the warning copy first so the
351+
// user knows to stop, flip to PROXY_ONLY, and Connect again
352+
// before pasting the address into Psiphon.
353+
//
354+
// Vertical Column instead of horizontal Row: in Persian and at
355+
// larger font sizes the label + monospace address + copy button
356+
// would cramp into a single line. Stacking lets each part wrap
357+
// naturally and keeps the copy button reachable.
358+
if (isVpnRunning && cfg.mode == Mode.DIRECT) {
359+
Spacer(Modifier.height(8.dp))
360+
val clipboard = LocalClipboardManager.current
361+
val ctx = LocalContext.current
362+
val upstreamPort = cfg.listenPort
363+
val upstream = "127.0.0.1:$upstreamPort"
364+
val copiedMsg = stringResource(R.string.snack_upstream_copied, upstream)
365+
val proxyOnly = cfg.connectionMode == ConnectionMode.PROXY_ONLY
366+
Column(
367+
modifier = Modifier.fillMaxWidth(),
368+
verticalArrangement = Arrangement.spacedBy(4.dp),
369+
) {
370+
if (!proxyOnly) {
371+
Text(
372+
stringResource(R.string.direct_upstream_vpn_tun_warning),
373+
style = MaterialTheme.typography.labelSmall,
374+
color = ErrRed,
375+
)
376+
}
377+
Text(
378+
stringResource(R.string.direct_upstream_label),
379+
style = MaterialTheme.typography.labelSmall,
380+
color = MaterialTheme.colorScheme.onSurfaceVariant,
381+
)
382+
Row(
383+
modifier = Modifier.fillMaxWidth(),
384+
verticalAlignment = Alignment.CenterVertically,
385+
) {
386+
SelectionContainer(modifier = Modifier.weight(1f)) {
387+
Text(
388+
upstream,
389+
style = MaterialTheme.typography.bodyMedium.copy(
390+
fontFamily = FontFamily.Monospace,
391+
),
392+
color = if (proxyOnly) OkGreen else MaterialTheme.colorScheme.onSurfaceVariant,
393+
)
394+
}
395+
TextButton(
396+
onClick = {
397+
clipboard.setText(AnnotatedString(upstream))
398+
Toast.makeText(ctx, copiedMsg, Toast.LENGTH_SHORT).show()
399+
},
400+
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
401+
) {
402+
Text(
403+
stringResource(R.string.btn_copy_lower),
404+
style = MaterialTheme.typography.labelMedium,
405+
)
406+
}
407+
}
408+
}
409+
}
410+
340411
Spacer(Modifier.height(4.dp))
341412

342413
val appsScriptEnabled = cfg.mode == Mode.APPS_SCRIPT || cfg.mode == Mode.FULL
@@ -901,6 +972,13 @@ private fun ModeDropdown(
901972
style = MaterialTheme.typography.labelSmall,
902973
color = MaterialTheme.colorScheme.onSurfaceVariant,
903974
)
975+
if (mode == Mode.DIRECT) {
976+
Text(
977+
stringResource(R.string.direct_upstream_help),
978+
style = MaterialTheme.typography.labelSmall,
979+
color = MaterialTheme.colorScheme.onSurfaceVariant,
980+
)
981+
}
904982
}
905983
}
906984

android/app/src/main/res/values-fa/strings.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@
9595
<string name="btn_view_quota_on_google">مشاهدهٔ سهمیه در گوگل ←</string>
9696
<string name="usage_today_note">تخمینی — این همان چیزی است که از این دستگاه رد شده. عدد دقیق در داشبورد گوگل قابل مشاهده است.</string>
9797

98+
<!-- Direct mode upstream-proxy banner (see docs/use-as-upstream.fa.md). -->
99+
<string name="direct_upstream_help">به‌عنوان پروکسی upstream برای سایفون / xray هم کار می‌کند — هاست‌های fronted نشده به‌صورت TCP خام عبور می‌کنند.</string>
100+
<string name="direct_upstream_label">آدرس upstream برای سایفون / xray:</string>
101+
<string name="direct_upstream_vpn_tun_warning">سایفون به تنها اسلات VPN اندروید نیاز دارد. mhrv-rs را متوقف کن، Connection mode را به PROXY_ONLY تغییر بده، بعد دوباره Connect را بزن — تنها در این حالت سایفون می‌تواند از آدرس زیر استفاده کند.</string>
102+
<string name="btn_copy_lower">کپی</string>
103+
<string name="snack_upstream_copied">%1$s کپی شد</string>
104+
98105
<!-- "How to use" guide body. Localized — EN copy lives in values. -->
99106
<string name="help_how_to_use">۱. یک یا چند آدرس deployment از Apps Script (یا فقط ID خام) و همراه آن auth_key خود را جای‌گذاری کنید.\n۲. روی «نصب گواهی MITM» بزنید و پیام تأیید را قبول کنید — گواهی در Downloads/mhrv-ca.crt ذخیره می‌شود و برنامهٔ Settings باز می‌شود. داخل Settings از نوار جست‌وجو «CA certificate» را پیدا کنید و روی همان نتیجه بزنید (نه «VPN &amp; app user certificate» و نه «Wi-Fi»)، سپس mhrv-ca.crt را از Downloads انتخاب کنید. اگر قفل صفحه ندارید، اندروید می‌خواهد یکی تنظیم کنید (الزام سیستم).\n۳. قبل از Start، بخش «مجموعهٔ SNI + تستر» را باز کنید و «تست همه» را بزنید. اگر همه تایم‌اوت شدند یعنی google_ip در دسترس نیست — آن را با یک IP جایگزین کنید که روی شبکهٔ سالم resolve می‌شود (مثلاً `nslookup www.google.com` روی هر دستگاه سالم).\n۴. Start را بزنید و درخواست VPN را تأیید کنید. پل TUN کامل، تمام برنامه‌های دستگاه را خودکار از پروکسی رد می‌کند — نیاز به تنظیم per-app نیست.\n۵. اگر Chrome پیام «504 Relay timeout» نشان داد: deployment شما پاسخ نمی‌دهد. اسکریپت را دوباره deploy کنید، URL جدید /exec را بگیرید و بالا جای‌گذاری کنید. در «لاگ زنده» ببینید خطا از نوع «Relay timeout» است یا «connect:» — نوع خطا مشخص می‌کند کدام لایه مقصر است.\n\nمحدودیت شناخته‌شده — Cloudflare Turnstile («Verify you are human») روی اکثر سایت‌های پشت Cloudflare به‌طور بی‌پایان loop می‌زند. هر درخواست Apps Script از یک IP خروجی چرخشی دیتاسنتر گوگل + یک User-Agent ثابت «Google-Apps-Script» + اثرانگشت TLS گوگل عبور می‌کند. کوکی cf_clearance به tuple (IP, UA, JA3) مربوط به زمان حل چالش گره خورده است، پس درخواست بعدی — از یک IP خروجی متفاوت — دوباره چالش می‌خورد. این مسئله در این برنامه قابل‌حل نیست؛ ذات رلهٔ Apps Script است. سایت‌هایی که فقط بارگذاری اولیه را gate می‌کنند (نه هر درخواست) بعد از یک بار حل، کار خواهند کرد.</string>
100107
</resources>

android/app/src/main/res/values/strings.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@
113113
<string name="btn_view_quota_on_google">View quota on Google →</string>
114114
<string name="usage_today_note">Estimate — this is what this device relayed. Google\'s dashboard has the authoritative number.</string>
115115

116+
<!-- Direct mode upstream-proxy banner (see docs/use-as-upstream.md). -->
117+
<string name="direct_upstream_help">Also works as an upstream proxy for Psiphon / xray — unfronted hosts pass through as raw TCP.</string>
118+
<string name="direct_upstream_label">Upstream for Psiphon / xray:</string>
119+
<string name="direct_upstream_vpn_tun_warning">Psiphon needs Android\'s single VPN slot. Stop mhrv-rs, switch Connection mode to PROXY_ONLY, then Connect again — only then can Psiphon use the address below.</string>
120+
<string name="btn_copy_lower">copy</string>
121+
<string name="snack_upstream_copied">Copied %1$s</string>
122+
116123
<!-- "How to use" guide body. Localized — FA copy lives in values-fa. -->
117124
<string name="help_how_to_use">1. Paste one or more Apps Script deployment URLs (or bare IDs) and your auth_key.\n2. Tap Install MITM certificate. Confirm the dialog — the cert is saved to Downloads/mhrv-ca.crt and the Settings app opens. Use Settings\' search bar to find \"CA certificate\", tap that result (NOT \"VPN &amp; app user certificate\" or \"Wi-Fi\"), and pick mhrv-ca.crt from Downloads. You\'ll be asked to set a screen lock if you don\'t have one (Android requirement).\n3. Before tapping Start, expand \"SNI pool + tester\" and hit \"Test all\". If every entry times out, your google_ip is unreachable — replace it with one that resolves locally (e.g. `nslookup www.google.com` on any working device).\n4. Tap Start. Accept the VPN prompt. The full TUN bridge routes every app on the device through the proxy — no per-app setup needed.\n5. If Chrome shows \"504 Relay timeout\": your Apps Script deployment isn\'t responding. Redeploy the script, grab the new /exec URL, and paste it above. Watch Live logs for \"Relay timeout\" vs \"connect:\" errors to tell which layer is failing.\n\nKnown limitation — Cloudflare Turnstile (\"Verify you are human\") will loop endlessly on most CF-protected sites. Every Apps Script request uses a rotating Google-datacenter egress IP + a fixed \"Google-Apps-Script\" User-Agent + a Google TLS fingerprint. The cf_clearance cookie is bound to the (IP, UA, JA3) tuple the challenge was solved against, so the NEXT request — from a different egress IP — gets re-challenged. Nothing in this app can fix that; it\'s inherent to Apps Script as a relay. Sites that only gate the initial page load (not every request) will work after one solve.</string>
118125
</resources>

docs/use-as-upstream.fa.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<div dir="rtl">
2+
3+
# استفاده از mhrv-rs به‌عنوان پروکسی upstream (سایفون، xray، مرورگرها)
4+
5+
نسخهٔ انگلیسی: [docs/use-as-upstream.md](use-as-upstream.md).
6+
7+
به‌طور پیش‌فرض، mhrv-rs یک پروکسی HTTP محلی روی `127.0.0.1:8085` و یک پروکسی SOCKS5 روی `127.0.0.1:8086` در دسترس قرار می‌دهد (پیش‌فرض اندروید: HTTP `8080` و SOCKS5 `1081`). هر ابزاری که تنظیم upstream proxy داشته باشد می‌تواند از این پروکسی عبور کند.
8+
9+
حالت رایج: سرورهای bootstrap سایفون مسدودند، پس upstream proxy سایفون را روی mhrv-rs می‌گذاری تا اولین hop سایفون از طریق تونل SNI-fronting ما به شبکه‌اش برسد.
10+
11+
## از حالت `direct` استفاده کن
12+
13+
حالت‌های `apps_script` و `full` تلاش می‌کنند هر هاست را از رلهٔ Apps Script عبور دهند، که با پروتکل باینری سایفون سازگار نیست. حالت `direct` رله را کنار می‌گذارد: SNI-rewrite برای هاست‌هایی که mhrv-rs می‌شناسد، TCP خام برای بقیه. این دقیقاً همان چیزی است که سایفون نیاز دارد — رمزنگاری end-to-end خودش دست‌نخورده می‌ماند و cert pinning نمی‌شکند.
14+
15+
در رابط دسکتاپ / برنامهٔ اندروید گزینهٔ **Direct (no relay)** را انتخاب کن، یا در کانفیگ بگذار:
16+
17+
<div dir="ltr">
18+
19+
```jsonc
20+
{
21+
"mode": "direct",
22+
"listen_host": "127.0.0.1",
23+
"listen_port": 8085,
24+
"socks5_port": 8086
25+
}
26+
```
27+
28+
</div>
29+
30+
## سایفون — ویندوز / مک / لینوکس
31+
32+
۱. برنامهٔ mhrv-rs را در حالت `direct` اجرا کن. host:port زیر دکمهٔ Start نمایش داده می‌شود — روی **copy** بزن.
33+
34+
۲. در سایفون این مسیر را باز کن: **Options****Proxy settings****Upstream proxy**.
35+
36+
۳. تیک **Connect through an upstream proxy** را بزن.
37+
38+
۴. در فیلد **Hostname** مقدار `127.0.0.1`، در **Port** مقدار `8085` و در **Type** گزینهٔ `HTTP` را انتخاب کن. (یا SOCKS5 روی پورت `8086`.)
39+
40+
۵. روی **Save** بزن، بعد در سایفون **Connect** را بزن.
41+
42+
## سایفون — اندروید
43+
44+
اندروید همزمان فقط یک VPN فعال اجازه می‌دهد و سایفون به این اسلات نیاز دارد. قبل از شروع: برنامهٔ mhrv-rs را باز کن و در بخش Network مقدار **Connection mode** را روی **PROXY_ONLY** بگذار. در این حالت mhrv-rs فقط پروکسی محلی را اجرا می‌کند و اسلات VPN را برای سایفون آزاد می‌گذارد.
45+
46+
۱. در mhrv-rs ابتدا Connection mode را روی `PROXY_ONLY` بگذار، سپس حالت `Direct` را انتخاب کن و **Connect** را بزن. host:port زیر دکمهٔ Connect نمایش داده می‌شود — روی **copy** بزن.
47+
48+
۲. در برنامهٔ سایفون این مسیر را باز کن: **Options****Proxy****Upstream proxy**.
49+
50+
۳. در فیلد **Host** مقدار `127.0.0.1` و در **Port** مقدار `8080` (برای HTTP) یا `1081` (برای SOCKS5) را وارد کن.
51+
52+
۴. در سایفون **Connect** را بزن.
53+
54+
## تنظیم xray / v2ray
55+
56+
یک outbound از نوع `http` (یا `socks`) اضافه کن که به mhrv-rs اشاره کند:
57+
58+
<div dir="ltr">
59+
60+
```jsonc
61+
{
62+
"outbounds": [
63+
{
64+
"tag": "proxy",
65+
"protocol": "http",
66+
"settings": {
67+
"servers": [
68+
{ "address": "127.0.0.1", "port": 8085 }
69+
]
70+
}
71+
}
72+
]
73+
}
74+
```
75+
76+
</div>
77+
78+
## مرورگرها / SwitchyOmega
79+
80+
پروکسی را روی `127.0.0.1:8085` تنظیم کن. چیز دیگری لازم نیست.
81+
82+
## رفع اشکال
83+
84+
- **سایفون روی «در حال اتصال…» می‌ماند** — مطمئن شو mhrv-rs در حالت `direct` است و پورت با چیزی که در سایفون وارد کردی یکی است. در پنل log اخیر رابط mhrv-rs هر CONNECT را که می‌بیند نمایش می‌دهد؛ باید هاست‌های سایفون را آنجا با برچسب `raw-tcp (direct mode: no relay)` ببینی.
85+
86+
- **یک هاست خاص MITM می‌شود در حالی که نمی‌خواهی** — آن را به `passthrough_hosts` در `config.json` اضافه کن. این لیست بر همهٔ تصمیم‌های دیگر dispatch اولویت دارد.
87+
88+
- **برعکس زنجیر کن (outbound از mhrv-rs از طریق سایفون یا xray)** — در `config.json` فیلد `upstream_socks5` را روی پورت SOCKS5 محلی آن ابزار تنظیم کن. flowهای raw-TCP / passthrough از آنجا خارج می‌شوند. ترافیک رلهٔ Apps Script طبق طراحی همچنان از edge گوگل می‌گذرد.
89+
90+
## همچنین ببین
91+
92+
- راهنمای [fronting-groups.md](fronting-groups.md) — افزودن CDNهای غیرگوگلی (Vercel، Fastly، Netlify) به مسیر SNI-rewrite.
93+
- مرجع کامل [حالت direct در راهنمای کامل](guide.fa.md#حالت-direct).
94+
95+
</div>

0 commit comments

Comments
 (0)