Changed
- Repository moved to the
MotherofallVPNsorg —shayanb/MoaVis nowMotherofallVPNs/moav, joiningmoav-clientandmoav-siteunder one org. GitHub 301-redirects the old paths (clones, issues, PRs, releases) so existing installs and links keep working. Swept the hardcodedshayanb/MoaVreferences across the repo (install one-linerREPO_URL, README badges, docs, issue templates, CHANGELOG links, thedns-routerGo module path) to the new home; the personal GitHub Sponsors link is intentionally unchanged.
Added
moav uninstallnon-interactive flags —--yes/-yskips the confirmation prompts, and--remove-imagesalso deletes the Docker images (otherwise--yeskeeps 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 andREADME.html, which are the bulk). Paste it into themoav-cliente2e'sbundle_b64dispatch 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.htmlmeta/JSON-LD,site/llms.txt), most visibly AnyTLS and Shadowsocks-2022 missing from some. There is now one canonical roster: thedocs/protocols.mdoverview table is generated from it (marker-delimited, idempotent), andsite/index.html+README.md+site/llms.txtare drift-checked against it —gen-protocol-docs.py --checkfails 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 inllms.txt. - CI pipeline (
.github/workflows/ci.yml) — runs on every PR todev/main:shellcheck --severity=erroracross all 54 shell scripts,bash -nparse checks,go vet+go test -racefordns-router,docker compose configvalidation, 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) inmoav.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 runsmoav 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), anddomainless— 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 themoav_certsvolume 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 indocs/devdocs/E2E-TESTING.md.
Testing
moav testnow covers Shadowsocks-2022 and XDNS — the two protocols that are on by default but previously had no connectivity test.test_ssdecodes the bundle'sss://SIP002 URI (base64urlmethod:server_psk:user_psk) and drives a sing-box shadowsocks outbound over a local SOCKS port;test_xdnsruns 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 testnow covers the remaining protocols —test_cdndrives 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_wstunnelvalidates the WireGuard-over-wstunnel config and verifies the tunnel port actually speakswss://TLS (the 1.9.0 hardening);test_telemtwas upgraded from a barenc -zto a Fake-TLS handshake probe (it decodes the fronted domain from thetg://secret and confirms telemt answers a TLS hello, not just an open socket).test_masterdnsandtest_gooserelayskiphonestly 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:xhttpwas being tested but omitted from the result summaries — it (andcdn/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.moavCLI 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, auser add→revokelifecycle plususer add --batch,admin password(non-interactive),restart, anexport→importround-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
fullworkflow 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 ofmoav uninstall. The standard (default) run stays fast; FULL is opt-in for release/thorough validation. moav testnow validates the generated bundleREADME.html— atest_readme_bundlecheck 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 underreadmeand 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 onetests/directory with a README describing the three layers, instead of scattered acrossscripts/+scripts/lib/. Updated the references (Dockerfile.clientCOPY,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 SIP002base64url(method:server_psk:user_psk)userinfo encoding) were built identically in two places,scripts/singbox-user-add.sh(host add-user path) andscripts/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-configjqmutations and thegenerate-single-user.shmerge follow next).
Documentation
CONTRIBUTING.md— top-level contributor entry point: dev setup, the PRs-target-devrule, 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 anXDNS_METHOD(txt/aaaa) row todocs/protocols.md, and updated the wstunnel entry to describewss://+ the path secret; added AnyTLS todocs/architecture.md(plus a new Security & isolation / Service lifecycle section) and to the human-visible/structured surfaces ofsite/index.html, and fixed a stale "version 1.8.2" string there; documented the admin empty-password fail-closed (503) behavior indocs/OPSEC.md; refreshedsite/llms.txt
Changed
- wstunnel now wraps WireGuard in real TLS (
wss://) instead of plainws://— 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 serveswss://using that cert whenDOMAINis set (runs as root to read the root-owned cert, copies it to a tmpfs, then drops to the unprivilegedmoavuser viasetpriv— same pattern sing-box uses), and falls back to plainws://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:8080can't complete the upgrade without knowing the path. Generated client commands (bundle instructions + HTML guide, which previously showed an incorrectwss://command with the wrong forwarding target) now come from a single shared helper.moav cert renewrestarts wstunnel alongside the other cert consumers. Existing installs: re-runmoav bootstrap(ormoav updatethen restart wstunnel) to generate the secret and pick upwss://; until then it degrades gracefully to the previousws://behavior. Requires rebuilding the wstunnel image (moav build wstunnel)
Added
- XDNS record-mode selector
XDNS_METHOD(txtdefault,aaaaopt-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 ontxtfor the widest client compatibility; setXDNS_METHOD=aaaato emitx.<domain>:aaaa+udp://…resolvers. Requires an Xray client core ≥ v26.6.1 (Happ / Xray CLI); server side needs nothing - Opt-in telemt anti-DPI knobs —
TELEMT_CLIENT_MSS/TELEMT_CLIENT_MSS_BULKexpose 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_BULKvalue 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, andmoav doctor dnsnow checks both delegations when the public alias is set
Fixed
moav user addcould hang at "Adding to AmneziaWG…", and generated a broken AmneziaWG key when it didn't. Two bugs: (1)compose_timeoutrantimeout Nwithout-k, so ifdocker compose execwedged against a stuck container and ignored the SIGTERM, the deadline never fired anduser addhung until Ctrl-C — nowtimeout -k 5force-kills it. (2) The amneziawg container'sawg genkeyemits CRLF, and$()strips only the trailing\n; the leftover\rmade the key 45 chars, soawg pubkeyrejected 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/hostwg/awgkey 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, ormoav regenerate-users).- Bootstrap-generated bundles left raw
{{…}}placeholders in the Shadowsocks + XHTTP sections ofREADME.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, butgenerate-user.sh(themoav bootstrap/INITIAL_USERSpath) never did — even though it generates theshadowsocks.txt/xhttp-vless.txtconfigs + 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.shnow substitutes the same placeholders; a new bundle-integrity test guards against this class of regression (see Testing). moav exportfailed 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-sidetar(run withoutsudo) then couldn't readstate/keys/*.keyor 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 aSIGPIPEonce a backup had >30 entries — any real deployment — which underset -o pipefailfailed the whole command; the broken pipe is now tolerated.- The
moav-clientimage couldn't runsing-boxat 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 failedcannot 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. Affectsmoav testandmoav client connect. moav testreused a stale client image — it only (re)builtmoav-clientwhen the image was missing, so after amoav updatebumped a pinned client version (sing-box/xray/wstunnel),moav testkept testing the old client. It now always builds (Docker's layer cache keeps it cheap when nothing changed).moav test --jsonemitted truncated JSON —client-test.shruns underset -euo pipefailand 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 anupdate+bootstrap. The sing-box/xray config templates interpolate${REALITY_SHORT_ID}, and docker-compose injectsREALITY_SHORT_ID=${REALITY_SHORT_ID:-}from.envinto the bootstrap container — but the short_id's source of truth is the state (state/keys/reality.env), not.env. So if.env'sREALITY_SHORT_IDis empty (it ships empty in.env.example, and an update can reset it), that empty value shadowed the real one at render time, producingshort_id: [""]. The server then rejected every client (which sends the realsid) withREALITY: processed invalid connection(clients sawreceived real certificate) — even though keys, bundles, and services were all intact and unchanged. Both render blocks now re-sourcestate/keys/reality.envimmediately beforeenvsubstso the authoritative state value always wins. (The private key was never affected — it isn't passed through.env.) Recovery without updating: put the realREALITY_SHORT_ID(fromstate/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 updatecould fail onDockerfile.gooserelay(andDockerfile.clash-exporter) whenproxy.golang.orgTLS-handshake-timed-out — those two Go builders were missing theGOPROXYpipe-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 withnet/http: TLS handshake timeoutinstead 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 GOPROXYdefault (works even if compose doesn't pass it) and the gooserelay compose build block forwards the operator's.envGOPROXY/GOSUMDBoverrides- 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 fromVERSION; 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/tcpprobe always failed undersh). Alldocker composecalls inuser-add.share 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 | bashThis will install MoaV to /opt/moav and guide you through setup.
Documentation
moav.sh/docs — Full documentation
- Setup Guide — Complete installation instructions
- Client Setup — How to connect from devices
- DNS Configuration — DNS records setup
- CLI Reference — All commands and options
- Monitoring — Grafana dashboards and metrics
- Troubleshooting — Common issues and solutions