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:
therealaleph
2026-04-25 11:54:45 +03:00
parent 7efd12d8d3
commit fb552c227d
10 changed files with 412 additions and 28 deletions
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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