Monitoring stopped recording which user visited which site. If you have ever run the monitoring profile, please read the first section — there is data on your disk right now.
The rest of the release is a monitoring overhaul that fell out of that work — Snowflake now reports properly, every dashboard was reviewed, and moav doctor gained a host-health command — plus the fixes a real 2.1.0 → 2.2 upgrade turned up.
Keys, users and certificates are untouched.
Upgrade
cd /opt/moav
moav update
moav build snowflake # 2.14.1, metrics endpoint baked in
moav start # NOT `moav restart` — several images are rebuilt
moav doctor # new host + net checks report on this box
moav net apply # if doctor says the tuning file is from an older MoaVmoav update will offer new .env variables. Accepting the defaults is correct for almost everyone.
1. Security: your monitoring stack was keeping a browsing log
Two places recorded which client connected to which hostname.
The monitoring stack. MoaV shipped ghcr.io/zxh326/clash-exporter with -collectDest, which emits one metric series per (client IP, destination hostname) pair. Measured on one live server:
| distinct hostnames | 7,592 |
| distinct client IPs | 4,849 |
| series kept | 389,324 |
| share of the whole database | 83% |
Retained 15 days. No dashboard ever read it — it was that exporter's default behaviour, and we did not notice.
sing-box's own log, on every server whether or not monitoring was enabled: [<username>] inbound connection to <host>:443, at the default log level.
What changed
clash-exporteris removed, not filtered — so re-adding a scrape job cannot bring it back.CLASH_EXPORTER_VERSIONandIMAGE_CLASH_EXPORTERno longer do anything.- The metrics your dashboards used now come from MoaV's own sing-box exporter, which gained
singbox_active_connections,singbox_version_info, and correct per-user country attribution. - Destinations are stripped from the sing-box log before anything is written. Username, client IP and connection id are kept — those drive per-user metrics; nothing needed the hostname. The pre-upgrade log is cleared once on start.
- Per-user volume and liveness stay. The problem was the link between a user and a destination, not visibility. You still get username, bytes and country for quota and abuse handling.
Your data is still on disk
Nothing new is recorded from here on, and it expires with retention. To remove it now:
cd /opt/moav
docker compose stop prometheus
docker volume rm moav_moav_prometheus # deletes ALL metrics history
docker compose up -d prometheusFull policy and the reasoning: OPSEC → What MoaV Records. Audit and decisions: #297.
New: site analytics (opt-in, off by default)
To know what your proxy is used for without knowing who did what, set ENABLE_SITE_ANALYTICS=true:
- domains only, folded to the registrable name (
edge-42.example-cdn.net→example-cdn.net) - a domain is named only after at least 5 distinct clients reached it in the same bucket; below that it counts as
other - ranked by how many people used a domain, not by bytes
- counters advance once per hour, so the chart cannot be lined up against per-user connection counts
- no client identifier is ever produced — not filtered afterwards, never created
Measured on a real server, that threshold drops 94% of domains while still accounting for ~89% of traffic, because the median domain is visited by exactly one person.
On a small server, be careful. With a handful of active users even aggregates leak, which is why this is opt-in. If too much lands under other, widen SITE_ANALYTICS_BUCKET_SECONDS — do not lower SITE_ANALYTICS_MIN_CLIENTS. The new Threshold Cost panel shows whether that would help.
ENABLE_SITE_ANALYTICS_RESEARCH=true also records destination port and protocol. Separate switch, because a rare port identifies far more than a popular domain.
2. Snowflake reports properly now
If you donate bandwidth through Tor Snowflake, the dashboard was misleading and is rebuilt.
- Snowflake proxy → 2.14.1, which serves its own metrics endpoint. MoaV's log-parsing snowflake exporter is deleted — the numbers come straight from the proxy, so a restart reads as a counter reset instead of a cliff.
- Per-country users served (#183). The proxy has exposed
tor_snowflake_proxy_connections_total{country}since 2.11 and MoaV was not reading it. That needs a Tor-format GeoIP database, now fetched into its own volume (385,622 IPv4 + 276,646 IPv6 ranges) by a one-timetor-geoip-updater. Without it the proxy logsError loading geoip dband labels every connection"". - The dashboard is rebuilt: people and countries served, rendezvous success rate, relay throughput, failed connections per hour, and bandwidth donated — each counted over the time range instead of as a flat lifetime total, and using
max_over_timewhere the counter is too sparse forincreaseto see a country's first connection.
3. Every Grafana dashboard reviewed
Prompted by "the numbers look wrong". They were.
- Totals silently reset. Counters restart with their container, and panels read them raw — one server showed
Total Downloadedas 779 KiB whileDownloadedover the same data read 31.5 MiB. Audited across all ten dashboards; all 219 queries re-checked against a live Prometheus. - Three separate bugs behind the sing-box country panels: connections counted once per poll instead of once per connection (42,428 vs a true count in the low hundreds), every user's country reported as
XX, and "Users by Country" querying the connections metric. - Panels that ignored the time picker, and panels sampled at a resolution their data does not have.
- One country now keeps one colour across time-series panels.
- New site-analytics panels, in a collapsed row that stays out of the way when the flag is off.
4. moav doctor gained two checks
moav doctor host— CPU load against the core count, swap in use, zombie processes, and the busiest container (it names a bandwidth-donation container when that is what is saturating a small host). Working out why one server felt slow used to take four rounds ofuptime,top,docker statsand apspipeline; this is that in one command.moav doctor netanswers its own question. Kernel packet-drop counters are since-boot and never reset, so aftermoav net applyfixed the cause the warning could never clear — and doctor told you to runnstattwice and compare by eye. It now samples for you and says which counters are still climbing.
5. Longer retention, now configurable
PROMETHEUS_RETENTION_TIME goes from a hardcoded 15 days to 90 days, with PROMETHEUS_RETENTION_SIZE (2 GB) as the real guard — whichever is reached first wins. A longer window is only affordable because the per-user destination series are gone: roughly 7,000 active series where there were 389,000. LOG_MAX_SIZE and LOG_MAX_FILES now control container log rotation.
6. Everything a live upgrade exposed
Found by upgrading real servers, not in CI:
moav updatesaid nothing after changingdocker-compose.yml, and suggested rebuilding a service that no longer exists- a bind-mounted config file kept its old contents after an update — a file mount follows the inode, and git replaces the file, so the container read the old copy forever (
moav updatenow prints the--force-recreateit needs) - a setup-profile job added by a release never ran on upgrade, so Snowflake started with no GeoIP database
- the GeoIP database was re-downloaded on every
moav start; the build cache cap ignored the disk moav doctor peers --fixdied silently on any busy server, and when it did run the reset did not stick- CDN could be switched off by upgrading; a donated user could lose their whole bundle; the CDN WebSocket path rotated in silence
- zombie processes in the admin and grafana containers
moav statuslisted one-shot plumbing (geoip-updater,bootstrap) as failures- exporter code changes never reached a running container — the code is baked into the image, so a fix silently did not apply without a rebuild
FUNDING.ymladdresses are quoted, so a YAML parser stops truncating the ETH address
Project charts (star history, per-release test counts, translation coverage) are generated on a dedicated chart branch and embedded by URL, so nothing generated is committed to main.
Breaking
clash-exporteris removed. Any dashboard or alert you wrote againstclash_*metrics stops returning data — the equivalents aresingbox_*.- The Snowflake metric names changed.
served_people/download_gb/upload_gbremain as deprecated aliases so your own panels do not go blank, but new panels should usesnowflake_connections_totalandsnowflake_relayed_bytes_total. The dashboard's history does not carry over. moav start, notmoav restart. The sing-box image, the sing-box exporter and Snowflake are rebuilt, and a restart reuses the old image.
Thanks
Nearly everything here was found by running MoaV on live servers under real load, not by reading the code.
Full changelog: https://github.com/MotherofallVPNs/MoaV/blob/main/CHANGELOG.md
Full Changelog: v2.1.0...v2.2.0
Quick Install
curl -fsSL https://moav.sh/install.sh | bashThis will install MoaV to /opt/moav and guide you through setup.
Documentation
moav.sh/docs — full documentation
Get started
- Quick Start — install to first user in ~10 minutes
- Client Setup — connect from phones and desktops
- DNS Configuration — records, delegations, freeing port 53
Reference
- CLI Reference — every command and flag
- Setup Guide — every option, in depth
- Monitoring — Grafana dashboards and metrics
- Troubleshooting — symptom-first fixes
Understand it
- Supported Protocols — per-protocol ports, ciphers, stealth
- Architecture — container topology and bundle flow
- Threat Model — what is and is not protected
- OPSEC Guide — operator-side hardening
Help out
- Support MoaV — run a server, contribute, translate, donate
- Translating the Docs — one page is a complete contribution
Running it with an AI agent? llms.txt is a compact
orientation for coding agents; llms-full.txt is the
whole corpus. Both ship as release assets.