Skip to content

v2.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 09 Aug 20:27
7c56fe2

MoaV v2.0.0

Same protocols. Rebuilt underneath. No new transports — instead a month of work on the things that decide whether a circumvention tool actually holds up: security, reliability, and being able to change the code without breaking it.

100 PRs · 190 commits · 162 files · +16,801 / −25,987 lines (net −9,186) · tests 3 → 24

Your keys, users and certificates are preserved. Upgrading: Migration guide.

Why a full refactor and review

1.9.x worked, but it had accumulated the failure modes of a fast-moving bash project: a 9,483-line dispatcher nobody could safely edit, secrets written world-readable, containers holding privileges they didn't need, entrypoints that ran without strict mode and failed silently, and a provisioning path duplicated three times so a fix in one place quietly missed the others. None of that is visible in a feature list — all of it decides whether the tool works on a bad day, and whether a bug found once stays fixed.

What's better

Modularity — the dispatcher is 89% smaller

moav.sh went from 9,483 lines to 1,076, with the logic in 15 focused lib/*.sh modules (service, users, bootstrap, doctor, cert, migrate, donate, nettune, dns, …). Provisioning had three near-duplicate user generators; there is now one shared path used by the CLI, the dashboard and the bundle generator, so a fix lands everywhere at once. Dead code and redundant bundle artifacts were deleted rather than carried.

Workstreams: #168#179 (provisioning), #181#190 (dispatcher), #239 #243 #256

Security — least privilege, and secrets that stay secret

  • Secrets are no longer world-readable. State keys are 0600 root-owned; .env is 0600; the Clash API secret moved to an entrypoint env hand-off. Existing installs are repaired on upgrade, through a single guarded path no start/restart route can bypass. (#241 #261)
  • User bundles are no longer world-writable. chmod 777 is replaced by deterministic uid-2000 ownership — client private keys were readable and writable by any local account. (#242)
  • Exporters dropped the raw Docker socket (4 of 4). Metrics collection no longer holds an unrestricted handle on the host. (#226 #227)
  • The admin panel is TLS-only, proven functionally rather than assumed. (#228 #229)
  • A management-only network isolates the Docker socket-proxy from the 29 containers that terminate untrusted internet traffic.
  • An independent adversarial pass before tagging found no critical/high issues and one real medium: sing-box was re-downgrading every state secret's ownership on each start — the actual source of a bug we'd been treating as stale images. (#259 #262)
  • Reality/CDN hardening: the render guard anchors to the real JSON field (an 8-hex id inside a UUID could give a false PASS) and the CDN WebSocket path uses 48-bit entropy instead of bash $RANDOM — it's an active-probing barrier, not a cosmetic string.

Reliability — strict mode, deadlines, no silent failures

  • Every container entrypoint runs under strict mode. Getting there meant fixing the landmines first: SIGPIPE under pipefail, ((x++)) returning 1 from 0, dash having no pipefail (where the usual || true guard is fatal), and cert-wait paths that died before the loop they existed for. (#232 #233 #236)
  • Every container call has a hard deadline, so a wedged container can no longer freeze moav user add forever. (#243)
  • user add no longer depends on Docker for key material. WireGuard keys are X25519, so they're generated locally with openssl; UUIDs likewise. This removed a whole class of intermittent failure — including one that only reproduced from an interactive terminal, because docker exec -i attaches stdin and blocks on a TTY. (#267 #270 #271)
  • New users are actually applied. sing-box has no reload and runs from a config copy taken at start, so a dashboard-created user could receive a perfectly valid bundle that authenticated against nothing. The restart now happens and is verified, with a loud warning if the user isn't live. (#272)
  • Generated-secret desync (the Reality short_id total-outage class) is hardened: renders re-source state immediately before writing, so an empty .env value can't blank a live secret. (#247)

Operability

  • .env.example is one curated file: ~29 commonly-configured variables with descriptions on top, a hard "only change things below if you know what you're doing" separator, everything else below. (#250)
  • Bundles show only the protocols the user actually has — no more "No X config available" filler. (#248)
  • Every config is identifiable in the client app: MoaV-<server>-<protocol>-<user>, including WireGuard, which takes its tunnel name from the filename. (#273)
  • moav doctor gained real checks and stopped failing healthy hosts; monitoring defaults sensibly; Grafana is branded and no longer crash-loops on a read-only image dir. (#249 #252 #268)
  • Docs: a v2 migration guide, plus llms.txt and AGENTS.md so an AI agent operating a server has accurate, safety-aware instructions. (#253 #257 #266)

Testing — the part that makes it stick

The suite went from 3 scripts to 24 (21 in CI), plus an end-to-end workflow that builds the stack on a real VPS with a real domain and probes every protocol — the merge bar for anything touching provisioning.

The rule that shaped this release: every bug found gets a regression test in the same PR, named after the class it pins. Several of those tests earned their place immediately by catching mistakes made while fixing something else — a false-PASS render guard, a MoaV-- double dash, a pty probe that proved nothing, and an e2e assertion that checked a filename spelling instead of the thing it meant to assert.

Verification

Beyond CI and e2e: a full 1.9.1 → v2 in-place upgrade on a live server (which surfaced four regressions, all fixed and pinned — #244 #245), and repeated fresh v2 installs driven from the install script, with every protocol validated end-to-end from real clients.

Breaking changes

None to protocols or credentials. Operational notes:

  • moav build is required when coming from 1.9.x — every image predates v2.
  • WireGuard/AmneziaWG bundle files are renamed (moav-<server>-wg.conf); readers accept both, and moav regenerate-users converts existing bundles.
  • Config-file permissions tighten on first start; the repair is automatic.

Full detail: CHANGELOG.md


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