Skip to content

feat(direct): use mhrv-rs as upstream proxy for Psiphon / xray#1189

Open
dazzling-no-more wants to merge 2 commits into
therealaleph:mainfrom
dazzling-no-more:feat/upstream-proxy-for-psiphon
Open

feat(direct): use mhrv-rs as upstream proxy for Psiphon / xray#1189
dazzling-no-more wants to merge 2 commits into
therealaleph:mainfrom
dazzling-no-more:feat/upstream-proxy-for-psiphon

Conversation

@dazzling-no-more
Copy link
Copy Markdown
Contributor

Summary

Adds a documented "use mhrv-rs as upstream proxy" path for Psiphon, xray, SwitchyOmega, etc. Direct mode already does the right thing technically — unmatched hosts fall through to plain TCP — but nothing surfaced this to users. Now the UI shows the copyable upstream address when running in Direct mode, and a new doc page walks through Psiphon setup on every platform.

Why

A few projects in the censorship-circumvention space already document this pairing on Windows: patterniha/MITM-DomainFronting (Xray config) and B3hnamR/PsiphonOverMITM (PowerShell launcher that bundles xray.exe + that config). The Persian-speaking user base in particular has been chaining them manually — set Psiphon's upstream proxy to the local Xray, so Psiphon's bootstrap traffic reaches its servers via fronted SNI.

We already implement everything they do (MITM + domain fronting in direct mode) — just nobody knew. This PR closes the discoverability gap, cross-platform and without bundling anything.

What's in this PR

Desktop UI (src/bin/ui.rs)

  • Hint under the Direct mode dropdown: "Also works as upstream proxy for Psiphon / xray".
  • When running in Direct mode, a copyable banner below the Start button shows the listen address (127.0.0.1:8085 [copy]).
  • Wildcard binds (0.0.0.0, [::], empty) collapse to 127.0.0.1 in the copy text so Windows users don't paste an address Winsock will reject as a connect target.
  • When bound on all interfaces, a second line shows the LAN IP (from detect_lan_ip()) for pasting into Psiphon on a different device.

Shared helper (src/lan_utils.rs)

  • New advertise_proxy_host() that normalizes wildcard / empty binds to 127.0.0.1.
  • 1 new unit test covering wildcard, IPv6 wildcard, empty, loopback, and explicit-LAN inputs (4 lan_utils tests passing total).

Android UI (HomeScreen.kt, strings.xml, values-fa/strings.xml)

  • Same hint + banner, fully localized via stringResource (5 new keys in each strings.xml).
  • Banner layout uses a Column instead of a Row so the label/address/copy stack vertically — survives Persian text and large font sizes without cramping.
  • Gated on Connection mode: the actionable banner only renders the address in green when connectionMode == PROXY_ONLY. In VPN_TUN mode (the default), a red warning appears explaining that Android allows only one active VPN slot and Psiphon needs it — user has to stop mhrv-rs, switch to PROXY_ONLY, and reconnect first.

Docs

  • New docs/use-as-upstream.md + Persian mirror docs/use-as-upstream.fa.md.
  • Both lead with the PROXY_ONLY caveat in the Android section so the setup sequence is impossible to misread.
  • One new FAQ entry in README.md (English + Persian) pointing at the new doc.

Engineering note

No new transport, no new Mode variant, no protocol change. Direct mode's dispatch in src/proxy_server.rs already routes unmatched hosts to plain_tcp_passthrough, which is exactly what Psiphon's bootstrap traffic needs — its end-to-end crypto stays intact and cert pinning isn't broken. This PR is UX + docs only.

Test plan

  • cargo check --all --features ui — clean
  • cargo test --lib lan_utils — 4 passed (advertise_proxy_host_collapses_wildcards_to_loopback new)
  • ./gradlew :app:compileDebugKotlin — clean
  • Manual (Windows): mhrv-rs in Direct mode → click copy on the upstream banner → paste into Psiphon's Upstream Proxy field → Psiphon connects through mhrv-rs
  • Manual (Windows, share-on-LAN enabled): verify the LAN IP line appears as a second copyable address
  • Manual (Android, VPN_TUN mode): verify the red warning appears and the address is greyed out
  • Manual (Android, PROXY_ONLY mode): verify the address renders green and copy works
  • Persian render check: open docs/use-as-upstream.fa.md on GitHub, confirm every numbered item renders RTL (first strong character of each item is a Persian letter, not a Latin word)

Persian rendering rule (for future reviewers)

The Unicode bidi algorithm picks paragraph direction from the first strong character. Persian numerals (۱ ۲ ۳), markdown markers (**, [, #), and bullets are all bidi-weak. A Persian list item like ۱. mhrv-rs را باز کن reads its first strong char as m in mhrv-rs and flips LTR (visibly broken). Lead each Persian paragraph or list item with a Persian word — never a product name or English UI label.

Credits

The Psiphon-upstream chain pattern documented here was already practiced in the community via patterniha/MITM-DomainFronting and packaged for Windows by B3hnamR/PsiphonOverMITM. This PR makes the same workflow work natively, on every platform mhrv-rs already supports, without bundling an external engine.

@github-actions github-actions Bot added the type: feature feat: PR — auto-applied by release-drafter label May 14, 2026
dazzling-no-more added a commit to dazzling-no-more/rahgozar that referenced this pull request May 15, 2026
dazzling-no-more added a commit to dazzling-no-more/rahgozar that referenced this pull request May 15, 2026
- ConfigStore.toJson(): drop a duplicate `fronting_groups` put left
  behind when therealaleph#1033 (fronting groups) + therealaleph#1057 (extras passthrough) +
  therealaleph#1189 (draft-drop filter) were squash-merged in sequence. The
  later filtered put was being overwritten by the older unfiltered
  one, breaking the ConfigStoreFrontingGroupsTest expectation that
  draft groups never reach disk.
- ProfileStoreTest.applyProfile_*_returns_partial: the snapshot
  config was missing a deployment ID, so validateRuntimeShape
  rejected it as 'apps_script mode requires script_id' before the
  test's injected write failure could be exercised. Pre-existing
  bug in therealaleph#1057's tests; only surfaced now that we run the full
  suite on the integrated branch.
@therealaleph
Copy link
Copy Markdown
Owner

Reviewed via Anthropic Claude. Big PR — 2591 lines across desktop UI, Android UI, JNI, config, new cdn_discover.rs module, plus bilingual docs. Read through the description + structural diff. Substantive feature.

Strong yes on the core idea. The Iranian community has been chaining mhrv-rs + Psiphon/xray manually for months (see patterniha/MITM-DomainFronting + B3hnamR/PsiphonOverMITM on Windows). Direct mode already does the right thing technically; this PR closes the discoverability gap with proper UI surfacing + cross-platform docs. That's a real win.

Things I want to verify before merging (asking for some answers rather than blocking):

1. cdn_discover.rs (670 lines) — what does it do? The diff mentions cdn_discover and DiscoverParserTest.kt. Is this:

  • (a) A static catalog of known fronting CDN edges baked into the binary, used at runtime to suggest IP/SNI pairs to users? OR
  • (b) Active probing that does outbound DNS / TCP / TLS handshake checks to discover working fronting endpoints?

Option (a) is fine. Option (b) is a network-behavior change worth flagging — outbound probes from a censorship-circumvention tool can leak side-channel signals on hostile networks. If active probing, would want it opt-in via config and disabled-by-default.

2. UI banner + LAN IP surfacing — confirm the LAN IP is only shown when share_on_lan: true (or equivalent wildcard bind). Iranian users sometimes screenshot the UI for support questions; we don't want LAN IPs leaking in those screenshots when the user didn't intend to share.

3. Android HomeScreen.kt (408 lines) — Android UI surface for the upstream-proxy flow. Will need a smoke test on emulator to confirm no regressions (Android UI batch has a history of layout regressions — see #1015 path).

4. Default share_on_lan for upstream-proxy use case — when a user enables Direct mode + intends to use it as upstream for Psiphon, does the UI default to 127.0.0.1 bind (safer) or 0.0.0.0 (LAN-shareable)? If Psiphon is on the same machine, 127.0.0.1 is enough; default-public would be a footgun.

Plan:

Could you respond to (1)-(4) above + add the smoke-test checklist to the PR body? That's enough for me to land it.

Re: the bundled docs (docs/use-as-upstream.md + .fa.md) — those are clean wins on their own. Even if we delay the UI changes for review, I'd merge those docs separately if you want.


[reply via Anthropic Claude | reviewed by @therealaleph]

@therealaleph
Copy link
Copy Markdown
Owner

Follow-up verification from today's DOPR:

  • cargo test --lib passed locally: 249 tests.
  • cargo check --all --features ui passed locally.
  • cargo build --release passed locally.
  • Android verification could not run on this machine because there is no Java runtime available (Unable to locate a Java Runtime from both :app:testDebugUnitTest and :app:compileDebugKotlin).

I also checked the earlier review questions against the current diff:

  • cdn_discover.rs is active probing, but it is opt-in from the Discover button / Native.discoverFront; it is not a background probe.
  • Desktop LAN address surfacing is gated behind wildcard/LAN bind (is_share_on_lan), while the same-device copy target is normalized to 127.0.0.1.
  • The Android upstream-proxy banner is gated to Direct mode and warns when VPN_TUN is active.

Given the size of this PR and the previous note about a 5-7 day community-testing window, I am leaving it open rather than merging today. The Rust side looks healthy; the remaining risk is Android UI/build verification plus real user testing for Psiphon/xray chaining.


[reply via Anthropic Claude | reviewed by @therealaleph]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature feat: PR — auto-applied by release-drafter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants