v0.5.1: static musl builds for OpenWRT (amd64 + arm64)

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.
This commit is contained in:
therealaleph
2026-04-22 02:29:26 +03:00
parent d51b84a430
commit 6c5b62e5e6
5 changed files with 136 additions and 64 deletions
+58 -15
View File
@@ -29,6 +29,12 @@ jobs:
- target: x86_64-pc-windows-gnu - target: x86_64-pc-windows-gnu
os: windows-latest os: windows-latest
name: mhrv-rs-windows-amd64 name: mhrv-rs-windows-amd64
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
name: mhrv-rs-linux-musl-amd64
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
name: mhrv-rs-linux-musl-arm64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -61,6 +67,7 @@ jobs:
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
- name: Install Windows MinGW toolchain - name: Install Windows MinGW toolchain
if: matrix.target == 'x86_64-pc-windows-gnu' if: matrix.target == 'x86_64-pc-windows-gnu'
id: msys2 id: msys2
@@ -80,13 +87,32 @@ jobs:
Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value "linker = '$gcc'" Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value "linker = '$gcc'"
- name: Build CLI - name: Build CLI
if: "!endsWith(matrix.target, '-linux-musl')"
run: cargo build --release --target ${{ matrix.target }} --bin mhrv-rs run: cargo build --release --target ${{ matrix.target }} --bin mhrv-rs
# Fully-static musl builds for OpenWRT / Alpine / libc-less systems.
# messense/rust-musl-cross ships a pre-built musl toolchain so `ring`
# (rustls' crypto backend) cross-compiles cleanly on both archs.
- name: Build CLI (musl via docker)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/src -w /src \
messense/rust-musl-cross:x86_64-musl \
cargo build --release --target x86_64-unknown-linux-musl --bin mhrv-rs
- name: Build CLI (musl via docker, arm64)
if: matrix.target == 'aarch64-unknown-linux-musl'
run: |
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/src -w /src \
messense/rust-musl-cross:aarch64-musl \
cargo build --release --target aarch64-unknown-linux-musl --bin mhrv-rs
# UI build: we try to build the UI binary on every platform. If it fails # UI build: we try to build the UI binary on every platform. If it fails
# on cross-compile for linux-arm64 (missing arm64 system libs cross), # on cross-compile for linux-arm64 (missing arm64 system libs cross),
# we still ship the CLI. # we still ship the CLI. We also skip the UI on musl targets (OpenWRT etc.
# are headless, bundling X11 makes no sense).
- name: Build UI - name: Build UI
if: matrix.target != 'aarch64-unknown-linux-gnu' if: matrix.target != 'aarch64-unknown-linux-gnu' && !endsWith(matrix.target, '-linux-musl')
run: cargo build --release --target ${{ matrix.target }} --features ui --bin mhrv-rs-ui run: cargo build --release --target ${{ matrix.target }} --features ui --bin mhrv-rs-ui
- name: Package (unix) - name: Package (unix)
@@ -99,12 +125,22 @@ jobs:
cp target/${{ matrix.target }}/release/mhrv-rs-ui dist/mhrv-rs-ui cp target/${{ matrix.target }}/release/mhrv-rs-ui dist/mhrv-rs-ui
chmod +x dist/mhrv-rs-ui chmod +x dist/mhrv-rs-ui
fi fi
cp assets/launchers/run.sh dist/run.sh # OpenWRT / musl archives get the procd init script instead of run.sh,
chmod +x dist/run.sh # since routers don't have a CA to install and run headless via procd.
if [ "${{ runner.os }}" = "macOS" ]; then case "${{ matrix.target }}" in
cp assets/launchers/run.command dist/run.command *-linux-musl)
chmod +x dist/run.command cp assets/openwrt/mhrv-rs.init dist/mhrv-rs.init
fi chmod +x dist/mhrv-rs.init
;;
*)
cp assets/launchers/run.sh dist/run.sh
chmod +x dist/run.sh
if [ "${{ runner.os }}" = "macOS" ]; then
cp assets/launchers/run.command dist/run.command
chmod +x dist/run.command
fi
;;
esac
- name: Build macOS .app bundle - name: Build macOS .app bundle
if: runner.os == 'macOS' if: runner.os == 'macOS'
@@ -130,13 +166,20 @@ jobs:
shell: bash shell: bash
run: | run: |
cd dist cd dist
if [ "${{ runner.os }}" = "Windows" ]; then case "${{ matrix.target }}" in
7z a -tzip "${{ matrix.name }}.zip" mhrv-rs.exe mhrv-rs-ui.exe run.bat *-pc-windows-*)
elif [ "${{ runner.os }}" = "macOS" ]; then 7z a -tzip "${{ matrix.name }}.zip" mhrv-rs.exe mhrv-rs-ui.exe run.bat
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh run.command ;;
else *-apple-darwin)
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh 2>/dev/null || tar czf "${{ matrix.name }}.tar.gz" mhrv-rs run.sh tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh run.command
fi ;;
*-linux-musl)
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs.init
;;
*)
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh 2>/dev/null || tar czf "${{ matrix.name }}.tar.gz" mhrv-rs run.sh
;;
esac
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
Generated
+1 -47
View File
@@ -158,28 +158,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@@ -372,15 +350,6 @@ dependencies = [
"error-code", "error-code",
] ]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -572,12 +541,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "ecolor" name = "ecolor"
version = "0.28.1" version = "0.28.1"
@@ -803,12 +766,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.32" version = "0.3.32"
@@ -1360,7 +1317,7 @@ dependencies = [
[[package]] [[package]]
name = "mhrv-rs" name = "mhrv-rs"
version = "0.5.0" version = "0.5.1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -2100,8 +2057,6 @@ version = "0.23.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [ dependencies = [
"aws-lc-rs",
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@@ -2134,7 +2089,6 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [ dependencies = [
"aws-lc-rs",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mhrv-rs" name = "mhrv-rs"
version = "0.5.0" version = "0.5.1"
edition = "2021" edition = "2021"
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting" description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
license = "MIT" license = "MIT"
@@ -24,7 +24,7 @@ ui = ["dep:eframe"]
[dependencies] [dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "io-util", "signal", "sync"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "io-util", "signal", "sync"] }
tokio-rustls = "0.26" tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pemfile = "2" rustls-pemfile = "2"
webpki-roots = "0.26" webpki-roots = "0.26"
+42
View File
@@ -211,6 +211,27 @@ Example config fragment (both UI and JSON):
HTTP/HTTPS continues to route through the Apps Script relay (no change), and the SNI-rewrite tunnel for `google.com` / `youtube.com` / etc. keeps bypassing both — so YouTube stays as fast as before while Telegram gets a real tunnel. HTTP/HTTPS continues to route through the Apps Script relay (no change), and the SNI-rewrite tunnel for `google.com` / `youtube.com` / etc. keeps bypassing both — so YouTube stays as fast as before while Telegram gets a real tunnel.
## Running on OpenWRT (or any musl distro)
The `*-linux-musl-*` archives ship a fully static CLI that runs on OpenWRT, Alpine, and any libc-less Linux userland. Put the binary on the router and start it as a service:
```sh
# From a machine that can reach your router:
scp mhrv-rs root@192.168.1.1:/usr/bin/mhrv-rs
scp mhrv-rs.init root@192.168.1.1:/etc/init.d/mhrv-rs
scp config.json root@192.168.1.1:/etc/mhrv-rs/config.json
# On the router:
chmod +x /usr/bin/mhrv-rs /etc/init.d/mhrv-rs
/etc/init.d/mhrv-rs enable
/etc/init.d/mhrv-rs start
logread -e mhrv-rs -f # tail its logs
```
LAN devices then point their HTTP proxy at the router's LAN IP (default port `8085`) or their SOCKS5 at `<router-ip>:8086`. Set `listen_host` to `0.0.0.0` in `/etc/mhrv-rs/config.json` so the router accepts LAN connections instead of localhost-only.
Memory footprint is ~15-20 MB resident — fine on anything with ≥128 MB RAM. No UI is shipped for musl (routers are headless).
## Diagnostics ## Diagnostics
- **`mhrv-rs test`** — sends one request through the relay and reports success/latency. Use this first whenever something breaks — it isolates "relay is up" from "client config is wrong". - **`mhrv-rs test`** — sends one request through the relay and reports success/latency. Use this first whenever something breaks — it isolates "relay is up" from "client config is wrong".
@@ -429,6 +450,27 @@ Firefox cert store خودش را جدا دارد؛ installer تلاش می‌ک
HTTP/HTTPS هیچ تغییری نمی‌کند (همچنان از Apps Script می‌رود) و تونل SNI-rewrite برای `google.com` / `youtube.com` / … هم سر جای خودش است — پس یوتوب مثل قبل سریع می‌ماند و تلگرام بالاخره یک تونل واقعی می‌گیرد. HTTP/HTTPS هیچ تغییری نمی‌کند (همچنان از Apps Script می‌رود) و تونل SNI-rewrite برای `google.com` / `youtube.com` / … هم سر جای خودش است — پس یوتوب مثل قبل سریع می‌ماند و تلگرام بالاخره یک تونل واقعی می‌گیرد.
### اجرا روی OpenWRT (یا هر سیستم musl)
آرشیوهای `*-linux-musl-*` یک CLI کاملاً static می‌دهند که روی OpenWRT، Alpine و هر userland لینوکسی بدون glibc اجرا می‌شود. باینری را روی روتر بگذارید و به‌عنوان سرویس راه بیندازید:
```sh
# از یک ماشین که به روترتان می‌رسد:
scp mhrv-rs root@192.168.1.1:/usr/bin/mhrv-rs
scp mhrv-rs.init root@192.168.1.1:/etc/init.d/mhrv-rs
scp config.json root@192.168.1.1:/etc/mhrv-rs/config.json
# روی خود روتر:
chmod +x /usr/bin/mhrv-rs /etc/init.d/mhrv-rs
/etc/init.d/mhrv-rs enable
/etc/init.d/mhrv-rs start
logread -e mhrv-rs -f
```
بعدش دستگاه‌های LAN، proxy HTTP خودشان را روی IP روتر پورت `8085` (یا SOCKS5 روی `8086`) بگذارند. در `/etc/mhrv-rs/config.json` مقدار `listen_host` را به `0.0.0.0` تغییر دهید تا روتر از LAN هم connection بپذیرد (نه فقط localhost).
مصرف حافظه حدود ۱۵-۲۰ مگابایت است — روی هر روتری با حداقل ۱۲۸ مگابایت RAM اجرا می‌شود. UI برای musl ساخته نمی‌شود (روترها بدون صفحه‌نمایش هستند).
### محدودیت‌های شناخته‌شده ### محدودیت‌های شناخته‌شده
این‌ها محدودیت‌های ذاتی روش Apps Script + SNI هستند، نه باگ در این کلاینت. نسخهٔ اصلی پایتون هم دقیقاً همین‌ها را دارد. این‌ها محدودیت‌های ذاتی روش Apps Script + SNI هستند، نه باگ در این کلاینت. نسخهٔ اصلی پایتون هم دقیقاً همین‌ها را دارد.
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh /etc/rc.common
# OpenWRT procd init script for mhrv-rs.
# Install as /etc/init.d/mhrv-rs, then:
# /etc/init.d/mhrv-rs enable
# /etc/init.d/mhrv-rs start
#
# Expects:
# /usr/bin/mhrv-rs (the static musl binary from the release)
# /etc/mhrv-rs/config.json (your config)
START=99
USE_PROCD=1
BIN=/usr/bin/mhrv-rs
CONFIG=/etc/mhrv-rs/config.json
start_service() {
[ -x "$BIN" ] || return 1
[ -f "$CONFIG" ] || return 1
procd_open_instance
procd_set_param command "$BIN" --config "$CONFIG" --no-cert-check
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param file "$CONFIG"
procd_close_instance
}
reload_service() {
stop
start
}