43 Commits

Author SHA1 Message Date
therealaleph 6c692441be ci(telegram): brief English bullets in announcement + cross-link, drop Persian-full
Telegram channel posts up through v1.9.9 inlined the full Persian half of `docs/changelog/v{version}.md` (often >2000 chars), with sub-bullets, contributor mentions, and architectural prose. In a chat-client viewport the result was an unreadable wall of mixed RTL Persian + LTR `<code>` / `<b>` spans + nested bullets that scrolled past most readers.

Switched to brief-extracted English instead:
- Added `brief_changelog(text)` — keeps only top-level `• ` bullets (drops sub-bullets), strips "by @user with full root cause + fix" / "from @user" prefatory phrases, replaces `[#nnn](url)` with `#nnn` for inline issue refs, cuts each bullet at the first natural sentence boundary (`:` after pos 30, `. `, ` — `), hard-caps at 200 chars per bullet, and trims any dangling unbalanced `(` or `[` left by the truncation.
- Both posts (files-channel announcement + main-channel cross-link) now use `english_brief = brief_changelog(english_notes)` instead of the full Persian.
- Title and footer chrome of both posts switched to English ("released" / "Files (Android, Windows, ...)" / "Channel:" / "or:").

The full Persian + full English text stays in `docs/changelog/v*.md` for archival; only the channel post becomes brief.

Verified locally on v1.9.7 / v1.9.8 / v1.9.9 — produces 246–458 char briefs with clean bullet structure, no dangling parens, no contributor noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:39:38 +03:00
therealaleph 50bb3d0eaf ci(telegram): include Persian changelog in announcement + main-channel cross-link
Up through v1.9.7 the Telegram posts said only "📦 mhrv-rs vX.Y.Z منتشر شد" + a hashtag and a link to the files channel — subscribers had to click through to GitHub to see what actually changed. Now the announcement post in the files channel and the cross-link post in the main channel both inline the Persian half of `docs/changelog/v{version}.md`.

How it works:
- New `load_changelog(repo_root, version)` reads `docs/changelog/v{version}.md`, strips the leading `<!-- ... -->` editor comment, splits on the lone `---` line that separates Persian from English. Returns (None, None) if the file doesn't exist (lets out-of-band re-publishes for old tags whose changelog file was never landed work without crashing).
- New `md_to_tg_html(md, max_len)` does a minimal markdown → Telegram-flavoured-HTML conversion: `**bold**`, `[text](url)`, `` `code` `` are translated; nested patterns (e.g. `[`code`](url)`, `**[`code`](url)**`) work via a placeholder/unwind pass that loops until stable. Truncates at the 4096-char sendMessage limit, snapping to a newline boundary so a span isn't cut in half, with a "see full notes on GitHub" tail.
- Falls back gracefully if the changelog file is missing — uses the old skeleton message.

Verified locally on docs/changelog/v1.9.7.md: 3039 chars after conversion, well under the 4096 limit, all bold / code / link spans render correctly including nested ones (`[`assets/apps_script/Code.gs`](url)` becomes `<a href="url"><code>assets/apps_script/Code.gs</code></a>`).

This change takes effect on the next release (v1.9.8+); v1.9.7 is already published with the old skeleton.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:23:01 +03:00
therealaleph 4aac9a793f feat: v1.9.4 — exit node for ChatGPT/Claude/Grok + drop duplicate Telegram post
Two changes addressing user-reported issues today:

1. Exit-node feature ported from upstream masterking32@464a6e1d, with
   hardening. Cloudflare-protected sites (chatgpt.com, claude.ai,
   grok.com, x.com, openai.com) flag Google datacenter IPs as bots and
   return Turnstile / CAPTCHA / 502 challenges. Apps Script's UrlFetchApp
   exits from those IPs, so v1.9.3 surfaces these as "Relay error: json:
   key must be a string..." with no apps_script-mode workaround.

   Now a small TypeScript HTTP endpoint (assets/exit_node/valtown.ts)
   deployed on val.town / Deno Deploy sits between Apps Script and the
   destination. Chain: client → Apps Script (Google IP) → val.town
   (non-Google IP) → destination. Destination sees val.town's IP, no
   CF challenge.

   Config:
     "exit_node": {
       "enabled": true,
       "relay_url": "https://...web.val.run",
       "psk": "<openssl rand -hex 32>",
       "mode": "selective",
       "hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
     }

   Hardening over upstream: PSK fail-closed if still placeholder (fresh
   deploy can't be open relay), loop guard (refuses fetch of own host),
   explicit 503 on misconfigured. Fallback to direct Apps Script on exit
   node failure (CF-affected sites fail, others keep working). Setup
   docs in English + Persian at assets/exit_node/README*.md. Example
   config at config.exit-node.example.json.

2. Removed the legacy `telegram` job from release.yml. With
   TELEGRAM_NOTIFY_ENABLED repo var set to true, every release was
   producing two duplicate APK posts on the main Telegram channel: the
   old bundled-APK-on-main job AND the newer per-file files-channel
   posts (telegram-publish-files.yml). Only the per-file flow is wanted.
   Legacy job and its helper telegram_release_notify.py are gone.
   Recoverable from git log if anyone needs the bundled pattern back.

169 mhrv-rs lib tests + 33 tunnel-node tests + UI build clean.
2026-05-01 11:52:32 +03:00
therealaleph c98ae73c92 ci(telegram-publish): use --clobber to survive partial-download retries
The v1.9.2 telegram publish failed because attempt 1 hit a transient
HTTP 500 from GitHub's release-asset CDN mid-download, leaving partial
files in assets/. Attempts 2 and 3 then errored on "already exists"
because gh release download refuses to overwrite without --clobber.

With --clobber, retries can complete cleanly even when an earlier
attempt left files behind. No change to the success path.
2026-04-30 19:55:46 +03:00
therealaleph 0d54c5c6fb ci(telegram): use public mhrv_rs link in main-channel post + add invite
Two related changes to the main-channel cross-post (one message per
release that points at the files channel):

1. Post-link now uses the public-username form `t.me/mhrv_rs/<msg_id>`
   instead of the private `t.me/c/<chat_id>/<msg_id>`. The latter only
   resolves for channel members; the former works for everyone, so
   recipients seeing the main-channel announcement can click through
   to a specific release post even if they're not yet subscribed.

   Wired via the existing `FILES_CHANNEL_USERNAME` workflow env var,
   now defaulting to `mhrv_rs` (the channel's public username) if the
   `vars.FILES_CHANNEL_USERNAME` repo variable is unset. Override per
   repo if the channel is renamed.

2. Channel-join links rendered in the body of the main-channel post,
   below the post-link:

       لینک کانال:
       https://t.me/mhrv_rs
       و یا: https://t.me/+R1OyoHX2boA1ZDgx

   Two forms cover the cases where one or the other is filtered:
   - `t.me/mhrv_rs` — public username form, prettier, surfaces in
     Telegram search
   - `t.me/+<hash>` — invite link, the only join path that works for
     private/restricted channels and for users whose client doesn't
     resolve public usernames cleanly

   Wired via new `FILES_CHANNEL_INVITE` env var, defaulting to the
   current invite hash; override via `vars.FILES_CHANNEL_INVITE` if
   rotated.

Per user request — not running this against v1.8.0 retroactively,
just wiring it up for the next release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:30:37 +03:00
therealaleph 0669b9310c ci(telegram): add SHA-256 to file captions + cross-link main channel to files channel
Two changes on top of last commit:

1. SHA-256 ("تایید اصالت") now in every file caption. Each artifact's
   caption gets a `<code>...</code>` line with the file's SHA-256 hex
   so recipients can `sha256sum <file>` after download and verify it
   matches what the channel posted. Defends against modified copies
   if the channel ever gets relayed through a third party.

   For chunked uploads (file > 45 MB), each part shows BOTH:
   - SHA-256 of that specific part (verifies the chunk downloaded
     intact before bothering to reassemble)
   - SHA-256 of the full reassembled file (verifies the final result
     after `cat <name>.part_* > <name>`)

2. Main channel post is now a cross-link, not files.

   Previously the legacy `telegram` job in release.yml posted the
   universal APK + full changelog as one sendDocument + sendMessage
   pair to the main announcement channel.

   New behaviour: telegram-publish-files.yml's last step posts a short
   message to the main channel saying "v1.8.0 released, click here
   for files" with a t.me link pointing at the files channel's
   announcement anchor post. Recipients land on the anchor, scroll
   to find the platform-specific artifact they need.

   Link format: `t.me/c/<chat_id>/<msg>` for private channels (works
   for members), or `t.me/<username>/<msg>` if `FILES_CHANNEL_USERNAME`
   repo variable is set (works for everyone — useful if the files
   channel is later made public).

   Legacy telegram job in release.yml stays in source, dormant,
   gated on `vars.TELEGRAM_NOTIFY_ENABLED == 'true'` (default false).
   Comment updated to note the new workflow is the canonical path.
   If both are turned on at once, the main channel gets two posts
   per release.

Tested manually for syntax + caption rendering — actual SHA-256 values
will appear on the next workflow_dispatch run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:22:27 +03:00
therealaleph 7e5e2c7313 ci(telegram): publish each release file individually to channel
New workflow + script that posts every artifact (Android APKs, Windows
ZIP, macOS .app + CLI tarballs, Linux glibc + musl, OpenWRT, Raspbian)
to the Telegram channel as separate sendDocument calls, each with a
Persian caption naming the platform variant and a `#v<NNN>` hashtag
(e.g. `#v180`, `#v1810`, `#v200`) so users can find a specific release
later via the channel's hashtag search.

Files larger than 45 MB (the Bot API's effective ceiling after multipart
+ caption overhead) are split into byte chunks named `<name>.part_aa`,
`.part_ab`, ... and posted with reassembly instructions in the caption.
For the v1.8.0 file set everything is ≤41 MB so the split path is
defensive.

Decoupled from `release.yml` so it can be re-triggered for any past tag
via `workflow_dispatch` without rebuilding artifacts — downloads from
the GitHub Release page directly via `gh release download`. Also
auto-runs on each successful `release.yml` completion via
`workflow_run`.

Hard-codes the channel ID `-1003966234444` (one well-known channel,
auditable in source). Reuses `secrets.TELEGRAM_BOT_TOKEN` which already
has post permissions there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:15:26 +03:00
therealaleph d5e5dbb5d7 chore: bump versions to 1.7.11 + drop i686 + harden release artifact download
v1.7.10 release run shipped no assets due to two CI failures stacked:

1. The i686-pc-windows-msvc job (added in v1.7.7 for Win7 support per
   #318) failed because Rust 1.77.2 — the last stable that produces
   Win7-loadable binaries — can't parse modern transitive crate
   manifests (`time` 0.3.47 in this case). Pinning transitives across
   the dep tree at every MSRV bump in our deps isn't sustainable, so
   the target is removed from the release matrix. Win7 32-bit users
   self-build per #318's instructions.

2. The `release` job hit `actions/download-artifact@v4`'s 5-retries-
   exhausted error on multiple artifacts. Same flake we worked around
   in #288 for `commit-releases`. The `release` and `telegram` jobs
   now use `gh run download` wrapped in a 3-attempt retry loop, mirror-
   ing the working pattern.

v1.7.11 is the first full release after v1.7.9; ships #337 (Apps
Script gzip-decoded range probe) and #344 (Android Paste button) that
were tagged in v1.7.10 but never published as assets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:44:23 +03:00
therealaleph 46a21b4f5b chore: bump versions to 1.7.10 (Cargo + Android) + fix Win7 i686 lockfile
Patch release for the changes shipped via #337 (Apps Script range
probe gzip-decoded body handling) and #344 (Android Paste button on
13+) plus a CI fix that restores the Win7 i686 binary missing from
v1.7.9 (Cargo.lock format mismatch with Rust 1.77).

The Cargo.lock version=4 (Rust 1.78+) wasn't readable by the pinned
1.77.2 toolchain on the i686 job. Workflow now regenerates the
lockfile with the pinned toolchain on that job only, leaving every
other target unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:28:20 +03:00
Shin (Former Aleph) 501d54edec ci(release): pin i686-pc-windows-msvc to Rust 1.77.2 for Win7 compat (#323)
* ci(release): pin i686-pc-windows-msvc to Rust 1.77.2 for Win7 compat

Fixes #318. Rust 1.78 (May 2024) raised the std MSRV for Windows from
Win7 to Win10 by switching std::time to GetSystemTimePreciseAsFileTime,
a kernel32 export that doesn't exist on Win7 SP1. Building the i686
binary with stable Rust (currently 1.86+) produces an exe that fails
to load on Win7 with "the procedure entry point
GetSystemTimePreciseAsFile could not be located in the dynamic link
library kernel32.dll" — making the whole reason we ship i686 (legacy
Win7 32-bit boxes per #272) moot.

Add a per-matrix `rust_toolchain` knob; only i686-pc-windows-msvc uses
it, pinning to 1.77.2 (last stable that supports Win7). Other targets
remain on @stable and pick up regular Rust updates.

dtolnay/rust-toolchain switches from `@stable` to `@master` because
the per-tag aliases (`@stable`, `@1.77.2`) can't be selected via a
matrix variable — `@master` accepts the toolchain string as input.

Cache key gains a toolchain suffix so the 1.77.2 cache doesn't collide
with the stable cache for the same target, and a future toolchain bump
invalidates only the affected slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): make i686-pc-windows-msvc continue-on-error

Companion to the Rust 1.77.2 pin: if the deps' MSRV ever moves above
1.77, the i686 target will fail to build, but we don't want it to
block the rest of the release. Mirror the mipsel-softfloat approach.
If/when this triggers, options are dropping i686 entirely or moving
to the tier-3 i686-win7-windows-msvc target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:51:13 +03:00
Amirali Shoja 35d2d91fa3 ci: add i686-pc-windows-msvc to release matrix (#272) (#288) 2026-04-27 00:47:53 +03:00
therealaleph a6db13bda5 ci: download release artifacts from GitHub Release page, not artifacts API
The commit-releases job's `actions/download-artifact@v4` step has
failed twice in a row (v1.7.5 retrigger, v1.7.6) with the same
shape: ~10 artifacts download successfully, then "Unable to
download artifact(s): Artifact download failed after 5 retries" on
the 11th-13th. The 10 that complete print their SHA256 digests
cleanly; the failure is unambiguously inside actions/download-
artifact, not on our side.

Workaround: pull from `gh release download` instead. The `release`
job populated the GitHub Release page a few seconds earlier with
the same artifacts; pulling from there reads from a different
CDN (Release-page blob store) with different retry / rate-limit
characteristics. Empirically more reliable for our 13-artifact
release size.

Filtered to *.tar.gz / *.zip / *.apk so we only fetch the user-
facing artifacts (skipping anything like checksum sidecars that
softprops/action-gh-release@v2 might add later).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:39:29 +03:00
therealaleph 109a02db4e ci: refresh in-repo releases/ folder on each release tag
Resume the practice (dropped after v1.1.0) of committing prebuilt
binaries to the repo's releases/ folder. Iranian users behind state
network filtering frequently can't reach the GitHub Releases page
(/releases/tag/...) but CAN reach the static source tree via
Code → Download ZIP — that pulls the in-repo releases/ folder along
with the source. Telegram channel feedback explicitly requested
this be resumed.

The new commit-releases job:
1. Runs after release+build+android succeed.
2. Wipes existing binary artifacts from releases/ (.apk, .tar.gz,
   .zip) but preserves README.md and .gitattributes.
3. Copies all desktop archives (which already have stable
   platform-suffixed names like mhrv-rs-linux-amd64.tar.gz).
4. Copies all per-ABI Android APKs (so users on slow connections
   can grab the ~37 MB arm64-v8a APK instead of the ~110 MB
   universal).
5. sed-updates the "Current version" line and APK filename refs
   in releases/README.md (both English and Persian copies).
6. Commits as github-actions[bot] and pushes to main.

The GitHub Release page itself keeps the canonical versioned
artifacts as before — this in-repo folder is the fallback for
users who can't reach that URL.

Tag protection rules don't apply to refs/heads/main so the push
isn't gated. release-drafter.yml triggers on push-to-main but only
updates the next-release draft, no cycle risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:59:38 +03:00
therealaleph e81974c204 Revert "ci: post macOS/Linux/Windows/Android binaries as Telegram media group"
This reverts commit e9ce03e697.
2026-04-26 21:10:50 +03:00
therealaleph e9ce03e697 ci: post macOS/Linux/Windows/Android binaries as Telegram media group
The Telegram release notifier used to post just the universal APK with
a single-document caption. This change ships the per-platform binaries
for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows
(amd64 UI), and Android (universal APK) as a single Telegram media
group with one caption listing every filename + SHA-256.

Workflow side (.github/workflows/release.yml):
- The telegram job now downloads ALL artifacts (was: APK only).
- New `Prepare files for Telegram media group` step extracts the raw
  binaries out of each per-platform .tar.gz / .zip (no archive
  wrappers in the channel) and renames them with version suffixes
  (mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe,
  etc.). Per-platform extraction is best-effort: a missing artifact
  emits a `::warning::` and skips that platform rather than failing
  the whole post.
- The post step builds a `--files <path>` arg list from tg-files/,
  sorted for deterministic order across runs, and invokes the
  notifier without --with-changelog (the script auto-replies with
  changelog whenever --files is used).

Script side (.github/scripts/telegram_release_notify.py):
- New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file
  → sendDocument with the same caption shape; 0 → error. Telegram's
  sendMediaGroup rejects single-item groups, so the 1-file fallback
  isn't optional.
- New build_media_group_caption() composes title + per-file
  filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file
  release; fallback to filename-only-list if a future swell pushes
  past Telegram's 1024-char caption cap.
- send_media_group() handles the multipart/form-data shape with each
  file referenced as `attach://fileN` from the media JSON. Caption is
  attached to file 0 only (Telegram clients render per-item captions
  inconsistently for media groups; first-item-only is the safe
  pattern).
- Legacy --apk path kept for any caller that hasn't migrated; either
  --apk or --files must be present (validated at startup).
- _content_type_for() picks application/vnd.android.package-archive
  for .apk and application/octet-stream for everything else, so
  Telegram clients label the APK with the Android icon and label
  desktop binaries by filename without a misleading icon.

Behavioural change for users:
- The Telegram channel now sees one grouped post per release with all
  primary platform binaries inline, instead of just the APK. macOS
  users wanting the gatekeeper-friendly .app.zip still grab it from
  the GitHub Releases page; the Telegram drop is for the "give me
  the binary, I'll run it" path.
- The Persian/English changelog reply that used to be opt-in (via
  TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files
  path because the per-file SHA list eats the caption budget that
  previously held the FA brief-note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:12:00 +03:00
dazzling-no-more 4b728058bd ci: add release-drafter + prepare-release for faster releases (#260) 2026-04-26 18:23:23 +03:00
therealaleph fb552c227d v1.5.0: long-poll Full Tunnel + Docker tunnel-node + brief FA release notes
Ships PR #173 (event-driven drain) plus three operational improvements:

PR #173 — long-poll tunnel mode. The tunnel-node's batch drain
switched from a fixed 150 ms sleep to an event-driven Notify wait;
idle sessions long-poll up to 5 s and wake on the first byte from
upstream. Push notifications and chat messages now arrive in roughly
RTT instead of waiting for the next client poll tick. Backward compat
with pre-#173 tunnel-nodes is automatic via a sticky AtomicBool that
detects fast empty replies and reverts to the legacy cadence.
92 client tests + 17 tunnel-node tests pass, including end-to-end
TCP-pair verification of the notify wiring.

Docker image for tunnel-node. Adds a hardened Dockerfile (BuildKit
cache mounts, non-root runtime user, ca-certificates for HTTPS
upstreams) and a .dockerignore to keep build context small. New
`tunnel-docker` job in the release workflow builds + pushes
multi-arch (linux/amd64 + linux/arm64) to
ghcr.io/therealaleph/mhrv-tunnel-node with `:latest`, `:1.5`, and
`:1.5.0` tags on every release. Setting up Full Tunnel mode goes
from "rustup + cargo build on a 1 GB VPS" (which fails on memory
half the time) to a one-liner. tunnel-node/README.md updated with
prebuilt-image + docker-compose recipes.

Brief Persian release note in Telegram caption. The release-post
caption now leads with a `<blockquote>`-wrapped FA bullet headlines
extracted from `docs/changelog/v<ver>.md`, above the existing two
links (repo + release). Markdown links → Telegram HTML <a> for
clickability. Cap-budget-aware truncation at bullet boundaries
keeps total caption under Telegram's 1024-char limit. Headlines-only
rather than full bullets so multiple "what's new" items fit
comfortably (the full bullets remain on the GH release page and as
the optional --with-changelog reply-threaded message).

GitHub Releases page bodies now lead with the changelog content
(Persian section + `---` + English) instead of just a Full Changelog
comparison link. The auto comparison link is appended at the bottom
via `append_body: true` rather than removed.

Workflow changes:
- New `permissions: packages: write` at the workflow level (required
  for ghcr push via docker/login-action).
- New `tunnel-docker` job needs `build` (not the full matrix) to
  serialize the QEMU buildx layer with the matrix cache.
- Release job composes the body from `docs/changelog/v${VER}.md`
  in a pre-step that handles both tag-push and workflow_dispatch
  paths (uses inputs.version || github.ref_name like the rest of
  the workflow).

Tested locally:
- `cargo test` — 92 lib tests pass
- `cargo test -p mhrv-tunnel-node` — 17 tests pass
- `docker build` of tunnel-node Dockerfile — 32 MB image, runs as
  non-root, /health returns "ok", auth rejection works correctly,
  legitimate requests open sessions to remote hosts
- Telegram script `--dry-run` mode added; rendered captions for
  v1.4.0, v1.4.1, v1.5.0 all fit under 900 chars

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:56:41 +03:00
therealaleph fdc0405465 ci(release): heal root-owned target/ + always-chown after mipsel docker
Two related fixes for the self-hosted Linux jobs failing on
`actions/checkout@v4` with `EACCES: permission denied unlink
target/.rustc_info.json`:

1. Pre-checkout cleanup. The previous mipsel docker step left root-
   owned files in target/ when its `cargo +nightly build` failed —
   the post-step `sudo chown -R …` line was AFTER the docker run,
   and bash -e short-circuited on docker non-zero, so chown never
   executed. Once those root-owned files exist on the runner,
   every subsequent self-hosted job's `actions/checkout@v4` clean
   step fails because it can't unlink them. Add a guarded
   pre-checkout step that uses `sudo rm -rf` to clear root-owned
   leftovers from $GITHUB_WORKSPACE. Gated on
   `contains(matrix.os, 'self-hosted')` so GitHub-hosted runners
   (macOS, Windows) skip it — they get fresh VMs anyway.

2. Always-chown trap. The mipsel docker step now uses
   `trap '…chown…' EXIT` so the chown runs whether the inner
   `cargo build` succeeded or failed. A transient mipsel compile
   regression (tier-3 target, occasional std-build breakage) no
   longer poisons the runner's workspace for the next run.

Together: (1) recovers from the existing broken state on next run,
(2) prevents the same poisoned state from recurring.

Triggered for the v1.4.0 redeploy that's trying to publish the
missing mhrv-rs-openwrt-mipsel-softfloat artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:06:13 +03:00
therealaleph dc74a3fb55 ci(release): add workflow_dispatch with version input
The original `on: push: tags: 'v*'` trigger is the right primary path
— a new tag = a new release = a fresh workflow run. But it has a
failure mode: if one matrix job fails (e.g. mipsel-softfloat is
tier-3 and occasionally regresses) and we push a fix to main, we
can't move the immutable tag (tag protection rule), so the original
artifact stays missing from that release forever.

`workflow_dispatch` with a `version` input lets us re-run the workflow
from main against an existing release tag:

    gh workflow run release.yml --ref main -f version=1.4.0

The build matrix runs against the current main commit (which has
the fix), and the release-upload step uploads to the existing
v1.4.0 release page with the same filename pattern, alongside the
artifacts that succeeded on the original push.

Three call sites had to learn the new path: the macOS .app bundle
build, the Android APK rename step, and the Telegram caption builder.
All three previously did `VER="${GITHUB_REF#refs/tags/v}"`, which on
a workflow_dispatch from main produces "heads/main" — the new
two-line `VER="${{ inputs.version || github.ref_name }}"; VER="${VER#v}"`
pattern handles both: tag pushes get the bare version from
`github.ref_name` ("v1.4.0" → "1.4.0"), workflow_dispatch gets it
from the explicit input.

The release job's `softprops/action-gh-release@v2` step also needed an
explicit `tag_name` — without it the action defaults to `github.ref`,
which on workflow_dispatch is the dispatch ref (`refs/heads/main`)
and would try to create a release named "main". Now it's:

    tag_name: ${{ inputs.version && format('v{0}', inputs.version) || github.ref_name }}

Pair with `gh variable set TELEGRAM_NOTIFY_ENABLED --body false` before
dispatch when the re-run is for the same version (no new content for
the channel to see) — the Telegram job is already gated on that
variable, no per-run flag needed.

Used right now to publish the mhrv-rs-openwrt-mipsel-softfloat artifact
that failed on v1.4.0's original push, after commit febeeca fixed the
underlying compile error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:50:08 +03:00
therealaleph 5bb26a4961 v1.3.0: per-deployment concurrency, ABI-split APKs, ONLY-mode fix
Rolls up the four post-v1.2.14 commits on main into a single tagged
release. Highlights:

- Per-deployment concurrency (#142): each deployment ID gets its own
  30-permit semaphore, so setups with deployments across multiple
  Google accounts get a genuine 30×N throughput ceiling. Single-account
  setups still cap at Google's per-account 30-simultaneous limit —
  docs (EN + FA) updated to call that out.
- Android app-splitting ONLY-mode bug fix (#143): the previous code
  called both addAllowedApplication and addDisallowedApplication,
  which Android documents as mutually exclusive. ONLY mode was
  silently failing establish(). Now fixed.
- Per-ABI Android APKs (#136): ships four split APKs (arm64-v8a ~21 MB,
  armeabi-v7a ~18 MB, x86_64 ~23 MB, x86 ~22 MB) alongside the ~53 MB
  universal. Huge distribution win for users on unreliable
  censorship-tunnel paths — the 21 MB arm64-v8a download succeeds
  where the universal doesn't.
- Honest IP-exposure note in Security Posture (#148): clarified that
  v1.2.9's forwarded-header stripping only covers the client-side leg;
  what Google's own infrastructure may add on the UrlFetchApp.fetch()
  second leg is outside this client's control. Full Tunnel mode is
  the recommendation for threat models where that matters.
- Telegram release-post format: added Persian preambles above both
  links (GitHub repo + full Persian guide; release page + desktop/
  router builds) so channel readers see the intent at a glance.

82 tests pass. Desktop + Android builds both verified clean locally
across the v1.2.15+ commit series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:11:52 +03:00
therealaleph cc04558437 android: publish per-ABI APKs in addition to universal (fix #136)
GitHub Releases is filtered from inside IR, and the 50 MB universal APK
is a bottleneck for users on slow/unstable censorship-tunnel paths that
can't reliably pull that much data. Per-ABI APKs are ~18–23 MB each —
small enough to succeed where the universal fails.

Build changes:
- android/app/build.gradle.kts: enabled `splits { abi { ... } }` with
  `isUniversalApk = true`, producing five release APKs:
    app-universal-release.apk     ~53 MB  (all 4 ABIs)
    app-arm64-v8a-release.apk     ~21 MB  (95%+ of modern devices)
    app-armeabi-v7a-release.apk   ~18 MB  (older 32-bit ARM)
    app-x86_64-release.apk        ~23 MB  (emulators, Chromebooks)
    app-x86-release.apk           ~22 MB  (legacy 32-bit Intel)
  abiFilters is retained for the universal build; splits.abi layers on
  per-ABI outputs without removing it.

- .github/workflows/release.yml: rename step now copies all 5 APKs to
  dist/ under versioned names (mhrv-rs-android-{abi}-v{VER}.apk),
  logs a warning if any per-ABI APK is missing, and hard-fails only if
  the universal is missing. Universal keeps its existing download path
  and filename so Telegram mirrors / previous-version update prompts
  keep working.

The release + telegram aggregation jobs downstream don't need changes —
they already use `files: dist/*` and `mhrv-rs-android-universal` artifact
name respectively.

Local build verified: clean assembleRelease produces all 5 APKs with the
expected size ratios (arm64-v8a is 41% the universal size).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:27:00 +03:00
therealaleph 658e72fe0d v1.2.6: rust-cache bin pruning fix + PR #83 scan-sni
v1.2.4 and v1.2.5 both cut clean tags but CI failed downstream for
different self-hosted reasons:

- v1.2.4 failed on parallel apt-lock race (fixed)
- v1.2.5 failed with "TOML parse error at line 5 column 9" because
  rust-cache v2's default cache-bin=true prunes $CARGO_HOME/bin of
  any binary not installed via `cargo install`. `rustup` itself is
  installed by rustup-init, not cargo install, so it got flagged as
  "unknown" and deleted on cache save. Next job hits the cargo
  symlink that points at a missing rustup, which resolves somehow
  to a very old cargo that can't parse our Cargo.toml.

Fix:
- Set `cache-bin: "false"` on every Swatinem/rust-cache@v2 call.
  We still cache target/ + registry (the big win), just not bin/.
  Binaries are stable across runs on our self-hosted box anyway.
- Reinstalled rustup inside each per-runner CARGO_HOME on the server
  to recover from the broken state.

Also in this release:
- PR #83: new `mhrv-rs scan-sni` subcommand. Pulls Google's
  published IP ranges, does PTR lookups via dns.google on each IP,
  filters to Google-related hostnames, then TLS-probes each
  discovered SNI against the configured google_ip to see which ones
  bypass DPI. Useful for rebuilding a working SNI pool on a new ISP.
  Adds the `url` crate dep.

Same user-facing code as v1.2.4/v1.2.5 (PRs #78, #79, README Android
note) plus PR #83 and the CI fixes on top.
2026-04-23 21:22:17 +03:00
therealaleph af44abbcd3 v1.2.5: CI self-hosted apt-lock fix (v1.2.4 release was incomplete)
v1.2.4 tagged cleanly but its CI failed — parallel Linux matrix jobs
on the self-hosted runners all raced on `/var/lib/apt/lists/lock` and
failed the `sudo apt-get install` step within ~20s. v1.2.4's release
job therefore skipped and no assets were published.

Fix:

- Pre-installed every apt dependency the workflow needs on both
  self-hosted runners (eframe system libs, gcc-aarch64-linux-gnu,
  gcc-arm-linux-gnueabihf).
- Seeded per-runner cargo linker configs at
  /home/ghrunner/cargo-{01,02}/config.toml so the "echo
  [target.xxx] linker = ..." workflow step is also unnecessary.
- Gated the "Install Linux eframe system deps" and the two cross-
  compile-toolchain steps on `runner.environment == 'github-hosted'`
  so only hosted runners call apt-get; self-hosted runners skip the
  whole thing and use pre-installed tooling.

Re-tagging as v1.2.5 since v1.2.4 is an abandoned tag (git tag exists
but no GitHub Release was cut for it).

Same code changes as what v1.2.4 was meant to ship: PR #78 range-
parallel validation, PR #79 port-collision rejection, README note
on Android 7+ user-CA trust.
2026-04-23 21:06:25 +03:00
therealaleph 15e3e38745 v1.2.3: move CI to self-hosted runners + rust-cache
Linux / Android / mipsel build jobs now run on two self-hosted runners
on a Hetzner 8-core / 31 GB Ubuntu 24.04 box with Rust, Android SDK+NDK
r26c, all cross-compile toolchains and Docker pre-installed. macOS and
Windows still run on GitHub-hosted — we don't self-host those OSes and
the free minutes on a public repo are plenty.

Adds Swatinem/rust-cache@v2 to every cargo-using job so target/ + cargo
registry survive between runs. With warm caches the Linux jobs take
~1min each and the Android job ~3-4min; cold runs are ~9min for
Android and ~2min for everything else. Release wall time before this
change was ~13m consistently; it should now sit around 6-7m.

No new user-facing code in this release — primarily an infra change
exercised by an actual tag-push so we verify the full pipeline works
end-to-end from the new runners.
2026-04-23 20:23:10 +03:00
Shin (Former Aleph) 5a108f73cb v1.1.5: merge upstream safety fixes + Telegram default = file + link only (#60)
Contains the three safety fixes from PRs #48/#49/#50 and the Persian
README RTL polishing from #58, all squashed into main. Merge details
already in their individual PR comments; summary:

  #48: reject truncated Content-Length relay responses (previously
       silently accepted whatever bytes arrived before EOF)
  #49: reject truncated or malformed (missing CRLF) chunked-encoding
       relay responses (same class of silent-acceptance bug)
  #50: restrict the SNI-rewrite tunnel dispatch to port 443. Plain
       HTTP (:80) targets that happened to match google.com / hosts
       override were being steered into the TLS tunnel and blocking
       waiting for a ClientHello that would never arrive.
  #58: trailing-whitespace line-breaks on Persian bullet lists in
       README so the RTL rendering doesn't collapse consecutive
       items into a single paragraph.

Test suite grew from 54 to 58 passing (three new negative tests for
the relay-reader correctness fixes + one SNI-rewrite port filter).

Telegram CI notify default switched to file-plus-link:
  - script gains a `--with-changelog` flag; default OFF
  - workflow only passes it when `vars.TELEGRAM_INCLUDE_CHANGELOG=true`
  - every routine release now posts just the APK + short caption
    (title + SHA-256 + repo URL + release URL) with no long body

To include bullets for a given release again:
  gh variable set TELEGRAM_INCLUDE_CHANGELOG --body true
The existing `vars.TELEGRAM_NOTIFY_ENABLED` job-level gate remains —
changelog toggle is orthogonal to enable/disable.

Also closes PR #55 without merging; ads/analytics domains were being
lumped under a YouTube-specific toggle, and the PR committed per-
machine \`.cargo/config.toml\` + zig-cc cross-compile helpers that
would have broken CI on actual Windows / macOS runners.
2026-04-23 14:40:15 +03:00
Shin (Former Aleph) 383bea008e v1.1.2: actually-green mipsel-softfloat (YAML comment-fold bug) (#44)
v1.1.1 still failed the mipsel CI matrix for a non-obvious reason.
The `Build CLI (mipsel-softfloat via docker)` step passed a
multi-line argument to `sh -c "..."` with `\` line continuations
and inline `#` comments:

    sh -c "set -eux; \
           # The image ships with a pre-installed nightly ... \
           rustup toolchain uninstall ... \
           ..."

YAML's `run: |` block-scalar folds that into a single line on the
shell side — backslash-newline collapses become spaces. The
payload handed to `sh -c` becomes one long line in which the
first `#` comments out everything that follows on that line, so
the only command that actually ran inside the container was
`set -eux;`. Everything after it was a comment. The container
exited successfully (set -eux + empty; is a zero-exit no-op),
the `target/` directory never got created, and the post-docker
`sudo chown -R "$(id -u):$(id -g)" target` failed with

    chown: cannot access 'target': No such file or directory
    Process completed with exit code 1.

which fooled me into thinking the toolchain logic failed, when
actually NO toolchain logic ran at all.

Fix: use bash with a single-quoted multi-line script. Single
quotes preserve newlines literally, so `#` stays a
line-terminating comment rather than collapsing. Heredoc-style
formatting; same commands as before.

No other changes. Version bumps only (Cargo + Android versionCode/
versionName). Telegram notify stays off via the repo-variable
gate we added yesterday.
2026-04-23 11:06:37 +03:00
therealaleph 54b3956e23 ci: gate Telegram auto-notify behind repo variable (default off)
The `telegram:` job now runs only when the repo-level variable
`TELEGRAM_NOTIFY_ENABLED` is set to the literal string \"true\".
Default (unset) = job skips silently.

Turn on:   gh variable set TELEGRAM_NOTIFY_ENABLED --body true
Turn off:  gh variable set TELEGRAM_NOTIFY_ENABLED --body false
           (or `gh variable delete TELEGRAM_NOTIFY_ENABLED`)

The Python script, secrets, and changelog format all stay in place
so re-enabling is a one-liner, not a workflow edit. Forks that don't
need Telegram don't have to touch anything — they get the clean
no-notify behaviour out of the box.
2026-04-23 10:52:44 +03:00
therealaleph a4e378d88c ci: mipsel-softfloat — fix rustup upgrade-in-place error
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.
2026-04-23 10:18:04 +03:00
therealaleph 6541eb013f ci: mipsel-softfloat — use mipsel-musl image + RUSTFLAGS=+soft-float
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.
2026-04-23 10:14:11 +03:00
therealaleph cbfdab582a ci(telegram): move notify step to Python, fix curl < parse bug
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.
2026-04-23 10:09:51 +03:00
therealaleph d51719b7b5 ci: auto-post CI-built Android APK to Telegram on every release tag
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.
2026-04-23 09:52:41 +03:00
therealaleph a25ce563ab ci: skip host rustup for mipsel-softfloat (docker handles its toolchain)
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.
2026-04-23 09:49:06 +03:00
Shin (Former Aleph) 28be8f67d5 v1.1.0: unified Connect button, proxy mode, app splitting, Persian UI, MIPS build (#41)
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>
2026-04-23 09:38:10 +03:00
Shin (Former Aleph) 91015b0594 v1.0.0: multi-arch Android APK + GitHub Actions release job + install docs (#30)
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>
2026-04-23 02:56:39 +03:00
therealaleph 3534747e68 v0.8.6: armhf glibc pin + Check-for-updates button (#15)
=== 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.
2026-04-22 19:03:14 +03:00
Hamed Soltani b617f67c6f add armhf arch to build targets 2026-04-22 18:18:44 +03:30
therealaleph 04661bfdec v0.7.1: Linux GLIBC 2.35 floor + scroll main window on short screens
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>
2026-04-22 11:08:57 +03:00
therealaleph fdd6c200d6 ci: drop --user on musl docker build (caused /root/.cargo permission denied)
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.
2026-04-22 02:32:56 +03:00
therealaleph 6c5b62e5e6 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.
2026-04-22 02:29:26 +03:00
therealaleph 899ef06f4a v0.4.1: launcher scripts (run.sh / run.command / run.bat)
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.
2026-04-21 22:17:25 +03:00
therealaleph 3eb0d9667a ci: simplify aarch64-linux cross-compile (CLI-only, skip UI)
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.
2026-04-21 21:48:39 +03:00
therealaleph e4fe2b5939 v0.4.0: add cross-platform desktop UI (egui)
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.
2026-04-21 21:36:52 +03:00
therealaleph 2dd8be72ca initial release: Rust port of MasterHttpRelayVPN apps_script mode
Faithful port of @masterking32's MasterHttpRelayVPN. All credit for
the original idea, protocol, and Python implementation goes to him.

Implemented:
- Local HTTP proxy (CONNECT + plain HTTP)
- MITM with on-the-fly per-domain cert generation via rcgen
- CA auto-install for macOS / Linux / Windows
- Apps Script JSON relay, protocol-compatible with Code.gs
- TLS client with SNI spoofing (connect to Google IP, SNI=www.google.com,
  inner HTTP Host=script.google.com)
- Connection pooling (45s TTL, max 20 idle)
- Multi-script round-robin for higher quota
- Header filtering (strips connection-specific + brotli)
- Config-driven, JSON schema matches Python version

Deferred (TODOs in code):
- HTTP/2 multiplexing
- Request batching / coalescing / response cache
- Range-based parallel download
- SNI-rewrite tunnels for YouTube/googlevideo
- Firefox NSS cert install
- domain_fronting / google_fronting / custom_domain modes
  (mostly broken post-Cloudflare 2024, not a priority)

13 unit tests pass, 2.4MB stripped release binary.
2026-04-21 18:03:03 +03:00