github MotherofallVPNs/MoaV v1.9.0

one hour ago

Changed

  • Repository moved to the MotherofallVPNs orgshayanb/MoaV is now MotherofallVPNs/moav, joining moav-client and moav-site under one org. GitHub 301-redirects the old paths (clones, issues, PRs, releases) so existing installs and links keep working. Swept the hardcoded shayanb/MoaV references across the repo (install one-liner REPO_URL, README badges, docs, issue templates, CHANGELOG links, the dns-router Go module path) to the new home; the personal GitHub Sponsors link is intentionally unchanged.

Added

  • moav uninstall non-interactive flags--yes/-y skips the confirmation prompts, and --remove-images also deletes the Docker images (otherwise --yes keeps the image cache). Makes uninstall scriptable for automation/CI; interactive behavior is unchanged when no flag is given.
  • moav bootstrap --yes — skips the confirmation prompts, including the "already bootstrapped, re-run?" one. Re-bootstrap is already idempotent (keeps keys/users, regenerates configs), but without a TTY the prompt defaulted to no and silently cancelled — so bootstrap was unscriptable. Makes it usable from automation/CI; interactive behavior is unchanged.
  • moav user base64 <user> — emits base64 of a text-only bundle (the config files + subscription.txt; excludes the QR PNGs and README.html, which are the bulk). Paste it into the moav-client e2e's bundle_b64 dispatch input to validate a deployed server against the current client, or use it for a quick client import (moav user base64 alice | pbcopy).
  • Single-source protocol roster (data/protocols.json + scripts/gen-protocol-docs.py) — the protocol list had drifted across 6+ surfaces (README, docs/protocols.md, site/index.html meta/JSON-LD, site/llms.txt), most visibly AnyTLS and Shadowsocks-2022 missing from some. There is now one canonical roster: the docs/protocols.md overview table is generated from it (marker-delimited, idempotent), and site/index.html + README.md + site/llms.txt are drift-checked against it — gen-protocol-docs.py --check fails if any protocol is missing from a surface (run in CI). Fixed the current drift: added the missing AnyTLS to the site's meta/OG/JSON-LD/keywords strings and named wstunnel in llms.txt.
  • CI pipeline (.github/workflows/ci.yml) — runs on every PR to dev/main: shellcheck --severity=error across all 54 shell scripts, bash -n parse checks, go vet + go test -race for dns-router, docker compose config validation, the share-link golden test, and the protocol-roster drift gate. First automated regression gate for the repo (previously only release/site-deploy workflows existed). Fixed the one error-severity shellcheck finding it surfaced — an unquoted array expansion (SC2068) in moav.sh's build path that could re-split service names.
  • Manual/per-release end-to-end test (.github/workflows/e2e.yml) — stands up the full Compose stack on a self-hosted runner (a test VPS with a real test domain), provisions a user, and runs moav test (client-test.sh) against the live server across every enabled protocol, plus the CLI smoke test. Triggered manually (workflow_dispatch) and on published releases (nightly is available but disabled by default). Inputs: verbose, full (see below), and domainless — a domain-less run that skips Let's Encrypt entirely, so it validates the IP-only protocols and the client-image build without touching the LE rate limit (and needs no test domain). Standard domain runs reuse the moav_certs volume across runs (issue once, reuse — LE allows only 5 certs/week per domain); domainless/full runs start from a clean slate. Not on the per-PR path because it builds ~25 images and needs TLS certs. Setup + manual-run instructions in docs/devdocs/E2E-TESTING.md.

Testing

  • moav test now covers Shadowsocks-2022 and XDNS — the two protocols that are on by default but previously had no connectivity test. test_ss decodes the bundle's ss:// SIP002 URI (base64url method:server_psk:user_psk) and drives a sing-box shadowsocks outbound over a local SOCKS port; test_xdns runs the bundle's own Xray client config (xdns-direct-config.json, falling back to the via-DNS config) and checks its SOCKS inbound, with a longer timeout and warn-on-failure since DNS tunnels are slow. Both appear in the human + JSON result summaries.
  • moav test now covers the remaining protocolstest_cdn drives a real sing-box VLESS+WebSocket outbound through the Cloudflare-fronted endpoint (warn-on-fail, since it depends on the operator's Cloudflare proxied-record + Origin-Rule + SSL setup); test_wstunnel validates the WireGuard-over-wstunnel config and verifies the tunnel port actually speaks wss:// TLS (the 1.9.0 hardening); test_telemt was upgraded from a bare nc -z to a Fake-TLS handshake probe (it decodes the fronted domain from the tg:// secret and confirms telemt answers a TLS hello, not just an open socket). test_masterdns and test_gooserelay skip honestly with what to check manually — MasterDNS has no standalone client in the harness (MahsaNG v16), and GooseRelay needs a user-deployed Google Apps Script forwarder to reach its exit. Also fixed a latent gap: xhttp was being tested but omitted from the result summaries — it (and cdn/wstunnel/masterdns/gooserelay) now appear in both the human and JSON output. This completes the protocol-test matrix; the e2e runner validates it end-to-end.
  • moav CLI smoke test (tests/cli-smoke-test.sh) — beyond the protocol tunnels, this exercises the tool itself against the live stack: help/version/status/users/profiles/cert status/logs/check/doctor/net status/conduit-offsets status, a user addrevoke lifecycle plus user add --batch, admin password (non-interactive), restart, an exportimport round-trip, update --help, donate status (display only — never actually donates), install.sh --help, and a non-interactive TUI-menu launch — each with a hang-guard timeout, asserting read/report commands exit clean and diagnostics don't crash. Wired into the e2e workflow so a broken CLI command fails the run.
  • e2e FULL mode — a full workflow input that, on top of the standard domain-mode run, also runs the domainless-mode lifecycle (reconfigure without a domain, bootstrap → start → provision → test the domainless protocols), moav build --local (monitoring images from source), and exercises the image-removal path of moav uninstall. The standard (default) run stays fast; FULL is opt-in for release/thorough validation.
  • moav test now validates the generated bundle README.html — a test_readme_bundle check asserts the client guide is fully rendered: no template placeholder ({{TOKEN}}) survives substitution, and no protocol whose config file is present in the bundle (i.e. enabled) still shows its "…not enabled/available" sentinel. This catches template↔generator drift — a new {{CONFIG_X}} wired into one generation path but not the other (exactly the Shadowsocks/XHTTP gap fixed above) — that connectivity tests can't see. Appears in the human + JSON summaries under readme and fails the e2e run.

Internal

  • Consolidated the test scripts into tests/client-test.sh (protocol harness), cli-smoke-test.sh (CLI smoke), and the share-link golden test (tests/singbox-links-test.sh) now live in one tests/ directory with a README describing the three layers, instead of scattered across scripts/ + scripts/lib/. Updated the references (Dockerfile.client COPY, ci.yml/e2e.yml, CONTRIBUTING, docs). Go unit tests stay next to their package (dns-router/main_test.go) per Go convention. No behavior change.
  • Deduplicated sing-box share-link construction into lib/sing-box.sh — the Reality/Trojan/AnyTLS/Hysteria2/CDN/Shadowsocks-2022 link templates (plus their IPv6 variants, and the SS SIP002 base64url(method:server_psk:user_psk) userinfo encoding) were built identically in two places, scripts/singbox-user-add.sh (host add-user path) and scripts/generate-user.sh (bundle generator), and had to be kept in sync by hand. They're now single-source builder functions (singbox_reality_link, singbox_trojan_link, singbox_anytls_link, singbox_hysteria2_link, singbox_cdn_link, singbox_ss_userinfo, singbox_ss_link) that both scripts call. A pure-function golden test (tests/singbox-links-test.sh, 12 cases) locks the exact output, so the extraction is provably byte-identical — no change to any generated link. First step of the v2 provisioning-code consolidation (the server-config jq mutations and the generate-single-user.sh merge follow next).

Documentation

  • CONTRIBUTING.md — top-level contributor entry point: dev setup, the PRs-target-dev rule, comment/CHANGELOG/test-domain conventions, the local CI commands (shellcheck, go test, the share-link golden test, the roster drift gate), how the single-source protocol roster works, and how to add a protocol. Cross-links the devdocs checklists.
  • v2 docs & compatibility sweep — reconciled the docs with the shipped 1.8.5 feature set: added a Certificates section to docs/CLI.md (moav cert status/renew/install/uninstall, CERT_AUTORENEW); added the missing Shadowsocks-2022 section + overview-table row and an XDNS_METHOD (txt/aaaa) row to docs/protocols.md, and updated the wstunnel entry to describe wss:// + the path secret; added AnyTLS to docs/architecture.md (plus a new Security & isolation / Service lifecycle section) and to the human-visible/structured surfaces of site/index.html, and fixed a stale "version 1.8.2" string there; documented the admin empty-password fail-closed (503) behavior in docs/OPSEC.md; refreshed site/llms.txt

Changed

  • wstunnel now wraps WireGuard in real TLS (wss://) instead of plain ws:// — the Let's Encrypt cert was already mounted into the container but unused, so the WebSocket upgrade travelled in cleartext and was trivially DPI-fingerprintable (defeating the whole point of tunnelling WireGuard through "HTTPS"). The entrypoint now serves wss:// using that cert when DOMAIN is set (runs as root to read the root-owned cert, copies it to a tmpfs, then drops to the unprivileged moav user via setpriv — same pattern sing-box uses), and falls back to plain ws:// only in domainless mode. Additionally, a per-install HTTP-upgrade path secret (state/keys/wstunnel-path.secret, generated at bootstrap) is enforced server-side (--restrict-http-upgrade-path-prefix) and emitted in every client bundle (--http-upgrade-path-prefix), so a scanner probing :8080 can't complete the upgrade without knowing the path. Generated client commands (bundle instructions + HTML guide, which previously showed an incorrect wss:// command with the wrong forwarding target) now come from a single shared helper. moav cert renew restarts wstunnel alongside the other cert consumers. Existing installs: re-run moav bootstrap (or moav update then restart wstunnel) to generate the secret and pick up wss://; until then it degrades gracefully to the previous ws:// behavior. Requires rebuilding the wstunnel image (moav build wstunnel)

Added

  • XDNS record-mode selector XDNS_METHOD (txt default, aaaa opt-in) — Xray's finalmask XDNS client can tunnel over AAAA records instead of TXT (upstream #6123), which is meaningfully higher-throughput per query. Generated client bundles stay on txt for the widest client compatibility; set XDNS_METHOD=aaaa to emit x.<domain>:aaaa+udp://… resolvers. Requires an Xray client core ≥ v26.6.1 (Happ / Xray CLI); server side needs nothing
  • Opt-in telemt anti-DPI knobsTELEMT_CLIENT_MSS / TELEMT_CLIENT_MSS_BULK expose telemt's handshake-MSS clamping (telemt ≥ 3.4.15/3.4.19): fragmenting the MTPROTO handshake defeats MSS-based DPI fingerprinting (e.g. Iran's TSPU), while the _BULK value restores MSS for bulk data so throughput isn't tanked. Both default empty (off) — this is a targeted counter to MSS-fingerprinting networks, not a universal default, and upstream ships it off. TELEMT_CONFIG_STRICT (default false) makes telemt fail fast on an unknown/typo'd generated key instead of silently ignoring it. Stale upstream doc links in the generated config's comments were refreshed
  • MASTERDNS_PUBLIC_SUBDOMAIN — optional alternate MasterDNS subdomain for client bundles (PR #112, thanks @vibecodegits) — lets operators hand out a different delegation label than the server's base one (useful when the base label is burned or for staged rotation); the server accepts both, the zone-file generator emits both NS records, and moav doctor dns now checks both delegations when the public alias is set

Fixed

  • moav user add could hang at "Adding to AmneziaWG…", and generated a broken AmneziaWG key when it didn't. Two bugs: (1) compose_timeout ran timeout N without -k, so if docker compose exec wedged against a stuck container and ignored the SIGTERM, the deadline never fired and user add hung until Ctrl-C — now timeout -k 5 force-kills it. (2) The amneziawg container's awg genkey emits CRLF, and $() strips only the trailing \n; the leftover \r made the key 45 chars, so awg pubkey rejected it (Key is not the correct length or format) and the peer was written with a broken public key (AmneziaWG then silently failed for that user). All container/host wg/awg key generation + Reality pubkey derivation (user-add.sh, wg-user-add.sh, singbox-user-add.sh, generate-single-user.sh) now strip \r. Existing users created before this fix have an invalid AmneziaWG key — regenerate them (moav user revoke NAME + moav user add NAME, or moav regenerate-users).
  • Bootstrap-generated bundles left raw {{…}} placeholders in the Shadowsocks + XHTTP sections of README.html — the two paths that fill the bundle guide had drifted: user-add.sh (moav user add) substituted the {{CONFIG_SHADOWSOCKS}} / {{CONFIG_XHTTP}} / {{QR_*}} / {{PORT_SS}} placeholders, but generate-user.sh (the moav bootstrap / INITIAL_USERS path) never did — even though it generates the shadowsocks.txt / xhttp-vless.txt configs + QR codes. So the very first users created at install time got a guide showing literal {{CONFIG_SHADOWSOCKS}} for two on-by-default protocols. generate-user.sh now substitutes the same placeholders; a new bundle-integrity test guards against this class of regression (see Testing).
  • moav export failed for a non-root operator — the state/conduit/cert data is staged into the backup by root containers (so it's root-owned), and the final host-side tar (run without sudo) then couldn't read state/keys/*.key or the conduit datastore (Permission denied), aborting the export. It now hands the staged copy back to the invoking user (via a root container) before archiving. A second, separately-triggered failure is also fixed: the post-archive contents listing (tar -tzf … | head) took a SIGPIPE once a backup had >30 entries — any real deployment — which under set -o pipefail failed the whole command; the broken pipe is now tolerated.
  • The moav-client image couldn't run sing-box at all — the prebuilt sing-box release is glibc-dynamic (interpreter /lib64/ld-linux-x86-64.so.2), but the client image is Alpine/musl, so every sing-box-based protocol test failed cannot execute: required file not found. The image now installs real glibc alongside musl (each binary uses its own loader, so the Alpine-musl tools like amneziawg-tools keep working) and creates the loader symlink the binaries name. Affects moav test and moav client connect.
  • moav test reused a stale client image — it only (re)built moav-client when the image was missing, so after a moav update bumped a pinned client version (sing-box/xray/wstunnel), moav test kept testing the old client. It now always builds (Docker's layer cache keeps it cheap when nothing changed).
  • moav test --json emitted truncated JSONclient-test.sh runs under set -euo pipefail and counted results with ((count++)), which returns exit 1 when a counter goes 0→1 (bash 4/5), killing the script before it finished printing the JSON. Any run with at least one result tripped it. Uses arithmetic that always succeeds now.
  • Bootstrap could silently blank the Reality short_id, breaking every Reality/XHTTP client after an update + bootstrap. The sing-box/xray config templates interpolate ${REALITY_SHORT_ID}, and docker-compose injects REALITY_SHORT_ID=${REALITY_SHORT_ID:-} from .env into the bootstrap container — but the short_id's source of truth is the state (state/keys/reality.env), not .env. So if .env's REALITY_SHORT_ID is empty (it ships empty in .env.example, and an update can reset it), that empty value shadowed the real one at render time, producing short_id: [""]. The server then rejected every client (which sends the real sid) with REALITY: processed invalid connection (clients saw received real certificate) — even though keys, bundles, and services were all intact and unchanged. Both render blocks now re-source state/keys/reality.env immediately before envsubst so the authoritative state value always wins. (The private key was never affected — it isn't passed through .env.) Recovery without updating: put the real REALITY_SHORT_ID (from state/keys/reality.env) into .env, rm configs/{sing-box,xray}/config.json, moav bootstrap --yes, moav restart sing-box xray — existing user bundles keep working, nothing to re-issue.
  • moav build / moav update could fail on Dockerfile.gooserelay (and Dockerfile.clash-exporter) when proxy.golang.org TLS-handshake-timed-out — those two Go builders were missing the GOPROXY pipe-fallback (proxy.golang.org|goproxy.cn|direct) that xray/dns-router/dnstt/snowflake already had, so a transient failure reaching Google's module CDN aborted the whole build with net/http: TLS handshake timeout instead of falling through to the mirror. Reported hitting this on a 1.7.x → 1.8.5 upgrade. The fallback is now baked into both Dockerfiles' ARG GOPROXY default (works even if compose doesn't pass it) and the gooserelay compose build block forwards the operator's .env GOPROXY/GOSUMDB overrides
  • DNS tunnel user-bundle drift + dns-router hardening (PR #112, thanks @vibecodegits) — dns-router now parses the full DNS question (type/class), answers authoritative NS/SOA at route apexes (some resolvers refuse to follow delegations without them), derives the default NS from the root DOMAIN, and derives the SOA serial from VERSION; table-driven Go tests added (golden-byte NS/SOA responses, malformed-packet handling). dnstt key provisioning now hard-fails instead of falling back to a world-readable key file. moav.sh's port-53 conflict check no longer flags MoaV's own dns-router listener. Doctor's Reality TCP probe gained nc/curl/bash fallbacks (the /dev/tcp probe always failed under sh). All docker compose calls in user-add.sh are timeout-bounded so a wedged Docker daemon can't hang user creation. User revoke now finds users that exist only in Xray/TrustTunnel/telemt inbounds, not just sing-box

Quick Install

curl -fsSL moav.sh/install.sh | bash

This will install MoaV to /opt/moav and guide you through setup.

Documentation

moav.sh/docs — Full documentation

Don't miss a new MoaV release

NewReleases is sending notifications on new releases.