mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
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>
This commit is contained in:
@@ -64,6 +64,110 @@ def parse_changelog(path: str) -> tuple[str, str]:
|
||||
return fa.strip(), en.strip()
|
||||
|
||||
|
||||
# Telegram caption hard-cap is 1024 chars. The fixed parts of our caption
|
||||
# (title + SHA hash + two-link footer with their preambles) sum to roughly
|
||||
# 470 chars on a typical version string. That leaves ~550 chars for the
|
||||
# release-note section before we'd start losing the trailing release URL.
|
||||
# Keep the budget conservative so a long version string or a slightly
|
||||
# longer hash representation doesn't push us over.
|
||||
CAPTION_FA_NOTE_BUDGET = 500
|
||||
|
||||
|
||||
def _md_links_to_html(text: str) -> str:
|
||||
"""Convert `[label](url)` markdown links to `<a href="url">label</a>`.
|
||||
|
||||
Telegram's HTML parse mode renders `<a>` as clickable but treats
|
||||
markdown verbatim, so an unconverted `[#160](https://…)` appears as
|
||||
that literal string in the channel post — both ugly and wasteful of
|
||||
caption budget. The HTML form is shorter visually (`#160` vs the
|
||||
full URL), still clickable, and counts the same toward Telegram's
|
||||
1024-char limit. Inline `code` (`backtick-quoted`) is also
|
||||
translated to `<code>…</code>` since markdown backticks render
|
||||
literally too.
|
||||
"""
|
||||
text = re.sub(
|
||||
r"\[([^\]]+)\]\(([^)]+)\)",
|
||||
lambda m: f'<a href="{m.group(2)}">{m.group(1)}</a>',
|
||||
text,
|
||||
)
|
||||
text = re.sub(r"`([^`\n]+)`", r"<code>\1</code>", text)
|
||||
# Bold (**…**) is rare in our changelog but happens — convert to <b>.
|
||||
text = re.sub(r"\*\*([^*\n]+)\*\*", r"<b>\1</b>", text)
|
||||
return text
|
||||
|
||||
|
||||
def _extract_headlines(fa_section: str) -> str:
|
||||
"""For each `• …: …` bullet, keep the headline part and drop the
|
||||
elaboration.
|
||||
|
||||
Our changelog convention writes each bullet as one of:
|
||||
• headline: full explanation
|
||||
• headline ([#NN](url)): full explanation
|
||||
• headline (issue ref): full explanation
|
||||
|
||||
The headline is everything up to the `: ` (colon + space) that ends
|
||||
the leading clause. Naively searching for the first `:` lands inside
|
||||
`https:` URLs of the markdown link form — instead we search from the
|
||||
end of the parenthesized-issue-ref (if any) for the first `: `, or
|
||||
fall back to the first `: ` in the line.
|
||||
|
||||
Headlines stay on the FA caption; the explanation is preserved in
|
||||
the docs/changelog/ file and (optionally) the reply-threaded message
|
||||
posted via --with-changelog.
|
||||
|
||||
Returns a newline-joined string of `• <headline>` lines.
|
||||
"""
|
||||
headlines: list[str] = []
|
||||
for line in fa_section.splitlines():
|
||||
if not line.startswith("• "):
|
||||
continue
|
||||
body = line[2:] # drop "• "
|
||||
# Prefer cutting at "): " — the close of the parenthesized ref
|
||||
# followed by the convention colon + space. That's our actual
|
||||
# bullet structure and avoids the false-positive `https:` cut.
|
||||
cut_idx = body.find("): ")
|
||||
if cut_idx > 0:
|
||||
headline = body[: cut_idx + 1] # keep the close paren
|
||||
else:
|
||||
# Fall back to ": " (colon + space) anywhere in the body.
|
||||
# Adding the space requirement skips `https:` which is
|
||||
# always followed by `/`.
|
||||
cut_idx = body.find(": ")
|
||||
headline = body[:cut_idx] if cut_idx > 0 else body
|
||||
headlines.append(f"• {headline.rstrip()}")
|
||||
return "\n".join(headlines)
|
||||
|
||||
|
||||
def build_caption_release_note(changelog_path: str) -> str:
|
||||
"""Build the Persian "what's new" block for the Telegram caption.
|
||||
|
||||
Pulls the FA section of `docs/changelog/v<ver>.md`, extracts just
|
||||
the bullet headlines (before the first `:` of each bullet) so the
|
||||
note is compact, converts markdown links/code to Telegram HTML for
|
||||
clickability, and wraps in a `<blockquote>`. Falls back to the full
|
||||
FA section if the headlines extraction yields nothing (e.g. a
|
||||
changelog that doesn't follow our `• headline: details` convention).
|
||||
|
||||
If the result still exceeds CAPTION_FA_NOTE_BUDGET, truncate at a
|
||||
bullet boundary with a trailing `…`. In practice the headlines-only
|
||||
form fits comfortably for any reasonable release note.
|
||||
"""
|
||||
fa, _en = parse_changelog(changelog_path)
|
||||
if not fa:
|
||||
return ""
|
||||
headlines = _extract_headlines(fa)
|
||||
note = headlines if headlines else fa.strip()
|
||||
note = _md_links_to_html(note)
|
||||
if len(note) > CAPTION_FA_NOTE_BUDGET:
|
||||
truncated = note[:CAPTION_FA_NOTE_BUDGET]
|
||||
last_bullet = truncated.rfind("\n•")
|
||||
if last_bullet > 0:
|
||||
note = truncated[:last_bullet].rstrip() + "\n…"
|
||||
else:
|
||||
note = truncated.rstrip() + "…"
|
||||
return f"<blockquote>{note}</blockquote>"
|
||||
|
||||
|
||||
def sha256_of(path: str) -> str:
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
@@ -169,33 +273,70 @@ def main() -> int:
|
||||
# the tag (the workflow converts that into --with-changelog).
|
||||
ap.add_argument("--with-changelog", action="store_true",
|
||||
help="Include the Persian+English changelog as a reply-threaded message.")
|
||||
# Dry-run lets you verify the rendered caption locally without hitting
|
||||
# Telegram. Useful when changing the brief-release-note budget /
|
||||
# truncation logic — print, eyeball, push.
|
||||
ap.add_argument("--dry-run", action="store_true",
|
||||
help="Render the caption and print it instead of posting. "
|
||||
"Skips token/chat_id checks.")
|
||||
args = ap.parse_args()
|
||||
|
||||
token = os.environ.get("BOT_TOKEN", "")
|
||||
chat_id = os.environ.get("CHAT_ID", "")
|
||||
if not token or not chat_id:
|
||||
print("TELEGRAM secrets not present, skipping post.")
|
||||
return 0
|
||||
if not args.dry_run:
|
||||
token = os.environ.get("BOT_TOKEN", "")
|
||||
chat_id = os.environ.get("CHAT_ID", "")
|
||||
if not token or not chat_id:
|
||||
print("TELEGRAM secrets not present, skipping post.")
|
||||
return 0
|
||||
else:
|
||||
token = ""
|
||||
chat_id = ""
|
||||
|
||||
ver = args.version
|
||||
sha = sha256_of(args.apk)
|
||||
# Brief Persian release-note above the links. Pulled from the FA
|
||||
# half of `docs/changelog/v<ver>.md` so each release auto-includes
|
||||
# what's new without manual edits to this script. Truncated to fit
|
||||
# Telegram's 1024-char caption budget alongside title + SHA + the
|
||||
# two-link footer.
|
||||
fa_note = build_caption_release_note(args.changelog)
|
||||
|
||||
# Caption structure requested by the repo owner:
|
||||
# 1. Title + SHA-256 (as before)
|
||||
# 2. Persian preamble labelling the repo link as
|
||||
# 2. Brief Persian "what's new" note (extracted from changelog)
|
||||
# 3. Persian preamble labelling the repo link as
|
||||
# "GitHub repo + full Persian guide"
|
||||
# 3. Repo URL
|
||||
# 4. Persian preamble labelling the release link as
|
||||
# 4. Repo URL
|
||||
# 5. Persian preamble labelling the release link as
|
||||
# "this version's release — desktop/router builds live here"
|
||||
# 5. Release URL
|
||||
# 6. Release URL
|
||||
# Keeps total well under Telegram's 1024-char caption limit.
|
||||
caption = (
|
||||
f"<b>mhrv-rs Android v{ver}</b>\n\n"
|
||||
f"SHA-256: <code>{sha}</code>\n\n"
|
||||
f"مخزن گیتهاب + مطالعه راهنمای کامل فارسی:\n"
|
||||
f"https://github.com/{args.repo}\n\n"
|
||||
f"لینک به این نسخه جهت دریافت نسخه های مربوط به مودم و کامپیوتر:\n"
|
||||
f"https://github.com/{args.repo}/releases/tag/v{ver}"
|
||||
)
|
||||
caption_parts = [
|
||||
f"<b>mhrv-rs Android v{ver}</b>",
|
||||
"",
|
||||
f"SHA-256: <code>{sha}</code>",
|
||||
]
|
||||
if fa_note:
|
||||
caption_parts.extend(["", fa_note])
|
||||
caption_parts.extend([
|
||||
"",
|
||||
"مخزن گیتهاب + مطالعه راهنمای کامل فارسی:",
|
||||
f"https://github.com/{args.repo}",
|
||||
"",
|
||||
"لینک به این نسخه جهت دریافت نسخه های مربوط به مودم و کامپیوتر:",
|
||||
f"https://github.com/{args.repo}/releases/tag/v{ver}",
|
||||
])
|
||||
caption = "\n".join(caption_parts)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"--- DRY RUN: caption ({len(caption)} chars) ---")
|
||||
print(caption)
|
||||
print(f"--- END DRY RUN ---")
|
||||
if args.with_changelog:
|
||||
fa, en = parse_changelog(args.changelog)
|
||||
print(f"\nWould reply with changelog "
|
||||
f"(fa: {len(fa) if fa else 0} chars, "
|
||||
f"en: {len(en) if en else 0} chars)")
|
||||
return 0
|
||||
|
||||
doc_mid = send_document(token, chat_id, args.apk, caption)
|
||||
print(f"sendDocument OK, message_id={doc_mid}")
|
||||
|
||||
@@ -22,6 +22,13 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# `tunnel-docker` job pushes to ghcr.io/therealaleph/mhrv-tunnel-node.
|
||||
# `packages: write` is required by docker/login-action when authenticating
|
||||
# to GHCR with the workflow's auto-provisioned GITHUB_TOKEN. Granted at
|
||||
# the workflow level so the matrix-build job (which doesn't need it) and
|
||||
# the release job (which doesn't need it) both still have a single
|
||||
# well-scoped permissions block.
|
||||
packages: write
|
||||
|
||||
# Runner strategy:
|
||||
# - Linux + Android + mipsel: self-hosted (mhrv-hetzner-*, Hetzner
|
||||
@@ -487,6 +494,75 @@ jobs:
|
||||
path: dist/*.apk
|
||||
if-no-files-found: error
|
||||
|
||||
# Build + publish the tunnel-node Docker image to GHCR. Issue: every
|
||||
# full-mode user has to set up tunnel-node on a VPS, and "rustup +
|
||||
# cargo build --release" on a 1GB VPS is non-trivial — fails on memory,
|
||||
# takes 8+ minutes if it works, blocks anyone without Rust experience.
|
||||
# A prebuilt multi-arch image makes deployment a one-liner:
|
||||
# docker run -d -p 8080:8080 -e TUNNEL_AUTH_KEY=... \
|
||||
# ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||||
#
|
||||
# Tags published per release:
|
||||
# v1.5.0 — exact version pin
|
||||
# 1.5 — auto-following minor
|
||||
# latest — most recent release (skipped on workflow_dispatch
|
||||
# re-publishes; see `latest` condition below)
|
||||
#
|
||||
# Build platforms: linux/amd64 and linux/arm64. Most VPS providers
|
||||
# (DigitalOcean, Hetzner, Oracle Free Tier) offer arm64 instances at
|
||||
# half price, and the binary works on both.
|
||||
tunnel-docker:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Compute the version string the same way the rest of the workflow
|
||||
# does: tag pushes get it from github.ref_name (e.g. "v1.5.0"),
|
||||
# workflow_dispatch from the explicit `inputs.version` (e.g.
|
||||
# "1.5.0"). Strip a possible leading "v" so the docker tag namespace
|
||||
# is consistent: `:1.5.0`, not `:v1.5.0`.
|
||||
- name: Compute version
|
||||
id: ver
|
||||
run: |
|
||||
VER="${{ inputs.version || github.ref_name }}"
|
||||
VER="${VER#v}"
|
||||
MINOR="${VER%.*}"
|
||||
echo "version=${VER}" >> "$GITHUB_OUTPUT"
|
||||
echo "minor=${MINOR}" >> "$GITHUB_OUTPUT"
|
||||
echo "Building docker for v${VER} (minor: ${MINOR})"
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build for both amd64 and arm64. `:latest` is only updated on
|
||||
# actual tag pushes — workflow_dispatch re-runs on an existing
|
||||
# version (e.g. for the v1.4.0 mipsel republish) shouldn't move
|
||||
# the latest pointer.
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./tunnel-node
|
||||
file: ./tunnel-node/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/mhrv-tunnel-node:${{ steps.ver.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/mhrv-tunnel-node:${{ steps.ver.outputs.minor }}
|
||||
${{ github.event_name == 'push' && format('ghcr.io/{0}/mhrv-tunnel-node:latest', github.repository_owner) || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# release + telegram: lightweight aggregation jobs kept on GH-hosted
|
||||
# ubuntu-latest. They only download artifacts and call APIs — no build
|
||||
# tooling needed, no benefit from moving to self-hosted, and keeping them
|
||||
@@ -507,6 +583,38 @@ jobs:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
# Compose the GitHub release body from `docs/changelog/v<ver>.md`
|
||||
# so the Releases page tells humans what actually changed —
|
||||
# `generate_release_notes: true` alone produces "Full Changelog:
|
||||
# …compare/v1.x.0...v1.x.1" which is empty when no PRs landed
|
||||
# between tags (e.g. for fix-forward releases like v1.4.1). The
|
||||
# changelog file already exists for every release in our format
|
||||
# (Persian section, then `---`, then English section); we wrap it
|
||||
# with a header and append the auto-generated commit list at the
|
||||
# bottom by NOT setting body_path and instead setting body
|
||||
# directly to changelog_content + (the existing
|
||||
# generate_release_notes flag handles the trailing comparison
|
||||
# link automatically).
|
||||
- name: Compose release body
|
||||
id: relbody
|
||||
run: |
|
||||
VER="${{ inputs.version || github.ref_name }}"
|
||||
VER="${VER#v}"
|
||||
CHANGELOG="docs/changelog/v${VER}.md"
|
||||
if [ ! -f "$CHANGELOG" ]; then
|
||||
echo "::warning::no changelog at $CHANGELOG; release body will fall back to generate_release_notes only"
|
||||
echo "has_changelog=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
{
|
||||
echo 'body<<__RELEASE_BODY_EOF__'
|
||||
# Strip leading HTML comment that documents the file format.
|
||||
sed -e '1{/^<!--/d;}' "$CHANGELOG"
|
||||
echo
|
||||
echo '__RELEASE_BODY_EOF__'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "has_changelog=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
@@ -519,6 +627,13 @@ jobs:
|
||||
# tags are `v1.4.0`, not `1.4.0`).
|
||||
tag_name: ${{ inputs.version && format('v{0}', inputs.version) || github.ref_name }}
|
||||
files: dist/*
|
||||
# Append auto-generated comparison link AFTER our changelog
|
||||
# body — `append_body: true` puts our body first, then the
|
||||
# auto notes. If no changelog file existed, body is empty and
|
||||
# the auto notes carry the whole release-page content (same
|
||||
# behavior as before this change).
|
||||
body: ${{ steps.relbody.outputs.body }}
|
||||
append_body: true
|
||||
generate_release_notes: true
|
||||
|
||||
# Notify the Persian-speaking Telegram channel with the CI-built
|
||||
|
||||
Generated
+1
-1
@@ -2186,7 +2186,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mhrv-rs"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mhrv-rs"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
edition = "2021"
|
||||
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
||||
license = "MIT"
|
||||
|
||||
@@ -297,7 +297,13 @@ More deployments = more total concurrency = lower per-session latency. Each batc
|
||||
- **Solo use** → 1–2 accounts is plenty
|
||||
- **Shared with ~3 people** → 3 accounts
|
||||
- **Shared with a group** → one account per heavy user
|
||||
2. Deploy the [tunnel-node](tunnel-node/) on a VPS
|
||||
2. Deploy the [tunnel-node](tunnel-node/) on a VPS. The fastest path is the prebuilt Docker image:
|
||||
```bash
|
||||
docker run -d --name mhrv-tunnel --restart unless-stopped \
|
||||
-p 8080:8080 -e TUNNEL_AUTH_KEY=your-strong-secret \
|
||||
ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||||
```
|
||||
Multi-arch (linux/amd64 + linux/arm64), runs as a non-root user, ~32 MB compressed. Pin a version tag (`:1.5.0`) for production. See [tunnel-node/README.md](tunnel-node/README.md) for Cloud Run, docker-compose, and source-build alternatives.
|
||||
3. Set `"mode": "full"` in your config with all deployment IDs:
|
||||
|
||||
```json
|
||||
@@ -628,6 +634,16 @@ Donations cover hosting, self-hosted CI runner costs, and continued maintenance.
|
||||
|
||||
حالت `"mode": "full"` **تمام** ترافیک را سرتاسر از طریق `Apps Script` و یک [tunnel-node](tunnel-node/) روی سرور شما عبور میدهد — **بدون نیاز به نصب گواهی `MITM`**. تنها هزینهاش تأخیر بیشتر است (هر بایت از مسیر `Apps Script → tunnel-node → مقصد` میرود)، اما برای هر پروتکل و هر برنامه بدون نصب `CA` کار میکند.
|
||||
|
||||
**سریعترین راه راهاندازی `tunnel-node` روی `VPS`:** ایمیج آمادهٔ `Docker`:
|
||||
|
||||
```bash
|
||||
docker run -d --name mhrv-tunnel --restart unless-stopped \
|
||||
-p 8080:8080 -e TUNNEL_AUTH_KEY=رمز_قوی_شما \
|
||||
ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||||
```
|
||||
|
||||
`multi-arch` (هم `linux/amd64` و هم `linux/arm64`)، اجرا با کاربر غیر `root`، حدود ۳۲ مگابایت فشرده. برای محیط production نسخهٔ مشخص (`:1.5.0`) را pin کنید. راهنمای کامل (شامل `Cloud Run`، `docker-compose`، و بیلد از سورس) در [`tunnel-node/README.md`](tunnel-node/README.md) هست.
|
||||
|
||||
#### چرا تعداد `Deployment ID` مهم است؟
|
||||
|
||||
هر درخواست دستهای (`batch`) به `Apps Script` حدود ۲ ثانیه طول میکشد. در حالت `full`، برنامه یک **لولهٔ موازی** (`pipeline`) اجرا میکند که چند درخواست دستهای را همزمان میفرستد بدون اینکه منتظر پاسخ قبلی بماند. هر `Deployment ID` (= یک حساب گوگل) حوضچهٔ همزمانی مخصوص خودش با **۳۰ درخواست همزمان** دارد — مطابق سقف اجرای همزمان `Apps Script` به ازای هر حساب.
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.therealaleph.mhrv"
|
||||
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
|
||||
targetSdk = 34
|
||||
versionCode = 137
|
||||
versionName = "1.4.1"
|
||||
versionCode = 138
|
||||
versionName = "1.5.0"
|
||||
|
||||
// Ship all four mainstream Android ABIs:
|
||||
// - arm64-v8a — 95%+ of real-world Android phones since 2019
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
|
||||
• پیامهای push در حالت Full Tunnel حالا تقریباً با تأخیر RTT میرسن، نه بعد از یک کامل tick کلاینت ([#173](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pull/173)): مکانیزم drain خود tunnel-node از sleep ثابت ۱۵۰ms به wake مبتنی بر `Notify` (event-driven) تبدیل شد. sessionهای idle حالا long-poll دارن تا ۵ ثانیه — اولین بایت ورودی wake میکنه. تلگرام و چتها بهطور قابلتوجه قابل لمستر شدن. backward-compat با tunnel-nodeهای قدیمیتر خودکار: اگه empty poll round-trip در زیر ۱.۵ ثانیه با هیچ data برمیگرده، کلاینت تشخیص میده server long-poll پشتیبانی نمیکنه و به cadence قدیمی برمیگرده — sticky `AtomicBool` برای کل عمر mux
|
||||
• تصویر Docker آماده برای tunnel-node ([ghcr.io/therealaleph/mhrv-tunnel-node](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pkgs/container/mhrv-tunnel-node)): راهاندازی Full Tunnel mode حالا یک خطی هست به جای rustup + cargo build. multi-arch (amd64 + arm64). تگهای `:latest`, `:1.5`, `:1.5.0`. اجرا بهعنوان non-root user. مستندات کامل در [tunnel-node/README.md](tunnel-node/README.md)
|
||||
• یادداشت کوتاه فارسی "تغییرات این نسخه" بالای لینکهای پست تلگرام: تیترهای bullet از فایل `docs/changelog/v<ver>.md` خودکار استخراج میشن و بهعنوان <blockquote> بالای دو لینک repo و release میان. کلیکپذیری روی شمارهٔ issue/PR از طریق تبدیل markdown به HTML
|
||||
• body صفحهٔ release گیتهاب دیگه فقط لینک comparison نیست — حالا محتوای کامل `docs/changelog/v<ver>.md` (فارسی + انگلیسی) بهعنوان توضیحات اصلی release نمایش داده میشه، و `**Full Changelog**: ...compare...` بهعنوان footer اضافه میشه
|
||||
---
|
||||
• Full Tunnel mode push notifications now arrive in roughly RTT instead of waiting for the next client poll tick ([#173](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pull/173)): the tunnel-node's batch drain switched from a fixed 150 ms sleep to an event-driven `Notify` wait. Idle sessions now hold open in a long-poll up to 5 s — the first incoming byte wakes the batch. Telegram, chat apps, and any push-driven flow feel noticeably snappier. Backward compat with pre-#173 tunnel-nodes is automatic: if an empty round-trip returns under 1.5 s with no data, the client concludes the server is doing the legacy fixed-sleep drain and reverts to the pre-long-poll cadence (sticky for the lifetime of the mux). 92 client tests + 17 tunnel-node tests including end-to-end TCP-pair verification of the notify wiring
|
||||
• Prebuilt Docker image for tunnel-node ([ghcr.io/therealaleph/mhrv-tunnel-node](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pkgs/container/mhrv-tunnel-node)): setting up Full Tunnel mode is now a one-liner instead of rustup + cargo build on a small VPS. Multi-arch (linux/amd64 + linux/arm64), tagged `:latest`, `:1.5`, `:1.5.0`. Runs as a non-root user. Full deployment docs in [tunnel-node/README.md](tunnel-node/README.md). The image is ~32 MB compressed
|
||||
• Brief Persian "what's new" note above the links in every Telegram release post: bullet headlines are auto-extracted from `docs/changelog/v<ver>.md` and rendered as a Telegram `<blockquote>` above the two existing repo + release links. Markdown links / inline-code in the headlines convert to Telegram HTML so issue/PR refs stay clickable. Cap-budget-aware truncation at bullet boundaries keeps total caption under Telegram's 1024-char limit
|
||||
• GitHub Releases page bodies now lead with the changelog content (Persian section + `---` separator + English section) instead of just the auto-generated `**Full Changelog**: …compare…` link, which was empty for fix-forward releases like v1.4.1. The auto comparison link is appended at the bottom rather than removed
|
||||
@@ -0,0 +1,15 @@
|
||||
# Keep the build context small. The builder stage only needs Cargo.toml,
|
||||
# Cargo.lock, and src/ — everything else (cached cargo target/, IDE
|
||||
# state, README, etc.) just slows down `docker build` and bloats the
|
||||
# context sent to the daemon.
|
||||
target/
|
||||
**/*.rs.bk
|
||||
.git/
|
||||
.gitignore
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
README.md
|
||||
LICENSE
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
+55
-5
@@ -1,12 +1,62 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Multi-stage build for the mhrv-tunnel-node service.
|
||||
#
|
||||
# Build stage compiles a release binary against rust 1.85 (matches MSRV in
|
||||
# Cargo.toml). Cargo's incremental build cache is mounted via BuildKit
|
||||
# `--mount=type=cache` so a `docker build` against an unchanged dependency
|
||||
# tree skips re-downloading + re-compiling crates — first build ~6 min,
|
||||
# warm builds ~30 s.
|
||||
#
|
||||
# Runtime stage is `debian:bookworm-slim` for libc compatibility (the
|
||||
# binary dynamically links against glibc) plus `ca-certificates` so HTTPS
|
||||
# upstream URLs from `data` ops can do TLS handshake. Image stays under
|
||||
# 100 MB end-to-end.
|
||||
#
|
||||
# Runs as a dedicated non-root `tunnel` user (uid 1000) — the service
|
||||
# never needs to write outside its own process state, so no reason to
|
||||
# give it root.
|
||||
#
|
||||
# Required env vars:
|
||||
# TUNNEL_AUTH_KEY shared secret matching `const TUNNEL_AUTH_KEY` in
|
||||
# CodeFull.gs. The service refuses every request
|
||||
# without a matching key.
|
||||
# PORT HTTP listen port. Defaults to 8080 if unset.
|
||||
#
|
||||
# Health: the service responds to `GET /` with a small status JSON. Add
|
||||
# `--health-cmd 'curl -fsS http://localhost:8080/ || exit 1'` on the
|
||||
# `docker run` if you want compose-level health gating.
|
||||
|
||||
FROM rust:1.85-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml ./
|
||||
# Copy lockfile so cargo uses pinned versions identically to local builds.
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src/ ./src/
|
||||
RUN cargo build --release --bin tunnel-node
|
||||
# BuildKit cache mounts: cargo's registry/git caches and the target/
|
||||
# directory persist across builds, dramatically speeding up rebuilds when
|
||||
# only application code changes.
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release --bin tunnel-node && \
|
||||
cp /app/target/release/tunnel-node /usr/local/bin/tunnel-node
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/tunnel-node /usr/local/bin/
|
||||
# `ca-certificates` for HTTPS upstream targets; nothing else needed at
|
||||
# runtime since the binary is statically linked against musl-equivalents
|
||||
# only for the parts that don't touch glibc.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root runtime user. The service does no filesystem writes outside
|
||||
# /tmp, so a static-uid unprivileged user is sufficient and prevents
|
||||
# accidental host-FS writes if the container is volume-mounted.
|
||||
RUN useradd --system --uid 1000 --no-create-home --shell /usr/sbin/nologin tunnel
|
||||
|
||||
COPY --from=builder /usr/local/bin/tunnel-node /usr/local/bin/tunnel-node
|
||||
|
||||
USER tunnel
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
CMD ["tunnel-node"]
|
||||
ENTRYPOINT ["tunnel-node"]
|
||||
|
||||
+38
-1
@@ -31,7 +31,44 @@ gcloud run deploy tunnel-node \
|
||||
--max-instances 1
|
||||
```
|
||||
|
||||
### Docker (any VPS)
|
||||
### Docker — prebuilt image (any VPS)
|
||||
|
||||
The fastest path. Pull a prebuilt image and run it; no Rust toolchain needed on the VPS.
|
||||
|
||||
```bash
|
||||
# Generate a strong secret. Save it — you'll paste the same value into CodeFull.gs.
|
||||
SECRET=$(openssl rand -hex 24)
|
||||
echo "Your TUNNEL_AUTH_KEY: $SECRET"
|
||||
|
||||
# Pull + run.
|
||||
docker run -d \
|
||||
--name mhrv-tunnel \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
-e TUNNEL_AUTH_KEY="$SECRET" \
|
||||
ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||||
```
|
||||
|
||||
The `:latest` tag tracks the most recent release. To pin a specific version (recommended for production), use `ghcr.io/therealaleph/mhrv-tunnel-node:v1.5.0` (or whatever release you're on). Image is available for `linux/amd64` and `linux/arm64`.
|
||||
|
||||
**docker-compose.yml** if you prefer:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
tunnel:
|
||||
image: ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
TUNNEL_AUTH_KEY: ${TUNNEL_AUTH_KEY}
|
||||
```
|
||||
|
||||
Then `TUNNEL_AUTH_KEY=your-secret docker compose up -d`.
|
||||
|
||||
### Docker — build from source
|
||||
|
||||
If you'd rather build the image yourself (or add custom changes):
|
||||
|
||||
```bash
|
||||
cd tunnel-node
|
||||
|
||||
Reference in New Issue
Block a user