The messense/rust-musl-cross:mipsel-musl image ships a pre-installed
nightly Rust in a state rustup can't cleanly upgrade over — the
in-place upgrade errors with
error: failure removing component 'clippy-preview-x86_64-unknown-linux-gnu',
directory does not exist: 'share/doc/clippy/README.md'
because the prior install is missing files rustup expects to delete.
Workaround: uninstall nightly first, then install fresh with the
minimal profile, then add rust-src as a separate step.
continue-on-error keeps this experimental target from blocking the
release — landing the fix on main so the NEXT tag attempt gets a
working mipsel artifact, without spinning up yet another retag dance
for v1.1.0.
The image tag I assumed existed (`mipsel-musl-softfloat`) isn't
published by messense; docker-pull errored with "manifest unknown".
Available mipsel tags are just `mipsel-musl` (hardfloat) and the
regional mirrors of same.
Fix: use `messense/rust-musl-cross:mipsel-musl` (the standard
hardfloat image) and force soft-float code generation via
`RUSTFLAGS=-C target-feature=+soft-float` on top of the nightly
`-Z build-std=std,panic_abort` we were already using. build-std
recompiles libstd with the same RUSTFLAGS, so libstd itself comes
out soft-float even though the image's gcc/musl is hardfloat. We
don't link anything beyond libc for mhrv-rs (ring is pure-asm for
the crypto hot paths), so the fact that musl libm isn't soft-float
doesn't bite us.
Net result: the binary emits no hardware FP instructions, which is
what MT7621 actually needs.
The v1.1.0 CI telegram job failed with curl exit 26 "Failed to
open/read local data from file/application" because:
-F "caption=<b>mhrv-rs Android v1.1.0</b>..."
curl's -F treats a value starting with `<` as "read from file
named ..." (the canonical way to put file CONTENTS into a text
form field). Our HTML captions start with `<b>`, so curl tried
to open a file named `b>mhrv-rs Android v1.1.0</b>...`, failed,
and the whole job went red.
Rewrote the step in Python (`.github/scripts/telegram_release_notify.py`).
stdlib urllib + http.client have no such value-interpretation
wart. Also:
- uses `application/vnd.android.package-archive` content-type
so Telegram shows the APK with an Android-package label, not
generic octet-stream
- proper sha256 hash (streaming, not shell-piped)
- consolidated the two shell-script HEREDOCs that were parsing
the changelog into one place
- clean exit codes: "no changelog file" and "no secrets" both
exit 0, a broken Telegram response exits non-zero
No behaviour change for callers — the workflow just calls the
script with the same four inputs.
New `telegram:` job in release.yml downloads the Android artifact
uploaded by the `android:` job, posts the APK with a short caption
(Telegram caps captions at 1024 chars, we blow past that), then
replies with the full changelog in two quote blocks — Persian first,
English second — matching the format the user wants.
Changelog content lives in `docs/changelog/v<tag>.md`. The file has
a comment header explaining the format, then:
- Persian bullets
- a bare `---` separator line
- English bullets
The workflow splits on that separator. No emojis. Missing changelog
file = the reply is skipped (doc post still lands).
Telegram credentials come from repo secrets:
TELEGRAM_BOT_TOKEN (set)
TELEGRAM_CHAT_ID (set)
Missing either = job logs a notice and returns 0. A forker who hasn't
set up Telegram gets a clean release with no notify attempt.
Also includes v1.1.0's changelog file so the first run of this job
has something to post.
v1.1.0 CI run (24820831905) failed at the dtolnay/rust-toolchain@stable
step for the mipsel-unknown-linux-musl matrix entry: that target is
tier-3 in stable Rust and `rust-std` isn't available via rustup, so
the `targets:` parameter errors out with
error: component 'rust-std' for target 'mipsel-unknown-linux-musl'
is unavailable for download
before the docker step that actually builds it ever runs. The docker
image we use (`messense/rust-musl-cross:mipsel-musl-softfloat`) bundles
its own Rust + std, so the host-level rustup isn't needed here at all.
Gated by `matrix.mipsel_softfloat != true` so the other 8 targets still
install their toolchain the normal way. `continue-on-error: true` on
the job level already prevented this from blocking the release, but
the failure was noisy and confusing — fix makes the whole matrix green.
Pushed as a small follow-up to v1.1.0 so the re-tag picks it up.
Major feature release across Android + desktop. Six items the user
asked for, verified end-to-end on the emulator.
Android
-------
* Unified Connect/Disconnect button. Single large button swaps
between green "Connect" (when the service is down) and red
"Disconnect" (when it's up). Tracks the real service state via a
new process-wide `VpnState` singleton flipped from the service's
startEverything() / teardown() — not optimistic, the button only
reports what the service actually did.
* Connection mode dropdown (issue #37). Two options: VPN (TUN) —
routes every app — and Proxy only — user configures per-app via
Wi-Fi proxy to 127.0.0.1:8080 (HTTP) / :1081 (SOCKS5). PROXY_ONLY
skips VpnService.prepare() entirely (no OS VPN grant prompt) and
the service just keeps the foreground listeners up. Default is
VPN_TUN so existing behaviour is preserved for users who upgrade
without looking at the dropdown.
* App splitting. In VPN_TUN mode you can pick All / Only selected /
All except selected, with a picker dialog that lists installed
user-visible apps (LazyColumn with search, "show system apps"
toggle, multi-select checkboxes). ONLY calls
`Builder.addAllowedApplication()` for each chosen package;
EXCEPT calls `addDisallowedApplication()` additive to the
mandatory self-exclude. Requires QUERY_ALL_PACKAGES — added to
the manifest along with a `<queries>` launcher-intent filter so
the picker rows can render app labels, not just package strings.
* Persian/English UI toggle with RTL. Top-bar TextButton cycles
AUTO → FA → EN → AUTO. Persian strings live in
`res/values-fa/strings.xml`; English in `res/values/strings.xml`.
`AppCompatDelegate.setApplicationLocales()` is used as the
persistence layer (plus `AppLocalesMetadataHolderService` meta
and `locales_config.xml` for the per-app-language OS entry on
API 33+). MainActivity overrides `attachBaseContext` to wrap the
context with the right locale at the earliest possible moment —
otherwise a saved preference wouldn't apply until the SECOND
process after toggling. RTL swaps automatically because Persian
is script="Arab" in Android's locale database.
* Collapsible How-to-use card. The big instruction block that used
to dominate the bottom of the screen now lives inside a
CollapsibleSection that starts expanded for a fresh install
(empty deployment URLs / auth_key) and collapsed otherwise.
* Update check auto-fires on first composition, silent-on-up-to-date,
snackbar-only-if-available. Still surfaces via the version badge
tap for manual checks.
* MhrvVpnService teardown guard was kept from v1.0.2 —
`AtomicBoolean` makes the second caller a no-op, which is the
SIGSEGV fix for "tap Stop, app closes" from before. Stress-tested
under rapid Connect/Disconnect cycles.
Desktop
-------
* Fix: Advanced section silently resetting on every Save. `ConfigWire`
was missing `fetch_ips_from_api` / `max_ips_to_scan` /
`scan_batch_size` / `google_ip_validation` — every persist dropped
them, every reload fell back to the serde defaults, user saw their
Advanced toggles reset. Added the fields to the wire struct (issue
surfaced by the user as "Advanced resets after reopening the app").
* Windows renderer fallback (issue #28). `eframe` is now built with
BOTH `glow` (OpenGL 2+) and `wgpu` (DX12/Vulkan/Metal); runtime
defaults to glow for compat but honours `MHRV_RENDERER=wgpu` for
boxes that crash with "egui_glow requires opengl 2.0+" — old
Windows hardware, RDP sessions, VMs without GPU acceleration.
`run.bat` auto-retries the UI with `MHRV_RENDERER=wgpu` if the
first launch exits non-zero, so users don't need to know about
the flag.
CI
--
* Added OpenWRT mipsel-softfloat build target (issue #26). MT7621
routers specifically need soft-float because the CPU has no FPU;
a hard-float binary segfaults on first fp op. Built via
`messense/rust-musl-cross:mipsel-musl-softfloat` docker image +
nightly Rust with `-Z build-std` (mipsel is Rust tier 3 since
1.72, no pre-built std). Marked `continue-on-error: true` — the
tier-3 target occasionally regresses and we'd rather ship the
rest of the release than block on MT7621 support.
Signature / versioning
----------------------
* versionCode 110, versionName 1.1.0; Cargo bumped to 1.1.0.
* Release APK signed with the committed `release.jks` (same as
v1.0.2), so v1.0.2 → v1.1.0 upgrades install in-place without
the uninstall-first dance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version bump reflects the scope — a unified Rust core that now ships
for desktop (Linux/macOS/Windows) AND Android from the same crate.
Android changes:
- build.gradle.kts: ABI filters expanded to arm64-v8a + armeabi-v7a
+ x86_64 + x86. cargoBuild{Debug,Release} pass all four ABIs to
cargo-ndk in a single invocation. normalizeTun2proxySo() walks every
ABI dir now (was arm64-only).
- Release buildType signs with the debug keystore — no Play Store
target, so signature identity doesn't matter, installability does.
Gradle auto-provisions ~/.android/debug.keystore if absent, so CI
runners inherit this without extra setup.
- versionName 1.0.0, versionCode 100 (room to bump monotonically).
CI:
- release.yml gets a dedicated `android:` job that sets up JDK 17,
Android SDK/NDK 26, all four rust-android targets, installs
cargo-ndk, runs assembleRelease, and uploads a single universal APK
named `mhrv-rs-android-universal-v<version>.apk` into the same
`dist/` collected by the release job downstream.
- `release:` job now gates on `needs: [build, android]` so tagging
v1.0.0 triggers both build matrices before cutting the GitHub
release.
Docs:
- docs/android.md — full 10-step install walk-through: APK sideload,
Apps Script deployment (with "Advanced → Go to (unsafe) → Allow"
reality check), config paste, SNI reachability test, MITM CA
install with OEM-specific nav paths (Pixel / Samsung / Xiaomi),
Start, troubleshooting common failure modes. Also documents the
known limitations — Cloudflare Turnstile loops (inherent to the
Apps Script egress IP pool), UDP/QUIC not tunnelled, IPv6 leaks,
Apps Script daily quota — so users know what to expect before
trying it on a site that won't work.
- releases/README.md — APK row added to the English and Persian
tables, version bumped everywhere to v1.0.0.
- Top-level README — Android listed under Platforms with a link
to docs/android.md.
Release artifact:
- releases/mhrv-rs-android-universal-v1.0.0.apk — 38 MB universal
APK built locally from this tree. Installs + launches on API 24+.
The CI job will regenerate it on tag push; this is the copy
committed for users who can't reach GitHub Releases.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
=== PR #14 follow-up: armhf build runs on Pi Bookworm/Bullseye ===
PR #14 (merged earlier) added arm-unknown-linux-gnueabihf to the
release matrix but pinned os=ubuntu-latest, which is 24.04 with GLIBC
2.39. Target armhf sysroot on 24.04 is Debian Trixie (GLIBC 2.39),
far too new for a Raspberry Pi 2/3 on Bookworm (2.36) or Bullseye
(2.31) — users would get 'GLIBC_2.39 not found' the same way the
Linux-amd64 issue #2 folks did before we pinned them to 22.04.
Fix: pin the armhf matrix entry to ubuntu-22.04, matching our other
linux-gnu targets. Binary will link against GLIBC 2.35 max, which
loads on Pi Bookworm and Bullseye. Also trimmed two trailing spaces.
Locally verified the cross-compile: rust:latest + gcc-arm-linux-
gnueabihf + proper CARGO_HOME config.toml produces a valid ARM 32-bit
ELF (2.9 MB, armhf EABI5).
=== Issue #15: 'Check for updates' button in the UI ===
New src/update_check.rs module. On the user's click (no polling):
1. Tcp-probes github.com:443 with a 5s budget. If unreachable, we
return Offline(reason) instead of a confusing 'update check
failed' — distinguishes 'you're offline' from 'GitHub API
misbehaved'.
2. HTTPS GET api.github.com/repos/.../releases/latest via the
tokio + rustls stack (same hand-rolled HTTP pattern as
domain_fronter — no new crate deps). Parses tag_name, strips
the v-prefix, loose-semver-compares to env!(CARGO_PKG_VERSION).
3. Returns one of four UpdateCheck variants: Offline / Error /
UpToDate / UpdateAvailable { release_url }.
New UI wiring (src/bin/ui.rs):
- Cmd::CheckUpdate enqueue variant
- UiState::last_update_check { InFlight, Done(result) }
- 'Check for updates' button next to the CA buttons
- Result displayed as a colored small-text line under the CA info:
green 'up to date', amber 'update available v0.8.5 → v0.8.6' with
a clickable release-page hyperlink, red for offline/error.
Verified end-to-end with a live github.com fetch (got a rate-limit
HTTP 403 from my IP because I've been hitting the API a lot, but
that's the expected Error() state — response classification was
correct). Three unit tests for is_newer() and a gated live test for
the full round-trip.
43 tests pass.
Two user-reported issues.
=== GLIBC too new (reported via twitter) ===
Our linux-amd64 and linux-arm64 gnu builds were compiled on
ubuntu-latest (24.04, GLIBC 2.39), which means the resulting binaries
refuse to load on anything older:
./mhrv-rs: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39'
not found (required by ./mhrv-rs)
Users on Ubuntu 22.04 / Mint 21 (GLIBC 2.35) — the typical user in Iran
where this project's target audience lives, and where they can't
dist-upgrade because they're behind exactly the kind of network
restriction this tool exists to bypass — could not run the gnu builds
at all.
Fix: pin the linux-gnu matrix entries to ubuntu-22.04 runners. GLIBC
2.35 is now the minimum; binaries load on Ubuntu 22.04, Mint 21,
Debian 12, Fedora 36+, RHEL 9+ and everything newer.
Users on older distros (Ubuntu 20.04, CentOS 7) can still use the
static musl builds (mhrv-rs-linux-musl-amd64.tar.gz et al.) which
have no GLIBC dependency at all.
=== Short-screen laptops — main window content clipped (PR #6) ===
Co-authored fix from @v4g4b0nd-0x76 in PR #6 (manually applied to
avoid pulling in 400 lines of unrelated cargo-fmt churn):
- Wrap the CentralPanel body in ScrollArea::vertical()
.auto_shrink([false; 2]) so everything stays reachable on short
screens.
- Lower the min_inner_size from [420, 540] to [420, 400] so laptops
with ~13" screens at default scaling can shrink the window without
clipping UI elements.
Closes#6.
Co-authored-by: v4g4b0nd-0x76 <v4g4b0nd-0x76@users.noreply.github.com>
The messense/rust-musl-cross images expect to run as root so cargo can
write to /root/.cargo. Overriding the container user to match the host
UID broke cargo's registry cache with 'Permission denied' before a
single file compiled. Drop the flag and chown the target/ tree after
the build instead.
A user on OpenWRT x86_64 reported the linux release doesn't run there —
root cause was glibc vs musl mismatch (our gnu binary was looking for a
dynamic linker that doesn't exist on router userlands). Add two musl
targets that produce fully static PIE binaries:
- x86_64-unknown-linux-musl -> mhrv-rs-linux-musl-amd64.tar.gz
- aarch64-unknown-linux-musl -> mhrv-rs-linux-musl-arm64.tar.gz
CI uses the messense/rust-musl-cross docker images (better-maintained
than cargo-zigbuild with a pinned zig, which has version regressions
on the ar wrapper between 0.13 and 0.16).
Locally verified:
- both archs cross-compile green in docker
- resulting x86_64 binary (3.3 MB) runs in an alpine:latest container,
--version / --help work, no dynamic lib requirements
The musl archive skips the UI (routers are headless) and swaps run.sh
for a procd init script (assets/openwrt/mhrv-rs.init) expecting the
binary at /usr/bin/mhrv-rs and config at /etc/mhrv-rs/config.json.
Side effect: switched tokio-rustls to default-features=false + ring
(was pulling aws-lc-rs transitively, which can't easily cross-compile
for musl). The main crate already uses ring explicitly, so no runtime
behavior change.
README gets a 'Running on OpenWRT (or any musl distro)' section in
both English and Persian with scp + procd enable/start recipe.
Closes#2.
First run needs the CLI to install the MITM CA into the system trust
store (sudo/admin prompt), which the UI alone can't do reliably from a
double-click. Add a small launcher for each platform that runs the CLI
with --install-cert once, then starts the UI. Each release archive now
contains a run.* script alongside the binaries.
The arm64 multiarch apt setup was failing because the Azure mirror on
GitHub runners only serves amd64. Since we don't build the UI for
aarch64-linux (server target), we only need the cross-compiler itself,
which is amd64-native. Drop the arm64 system libs and the best-effort
UI build step.
New bin 'mhrv-rs-ui' behind the 'ui' feature flag. CLI users pay
zero egui compile cost; UI users get a single static binary.
UI features:
- Config form (Apps Script ID, auth key, Google IP, front domain,
ports, log level, verify_ssl)
- Start/Stop buttons that spawn the proxy on a dedicated tokio thread
- Live stats (relay calls, failures, cache hit rate, bytes relayed,
blacklisted scripts) polled every ~700ms
- Test button (end-to-end relay probe)
- Install CA / Check CA buttons
- Recent log panel (last 200 lines)
- Dense, dark, utility-look: no emojis, no cards, no gradients
Architecture:
- Refactored crate into lib + two bins (mhrv-rs, mhrv-rs-ui).
src/lib.rs exposes all modules, main.rs uses them via 'use mhrv_rs::...'
- New src/data_dir.rs: platform-appropriate user data dir
(~/Library/Application Support/mhrv-rs on macOS,
~/.config/mhrv-rs on Linux, %APPDATA%\mhrv-rs on Windows).
CLI falls back to ./config.json for backward compat.
- CA moves to {data_dir}/ca/ca.crt (was ./ca/ca.crt).
- UI background thread owns the tokio runtime and proxy handle;
communicates with UI via std::mpsc commands + Arc<Mutex<UiState>>.
- macOS .app bundle: assets/macos/Info.plist template + build-app.sh
that assembles .app from the binary. Bundled into release zips.
- CI: Linux system libs (libxkbcommon, libwayland, libxcb*, libx11,
libgl, libgtk-3) installed on Ubuntu runners for eframe. aarch64
Linux UI is best-effort cross-compile. Windows MinGW, macOS native.
25 lib tests still pass. 5MB release UI binary on macOS.