# MoaV, full documentation
> Complete MoaV documentation in one file, concatenated from the MkDocs sources in nav order.
> Short index: https://moav.sh/llms.txt
> Source: https://github.com/MotherofallVPNs/moav-site
> Generated by scripts/build-llms-full.sh, do not edit by hand.
---
# MoaV Documentation
**MoaV (Mother of all VPNs)** is a free, open-source multi-protocol censorship circumvention stack. Deploy a full arsenal of [anti-censorship protocols](https://moav.sh/docs/protocols/) on any VPS or home server with a single command.
MoaV generates ready-to-use **client bundles** for each user, containing config files, QR codes, and step-by-step instructions (in English and Farsi) for each enabled and successfully configured user-facing protocol. Non-technical users simply open the bundle's `README.html` in their browser, scan a QR code, and connect. No manual configuration needed.
Read the concise [Mission](https://moav.sh/docs/mission/), the project's [Impact](https://moav.sh/docs/impact/) model, and the public [Threat Model](https://moav.sh/docs/threat-model/).
---
**New here?** Start with the [Quick Start Guide](https://moav.sh/docs/quick-start/) to get up and running in minutes.
---
## Guides
- **[Quick Start](https://moav.sh/docs/quick-start/)**: Install, create a user, and share a bundle
- **[Setup Guide](https://moav.sh/docs/SETUP/)**: Complete installation with all options
- **[DNS Configuration](https://moav.sh/docs/DNS/)**: DNS records for your domain and provider
- **[VPS Deployment](https://moav.sh/docs/DEPLOY/)**: One-click deploy on Hetzner, DigitalOcean, Vultr, Linode
- **[Video Tutorials](https://moav.sh/docs/video-tutorials/)**: Video Walkthroughs
## Using MoaV
- **[Client Apps](https://moav.sh/docs/CLIENTS/)**: Connect from iOS, Android, macOS, Windows, Linux
- **[MoaV Client](https://moav.sh/docs/client/)**: MoaV client with a dashboard, load-balancing, and automatic failover
- **[CLI Reference](https://moav.sh/docs/CLI/)**: All `moav` commands and options
- **[Monitoring](https://moav.sh/docs/MONITORING/)**: Grafana + Prometheus dashboards
- **[Troubleshooting](https://moav.sh/docs/TROUBLESHOOTING/)**: Common issues and fixes
## Security & Philosophy
- **[Mission](https://moav.sh/docs/mission/)**: What MoaV is, who it serves, and how to help
- **[Impact](https://moav.sh/docs/impact/)**: Theory of change, outcomes, and measurable progress
- **[Threat Model](https://moav.sh/docs/threat-model/)**: Honest limits, adversaries, metadata risks, and safety assumptions
- **[OPSEC Guide](https://moav.sh/docs/OPSEC/)**: Security best practices for operators
- **[Mission & Philosophy](https://moav.sh/docs/philosophy/)**: The longer manifesto behind the project
- **[Supported Protocols](https://moav.sh/docs/protocols/)**: All 16+ circumvention transports and fallback paths, plus optional Psiphon, Tor and MahsaNet donation integrations, including the [DNS tunnels](https://moav.sh/docs/protocols/#dns-tunnels) that can remain usable where ordinary traffic is blocked but recursive DNS still resolves
## Support the project
- **[Support MoaV](https://moav.sh/docs/support/)**: Run a server, donate bandwidth, contribute code, translate, or fund the infrastructure
## Developer
- **[Development & Testing](https://moav.sh/docs/development/)**: installing from a branch or tag, repo layout, how CI and the end-to-end suite work, and how to contribute
---
# Quick Start
Install MoaV, then hand someone a link that connects them. That's the whole path, and it takes about ten minutes.
MoaV deploys [16+ circumvention transports and fallback paths](https://moav.sh/docs/protocols/), plus optional Psiphon, Tor and MahsaNet donation integrations, and turns each user into a share-ready **bundle** — configs, QR codes, and plain-language instructions in English and Farsi. The person receiving it doesn't need to understand any of it. Curious why this exists? Read [the mission](https://moav.sh/docs/philosophy/).
## What you need
- **A server** — Debian 12 / Ubuntu 22.04 or 24.04, or a **Raspberry Pi 4+** (ARM64). 1 vCPU / 1 GB RAM is the floor; 2 GB if you want monitoring.
- **A domain** — optional, but worth it. It unlocks several additional transports, particularly the TLS-based proxies and all DNS tunnels, which can remain usable where ordinary traffic is blocked but recursive DNS still resolves. See [Do I need a domain?](https://moav.sh/docs/DNS/#do-i-need-a-domain).
!!! tip "Point your DNS *before* installing"
Certificate issuance needs the domain already resolving to the server, so adding the records first makes the install smooth. The exact records are in [DNS Configuration](https://moav.sh/docs/DNS/#with-a-domain-the-records) — and after setup, `moav doctor dns` writes them out for you as a file you can import straight into Cloudflare.
No VPS yet? [VPS Deployment](https://moav.sh/docs/DEPLOY/) has provider-by-provider steps from ~$5/month. On a home server or Pi, check for CGNAT first and forward the [protocol ports](https://moav.sh/docs/DNS/#ports-to-forward).
## 1. Install
SSH in and run:
```bash
curl -fsSL moav.sh/install.sh | bash
```
It installs Docker, clones MoaV, and asks for three things: your **domain** (blank for domainless), an **email** for Let's Encrypt, and an **admin password** — which is also your Grafana password, so pick a real one.
When it finishes it prints your dashboard URLs and the DNS records to add.
The install, start to finish — 14s. Nothing downloads until you press play (808 KB).
!!! tip "Prefer to watch someone do it?"
A community walkthrough covers the whole path end to end — see [Video Tutorials](https://moav.sh/docs/video-tutorials/). Farsi only for now.
## 2. Create your first user
Two ways. Use whichever you prefer — they do the same thing.
=== "Web dashboard"
Open **`https://your-server:9443`** and log in — **any username**, with the admin password you chose.
??? warning "Your browser will warn about the certificate"
In domainless mode the dashboard uses a self-signed certificate, so you'll see a privacy warning the first time. That's expected — proceed past it. With a domain configured it uses your Let's Encrypt certificate and there's no warning.
Click **+ New**, enter a name, and the user appears in the table with a badge for every protocol they got — Reality, Trojan, Hy2, CDN, WG, AWG, XHTTP and so on. Hit **.zip** to download their bundle.
The dashboard is the easiest place to run day to day: it lists every user with their creation date and protocols, downloads bundles on demand, and shows live server stats.
Creating and packaging a user from the dashboard — 22s, 284 KB.
=== "Command line"
```bash
moav user add alice # one user
moav user add alice --package # ...and build the .zip
moav user add --batch 10 # ten at once
moav user list # who exists
```
Bundles land in `outputs/bundles/alice/`.
Adding, listing and packaging users from the CLI — 31s, 636 KB.
Either way you get the same bundle:
- **`README.html`** — the file to actually send. Step-by-step instructions in English and Farsi, with QR codes. They open it, pick their platform, scan, and they're online.
- Config files and share links for each enabled and successfully configured user-facing protocol, plus a one-paste **subscription** for MahsaNG, v2rayNG and Hiddify.
Send it over something private — Signal, encrypted email, in person. See [Client Apps](https://moav.sh/docs/CLIENTS/) for what to tell them per platform.
## 3. Know these four commands
```bash
moav status # what's running, which profiles are up, health at a glance
moav doctor # diagnose problems: DNS, ports, certificates, resources
moav logs # tail a service when something misbehaves
moav test alice # prove alice's configs actually pass traffic, end to end
```
`moav doctor` is the one to reach for first when anything looks wrong — it checks the things that break most often and usually names the problem outright. `moav test` is the one to run before you tell someone their bundle works, because it connects through each protocol for real and reports the exit IP.
Running plain **`moav`** opens an interactive menu over all of it, so there's nothing to memorize. Every entry maps to a command in the [CLI Reference](https://moav.sh/docs/CLI/).
## 4. Watch it work (optional)
If you enabled monitoring, **Grafana** is at **`https://your-server:9444`** — user `admin`, same password.
Ten dashboards ship preconfigured, so there's nothing to build:
| Dashboard | What it tells you |
|---|---|
| **sing-box** | Per-user connections and throughput for Reality, Trojan, Hysteria2, Shadowsocks |
| **WireGuard** · **AmneziaWG** | Peer handshakes, transfer per peer |
| **DNS tunnels** | Traffic across dnstt, Slipstream, MasterDNS, XDNS |
| **Xray** · **telemt** | XHTTP/XDNS and Telegram MTProxy activity |
| **Conduit** · **Snowflake** | Bandwidth you're donating to Psiphon and Tor users |
| **System** · **Containers** | CPU, memory, disk, and per-container resource use |
Not running monitoring? `moav start monitoring` turns it on — it wants ~2 GB RAM. Details in [Monitoring](https://moav.sh/docs/MONITORING/).
## Where to go next
- **[Client Apps](https://moav.sh/docs/CLIENTS/)** — what your users install, per platform
- **[CLI Reference](https://moav.sh/docs/CLI/)** — every command
- **[DNS Configuration](https://moav.sh/docs/DNS/)** — records, DNS tunnels, CDN mode
- **[Setup Guide](https://moav.sh/docs/SETUP/)** — every configuration option in depth
- **[OPSEC Guide](https://moav.sh/docs/OPSEC/)** — running and sharing this safely
- **[Troubleshooting](https://moav.sh/docs/TROUBLESHOOTING/)** — when something breaks
---
# Mission
MoaV, Mother of all VPNs, is open-source resilient connectivity infrastructure for censorship and shutdown environments. It helps trusted operators deploy multi-protocol access nodes so users have fallback routes when VPNs, domains, apps, or protocols are blocked.
MoaV is not trying to be another commercial VPN brand. It is a public-interest deployment stack for people who need the open internet to keep working under pressure.
## Why MoaV exists
Internet shutdowns and protocol blocking are now routine tools of control. During protests, crackdowns, elections, and conflicts, governments can throttle bandwidth, block popular apps, fingerprint VPN protocols, and isolate people from the outside world.
Most circumvention tools help only if they are already reachable. A single blocked domain, protocol, app store, payment route, or infrastructure provider can cut off a large group of users at once.
MoaV is built around a different assumption: no single route survives every censor. The useful system is the one that degrades gracefully.
## What MoaV does
MoaV turns a low-cost server into a multi-protocol circumvention node. It automates setup, generates user bundles, and supports multiple transports so operators do not have to assemble a stack by hand.
The goal is simple:
- make deployment fast for trusted operators
- support many fallback protocols
- reduce dependence on one provider or domain
- make operator documentation clear
- keep the code open source and auditable
- help communities add capacity before the next shutdown
## Who MoaV serves
MoaV is designed for:
- people in censored environments who need access to the open internet
- journalists, activists, students, families, and civil-society groups affected by blocking
- diaspora communities that can donate servers, bandwidth, and operational support
- open-source internet-freedom projects that need reusable deployment tooling
The first community focus is Iran, but the design is not Iran-only. The same patterns apply anywhere networks are filtered, throttled, or partially shut down.
## What MoaV is not
MoaV is not a promise of perfect anonymity. It is not a token network. It is not a magic way around every form of surveillance. It is access infrastructure, and it should be used with a clear understanding of risk.
Read the [Threat Model](https://moav.sh/docs/threat-model/) for the limits, adversaries, and safety assumptions.
## How to help
If you want to help users reach the open internet:
1. [Deploy MoaV](https://moav.sh/docs/quick-start/) on a VPS.
2. Read the [OPSEC Guide](https://moav.sh/docs/OPSEC/) before sharing access.
3. Share user bundles only through trusted channels.
4. Enable donation paths such as [Psiphon Conduit](https://moav.sh/docs/support/#donate-bandwidth-instead-of-users), [Tor Snowflake](https://moav.sh/docs/support/#donate-bandwidth-instead-of-users), or [MahsaNet](https://moav.sh/docs/mahsanet/) when appropriate.
5. Contribute code, protocol support, documentation, testing, or translations.
## Related pages
- [Impact](https://moav.sh/docs/impact/)
- [Threat Model](https://moav.sh/docs/threat-model/)
- [Architecture](https://moav.sh/docs/architecture/)
- [Supported Protocols](https://moav.sh/docs/protocols/)
- [Mission & Philosophy](https://moav.sh/docs/philosophy/)
---
# Impact
MoaV is built on a practical theory of change: censorship becomes harder to enforce when access capacity is distributed across many small operators, many protocols, and many fallback paths.
The project does not depend on one perfect transport. It makes it easier to keep adding routes.
## The problem
When governments block the internet, people lose access to news, family, organizers, emergency information, and the outside world. Existing circumvention tools help, but many are centralized, protocol-specific, or difficult for ordinary supporters to operate.
Censors exploit that fragility. They block known VPN protocols, domains, app stores, payment routes, and large infrastructure providers. A tool that works today can fail during the next crisis.
## The intervention
MoaV gives trusted operators a repeatable way to deploy multi-protocol access nodes. The project combines:
- setup automation
- transport diversity
- per-user bundles
- operator documentation
- safety guidance
- monitoring and health checks
- bandwidth donation paths
This turns spare servers and community support into usable circumvention capacity.
## Expected outcomes
MoaV aims to produce:
- more independent access nodes before censorship events
- faster deployment when demand spikes
- more fallback routes when one protocol is blocked
- better operator safety and documentation
- reusable open-source infrastructure for the wider internet-freedom ecosystem
## What success looks like
Success is measurable. Useful metrics include:
- number of deployed nodes
- number of supported protocols
- time from fresh server to working node
- setup success rate
- number of user bundles generated
- number of operators onboarded
- protocol availability under blocking conditions
- security issues found and fixed
- documentation and localization coverage
- reuse by other open-source projects
Not every metric should be public. Live node inventory, active-user counts, and sensitive operational details can create risk. Public reporting should focus on aggregate progress and reusable outputs.
## Why open source matters
Open source is not just a value statement here. It is part of the safety model.
Public code lets operators inspect what they run, lets researchers audit the implementation, and lets other projects reuse the parts that work. Grant money and volunteer time should compound into public infrastructure, not a closed service.
## Where MoaV fits
MoaV sits between end-user VPN apps and low-level protocol research. It packages protocols, operational practice, and deployment automation into something a trusted operator can run.
That makes it useful to:
- internet-freedom funders
- open-source infrastructure maintainers
- diaspora support networks
- censorship measurement projects
- protocol developers who need real deployment paths
## Related pages
- [Mission](https://moav.sh/docs/mission/)
- [Threat Model](https://moav.sh/docs/threat-model/)
- [Architecture](https://moav.sh/docs/architecture/)
- [OPSEC Guide](https://moav.sh/docs/OPSEC/)
---
# Threat Model
MoaV is useful because it improves access resilience. It does not make every user anonymous against every adversary.
This page explains what MoaV is designed to protect, what it does not protect, and what operators should assume before deploying it.
## Who MoaV is meant to help
MoaV is designed for:
- people in censored environments who need access to the open internet
- journalists, civil-society groups, activists, students, and families affected by blocking
- diaspora communities that can provide trusted technical support
- operators who can safely run servers outside high-risk jurisdictions
## Adversaries
MoaV assumes adversaries may include:
- state censors blocking domains, IPs, protocols, app stores, and payment routes
- network operators performing DPI, throttling, or traffic shaping under state pressure
- active probers that connect to suspected proxy servers
- hosting providers that may suspend nodes after complaints
- attackers targeting users, operators, or project infrastructure
- malicious operators pretending to run trusted nodes
## What MoaV tries to protect
MoaV tries to improve:
- access to blocked websites and services
- availability of fallback routes during blocking events
- operator ability to deploy and rotate nodes quickly
- integrity of open-source deployment scripts and documentation
- user safety through clear limits and safer defaults
## What MoaV does not fully protect
MoaV does not fully protect against:
- device compromise
- targeted surveillance by a capable state actor
- unsafe user behavior after connecting
- metadata exposure from the local network, hosting provider, payment system, or destination service
- trust problems caused by unknown or malicious node operators
- legal risk for operators in hostile jurisdictions
If a user's device is compromised, a network tool cannot make that device safe. If an operator is in a jurisdiction where running circumvention infrastructure is dangerous, MoaV does not remove that legal or physical risk.
## Metadata risks
Even when traffic content is encrypted, metadata can remain visible. Examples include:
- server IP addresses
- connection timing
- protocol choice
- traffic volume
- cloud-provider billing records
- operator logs
- destination-side account activity
MoaV should minimize unnecessary logs and make safer defaults easy, but operators remain responsible for how they host, monitor, and share access.
## Operator safety principles
Operators should:
- run nodes only where they can safely do so
- avoid publishing sensitive node inventory
- use unique credentials per user
- revoke compromised users quickly
- keep admin panels locked down
- avoid unnecessary logging
- keep systems updated
- separate personal identity from operational infrastructure where possible
- read the [OPSEC Guide](https://moav.sh/docs/OPSEC/)
## User safety principles
Users should:
- get configs only from trusted sources
- keep devices updated
- avoid logging into high-risk personal accounts when unnecessary
- assume local network observers may see that they are using unusual traffic
- understand that MoaV provides access, not total anonymity
## Misuse and abuse
Any access tool can be used outside its intended beneficiary group. MoaV's answer is transparency and careful operation, not centralized surveillance.
The project should avoid collecting user identities, avoid building unnecessary tracking features, and keep deployment auditable. Public operators should still have abuse-response plans and should understand the policies of their hosting providers.
## Public reporting
Do not publish live operational details that could help censors block active users. Public reports should focus on aggregate progress, security fixes, documentation, and reusable open-source outputs.
## Related pages
- [OPSEC Guide](https://moav.sh/docs/OPSEC/)
- [Architecture](https://moav.sh/docs/architecture/)
- [Supported Protocols](https://moav.sh/docs/protocols/)
- [Mission](https://moav.sh/docs/mission/)
---
# Architecture
How MoaV is wired together. For protocol-level details see [protocols.md](https://moav.sh/docs/protocols/); for CLI behavior see [CLI.md](https://moav.sh/docs/CLI/); for DNS-tunnel mechanics see [DNS.md](https://moav.sh/docs/DNS/).
## How traffic flows
What a MoaV server looks like from the outside in: clients reach it by whichever protocol still works, everything egresses through the server, and two profiles additionally relay bandwidth for other people's networks.
```
┌───────────────┐ ┌───────────────┐
┌───────────────┐ │ Psiphon Users │ │ Tor Users │
│ Your Clients │ │ (worldwide) │ │ (worldwide) │
│ (private) │ └───────┬───────┘ └───────┬───────┘
└───────┬───────┘ │ │
│ │ │
├─────────────────┐ │ │
│ │ (when IP blocked) │ │
│ ┌──────┴───────┐ │ │
│ │ CDN (VLESS) │ │ │
│ └──────┬───────┘ │ │
┌──────────────╪─────────────────╪────────────────────────────────────╪──────────────────╪─────────┐
│ │ │ Restricted Internet │ │ │
└──────────────╪─────────────────╪────────────────────────────────────╪──────────────────╪─────────┘
╔══════════════╪═════════════════╪════════════════════════════════════╪══════════════════╪═════════╗
║ ┌────────┼─────────────────┼───────┼──────┐ │ │ ║
║ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ║
║ ┌─────────┐┌─────────┐┌───────┐┌─────────┐┌────────┐ ┌───────────┐ ┌───────────┐ ║
║ │ Reality ││WireGuard││ Trust ││ DNS ││Telegram│ │ Conduit │ │ Snowflake │ ║
║ │ Trojan ││AmneziaWG││Tunnel ││ tunnels ││MTProxy │ │ (donate │ │ (donate │ ║
║ │Hysteria2││wstunnel ││ ││ (4, on ││ │ │ bandwidth)│ │ bandwidth)│ ║
║ │ AnyTLS ││ ││ ││ port 53)││ │ │ │ │ │ ║
║ │ SS2022 ││ ││ ││ ││ │ │ │ │ │ ║
║ │ CDN WS ││ ││ ││ ││ │ │ │ │ │ ║
║ ├─────────┤└────┬────┘└───┬───┘└────┬────┘└───┬────┘ └─────┬─────┘ └─────┬─────┘ ║
║ │ sing-box│ │ │ │ │ ┌────────────────┐ │ │ MoaV ║
║ └────┬────┘ │ │ │ │ │ Grafana :9444 │ │ │ server ║
║ │ │ │ │ │ │ Admin :9443 │ │ │ ║
╚══════╪══════════╪═════════╪═════════╪═════════╪══╪════════════════╪═╪══════════════════╪═════════╝
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Open Internet │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
```
## Container topology
Every protocol is one or more containers grouped into a docker-compose **profile**. `moav start` reads the `ENABLE_*` flags from `.env` and brings up only the profiles whose flag is on (see [CLI → moav start](https://moav.sh/docs/CLI/#moav-start)).
| Profile | Containers | Protocols it serves |
|---|---|---|
| `proxy` | `sing-box` | Reality (VLESS), Trojan, AnyTLS *(opt-in)*, Hysteria2, Shadowsocks-2022, CDN VLESS+WS |
| `xhttp` | `xray` | VLESS + XHTTP + Reality |
| `wireguard` | `wireguard`, `wstunnel` | WireGuard direct UDP, plus a `wss://` WebSocket fallback for UDP-blocked networks |
| `amneziawg` | `amneziawg` | AmneziaWG (obfuscated WireGuard) |
| `dnstunnel` | `dns-router`, `dnstt`, `slipstream`, `masterdns`, `xray` | All four [DNS tunnels](https://moav.sh/docs/protocols/#dns-tunnels), sharing port 53 |
| `trusttunnel` | `trusttunnel` | TrustTunnel (HTTP/2 + QUIC over TLS) |
| `telegram` | `telemt` | Telegram MTProxy (fake-TLS) |
| `admin` | `admin`, `docker-proxy` | The dashboard on `:9443` |
| `monitoring` | `grafana`, `prometheus`, exporters, `cadvisor` | Metrics and dashboards on `:9444` |
| `conduit` | `psiphon-conduit` | Bandwidth donated to Psiphon |
| `snowflake` | `snowflake` + exporter | Bandwidth donated to Tor |
| `client` | `client` | Local testing (`moav test`) |
AnyTLS is opt-in because it resists TLS-in-TLS fingerprinting but has narrower client support. `all` starts every profile whose flag is on.
## DNS-router fan-out
All four DNS tunnels share **port 53** through a small Go service called `dns-router`, which inspects each query's subdomain prefix and forwards to the matching backend. Each tunnel container listens on its own internal port; only `dns-router` binds the public port.
```
Public 53/udp
│
┌──────▼──────┐
│ dns-router │
└──────┬──────┘
│
subdomain routing:
t.* ─────► dnstt
s.* ─────► slipstream
m.* ─────► masterdns
x.* ─────► xray (XDNS via FinalMask)
```
Delegating a tunnel only requires adding its NS record (`t.` / `s.` / `m.` / `x.`); see [DNS → NS Delegations](https://moav.sh/docs/DNS/#with-a-domain-the-records). Disabling a tunnel via `ENABLE_*=false` removes its container; `dns-router` simply has no backend to forward to.
## Bundle generation flow
User credentials and per-protocol configs originate inside the `bootstrap` container, then get rendered into per-user bundles on the host. The split exists because container-side bundle generation can't see the host's `outputs/` mount layout.
```
moav user add alice
│
▼
┌─────────────────────────────────────────────────────────┐
│ bootstrap container (sing-box-user-add.sh) │
│ - generates UUID + per-protocol keys │
│ - writes state/users/alice/credentials.env (volume) │
└────────────────────┬────────────────────────────────────┘
│ HOST sees state/users/ via volume
▼
┌─────────────────────────────────────────────────────────┐
│ host: generate-single-user.sh │
│ - reads credentials.env + .env │
│ - writes outputs/bundles/alice/{*.txt, *.json, *.png, │
│ subscription.txt, README.html, ...} │
└─────────────────────────────────────────────────────────┘
```
Bundles split into three groups:
- **V2Ray-compatible** (Reality, Trojan, AnyTLS, Hysteria2, SS-2022, CDN, XHTTP) — share-link `.txt`s, QR `.png`s, a single base64 `subscription.txt` importable by MahsaNG / v2rayNG / Hiddify / Streisand.
- **L3 VPNs** (WireGuard, AmneziaWG, TrustTunnel) — `.conf` / `.toml` configs + QR.
- **DNS tunnels** (dnstt, Slipstream, MasterDNS, XDNS) and **donations** (GooseRelay) — text instruction files + protocol-specific config blobs (`xdns-config.json`, `gooserelay-AppsScript.gs` + `gooserelay-client_config.json`, etc.).
`README.html` is a bilingual (EN/FA) collapsible bundle viewer with embedded QR images and one-click subscription import.
## Monitoring stack
The `monitoring` profile is opt-in. When enabled, it adds Prometheus + Grafana plus a set of exporters — one per protocol. Each exporter lives in the same Compose profile as its target service (not in `monitoring`), so disabling a protocol takes its metrics down too.
```
Exporters (each in its target's profile)
├── clash-exporter (sing-box Clash API)
├── singbox-exporter (log parser)
├── xray-exporter
├── telemt-exporter (REST /v1/health)
├── wireguard-exporter
├── amneziawg-exporter
├── snowflake-exporter (snowflake profile)
├── node-exporter (host metrics)
└── cAdvisor (container metrics)
│
│ scraped by
▼
┌──────────────┐
│ Prometheus │ + recording rules (e.g. Conduit lifetime)
└──────┬───────┘
│
▼
┌──────────────┐
│ Grafana │ (+ optional grafana-proxy → Cloudflare CDN)
│ dashboards │
└──────────────┘
```
Pre-built dashboards land in `configs/monitoring/grafana/dashboards/`. The Conduit lifetime panels depend on a recording rule plus an offset watcher — see [Monitoring → Conduit lifetime bandwidth](https://moav.sh/docs/MONITORING/#conduit-lifetime-bandwidth).
## Security & isolation model
Every service runs in its own container with least-privilege defaults applied in `docker-compose.yml`:
- **Capability drop + selective add** — services start from `cap_drop: ALL` and add back only what they need (e.g. `NET_ADMIN` for WireGuard, `NET_BIND_SERVICE` for privileged ports). Most also set `read_only: true` with a small `tmpfs` for `/tmp`, `no-new-privileges: true`, and `mem_limit`/`cpus` caps.
- **Non-root** — services that must read the root-owned Let's Encrypt cert (sing-box, wstunnel) start their entrypoint as root only long enough to copy the cert into a tmpfs, then drop to an unprivileged user via `setpriv` before exec'ing the daemon.
- **No direct Docker socket** — the admin dashboard reads container status through a read-only [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) scoped to `CONTAINERS`/`NETWORKS` only, never mounting `/var/run/docker.sock` into the app.
- **Admin auth fails closed** — the dashboard refuses to serve (HTTP 503) if `ADMIN_PASSWORD` is empty or one of the known-insecure defaults, and uses constant-time comparison; an optional IP allow-list narrows access further. See [OPSEC](https://moav.sh/docs/OPSEC/).
- **Secrets** live under the `moav_state` Docker volume (`state/keys/`, `state/users//`), generated with `openssl rand` / `wg genkey` / `x25519`, mounted read-only into the admin service.
## Service lifecycle & health
`ENABLE_*` flags in `.env` select which profiles `moav start` brings up. The `certbot` service is a one-shot that obtains the TLS cert before the cert-consuming services start; `moav cert install` schedules ongoing renewal (see [CLI → Certificates](https://moav.sh/docs/CLI/#certificates)). `restart: unless-stopped` recovers services across crashes and reboots. Core engines (sing-box, xray, telemt) declare Compose healthchecks so `moav doctor` and dependency ordering can distinguish "running" from "actually serving"; extending healthchecks to the remaining long-running services is in progress.
## See also
- [Setup Guide](https://moav.sh/docs/SETUP/) — step-by-step deployment walkthrough
- [DNS Configuration](https://moav.sh/docs/DNS/) — NS records, resolver-mode vs direct-mode XDNS, port 53
- [CLI Reference](https://moav.sh/docs/CLI/) — every `moav` command, including the disabled-profile prompt
- [Supported Protocols](https://moav.sh/docs/protocols/) — protocol-level cipher, port, and client-compat detail
- [Monitoring](https://moav.sh/docs/MONITORING/) — dashboards, Conduit lifetime, GeoIP setup
---
# Supported Protocols
MoaV deploys 16+ circumvention transports and fallback paths, plus optional Psiphon, Tor and MahsaNet donation integrations. Each has different stealth characteristics, speed profiles, and network requirements. This diversity ensures that when one protocol is blocked, others remain available.
## Protocol Overview
| Protocol | Port | Stealth | Speed | Domain Required |
|----------|------|---------|-------|-----------------|
| [Reality (VLESS)](#reality-vless) | 443/tcp | Very High | High | No |
| [Trojan](#trojan) | 8443/tcp | High | High | Yes |
| [AnyTLS](#anytls) | 8445/tcp | Very High | High | Yes |
| [Hysteria2](#hysteria2) | 443/udp | High | Very High | Yes |
| [Shadowsocks-2022](#shadowsocks-2022) | 8388/tcp+udp | High | Very High | No |
| [CDN (VLESS+WS)](#cdn-vlessws) | 443 via CDN | Very High | Medium | Cloudflare: yes · CloudFront: no |
| [TrustTunnel](#trusttunnel) | 4443/tcp+udp | Very High | High | Yes |
| [WireGuard](#wireguard) | 51820/udp | Medium | Very High | No |
| [AmneziaWG](#amneziawg) | 51821/udp | Very High | High | No |
| [WireGuard (wstunnel)](#wireguard-wstunnel) | 8080/tcp | High | High | No |
| [Telegram MTProxy](#telegram-mtproxy) | 993/tcp | High | Medium | No |
| [dnstt](#dnstt) | 53/udp | Medium | Low | Yes |
| [Slipstream](#slipstream) | 53/udp | Medium | Low-Medium | Yes |
| [MasterDNS](#masterdns) | 53/udp | Medium | Medium | Yes |
| [GooseRelay](#gooserelay) | 8444/tcp | Very High | Low-Medium | No |
| [Psiphon Conduit](#psiphon-conduit) | dynamic | High | Medium | No |
| [XHTTP (VLESS+XHTTP+Reality)](#xhttp-vlessxhttpreality) | 2096/tcp | Very High | High | No |
| [XDNS (VLESS+mKCP+DNS)](#xdns-vlessmkcpdns) | 53/udp | Medium | Low | Yes |
| [Tor Snowflake](#tor-snowflake) | dynamic | High | Low | No |
| [MahsaNet](#mahsanet) | — | — | — | No |
## Protocols in Detail
### Reality (VLESS)
**Primary protocol.** VLESS with Reality makes your proxy traffic resemble a real TLS connection to a legitimate website (e.g., `dl.google.com`). The server presents a genuine TLS certificate from the target site, passing even active probing.
- **Port:** 443/tcp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box)
- **Clients:** Streisand, Hiddify, v2rayNG, v2rayN, NekoBox
### Trojan
Password-authenticated TLS proxy. Traffic looks like normal HTTPS. Uses your domain's real TLS certificate from Let's Encrypt.
- **Port:** 8443/tcp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box)
- **Clients:** Streisand, Hiddify, v2rayNG, v2rayN, Shadowrocket
### AnyTLS
Password-authenticated TLS proxy designed to resist **TLS-in-TLS fingerprinting**. By varying record sizes and padding, AnyTLS removes the tell-tale TLS-inside-TLS pattern that DPI uses to detect TLS-tunneling proxies, giving it high stealth against the filtering techniques it targets. Reuses the same sing-box engine, the Trojan TLS certificate, and your server domain.
- **Port:** 8445/tcp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box) (1.13.x)
- **Clients:** Hiddify, sing-box (SFA/SFI), NekoBox/NekoRay, Mihomo Party, Shadowrocket 2.2.65+
- **Note:** Opt-in — enable with `ENABLE_ANYTLS=true`. Requires a domain (TLS). Client support is narrower than VLESS/Trojan; older or Clash-only clients (v2rayNG, Streisand, V2Box, Clash Verge) do **not** support AnyTLS.
### Hysteria2
QUIC-based protocol optimized for high throughput on lossy networks. Includes built-in obfuscation to bypass QUIC blocking.
- **Port:** 443/udp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box)
- **Clients:** Streisand, Hiddify, v2rayNG, v2rayN
- **Note:** Requires UDP. Blocked in some censored networks that drop all non-DNS UDP.
- **Congestion control:** `up_mbps`/`down_mbps` are left unset and `ignore_client_bandwidth: true` is set, keeping both ends on BBR and stopping a client-advertised bandwidth from switching the link to Brutal (which can saturate a low-RAM VPS). This BBR is Hysteria2's own QUIC-layer controller inside sing-box — unrelated to the kernel `tcp_bbr` module, so it has no host dependency.
### Shadowsocks-2022
AEAD-2022 Shadowsocks (`2022-blake3-aes-128-gcm`), the modern Shadowsocks generation with per-user keys and built-in resistance to active probing and replay attacks. Needs **no domain and no TLS certificate**, so it works in domainless mode and is a good fallback when certificate-based protocols aren't an option. Wire-compatible with the Outline app.
- **Port:** 8388/tcp + 8388/udp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box)
- **Clients:** Outline (iOS/Android/desktop), NekoBox/NekoRay, Hiddify, Streisand, sing-box — via the standard `ss://` URI
- **Note:** On by default (`ENABLE_SS=true`). If port 8388 is fingerprinted by your ISP, change `PORT_SS` in `.env` to a less-conspicuous port and `moav restart sing-box`.
### CDN (VLESS+WS)
Routes VLESS traffic through Cloudflare's CDN via WebSocket. When your server's IP is blocked, traffic goes through Cloudflare instead, which makes IP-based blocking substantially harder, because clients connect through CDN infrastructure.
**Opt-in:** `ENABLE_CDN=false` by default, because the link only works once the subdomain is proxied and rewritten to the CDN port. Turn it on after [CDN mode](https://moav.sh/docs/DNS/#cdn-mode) is set up.
- **Port:** 443 (Cloudflare) → 2082 (origin)
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box)
- **Clients:** Streisand, Hiddify, v2rayNG, v2rayN
- **Requires:** a Cloudflare-proxied domain you control; AWS CloudFront can use the distribution hostname and needs no custom domain
### TrustTunnel
Modern VPN protocol that looks like regular HTTPS traffic. Supports both HTTP/2 (TCP) and HTTP/3 (QUIC/UDP).
- **Port:** 4443/tcp + 4443/udp
- **Engine:** [TrustTunnel](https://github.com/TrustTunnel/TrustTunnel) (server) / [TrustTunnelClient](https://github.com/TrustTunnel/TrustTunnelClient) (client)
- **Clients:** TrustTunnel app (iOS, Android, macOS, Windows, Linux)
### WireGuard
Fast kernel-level VPN. Simple, audited, and widely supported. Direct UDP connection.
- **Port:** 51820/udp
- **Engine:** [sing-box](https://github.com/SagerNet/sing-box) + [wstunnel](https://github.com/erebe/wstunnel)
- **Clients:** WireGuard app (all platforms)
- **Note:** Easily fingerprinted by DPI. Use AmneziaWG or wstunnel variant in censored networks.
### AmneziaWG
Obfuscated WireGuard variant designed to resist common DPI signatures. Adds junk packets, changes handshake timing, and modifies header fields to avoid detection.
- **Port:** 51821/udp
- **Engine:** [amneziawg-tools](https://github.com/amnezia-vpn/amneziawg-tools)
- **Clients:** AmneziaVPN (iOS, Android, macOS, Windows, Linux)
### WireGuard (wstunnel)
WireGuard tunneled through WebSocket (TCP). Works when UDP is completely blocked. When a `DOMAIN` is configured the tunnel is served over **`wss://` (TLS)** using the server's Let's Encrypt certificate, so the WebSocket upgrade resembles ordinary HTTPS; it falls back to plain `ws://` only in domainless mode. A per-install **HTTP-upgrade path secret** is also required, so a scanner probing port 8080 can't complete the WebSocket upgrade blind. The exact client command (correct `wss://`/`ws://` scheme and path prefix) is emitted in each user bundle's `wireguard-instructions.txt`.
- **Port:** 8080/tcp
- **Engine:** [wstunnel](https://github.com/erebe/wstunnel) wrapping the WireGuard container
- **Clients:** WireGuard app + wstunnel binary
- **Note:** After upgrading an existing install, rebuild the image (`moav build wstunnel`) and re-bootstrap to generate the path secret and enable `wss://`; older bundles keep working over `ws://` until re-issued.
### Telegram MTProxy
Telegram-specific proxy with Fake-TLS V2. Emulates real TLS connections, including certificate mimicry and timing simulation. Provides direct access to Telegram when it's blocked.
- **Port:** 993/tcp (IMAPS port for stealth)
- **Engine:** [telemt](https://github.com/telemt/telemt)
- **Clients:** Telegram app (built-in proxy settings)
??? note "Anti-DPI Tuning Settings"
telemt has 17+ configurable settings for hostile network environments. All configurable in `.env`:
**Traffic Disguise (anti-DPI):**
| Setting | Default | Purpose |
|---------|---------|---------|
| `TELEMT_KEEPALIVE_RANDOM` | `true` | Randomize keepalive payload to break DPI pattern-matching |
| `TELEMT_KEEPALIVE_JITTER` | `4` | ±N seconds randomness on keepalive timing |
| `TELEMT_KEEPALIVE_INTERVAL` | `20` | Base keepalive interval in seconds |
| `TELEMT_WARMUP_JITTER` | `200` | Randomize connection establishment timing (ms) |
**Connection Pool Resilience:**
| Setting | Default | Purpose |
|---------|---------|---------|
| `TELEMT_POOL_SIZE` | `12` | Number of persistent connections to Telegram DCs |
| `TELEMT_REINIT_SECS` | `600` | Rebuild all connections every N seconds (prevents long-connection fingerprinting) |
| `TELEMT_HARDSWAP` | `true` | Build new pool before tearing down old (zero-downtime rotation) |
| `TELEMT_HARDSWAP_DELAY_MIN` | `500` | Min delay between new connections during swap (ms) |
| `TELEMT_HARDSWAP_DELAY_MAX` | `1200` | Max delay between new connections during swap (ms) |
**Fast Reconnect:**
| Setting | Default | Purpose |
|---------|---------|---------|
| `TELEMT_FAST_RETRIES` | `10` | Quick retries before exponential backoff |
| `TELEMT_BACKOFF_BASE` | `300` | Backoff start interval (ms) |
| `TELEMT_BACKOFF_CAP` | `10000` | Maximum backoff interval (ms) |
**Config Stability:**
| Setting | Default | Purpose |
|---------|---------|---------|
| `TELEMT_STABLE_SNAPSHOTS` | `3` | Require N consistent config snapshots before applying changes |
| `TELEMT_APPLY_COOLDOWN` | `120` | Minimum seconds between config changes |
**For aggressive censorship** (e.g., Iran during shutdowns): increase `TELEMT_POOL_SIZE` to 16-20, decrease `TELEMT_REINIT_SECS` to 300, and increase `TELEMT_FAST_RETRIES` to 20.
Full tuning docs: [telemt TUNING.en.md](https://github.com/telemt/telemt/blob/main/docs/TUNING.en.md) | [API docs](https://github.com/telemt/telemt/blob/main/docs/API.md)
### GooseRelay
SOCKS5 tunnelled through a **Google Apps Script** web app that the user deploys in their own Google account, which forwards to this VPS exit server. On the wire the client only ever appears to make a domain-fronted HTTPS request to `google.com` — the payload is AES-256-GCM encrypted end-to-end between the GooseRelay endpoints, so Apps Script relays ciphertext, but Google can still observe request metadata. This is the **GooseRelay** component bundled in MahsaNG v16. Extremely stealthy (looks like Google traffic), but throughput is capped by the Apps Script ~20k-calls/day-per-account quota.
- **Port:** `${PORT_GOOSE}`/tcp (default 8444 on the host → 8443 in the container; 8443 on the host is Trojan's)
- **Engine:** [GooseRelayVPN](https://github.com/kianmhz/GooseRelayVPN) (Go), server built from source
- **Clients:** MahsaNG v16+, or the standalone GooseRelay client + a user-deployed Apps Script forwarder
- **Encryption:** AES-256-GCM, shared 64-hex `tunnel_key` (in each user's `gooserelay-instructions.txt`)
- **Requires:** No domain. `PORT_GOOSE` must be reachable from Google's network. The user sets `RELAY_URLS = ['http://SERVER_IP:PORT_GOOSE/tunnel']` in their Apps Script.
- **Note:** Opt-in — set `ENABLE_GOOSERELAY=true` in `.env`. Egress is routed through sing-box. Real-time apps (Telegram/X) drain the Apps Script quota fast; add more deployments under different Google accounts for capacity.
### XHTTP (VLESS+XHTTP+Reality)
**Experimental.** VLESS over XHTTP transport with Reality TLS camouflage, powered by Xray-core. Uses the XHTTP (formerly splithttp) transport for multiplexed HTTP requests, making traffic look like regular web browsing. Reality handles TLS without needing a domain.
- **Port:** 2096/tcp
- **Engine:** [Xray-core](https://github.com/XTLS/Xray-core)
- **Clients:** V2rayNG, Hiddify, Streisand, V2Box, V2rayN, V2rayU, NekoBox
- **Note:** Uses Xray-core (separate from sing-box). Disable with `ENABLE_XHTTP=false` in `.env`.
### Psiphon Conduit
Bandwidth donation to the Psiphon network. Psiphon users worldwide route through your server. Not a protocol you connect to — it's a way to help others bypass censorship.
- **Engine:** [Psiphon Conduit](https://github.com/Psiphon-Inc/conduit)
- **Clients:** [Psiphon](https://psiphon.ca/) app (iOS, Android, Windows)
#### How your Conduit helps people in Iran
There are two ways your running Conduit reaches users:
1. **Public pool — automatic, nothing to share.** The moment Conduit is
running it donates bandwidth to the Psiphon network. Psiphon app users —
including in Iran — are brokered through your server automatically. They
don't need a link, an invite, or any setup. This is the main way Conduit
helps and requires zero action on the user's side.
2. **Personal Pairing — share a private path with specific people.** Psiphon's
Conduit lets you give friends/family a private, prioritized path through
your station. The Psiphon app has a "pairing URL" field for this. To set it
up: install Psiphon's **Ryve** app (the Conduit manager), import your
station with the claim link MoaV generates, then in Ryve enable Personal
Pairing and generate a pairing link to send to people in Iran.
#### `moav conduit link`
```bash
moav conduit link # Claim link + QR + step-by-step sharing guide
moav conduit status # Is it running + connected clients / bandwidth
```
This prints the **Ryve claim deep link** (`network.ryve.app://…claim=…`) and
its QR code, plus the sharing walkthrough above.
> **⚠ Security:** the claim link/QR embeds this Conduit's **private key** — it
> is for importing the station into *your own* phone's Ryve app. Treat it like
> a password; do **not** post it publicly (anyone with it can take over your
> station). The public-safe link you give to users is the **Personal Pairing**
> link generated *inside Ryve*, not the claim link. As of
> [Psiphon-Inc/conduit#205](https://github.com/Psiphon-Inc/conduit/issues/205)
> the pairing-URL export lives only in the Conduit/Ryve app UI, so MoaV
> surfaces the claim link and the steps rather than minting a pairing URL
> itself. (`moav donate info` is an alias for `moav conduit link`.)
### Tor Snowflake
Bandwidth donation to the Tor network. Acts as a Snowflake proxy, helping Tor users in censored regions connect. Like Conduit, this is about helping others.
- **Engine:** [Snowflake](https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake)
- **Clients:** [Tor Browser](https://www.torproject.org/) with Snowflake bridge
### MahsaNet
Config donation to [MahsaServer.com](https://www.mahsaserver.com/), a decentralized VPN config sharing platform for the [Mahsa VPN](https://www.mahsaserver.com/) app. With over 2 million users in Iran, Mahsa VPN connects to donated VPN configurations from servers worldwide. Unlike Conduit and Snowflake (which donate bandwidth), MahsaNet donates your server's VPN config links — Mahsa VPN users then connect directly to your server.
- **Supported protocols:** Reality (VLESS), Hysteria2, Trojan, CDN (VLESS+WS)
- **Clients:** [Mahsa VPN](https://www.mahsaserver.com/) app (Android, iOS)
- **Setup:** Register on MahsaServer.com, get API key, then `moav donate`
- **Dashboard:** Donate, list, and manage configs from the Admin Dashboard
## DNS Tunnels
The last transports standing. When a network blocks or throttles almost everything else, DNS usually still resolves — breaking it breaks the whole internet for everyone. DNS tunnels encode traffic inside DNS queries, so they can remain usable where ordinary traffic is blocked but recursive DNS still resolves.
They are **slow**. Treat them as the fallback that keeps chat and messaging alive, not as an everyday transport.
### How it works
MoaV runs **four** DNS tunnels **simultaneously** on the same public port 53. A small Go service, `dns-router`, is the only thing bound to that port; it reads each query's subdomain prefix and forwards to the matching tunnel container on an internal port.
```
Public 53/udp
│
┌──────▼──────┐
│ dns-router │ ← the only listener on 53
└──────┬──────┘
│ routes by subdomain prefix
t.* ─────► dnstt (KCP + Noise)
s.* ─────► slipstream (QUIC-over-DNS)
m.* ─────► masterdns (ARQ, MahsaNG-native)
x.* ─────► xray (XDNS via FinalMask)
│
▼
sing-box ──► internet
```
Because they are separated by subdomain rather than by port, there is no "pick one" decision: a user connects with whichever tunnel their client supports, and all four can serve traffic at once.
### What MoaV sets up for you
Each tunnel needs an **NS delegation** handing its subdomain to your server, plus one A record for the nameserver itself:
```
dns.yourdomain.com A YOUR_SERVER_IP # the nameserver
t.yourdomain.com NS dns.yourdomain.com # dnstt
s.yourdomain.com NS dns.yourdomain.com # Slipstream
m.yourdomain.com NS dns.yourdomain.com # MasterDNS
x.yourdomain.com NS dns.yourdomain.com # XDNS
```
`moav doctor dns` writes exactly the records your configuration needs to `outputs/dns-records.txt`, ready to import into Cloudflare. Full walkthrough: [DNS Configuration](https://moav.sh/docs/DNS/#with-a-domain-the-records).
All four are on by default. Toggle them individually with `ENABLE_DNSTT` / `ENABLE_SLIPSTREAM` / `ENABLE_MASTERDNS` / `ENABLE_XDNS`, or set the combination in one command:
```bash
moav switch-dns # show what is on
moav switch-dns dnstt+slipstream+masterdns+xdns # all four
moav switch-dns dnstt+slipstream # the classic pair
moav switch-dns off # no DNS tunnels
```
A disabled tunnel's container stays down and `dns-router` simply has no backend to forward to. Port **53/udp** must reach the server; some ISPs block it outright on residential lines.
### Which one should I use?
| Tunnel | Subdomain | Speed vs dnstt | Loss resilience | Best for |
|---|---|---|---|---|
| **dnstt** | `t` | 1× *(baseline)* | low | **Widest client support** — standalone client on 25+ platforms |
| **Slipstream** | `s` | 1.5–5× | medium | Faster general use where a Slipstream client exists |
| **MasterDNS** | `m` | up to 9× | **high** *(ARQ + packet duplication + multi-resolver)* | **Harsh shutdowns**; native in [MahsaNG v16](https://moav.sh/docs/mahsanet/) |
| **XDNS** | `x` | ~1× | low | FinalMask clients (Happ, Xray CLI); per-user auth |
**Short answer:** in Iran during heavy throttling or a blackout, **MasterDNS** is the strongest and works straight from the MahsaNG app. Offer **dnstt** too, because its client runs almost everywhere.
!!! warning "The client's resolver matters more than the tunnel"
Every DNS tunnel depends on a public resolver **the client can still reach**, and `1.1.1.1` / `8.8.8.8` are commonly throttled or null-routed exactly when a tunnel is needed. XDNS round-robins across `XDNS_RESOLVERS`; dnstt and Slipstream take a resolver flag client-side. [findns](https://github.com/SamNet-dev/findns) and [dns-mns](https://gitlab.com/E-Gurl/dns-mns) scan for resolvers that still work on a given network — see [reachable resolvers](#reachable-dns-resolvers).
### Reachable DNS resolvers
Every DNS tunnel is only as good as the resolver the **client** can reach. During shutdowns the well-known ones (`1.1.1.1`, `8.8.8.8`, `9.9.9.9`) are routinely throttled, hijacked or null-routed, and a tunnel that worked yesterday will look broken.
Two scanners find resolvers that still answer on a given network:
- **[findns](https://github.com/SamNet-dev/findns)** — sweeps a range and reports which resolvers respond correctly
- **[dns-mns](https://gitlab.com/E-Gurl/dns-mns)** — same idea, maintained separately
Feed the survivors to the client: `XDNS_RESOLVERS` accepts a comma-separated list that XDNS round-robins across, and the dnstt / Slipstream clients each take a resolver flag. It's worth shipping users two or three known-good resolvers rather than one.
### dnstt
Encodes a TCP stream inside DNS queries using KCP + Noise. Extremely hard to block without breaking DNS itself; the slowest of the four and the most portable.
- **Port:** 53/udp *(subdomain `t`)* · **Engine:** [dnstt](https://www.bamsoftware.com/software/dnstt/)
- **Clients:** standalone dnstt client on 25+ platforms
- **Requires:** domain + NS delegation
### Slipstream
The same idea over **QUIC**, which buys real throughput — typically 1.5–5× dnstt.
- **Port:** 53/udp *(subdomain `s`)* · **Engine:** [slipstream-rust](https://github.com/Mygod/slipstream-rust) · [pre-built binaries](https://github.com/net2share/slipstream-rust-build/releases)
- **Requires:** domain + NS delegation
### MasterDNS
The most loss-resilient of the four: low-overhead ARQ, packet duplication and resolver load-balancing, which is what keeps it usable on throttled links. This is the MasterDNS component bundled in **MahsaNG v16**, so that app connects with no extra client.
- **Port:** 53/udp *(subdomain `MASTERDNS_SUBDOMAIN`, default `m`)* · **Engine:** [MasterDnsVPN](https://github.com/masterking32/MasterDnsVPN) (Go)
- **Clients:** MahsaNG v16+, or the standalone client (Linux/Windows/macOS/Termux)
- **Encryption:** AES-256-GCM (`DATA_ENCRYPTION_METHOD=5`); the shared key ships in each user's bundle
- **Extra:** `MASTERDNS_PUBLIC_SUBDOMAIN` publishes a *different* delegation name than the one used internally — generated bundles then use the public one
### XDNS (VLESS+mKCP+DNS)
**Experimental.** Xray-core's mKCP transport with FinalMask, and the only DNS tunnel here with **per-user authentication** — at the cost of needing a FinalMask-aware client.
- **Port:** 53/udp *(subdomain `x`)* · **Engine:** [Xray-core](https://github.com/XTLS/Xray-core) *(built from main for FinalMask)*
- **Clients:** Happ (beta), Xray CLI. **Not** standard v2rayNG yet.
- **Best for:** Telegram and light chat apps — not fast enough for browsing
??? note "XDNS Tuning"
| Setting | Default | Purpose |
|---------|---------|---------|
| `XDNS_MTU` | `35` | mKCP packet size. Smaller = works with more DNS resolvers. 35=safest, 67=most, 130=unrestricted |
| `XDNS_SUBDOMAIN` | `x` | Subdomain for XDNS queries (x.yourdomain.com) |
| `XDNS_RESOLVERS` | `1.1.1.1,8.8.8.8` | CSV of public DNS resolvers the client round-robins across in a single mKCP session (Xray v26.4.13+, [PR #5872](https://github.com/XTLS/Xray-core/pull/5872)). See [Reachable DNS resolvers](#reachable-dns-resolvers) — replace the defaults with resolvers that actually answer on your network. Set empty to fall back to single-resolver mode. |
| `XDNS_METHOD` | `txt` | Finalmask record mode in generated client bundles. `txt` is the widest-compatibility default; `aaaa` ([Xray #6123](https://github.com/XTLS/Xray-core/pull/6123)) gives higher throughput per query but **requires an Xray client core ≥ v26.6.1** (Happ / Xray CLI). Server side needs no change. |
MTU depends on domain name length — shorter domain allows higher MTU. The values above are for ~19-character domains.
For aggressive censorship: use `MTU=35` and connect via a DNS resolver you can actually reach from inside the censored network (see below).
## Choosing Protocols
**For censored networks (Iran, China, Russia):**
1. Start with **Reality** — high stealth against the filtering techniques it targets, a strong first choice on networks where it currently works
2. Add **CDN mode** — works when your server IP is blocked
3. Enable **AmneziaWG** — for full VPN when WireGuard is fingerprinted
4. Enable **DNS tunnels** — last resort when almost everything is blocked
**For general privacy:**
1. **WireGuard** — fastest, simplest
2. **Reality** — when WireGuard is blocked
**For helping others:**
1. **Conduit** — donate bandwidth to Psiphon users
2. **Snowflake** — donate bandwidth to Tor users
3. **MahsaNet** — donate VPN configs to Mahsa VPN users in Iran
---
# Mission & Philosophy
This is the longer manifesto behind MoaV. For the concise project description, start with [Mission](https://moav.sh/docs/mission/). For limits and safety assumptions, read the [Threat Model](https://moav.sh/docs/threat-model/). For outcomes and measurement, read [Impact](https://moav.sh/docs/impact/).
Internet freedom doesn't happen by accident. It doesn't come from governments deciding to be generous, or from corporations choosing not to surveil. It comes from people: engineers, activists, diaspora communities, and strangers with a spare VPS, building the infrastructure that makes it real.
We built MoaV because the tools already existed but the friction was too high. Running a reliable multi-protocol circumvention server shouldn't require a week of configuration and a systems-engineering background. With MoaV it takes ten minutes. One command. A $5 server. And you're part of the network that keeps people connected when their governments decide they shouldn't be.
This is not someone else's problem to solve. The window to act is between blackouts, not during them.
## Internet Access Is a Human Right
The United Nations Human Rights Council has repeatedly affirmed that the same rights people have offline must also be protected online. The Universal Declaration of Human Rights, Article 19:
> *Everyone has the right to freedom of opinion and expression; this right includes freedom to hold opinions without interference and to seek, receive and impart information and ideas through any means and regardless of frontiers.*
Internet shutdowns are not abstract policy debates. They cut people off from family, healthcare information, financial services, education, and the ability to document what is happening around them. They are used deliberately during protests and crises, precisely when communication matters most.
## Why MoaV Exists
MoaV was created to **democratize access to anti-censorship tools**. Running a VPN server shouldn't require deep technical expertise. Running *multiple* protocols, so users can find one that works when others are blocked, shouldn't require managing a dozen separate tools.
MoaV packages 16+ circumvention transports and fallback paths, plus optional Psiphon, Tor and MahsaNet donation integrations, into a single deployment. One command deploys all of them. A $5 VPS is enough. The goal is simple: make it as easy as possible for anyone with a spare server to provide reliable internet access to people who need it.
## What Infrastructure Actually Means
Here is the difference between using a tool and being infrastructure.
**Using a tool**: you install a VPN app. It works, or it doesn't. When it gets blocked, you try the next one. You are a consumer of access.
**Being infrastructure**: you run a server. Other people connect through it. When the protocols they're using get blocked, your server already has the fallback ready. You are a node in the network of free communication.
MoaV was built to make the second thing as easy as the first.
## Why Multi-Protocol Matters
No single protocol survives all censorship regimes. Governments invest heavily in Deep Packet Inspection (DPI) and adapt their blocking continuously:
- **Protocol whitelisting**: only DNS, HTTP, and HTTPS allowed; everything else dropped.
- **SNI inspection**: TLS handshakes inspected to block connections to non-approved domains.
- **QUIC/UDP blocking**: all UDP except DNS dropped, killing WireGuard and Hysteria2.
- **Active probing**: censors connect to suspected proxy servers to verify they're running proxy software.
- **Throttling**: connections not outright blocked but throttled to unusable speeds.
MoaV's approach: run multiple fallback routes. Each protocol uses a different gap in the censor's capabilities.
| Censorship method | MoaV counter |
|---|---|
| Protocol blocking | Reality mimics legitimate TLS to approved sites |
| UDP blocking | WireGuard tunneled through WebSocket (TCP) |
| IP blocking | CDN mode routes through Cloudflare's network |
| DPI on SNI | TrustTunnel looks like regular HTTPS traffic |
| Total shutdown | DNS tunnels work when only DNS is allowed |
| Active probing | Shadowsocks-2022 AEAD ciphers resist probes; decoy site serves innocent content |
| Throttling | Hysteria2's QUIC maximizes throughput on constrained links |
You don't know which protocol will survive the next shutdown. Neither does the censor. Running many of them is not inefficient. It is the point. See [Supported Protocols](https://moav.sh/docs/protocols/) for the full list.
## The Internet Is Closing
Iran has repeatedly shown why resilient access infrastructure matters. In 2019, the state imposed a near-total shutdown during fuel-price protests. In 2022 and 2023, during the Woman, Life, Freedom movement, authorities blocked major platforms, throttled bandwidth, and targeted circumvention tools. More recently, connectivity disruptions and protocol-level blocking have continued during periods of political pressure and regional tension.
The pattern is familiar: access is restricted, people rush to find working tools, and demand for circumvention spikes exactly when normal distribution channels are most fragile.
This is the moment MoaV is built for. Not because one stack solves censorship everywhere, but because every extra working route matters when the easy routes disappear.
## A Global Pattern
Iran is not alone. Every time a government restricts connectivity, the response is the same: millions of people immediately try to route around it, and most of them don't know how.
- **Uganda**, January 2026 elections: VPN demand spiked **2,557%** after social platforms were blocked.
- **Nepal**, September 2025: peaked at **2,892%** when social media was banned.
- **United Kingdom**, July 2025: age-verification rollout drove a single-day spike of **1,987%**.
- **Myanmar**, February 2021: the military cut the internet hours after the coup. Mobile first. Then broadband. Then only a few whitelisted ports.
- **Russia**, post-February 2022: did not cut the internet. It blocked the tools used to circumvent filtering, including Psiphon, Tor, and Signal. The lesson: you do not have to turn off the internet to control it; you can make resistance harder to use.
- **China**: two decades of the Great Firewall. Protocol after protocol identified, fingerprinted, blocked. VPN vendors in a permanent arms race.
And the laws that seem unthinkable right now keep becoming law in democracies. The EU's Chat Control proposal would mandate client-side scanning of encrypted messages. France has proposed banning end-to-end encryption for apps used to coordinate "criminal" activity. The UK Online Safety Act gives regulators the power to demand backdoors. The infrastructure that keeps communication free in Iran is the same infrastructure that will matter in Europe when those laws come into force.
## The Arms Race Gets Creative
One of the things that gives us hope, and that governments consistently underestimate, is the ingenuity of people building tools to stay connected under pressure.
Every censorship technique creates its own workaround. Block VPN protocols, developers build obfuscated ones. Block obfuscated VPNs, they route through CDNs too large to block. Block CDNs, they build DNS tunnels. Block DNS, and you break your own country's domestic internet.
But the creativity doesn't stop at DNS:
- **[BaleVPN](https://github.com/kookoo1sabzy/BaleVPN)** routes traffic through Bale, Iran's officially approved video-call platform. The tunnel encodes IP traffic as what looks like a Bale voice call. To the network, an approved domestic app doing approved domestic things. To the user, internet access. The government built the infrastructure for its own circumvention.
- **[GooseRelay](https://moav.sh/docs/protocols/#gooserelay)** (shipped natively in MoaV) routes traffic through Google Apps Script. To the censor, an HTTPS request to a Google Workspace serverless function used by millions of businesses. Blocking GooseRelay means blocking Apps Script globally, which means breaking every company and university using it in the country. The cost of the block exceeds the benefit.
- **[SNI Spoofing](https://github.com/aleskxyz/SNI-Spoofing-Go)** uses a different opening: the unencrypted hostname in the TLS handshake. A local proxy sends a fake ClientHello with a decoy hostname, for example `microsoft.com`, while the real connection continues underneath. The DPI box sees something benign; the traffic gets through.
The censor's playbook has a finite number of pages. The circumvention community keeps adding new chapters.
## You Are Donating Bandwidth (Or You Could Be)
Three of the donation paths MoaV bundles are not for you. They are for everyone else.
**[Psiphon Conduit](https://psiphon.ca)** turns your server into a relay node for Psiphon users, people who can't reach the app directly and need a trusted intermediary. Psiphon has tens of millions of users in Iran, Russia, Belarus, Venezuela, and dozens of other censored countries. Your VPS becomes part of the network they depend on.
**[Tor Snowflake](https://snowflake.torproject.org/)** does the same for Tor. Your server becomes a Tor bridge, a relay that Tor users can connect to when the public ones are blocked. You're not an exit node; you're handling the first step for someone who otherwise can't reach the network at all.
**[MahsaNet](https://moav.sh/docs/mahsanet/)** is the MahsaNG peer network, and it's worth understanding what makes it different. It's not just a relay pool, it's a distribution channel. When you donate your server config to MahsaNet, you're publishing your server's address and credentials to a network that MahsaNG users can query directly from the app. No diaspora contact required. No Telegram group to find. The app discovers your server automatically, the moment it's needed. MahsaNG has its own distribution infrastructure that already reaches the 2 million+ people using the app.
The marginal cost is bandwidth, a few dollars a month at most on a standard VPS plan. The marginal impact is someone being able to say they're alive.
## Iran's Shutdown History
Iran's censorship infrastructure operates at multiple layers: the National Information Network (NIN, the domestic intranet), Deep Packet Inspection at major peering points, protocol whitelisting during heavy periods (only ports 53/80/443 permitted), active probing of suspected proxy servers, and throttling that maintains the appearance of connectivity while making it unusable.
The chronology that shaped MoaV's design:
- **November 2019**: near-total internet shutdown during fuel-price protests. Amnesty International documented at least 304 deaths; other estimates are higher. The shutdown prevented documentation of events and coordination of emergency response.
- **September 2022 to 2023, Woman, Life, Freedom**: following Mahsa (Jina) Amini's death in morality-police custody, nationwide protests met months of internet disruption. WhatsApp, Instagram, and Signal were blocked, bandwidth was throttled, and VPN protocols were targeted.
- **Ongoing disruptions**: Iran continues to combine platform blocking, throttling, protocol filtering, and domestic-network dependency during periods of pressure. That uncertainty is enough to justify building capacity before the next crisis.
## The Window Is Open. It Won't Stay That Way.
This is the trap. The state allows a partial reopening after protests wind down, enough for people to feel like things are normalizing, enough to make the isolation seem temporary. Meanwhile, everything the government learned about circumvention tool usage during a blackout goes into improving the next one. Which DNS resolvers kept working. Which protocols leaked through. Which apps people used when everything else was blocked.
That data is now in the hands of the people planning the next shutdown.
The time to build capacity is not during the shutdown. During a shutdown, new server deployments can't reach the people who need them. Domain names can't be shared through a blocked internet. Configuration files can't be distributed when the distribution channels are offline.
**The servers that matter in the next crisis are the ones being deployed right now, while the window is open.**
## The Argument in One Line
Every time a government has tried to cut the internet completely, it has proven why distributed infrastructure matters. And every time, the people who kept the connections alive were engineers and activists who had set up their servers before the crisis, not during it.
We can be those people. Not as a political statement, not as an act of heroism, but as a practical decision, made now, that runs in the background and serves people we'll never know.
That's how infrastructure works.
## Contributing
If you have a VPS and want to help:
1. **[Deploy MoaV](https://moav.sh/docs/quick-start/)** and share access with people who need it.
2. **[Enable Conduit](https://moav.sh/docs/support/#donate-bandwidth-instead-of-users)** to relay bandwidth for Psiphon users worldwide.
3. **[Enable Snowflake](https://moav.sh/docs/support/#donate-bandwidth-instead-of-users)** to relay bandwidth for Tor users.
4. **[Donate configs to MahsaNet](https://moav.sh/docs/mahsanet/)** so 2M+ MahsaNG users discover your server automatically.
5. **[Contribute code](https://github.com/MotherofallVPNs/moav)**: fix bugs, add protocols, improve documentation.
## Sources & Further Reading
References and starting points:
- Iran shutdown timeline, NIN architecture: [NetBlocks](https://netblocks.org/), [Access Now Shutdown Tracker](https://www.accessnow.org/keepiton/)
- VPN demand statistics: [Top10VPN demand statistics](https://www.top10vpn.com/research/vpn-demand-statistics/)
- Internet traffic analysis: Doug Madory / [Kentik](https://www.kentik.com/)
- Mahsa Amini protests: Amnesty International reports, 2022 to 2023
- BaleVPN, TCP over Bale voice calls: [github.com/kookoo1sabzy/BaleVPN](https://github.com/kookoo1sabzy/BaleVPN)
- SNI Spoofing Go, local proxy sending decoy ClientHello: [github.com/aleskxyz/SNI-Spoofing-Go](https://github.com/aleskxyz/SNI-Spoofing-Go)
---
# MoaV Setup Guide
Complete guide to deploy MoaV on a VPS or home server.
## Prerequisites
**Server Requirements:**
- Debian 12, Ubuntu 22.04, or Ubuntu 24.04 (Raspberry Pi OS works too)
- Architecture: x64 (AMD64) or ARM64 (Raspberry Pi 4, Apple Silicon)
- Minimum: 1 vCPU, 1GB RAM, 10GB disk
- Public IPv4 address
- Public IPv6 address (optional, see [IPv6 Support](#ipv6-support))
**Domain (Optional but Recommended):**
- Required for: Trojan, AnyTLS, Hysteria2, TrustTunnel, CDN mode, DNS tunnels (dnstt, Slipstream, MasterDNS, XDNS)
- Not required for: Reality, XHTTP, Shadowsocks-2022, WireGuard, AmneziaWG, Telegram MTProxy, Admin dashboard, Conduit, Snowflake
- Per-protocol checklist: [Do I need a domain?](https://moav.sh/docs/DNS/#do-i-need-a-domain)
- See [Domainless Mode](#domainless-mode) if you don't have a domain
**Ports to Open:**
| Port | Protocol | Service | Requires Domain |
|------|----------|---------|-----------------|
| 443/tcp | TCP | Reality (VLESS) | No — borrows a public SNI via `REALITY_TARGET` |
| 443/udp | UDP | Hysteria2 | Yes |
| 8443/tcp | TCP | Trojan | Yes |
| 8445/tcp | TCP | AnyTLS | Yes |
| 8388/tcp+udp | TCP+UDP | Shadowsocks-2022 | No |
| 4443/tcp+udp | TCP+UDP | TrustTunnel | Yes |
| 2082/tcp | TCP | CDN WebSocket | Yes (Cloudflare) or No (CloudFront) |
| 51820/udp | UDP | WireGuard | No |
| 51821/udp | UDP | AmneziaWG | No |
| 8080/tcp | TCP | wstunnel | No |
| 9443/tcp | TCP | Admin dashboard | No |
| 9444/tcp | TCP | Grafana (monitoring) | No |
| 993/tcp | TCP | Telegram MTProxy (telemt) | No |
| 2096/tcp | TCP | XHTTP (VLESS+XHTTP+Reality) | No |
| 53/udp | UDP | DNS tunnels — dnstt, Slipstream, MasterDNS, XDNS (all 4 share port 53 via dns-router) | Yes |
| 8444/tcp | TCP | GooseRelay (when `ENABLE_GOOSERELAY=true`) | No |
| 80/tcp | TCP | Let's Encrypt | Yes (during setup) |
---
## Quick Start
!!! tip "The whole install is one command"
```bash
curl -fsSL moav.sh/install.sh | bash
```
It installs Docker, clones MoaV, and asks for three things: your **domain** (leave blank for domainless), an **email** for Let's Encrypt, and an **admin password**. When it finishes it prints your dashboard URLs and the DNS records to add.
If that's all you need, follow it through to your first user in [**Quick Start**](https://moav.sh/docs/quick-start/) — about ten minutes end to end.
The rest of this page is the reference: every option, and what to do when the defaults don't fit.
## Step-by-Step Setup
### Step 1: Get a Server
Choose a VPS provider and create a server:
| Provider | Minimum Plan | Price | Deploy Guide |
|----------|--------------|-------|--------------|
| Hetzner | CX22 (2 vCPU, 4GB) | €5.39/mo | [DEPLOY.md#hetzner](https://moav.sh/docs/DEPLOY/#hetzner) |
| DigitalOcean | Basic (1 vCPU, 1GB) | $6/mo | [DEPLOY.md#digitalocean](https://moav.sh/docs/DEPLOY/#digitalocean) |
| Vultr | 25GB SSD (1 vCPU, 1GB) | $5/mo | [DEPLOY.md#vultr](https://moav.sh/docs/DEPLOY/#vultr) |
| Linode | Nanode 1GB | $5/mo | [DEPLOY.md#linode](https://moav.sh/docs/DEPLOY/#linode) |
- VPS Price Trackers: [VPS-PRICES](https://vps-prices.com/)، [VPS Price Tracker](https://vpspricetracker.com/), [Cheap VPS Price Cheat Sheet](https://docs.google.com/spreadsheets/d/e/2PACX-1vTOC_THbM2RZzfRUhFCNp3SDXKdYDkfmccis4vxr7WtVIcPmXM-2lGKuZTBr8o_MIJ4XgIUYz1BmcqM/pubhtml)
- [Time4VPS](https://www.time4vps.com/?affid=8471): 1 vCPU، 1GB RAM، IPv4، 3.99€/ماه
**Home Server:** Raspberry Pi 4 (2GB+ RAM) or any ARM64/x64 Linux works. See [DNS.md](https://moav.sh/docs/DNS/#home-server-raspberry-pi) for dynamic DNS setup.
### Step 2: Configure DNS
Point your domain to your server **before** running setup.
**Minimum DNS Records:**
| Type | Name | Value | Notes |
|------|------|-------|-------|
| A | @ | YOUR_SERVER_IP | Main domain |
**Additional Records (for all features):**
| Type | Name | Value | Notes |
|------|------|-------|-------|
| A | dns | YOUR_SERVER_IP | For DNS tunnel NS delegation |
| NS | t | dns.yourdomain.com | dnstt tunnel subdomain |
| NS | s | dns.yourdomain.com | Slipstream tunnel subdomain |
| NS | m | dns.yourdomain.com | MasterDNS tunnel subdomain |
| NS | x | dns.yourdomain.com | XDNS tunnel subdomain |
| A | cdn | YOUR_SERVER_IP | CDN mode (Cloudflare: **Proxied** orange cloud) |
**Important:** For Cloudflare users, the main `@` record must be **DNS only** (gray cloud). Only the `cdn` record should be **Proxied** (orange cloud).
See [DNS.md](https://moav.sh/docs/DNS/) for provider-specific instructions.
**Verify DNS is working:**
```bash
dig +short yourdomain.com
# Should return your server IP
# Or use MoaV's built-in DNS check (after install):
moav doctor dns
```
### Step 3: Install MoaV
SSH into your server and run:
```bash
curl -fsSL moav.sh/install.sh | bash
```
This installs:
- Docker and Docker Compose
- Git and qrencode
- MoaV to `/opt/moav`
- `moav` command (available globally)
**Manual Installation** (if you prefer):
```bash
# Install Docker
curl -fsSL https://get.docker.com | sh
# Install dependencies
apt install -y git qrencode
# Clone MoaV
git clone https://github.com/MotherofallVPNs/moav.git /opt/moav
cd /opt/moav
```
### Step 4: Configure Environment
```bash
cd /opt/moav
cp .env.example .env
nano .env
```
**Required Settings:**
```bash
# Your domain (must match DNS from Step 2)
DOMAIN=yourdomain.com
# Email for Let's Encrypt certificates
ACME_EMAIL=you@example.com
# Admin dashboard password (change this!)
ADMIN_PASSWORD=your-secure-password
```
**Optional Settings:**
```bash
# Server IP (auto-detected if empty)
SERVER_IP=
# Initial users to create during bootstrap
INITIAL_USERS=1
# Reality target (site to impersonate)
# Good choices: dl.google.com, www.apple.com, www.doi.org
REALITY_TARGET=dl.google.com:443
# CDN mode. OFF by default: a CDN link only works once the record is proxied and
# port-rewritten, so set ENABLE_CDN=true once that is in place. CDN_SUBDOMAIN is
# the record you proxy (Cloudflare); CDN_ADDRESS and CDN_SNI override the host
# clients dial, which is how CloudFront is configured.
ENABLE_CDN=true
CDN_SUBDOMAIN=cdn
CDN_ADDRESS=
CDN_SNI=
CDN_TRANSPORT=ws
# Enable/disable services
ENABLE_REALITY=true
ENABLE_TROJAN=true
ENABLE_HYSTERIA2=true
ENABLE_ANYTLS=false
ENABLE_SS=true
ENABLE_XHTTP=true
ENABLE_WIREGUARD=true
ENABLE_AMNEZIAWG=true
ENABLE_TELEMT=true
ENABLE_DNSTT=true
ENABLE_SLIPSTREAM=true
ENABLE_MASTERDNS=true
ENABLE_XDNS=true
ENABLE_TRUSTTUNNEL=true
ENABLE_GOOSERELAY=false
ENABLE_CONDUIT=true
ENABLE_ADMIN_UI=true
```
### Step 5: Run Bootstrap
Initialize MoaV (generates keys, obtains certificates, creates users):
```bash
moav bootstrap
# Or manually:
docker compose --profile setup run --rm bootstrap
```
This will:
1. Generate Reality and dnstt keypairs
2. Obtain TLS certificate from Let's Encrypt
3. Generate WireGuard server keys
4. Create initial users (default: 5)
5. Generate user bundles with configs and QR codes
> The domain prompt accepts your domain in any form (`example.com`, `https://example.com/`, `example.com:443` — all work). If you stop mid-way, re-running `moav bootstrap` picks up where you left off.
Bootstrap: domain prompt, key generation, certificate issuance, first users — 24s, 1.7 MB.
**DNS Tunnel Preparation** (optional):
If you want to use the DNS tunnel, free port 53 first:
```bash
# Stop systemd-resolved (uses port 53)
systemctl stop systemd-resolved
systemctl disable systemd-resolved
# Set up direct DNS
echo -e "nameserver 1.1.1.1\nnameserver 8.8.8.8" > /etc/resolv.conf
```
### Step 6: Start Services
Starting services and reading the status table — 26s, 1.3 MB.
```bash
# Start all services
moav start
# Or start specific profiles
moav start proxy admin # Main proxy + dashboard
moav start proxy admin wireguard # Add WireGuard
moav start all # Everything
```
See [CLI Reference → Profiles](https://moav.sh/docs/CLI/#profiles) for the full profile/service/`ENABLE_*` matrix. Common profiles: `proxy`, `xhttp`, `wireguard`, `amneziawg`, `dnstunnel`, `trusttunnel`, `telegram`, `admin`, `conduit`, `snowflake`, `gooserelay`, `monitoring`. From 1.8.2, `moav start` filters profiles whose `ENABLE_*` is `false` in `.env` — disabled services never start by accident.
**Open Firewall Ports:**
```bash
# Proxy services
ufw allow 443/tcp # Reality
ufw allow 443/udp # Hysteria2
ufw allow 8443/tcp # Trojan
ufw allow 8445/tcp # AnyTLS
ufw allow 8388 # Shadowsocks-2022
# TrustTunnel
ufw allow 4443/tcp # HTTP/2
ufw allow 4443/udp # HTTP/3 (QUIC)
# CDN (if using)
ufw allow 2082/tcp # CDN WebSocket
# WireGuard
ufw allow 51820/udp # Direct
ufw allow 8080/tcp # wstunnel
# AmneziaWG
ufw allow 51821/udp # Obfuscated WireGuard
# XHTTP
ufw allow 2096/tcp # VLESS+XHTTP+Reality
# DNS tunnel
ufw allow 53/udp
# Admin
ufw allow 9443/tcp
# Monitoring (Grafana)
ufw allow 9444/tcp
```
**Verify Services:**
```bash
moav status
moav doctor # Run all diagnostic checks
```
### Step 7: Download User Bundles
What a bundle contains: the connection guide, per-protocol configs and QR codes — 14s, 948 KB.
User bundles are ready in `outputs/bundles/`:
```bash
ls outputs/bundles/
# user01/ user02/ user03/ user04/ user05/
```
**Each bundle contains:**
- `README.html` - User instructions (English + Farsi)
- `reality.txt` - Reality share link + QR code
- `trojan.txt` - Trojan share link
- `anytls.txt` - AnyTLS share link (if `ENABLE_ANYTLS=false`)
- `shadowsocks.txt` / `shadowsocks-qr.png` - Shadowsocks-2022 `ss://` URI + QR
- `hysteria2.txt` - Hysteria2 share link
- `cdn-vless.txt` - CDN share link (if CDN_DOMAIN set)
- `wireguard.conf` - WireGuard config + QR code
- `wireguard-wstunnel.conf` - WireGuard over WebSocket
- `amneziawg.conf` - AmneziaWG config (if enabled)
- `trusttunnel.txt` - TrustTunnel credentials (if enabled)
- `xhttp.txt` - XHTTP share link (if enabled)
- `dnstt-instructions.txt` - DNS tunnel instructions
**Download Options:**
**1. Admin Dashboard (Easiest):**
1. Open `https://your-server:9443` in browser
2. Log in with **any username** and your `ADMIN_PASSWORD` — only the password is checked
3. Click **Download** next to any user in the "User Bundles" section
**Creating users from the dashboard:**
1. Click **+ Create User** in the User Bundles section
2. Enter a username (e.g. `alice`)
3. For multiple users, check **Batch** and enter a count — creates `alice_01`, `alice_02`, etc.
4. Click **Create** and wait for completion
**2. Create a Zip Package:**
```bash
moav user package user01
# Creates: outputs/bundles/user01.zip
```
**3. SCP Download:**
```bash
# From your local machine
scp root@YOUR_SERVER:/opt/moav/outputs/bundles/user01.zip ./
# Or the whole folder
scp -r root@YOUR_SERVER:/opt/moav/outputs/bundles/user01 ./user01-bundle/
```
### Step 8: Distribute to Users
Send the bundle (or just the README.html + relevant protocol files) to users.
**Secure Distribution:**
- **In-person** - Safest. Show QR code or AirDrop
- **Signal** - Send files with disappearing messages
- **Encrypted email** - PGP or ProtonMail-to-ProtonMail
**Avoid:**
- Unencrypted email
- Public file sharing links
- SMS/Telegram regular chats
Users open `README.html` in their browser for instructions and QR codes.
---
## Domainless Mode
Leave `DOMAIN=` empty in `.env` and MoaV starts only the transports that need no certificate.
Which protocols those are, the ports to forward, and the home-server/dynamic-IP caveats are all in
**[DNS → Without a domain](https://moav.sh/docs/DNS/#without-a-domain)**.
Adding a domain later is non-destructive: set `DOMAIN=` and run `moav bootstrap`. Existing users keep
working and pick up the new protocols on their next bundle.
## CDN-Fronted Mode (Cloudflare)
CDN mode fronts VLESS+WebSocket behind a CDN so the client appears to talk to Cloudflare or AWS.
The DNS records, the two mandatory Cloudflare settings (Origin Rule → port 2082 and SSL/TLS
Flexible), the AWS CloudFront alternative that needs no domain, and the `521`/`525` diagnosis table
are all in **[DNS → CDN mode](https://moav.sh/docs/DNS/#cdn-mode)** and the provider tabs beside it.
What lives here is the MoaV side — the `.env` variables:
| Variable | Purpose |
|---|---|
| `ENABLE_CDN` | Master switch for CDN links, **`false` by default**. Turn on after the record is proxied and rewritten to `PORT_CDN`; check with `moav doctor dns` |
| `CDN_SUBDOMAIN` | Cloudflare subdomain to front (default `cdn`); leave empty when using CloudFront |
| `CDN_DOMAIN` | Optional override for the hostname the CDN serves. Derived from `CDN_SUBDOMAIN` + `DOMAIN` when unset, so it is absent from `.env.example`; set it explicitly for CloudFront (`d123.cloudfront.net`) |
| `CDN_ADDRESS` | What clients actually connect to — set to `www.yourdomain.com` for stealth |
| `CDN_SNI` | SNI presented by the client |
| `CDN_TRANSPORT` | `ws` (the shipped default, and **required** for CloudFront) or `httpupgrade` |
| `CDN_WS_PATH` | Generated automatically with 48-bit entropy; treat it as a secret |
After changing any of these, run `moav bootstrap` to re-render, then `moav regenerate-users` so
existing bundles carry the new CDN link.
## Choosing a Reality Target (SNI)
Reality protocols (VLESS+Reality and XHTTP+Reality) impersonate a legitimate website during the TLS handshake. The **Reality target** (also called SNI) is the domain your proxy pretends to be. DPI sees a normal TLS connection to that domain, not a proxy.
### Requirements
The target domain **must** support:
- **TLS 1.3** — required for Reality's handshake
- **HTTP/2 (h2)** — required for ALPN negotiation
### How to Verify a Target
Test any domain **from your server** (not locally — some domains are geo-restricted):
```bash
curl -vsI --tlsv1.3 --http2 https://TARGET_DOMAIN 2>&1 | grep -iE "SSL|ALPN|TLSv1.3"
```
**Good output** (both TLS 1.3 and h2 — wording varies by curl version):
```
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
```
If you get no output or the connection closes immediately, the domain either doesn't support TLS 1.3/H2 or is unreachable from your server — don't use it.
### Choosing a Good Target
**For censored regions (Iran, China, Russia, etc.):**
Avoid well-known targets like `google.com` or `microsoft.com` — censors monitor these heavily and can detect Reality by comparing your handshake to the real site.
Instead, choose a domain that:
1. **Is popular domestically** — blocking it would cause collateral damage (banks, fintech, e-commerce)
2. **Has heavy TLS traffic** — your connection blends in with millions of real users
3. **Isn't commonly used as a proxy target** — novel targets are harder to fingerprint
**Examples for Iran:**
| Domain | Why |
|--------|-----|
| `blubank.com` | Major fintech app, high traffic, can't be easily blocked |
| `divar.ir` | Popular classifieds site |
| `snapp.ir` | Ride-hailing app (like Uber) |
**Generic (less optimal but widely compatible):**
| Domain | Notes |
|--------|-------|
| `dl.google.com` | Default, works everywhere but well-known |
| `www.doi.org` | Academic, low profile |
| `gateway.icloud.com` | Apple services |
> **Tip:** The best target is one that your ISP cannot afford to block. A domestic banking site is harder to block than a foreign tech company.
### Configuration
```bash
# In .env — for sing-box (Reality VLESS)
REALITY_TARGET=blubank.com:443
# For Xray-core (XHTTP) — can be different from sing-box
XHTTP_REALITY_TARGET=blubank.com:443
```
After changing targets, re-bootstrap and regenerate user bundles:
```bash
moav bootstrap
moav user regenerate
```
> **Note:** `REALITY_TARGET` and `XHTTP_REALITY_TARGET` are independent — you can use different targets for each protocol to diversify your fingerprint.
---
## Managing Users
```bash
moav user add alice # create a user (keys, configs, QR codes)
moav user add alice --package # ...and build the distributable zip
moav user add --batch 10 # bulk-create
moav user list # who exists
moav user base64 alice # that user's subscription string
moav user revoke alice # remove access (destructive)
moav regenerate-users # rebuild every bundle from state; keys unchanged
```
Bundles land in `outputs/bundles//`. Every flag and subcommand: **[CLI Reference](https://moav.sh/docs/CLI/)**.
## Service Management
```bash
moav status # per-container health and which profiles are up
moav start [service|profile] # 'moav start all' brings up everything
moav stop [service]
moav restart [service]
moav logs [service] # first stop when something misbehaves
moav doctor # diagnostics: DNS, ports, certs, resources
```
Profiles group the services (`proxy`, `wireguard`, `amneziawg`, `dnstunnel`, `trusttunnel`, `xhttp`,
`telegram`, `admin`, `conduit`, `snowflake`, `monitoring`). Full reference: **[CLI](https://moav.sh/docs/CLI/)**.
## Server Migration
Export your MoaV configuration and migrate to a new server.
**Export:**
```bash
moav export
# Creates: moav-backup-YYYYMMDD_HHMMSS.tar.gz
```
Includes: `.env`, keys, user credentials, bundles.
**Import on New Server:**
```bash
# 1. Install MoaV on new server (Steps 1-3)
# 2. Copy backup to new server
scp moav-backup-*.tar.gz root@NEW_SERVER:/opt/moav/
# 3. Import
cd /opt/moav
moav import moav-backup-*.tar.gz
# 4. Update to new IP
moav migrate-ip $(curl -s https://api.ipify.org)
# 5. Update DNS to point to new server
# 6. Start services
moav start
```
---
## IPv6 Support
MoaV supports dual-stack (IPv4 + IPv6). When enabled, user bundles include both IPv4 and IPv6 connection options.
**Enable:**
1. Enable IPv6 on your VPS (usually in provider control panel)
2. Verify: `curl -6 -s https://api6.ipify.org`
3. If already set up, regenerate bundles: `moav regenerate-users`
**Disable:**
```bash
# In .env
SERVER_IPV6=disabled
```
**Note:** IPv6 is optional. Most censored regions have low IPv6 adoption, so it's a "nice to have" but not critical for circumvention.
---
## Bandwidth Donation (Conduit & Snowflake)
Donate bandwidth to help others bypass censorship. Both services can run simultaneously.
**Psiphon Conduit** — Donate bandwidth to Psiphon's relay network (millions of users worldwide):
```bash
# Start
moav start conduit
# Configure bandwidth and max clients (interactive)
moav donate setup # Select option 2: Conduit
# View Ryve deep link and QR code (for claiming in Ryve app)
moav donate info
# Check stats (connected clients, bandwidth donated)
moav donate status
```
**Tor Snowflake** — Donate bandwidth as a Tor Snowflake proxy:
```bash
# Start
moav start snowflake
# Configure bandwidth and capacity (interactive)
moav donate setup # Select option 3: Snowflake
# Check stats (people served, bandwidth relayed)
moav donate status
```
**Configuration in `.env`:**
```bash
CONDUIT_BANDWIDTH=100 # Mbps limit (default: 100)
CONDUIT_MAX_COMMON_CLIENTS=200 # Max concurrent clients (default: 200)
SNOWFLAKE_BANDWIDTH=5 # Mbps limit (default: 5)
SNOWFLAKE_CAPACITY=50 # Max concurrent clients (default: 50)
```
Changes via `moav donate setup` are written to `.env` and the service is restarted automatically.
---
## MahsaNet Config Donation
> Donating configs is a way to support the network rather than a setup step; this will move to a dedicated Support page. It stays here for now because that page does not exist yet.
Donate your server's VPN configs to [MahsaServer.com](https://www.mahsaserver.com/), a decentralized config sharing platform for the Mahsa VPN app (2M+ users in Iran). Mahsa VPN users connect directly to your donated configs.
### Prerequisites
1. **Register** at [mahsaserver.com](https://www.mahsaserver.com/) and verify your email
2. **Become a verified donor** — fill out the verified donor form on the website
3. **Generate an API key** at [mahsaserver.com/user/api](https://www.mahsaserver.com/user/api)
### Setup
```bash
# Set up your API key (interactive — validates the key)
moav donate setup # Select option 1: MahsaNet
```
Or manually add to `.env`:
```bash
MAHSANET_API_KEY=your_api_key_here
```
### Configuration
Configure in `.env`:
```bash
# Protocols to donate (space-separated)
# Supported: reality, hysteria2, trojan, cdn, xhttp, telegram
MAHSANET_PROTOCOLS="reality hysteria2"
# Pool determines where configs appear in the Mahsa VPN app
# Options: mahsa (default), warp, popup, telegram
# Note: telegram protocol configs are always sent to the "telegram" pool regardless of this setting
MAHSANET_POOL=mahsa
```
**Protocol notes:**
- `reality` — VLESS+Reality, works without a domain, recommended
- `hysteria2` — QUIC-based, fast, requires domain + UDP
- `trojan` — TLS-based, requires domain
- `cdn` — VLESS+WS via Cloudflare, requires CDN setup
- `xhttp` — VLESS+XHTTP+Reality via Xray-core, requires xhttp profile
- `telegram` — Telegram MTProxy link, automatically goes to the "telegram" pool
### Donating Configs
```bash
# Generate new users and donate their configs
moav donate
# You'll be prompted for:
# - Number of users to create (default: 1)
# - Username prefix (default: mahsa)
```
This creates dedicated users (e.g., `mahsa01`, `mahsa02`) and submits their config share links to MahsaNet.
### Managing Donations
```bash
# Show all donation services status (MahsaNet + Conduit + Snowflake stats)
moav donate status
# List your donated MahsaNet configs
moav donate list
# Select and delete specific configs
moav donate delete
# Remove all donated configs from MahsaNet
moav donate remove
```
### Admin Dashboard
When `MAHSANET_API_KEY` is set, the Admin Dashboard shows a **MahsaNet** section where you can:
- View donation stats (total, active, inactive configs)
- Donate new configs (with count, prefix, and protocol selection)
- See all donated configs with health status and usage count
- Collapse the section to a one-liner summary
### How It Works
1. `moav donate` creates new MoaV users with the standard user provisioning pipeline
2. For each user, it reads the share link files (e.g., `reality.txt`, `hysteria2.txt`)
3. Each link is validated (correct prefix, structure, length)
4. Links are submitted to the MahsaNet API as config donations
5. Mahsa VPN users worldwide can then connect through your server
---
## Monitoring (Grafana + Prometheus)
Optional Grafana + Prometheus stack. Enable with `ENABLE_MONITORING=true` (the installer defaults
it on above ~1 GB RAM) and start it with `moav start monitoring`.
Dashboards, the exporters, reaching Grafana, and the CDN-accelerated option are covered in
**[Monitoring](https://moav.sh/docs/MONITORING/)**.
## Updating MoaV
```bash
moav update
```
Or manually:
```bash
cd /opt/moav
git pull
docker compose --profile all build
moav restart
```
### Breaking Changes
Some updates include breaking changes that require regenerating configs. Check the [CHANGELOG](https://github.com/MotherofallVPNs/moav/blob/main/CHANGELOG.md) for breaking change notices.
**If an update has breaking changes:**
```bash
# Option 1: Rebuild configs (keeps users, regenerates server config)
moav config rebuild
moav restart
# Option 2: Fresh start (new keys, new users)
moav uninstall --wipe
cp .env.example .env
nano .env # Configure domain, email, password
./moav.sh bootstrap
```
After breaking changes, you must redistribute new config bundles to all users.
### Testing a Development Branch
```bash
moav update -b dev # Switch to dev branch
# Return to stable:
moav update -b main
```
---
## Uninstalling MoaV
### Keep Data (Reinstall Later)
Remove containers but preserve configuration for later:
```bash
moav uninstall
```
This removes:
- All Docker containers
- Global `moav` command
Preserves: `.env`, keys, certificates, user bundles, Docker volumes
To reinstall:
```bash
./moav.sh install
moav start
```
### Complete Removal (Fresh Start)
Remove everything for a completely fresh installation:
```bash
moav uninstall --wipe
```
This removes:
- All Docker containers and volumes
- `.env` and all generated configs
- All keys and certificates
- All user bundles
To start fresh:
```bash
cp .env.example .env
nano .env # Configure domain, email, password
./moav.sh
```
---
## Re-bootstrapping
If you need to regenerate keys without a full wipe:
```bash
# Remove bootstrap flag only
docker run --rm -v moav_moav_state:/state alpine rm /state/.bootstrapped
# Re-run bootstrap
moav bootstrap
```
---
## CLI Reference
See [CLI.md](https://moav.sh/docs/CLI/) for complete command reference.
## Troubleshooting
See [TROUBLESHOOTING.md](https://moav.sh/docs/TROUBLESHOOTING/) for common issues and solutions.
## Security
See [OPSEC.md](https://moav.sh/docs/OPSEC/) for security best practices.
---
# DNS Configuration
What DNS records MoaV needs, and how to add them. Most setups need **one to six records**; the rest of this page is provider quirks and edge cases, folded away until you need them.
## Do I need a domain?
**Not strictly — but get one.** MoaV runs fine on a bare IP, and if you can't register a domain, skip to [without a domain](#without-a-domain). But a domain **unlocks several additional transports**, and it is the only way to run the **DNS tunnels** — which can remain usable where ordinary traffic is blocked but recursive DNS still resolves. A domain costs a few dollars a year; see [getting a domain](#getting-a-domain).
| Protocol | Bare IP | With a domain |
|---|:-:|:-:|
| Reality (VLESS) | ✅ | ✅ |
| XHTTP (VLESS+XHTTP+Reality) | ✅ | ✅ |
| WireGuard *(direct + wstunnel)* | ✅ | ✅ |
| AmneziaWG | ✅ | ✅ |
| Telegram MTProxy | ✅ | ✅ |
| Shadowsocks-2022 | ✅ | ✅ |
| CDN-fronted VLESS *(via AWS CloudFront)* | ✅ | ✅ |
| Admin dashboard · Conduit · Snowflake | ✅ | ✅ |
| **Trojan** | — | ✅ |
| **AnyTLS** | — | ✅ |
| **Hysteria2** | — | ✅ |
| **TrustTunnel** | — | ✅ |
| **CDN-fronted VLESS** *(via Cloudflare)* | — | ✅ |
| **dnstt · Slipstream · MasterDNS · XDNS** *(DNS tunnels)* | — | ✅ |
The domain-only ones need either a Let's Encrypt certificate or an NS delegation, and both require a real domain. Nothing is lost by adding a domain later: set `DOMAIN=` and re-run `moav bootstrap`.
## With a domain: the records
Add only the rows for the features you enable. This table is the whole story — everything below it is provider-specific detail.
| Record | Name | Value | Cloudflare proxy | Needed for |
|---|---|---|:-:|---|
| A | `@` | server IP | DNS only | **Always** — Trojan, Hysteria2, Reality, TLS |
| A | `dns` | server IP | DNS only | Any DNS tunnel (the nameserver for the delegations below) |
| NS | `t`, `s`, `m`, `x` | `dns.yourdomain.com` | — | One per [DNS tunnel](https://moav.sh/docs/protocols/#dns-tunnels) you expose |
| A | `cdn` | server IP | **Proxied** | [CDN mode](#cdn-mode) |
| A | `www` | server IP | **Proxied** | [CDN mode](#cdn-mode) stealth connect address (`CDN_ADDRESS=www.…`) |
| A | `grafana` | server IP | **Proxied** | Faster Grafana over the CDN *(optional)* |
**Minimum** (no DNS tunnels): just the `@` A record. That already enables Reality, Trojan, Hysteria2 and TrustTunnel.
Each `NS` row is a separate delegation handing that subdomain to your own server:
```
t.yourdomain.com NS dns.yourdomain.com # dnstt
s.yourdomain.com NS dns.yourdomain.com # Slipstream
m.yourdomain.com NS dns.yourdomain.com # MasterDNS
x.yourdomain.com NS dns.yourdomain.com # XDNS
```
Add only the tunnels you want — see [DNS Tunnels](https://moav.sh/docs/protocols/#dns-tunnels) for what each one is and which to pick. Some registrars require a **trailing dot** on NS values (`dns.yourdomain.com.`); Cloudflare and most modern panels do not.
!!! tip "MoaV writes the zone file for you"
`moav doctor dns` generates **`outputs/dns-records.txt`** — a BIND-style zone file containing exactly the records your configuration needs, with the enabled/disabled state of each tunnel noted in comments. In Cloudflare you can feed it straight to **DNS → Records → Import and Upload** instead of adding records by hand.
## CDN mode
CDN mode fronts VLESS+WebSocket behind a CDN, so the client appears to talk to Cloudflare/AWS rather than to your server. Two things are true of **any** CDN you put in front of MoaV:
- **The origin port is `2082`.** MoaV's CDN listener does not bind 80 or 443, so the CDN must be told to reach your server on 2082.
- **The CDN terminates TLS.** That inbound is plain HTTP by design — the encryption users get is VLESS's own, inside the WebSocket.
How you express those two facts differs per provider, which is the next section. Verifying it works is the same everywhere: see [CDN returns 521 / 525](#troubleshooting).
!!! note "CDN links are off until you turn them on"
`ENABLE_CDN=false` is the default, because a CDN link generated before the CDN is actually fronting traffic looks valid to the user and cannot connect. Set `ENABLE_CDN=true` in `.env` once the steps below are done, then confirm with `moav doctor dns` — it checks the record is *proxied*, not merely resolving.
## Provider setup
The records are the same everywhere; only the UI differs. Cloudflare additionally needs two settings for CDN mode.
=== "Cloudflare"
DNS → Records. Set every record to **DNS only** (gray cloud) **except** `cdn` / `www` / `grafana`, which must be **Proxied** (orange cloud).
**CDN mode needs two extra settings** (both required):
1. **Origin Rule** (Rules → Origin Rules): when hostname = `cdn.yourdomain.com`, rewrite **Destination Port → 2082**. MoaV's CDN listener doesn't bind 80/443.
2. **SSL/TLS → Overview → Flexible**: MoaV's CDN inbound is plain HTTP; Cloudflare terminates TLS for the client. If other subdomains need Full (Strict), scope a **Configuration Rule** to `cdn.` only.
Verify both settings: `curl -so /dev/null -w "%{http_code}" https://cdn.yourdomain.com/x` — `400`/`404` means sing-box is answering and CDN mode works. Anything else is diagnosable: see [CDN returns 521 / 525](#troubleshooting).
=== "AWS CloudFront"
An alternative CDN that **needs no domain** — you get a `*.cloudfront.net` name automatically. CloudFront rejects bare-IP origins, so use free wildcard DNS: `YOUR_IP.sslip.io` (pure DNS, no traffic passes through it).
??? example "Create the distribution (console or CLI)"
**Origin:** `YOUR_IP.sslip.io`, HTTP only, port **2082**.
**Behavior:** viewer protocol **HTTPS only**; methods **GET,HEAD,OPTIONS,PUT,POST,PATCH,DELETE**; **CachePolicy = CachingDisabled**, **OriginRequestPolicy = AllViewer** (these two forward the WebSocket upgrade headers — omitting them causes `bad "Sec-WebSocket-Key" header`). `PriceClass_200`+ includes Middle East / Asia edges.
CLI create (replace the IP); the same two policy IDs also fix an existing distribution that's missing them:
```bash
aws cloudfront create-distribution --distribution-config '{
"CallerReference":"moav-'$(date +%s)'","Comment":"MoaV CDN","Enabled":true,
"Origins":{"Quantity":1,"Items":[{"Id":"moav","DomainName":"YOUR_IP.sslip.io",
"CustomOriginConfig":{"HTTPPort":2082,"HTTPSPort":443,"OriginProtocolPolicy":"http-only"}}]},
"DefaultCacheBehavior":{"TargetOriginId":"moav","ViewerProtocolPolicy":"https-only",
"AllowedMethods":{"Quantity":7,"Items":["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],
"CachedMethods":{"Quantity":2,"Items":["GET","HEAD"]}},
"CachePolicyId":"4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
"OriginRequestPolicyId":"216adef6-5c7f-47e4-b989-5492eafa07d3","Compress":false},
"PriceClass":"PriceClass_200"}'
```
Then in `.env` (`CDN_TRANSPORT=ws` is the default and is what CloudFront needs; `httpupgrade` will not work):
```bash
ENABLE_CDN=true
CDN_SUBDOMAIN=
CDN_DOMAIN=d123.cloudfront.net
CDN_ADDRESS=d123.cloudfront.net
CDN_SNI=d123.cloudfront.net
CDN_TRANSPORT=ws
```
`moav bootstrap`, then verify: `curl -so /dev/null -w "%{http_code}" https://d123.cloudfront.net/x` → `400`. AWS blocked domain fronting in 2018, so the SNI must be your distribution/CNAME. You can run Cloudflare **and** CloudFront together for redundancy.
## Verify
```bash
moav doctor dns # MoaV's own check
dig +short yourdomain.com # → your server IP
dig NS t.yourdomain.com # → dns.yourdomain.com in AUTHORITY
```
Propagation is usually 5–30 min (rarely up to 48 h). Cross-check worldwide at [dnschecker.org](https://dnschecker.org).
## Without a domain
Leave `DOMAIN=` empty in `.env`. MoaV detects this and starts only the transports that work on a bare IP (Reality uses `REALITY_TARGET`, e.g. `dl.google.com`, for TLS camouflage instead of your domain). No DNS records, no certificates, no port 80.
You can add a domain at any time — set `DOMAIN=` and run `moav bootstrap`. Existing users keep working and pick up the new protocols on their next bundle.
### Ports to forward
Only needed if the server sits behind a router (home, office, NAT).
| Port | Service | Needed when |
|---|---|:-:|
| `443/tcp` | Reality (VLESS) | always |
| `2096/tcp` | XHTTP | always |
| `51820/udp` | WireGuard | always |
| `8080/tcp` | wstunnel *(WireGuard over WebSocket, for UDP-blocked networks)* | always |
| `51821/udp` | AmneziaWG | always |
| `993/tcp` | Telegram MTProxy | always |
| `9443/tcp` | Admin dashboard | always |
| `80/tcp` | Let's Encrypt (ACME) | **domain only**, during issuance/renewal |
| `443/udp` | Hysteria2 | **domain only** |
| `8443/tcp` | Trojan | **domain only** |
| `4443/tcp` + `4443/udp` | TrustTunnel (HTTP/2 + QUIC) | **domain only** |
| `53/udp` | [DNS tunnels](https://moav.sh/docs/protocols/#dns-tunnels) *(all four via `dns-router`)* | **domain only** |
| `2082/tcp` | CDN origin | only if the CDN reaches your origin directly |
### Home server / Raspberry Pi
MoaV runs on a Pi 4+ (2 GB+ RAM) or any ARM64/x64 Linux box. Three things differ from a VPS:
**Check for CGNAT first.** `curl ifconfig.me` must match your router's WAN IP. If it doesn't, your ISP is sharing that address and **no amount of port forwarding will work** — you need a VPS, an IPv6-only setup, or a tunnel from a host that does have a public IP.
**Forward the ports above** to the server's LAN address, and give it a static DHCP lease so that address stops moving.
**Dynamic IP?** If you're using a domain, a residential IP that changes will silently break every record pointing at it. Run a DDNS updater on a 5-minute cron:
- **[DuckDNS](https://www.duckdns.org)** — free subdomain, works with no domain of your own
- **Your own domain** — a small script against your DNS provider's API (Cloudflare tokens are the usual choice)
After the IP moves, re-run `moav cert renew` if a certificate was issued against the old address, and remember that DNS-tunnel NS delegations point at `dns.yourdomain.com`, so that record needs the DDNS update too.
## Troubleshooting
**Not propagated** — wait, and test other resolvers: `dig @8.8.8.8 yourdomain.com`, `dig @1.1.1.1 …`.
**NS record not working** — confirm the `dns` A record exists, add a trailing dot if your registrar needs one, and give delegations longer to propagate.
**Certificate acquisition failed** — check the `@` A record, ensure port 80 is open and free during ACME, and remember domainless mode issues no certs.
**Can't connect from outside a home network** — verify port forwarding, rule out CGNAT (`curl ifconfig.me` vs router WAN IP), and test from mobile data rather than the same Wi-Fi.
**CDN returns 521 / 525** — probe it with
`curl -so /dev/null -w "%{http_code}" https://cdn.yourdomain.com/x`:
| Code | Meaning | Fix |
|---|---|---|
| `400` / `404` | sing-box is answering — CDN mode works | — |
| `521` | the CDN can't reach your origin on port 2082 | Cloudflare: the **Origin Rule** is missing or wrong. CloudFront: the origin's **HTTP port** isn't 2082 |
| `525` | TLS handshake to the origin failed | Cloudflare **SSL/TLS must be Flexible** — the CDN inbound is plain HTTP by design |
| `1016` / `NXDOMAIN` | the `cdn` A record is missing, or not **Proxied** | add it, orange cloud on |
**CDN connects but the client won't** — on CloudFront check `CDN_TRANSPORT=ws`; the default `httpupgrade` is sing-box-specific and fails there with `bad "Sec-WebSocket-Key" header`.
## Getting a domain
Any registrar with WHOIS privacy works; [Namecheap, Porkbun, Njalla](https://njal.la) accept crypto. Keep the name generic — it's the first thing DPI sees in the TLS SNI, so avoid `vpn`/`proxy`/`tunnel` and random strings, and favour boring infrastructure-style names. The full naming-and-SNI strategy lives in the [OPSEC Guide](https://moav.sh/docs/OPSEC/) so it stays in one place.
---
# VPS Deployment
Deploy MoaV on any VPS in minutes. The flow is the same everywhere: create an Ubuntu server, SSH in, and run the one-line installer.
## How It Works
1. **Create a VPS** with the recommended specs (1 vCPU, 2 GB RAM recommended) and an Ubuntu 22.04 image
2. **SSH into your server** once it boots (usually 1-2 minutes): `ssh root@YOUR_IP`
3. **Run the installer:**
```bash
curl -fsSL moav.sh/install.sh | bash
```
It installs Docker + prerequisites, clones MoaV to `/opt/moav`, installs the global `moav` command, and launches the interactive setup wizard.
The installer is safe to re-run and prompts for everything it needs. On low-RAM hosts it offers to add a swapfile so image builds don't get OOM-killed.
---
## Hetzner
Hetzner offers excellent value with servers starting at €3.79/month in European data centers.
### Steps
1. Go to [Hetzner Cloud Console](https://console.hetzner.cloud/)
2. Click **"Add Server"**
3. Choose:
- **Location**: Choose closest to your users
- **Image**: Ubuntu 22.04
- **Type**: CX22 (2 vCPU, 4 GB RAM) recommended, or CX11 (1 vCPU, 2 GB) minimum
- **Networking**: Enable IPv4 and IPv6
4. Add your SSH key
5. Click **"Create & Buy now"**
6. Wait 1-2 minutes, then SSH in: `ssh root@YOUR_IP`
7. Run `curl -fsSL moav.sh/install.sh | bash` and follow the setup wizard
### Recommended Specs
- **Minimum**: CX11 (1 vCPU, 2 GB RAM) - €3.79/month
- **Recommended**: CX22 (2 vCPU, 4 GB RAM) - €5.39/month
---
## Linode
Linode (now Akamai) offers reliable servers with good global coverage.
### Steps
1. Go to [Linode Cloud Manager](https://cloud.linode.com/)
2. Click **"Create Linode"**
3. Choose:
- **Image**: Ubuntu 22.04 LTS
- **Region**: Choose closest to your users
- **Linode Plan**: Shared CPU - Nanode 1 GB ($5/mo) or Linode 2 GB ($12/mo)
4. Set your root password and add your SSH key
5. Click **"Create Linode"**
6. Wait 1-2 minutes, then SSH in: `ssh root@YOUR_IP`
7. Run `curl -fsSL moav.sh/install.sh | bash` and follow the setup wizard
### Recommended Specs
- **Minimum**: Nanode 1 GB (1 vCPU, 1 GB RAM) - $5/month
- **Recommended**: Linode 2 GB (1 vCPU, 2 GB RAM) - $12/month
---
## Vultr
Vultr offers competitive pricing with many global locations.
### Steps
1. Go to [Vultr Dashboard](https://my.vultr.com/)
2. Click **"Deploy +"** → **"Deploy New Server"**
3. Choose:
- **Choose Server**: Cloud Compute - Shared CPU
- **Server Location**: Choose closest to your users
- **Server Image**: Ubuntu 22.04 LTS x64
- **Server Size**: 25 GB SSD ($5/mo) minimum
4. Add your SSH key
5. Click **"Deploy Now"**
6. Wait 1-2 minutes, then SSH in: `ssh root@YOUR_IP`
7. Run `curl -fsSL moav.sh/install.sh | bash` and follow the setup wizard
### Recommended Specs
- **Minimum**: 25 GB SSD (1 vCPU, 1 GB RAM) - $5/month
- **Recommended**: 55 GB SSD (1 vCPU, 2 GB RAM) - $10/month
---
## DigitalOcean
DigitalOcean is popular and beginner-friendly with excellent documentation.
### Steps
1. Go to [DigitalOcean Dashboard](https://cloud.digitalocean.com/)
2. Click **"Create"** → **"Droplets"**
3. Choose:
- **Region**: Choose closest to your users
- **Image**: Ubuntu 22.04 (LTS) x64
- **Size**: Basic → Regular → $6/mo (1 GB RAM) or $12/mo (2 GB RAM)
- **Authentication**: SSH Key (recommended)
4. Click **"Create Droplet"**
5. Wait 1-2 minutes, then SSH in: `ssh root@YOUR_IP`
6. Run `curl -fsSL moav.sh/install.sh | bash` and follow the setup wizard
### Recommended Specs
- **Minimum**: Basic (1 vCPU, 1 GB RAM) - $6/month
- **Recommended**: Basic (1 vCPU, 2 GB RAM) - $12/month
---
## After Installation
The setup wizard will guide you through:
1. Entering your domain name
2. Providing email for TLS certificates
3. Setting the admin dashboard password
4. Selecting which protocols to enable
5. Creating initial users
Once it finishes, `moav status` shows the running services and `moav help` lists everything else.
### Prerequisites Before Setup
Before running the setup, make sure:
1. **Domain configured**: Your domain's DNS A record points to your server's IP
2. **Ports open**: Most VPS providers have all ports open by default, but verify:
- 443/tcp (Reality)
- 443/udp (Hysteria2)
- 8443/tcp (Trojan)
- 4443/tcp+udp (TrustTunnel)
- 2082/tcp (CDN WebSocket, if using Cloudflare)
- 51820/udp (WireGuard)
- 80/tcp (Let's Encrypt verification)
---
## Troubleshooting
### Installer failed partway
The installer is idempotent — just re-run it:
```bash
curl -fsSL moav.sh/install.sh | bash
```
### Docker not running
```bash
sudo systemctl start docker
sudo systemctl enable docker
```
### DNS not propagated yet
Wait a few minutes and verify:
```bash
dig +short yourdomain.com
```
Should return your server's IP address.
---
See [SETUP.md](https://moav.sh/docs/SETUP/) for detailed manual installation and configuration instructions.
---
# Run MoaV with an AI agent
MoaV publishes its documentation in a form coding agents read directly, so you can hand the whole thing to Claude, ChatGPT, Cursor or Copilot and have it walk the setup with you. Most operators find this the fastest route from a bare VPS to a working server.
| File | What it is | When to use it |
|---|---|---|
| [moav.sh/llms.txt](https://moav.sh/llms.txt) | Compact index — install command, `.env` location, dashboard and Grafana URLs, and links to every page | Start here. Small enough for any context window. |
| [moav.sh/llms-full.txt](https://moav.sh/llms-full.txt) | The entire documentation in one file | When the agent can take the whole corpus, or you want it to work offline from one paste. |
## Getting started
Point your agent at the index and tell it what you want:
```text
Read https://moav.sh/llms.txt and follow it to set up a MoaV server for me.
Server: , Debian 12, I have root SSH access.
Domain: — DNS is at Cloudflare.
Walk me through it, and ask before anything destructive or anything that
needs a secret from me.
```
That is enough to get going. The agent will find the one-command install, the bootstrap flow, and `moav user add` from the index. If it can run commands on the server itself, it can do most of the work; if not, it will give you commands to paste.
Good things to ask for once the server is up:
- *"Add five users and package their bundles"* — `moav user add`, `--batch`, `--package`
- *"Why can't my friend connect on Reality?"* — `moav doctor`, `moav logs`, `moav test`
- *"Which transports are enabled, and which need a domain?"* — reads your `.env` against the protocol table
- *"Set up monitoring and show me the dashboards"* — the `monitoring` profile and Grafana
## Two things worth setting up first
Neither is required, but both remove a lot of back-and-forth.
**SSH key access.** If your agent can SSH into the server itself, it can run the install, read `moav doctor` output and fix what it finds, instead of handing you commands one at a time. Add your key with `ssh-copy-id`, confirm `ssh ` works without a password, and give the agent the hostname. Key-based auth is the right way to do this — see [OPSEC](https://moav.sh/docs/OPSEC/) before enabling anything broader.
**A Cloudflare API token**, if your DNS is there. The records MoaV needs — including the NS delegations for the DNS tunnels — are fiddly to enter by hand, and an agent with a scoped token can create them for you. Make it **DNS-edit only, for the one zone**, not a global key. `moav doctor dns` also writes the records out as an importable zone file if you would rather do it yourself: see [DNS Configuration](https://moav.sh/docs/DNS/#with-a-domain-the-records).
## Keep it safe
An agent operating a live server deserves the same care as a new admin.
- **Never paste secrets into a prompt or a public issue.** Not `.env`, not private keys, not a user's share link — those contain working credentials. Give the agent a path to read on the server instead.
- **Confirm before destructive commands.** `moav uninstall`, `moav user revoke`, re-bootstrapping and `docker system prune -a` all remove things that are awkward or impossible to get back.
- **Keep the admin dashboard closed.** Don't let an agent expose it, disable its auth, or widen `ADMIN_IP_WHITELIST` for convenience.
- **Don't publish server details.** Keep IPs, domains and share URIs out of anything public, including issues and pastebins used to share logs with an agent.
- **Review what it changed.** `git diff` in `/opt/moav`, and `moav doctor` afterwards.
## Contributing with an agent
The files above document *operating* a server. If you are changing MoaV itself, the repository has its own guide for agents — [AGENTS.md](https://github.com/MotherofallVPNs/MoaV/blob/main/AGENTS.md) covers the layout, the conventions that bite, and how to run the tests. [Development & Testing](https://moav.sh/docs/development/) is the human version.
---
# Video tutorials
Community-made walkthroughs of installing and running MoaV.
!!! info "No English tutorial yet — one in Farsi"
Switch the language in the top bar (**فارسی**) for a full server walkthrough by [@iaghapour](https://x.com/iaghapour).
**Made one, or thinking about it?** The fastest way in is the **✏️ edit button** in the corner of this page — it opens this file straight in GitHub, so you can add your video and send a PR without cloning anything. Or just [open an issue](https://github.com/MotherofallVPNs/moav-site/issues/new) with the link and we will add it.
A recording of a real install is worth more than any amount of prose, and it finds bugs the maintainers never hit — several fixes in [2.1.0](https://github.com/MotherofallVPNs/MoaV/releases/tag/v2.1.0) came from watching the Farsi one. Credit stays with the author; we link rather than re-host. (The docs live in [moav-site](https://github.com/MotherofallVPNs/moav-site), separate from the main MoaV repo.)
Prefer reading? [Quick Start](https://moav.sh/docs/quick-start/) is the same path in about ten minutes.
## What a good tutorial covers
Roughly the order a new operator needs. Nobody has to cover all of it — one clear piece is useful on its own.
**Getting a server running**
1. A VPS and a domain pointed at it — [VPS Deployment](https://moav.sh/docs/DEPLOY/), [DNS Configuration](https://moav.sh/docs/DNS/)
2. The one-command install, and what `moav bootstrap` asks for
3. **Running it at home** instead: a Raspberry Pi or spare box, checking for CGNAT first, forwarding the ports — [Home server / Raspberry Pi](https://moav.sh/docs/DNS/#home-server-raspberry-pi)
**Getting people connected**
4. Creating a user and sending them their bundle — `moav user add NAME --package`
5. Connecting from a phone: importing a config, and which app suits which protocol — [Client Apps](https://moav.sh/docs/CLIENTS/)
6. Choosing between protocols, and what to do when one is blocked — [Supported Protocols](https://moav.sh/docs/protocols/#which-one-should-i-use), then `moav doctor`
**Going further**
7. **The MoaV client** — running [moav-client](https://moav.sh/docs/client/) on a desktop so several protocols are probed and the fastest live one is used automatically, rather than switching by hand
8. **Routing rules and plugins**, which is where it stops being only a VPN. Send local and national sites *direct* so banking and government portals still work and your exit bandwidth is not wasted; block ad and tracker domains for every device on the tunnel; block torrent traffic on a donated server. The client's rule engine is hot-swappable, so this is a satisfying thing to demo live.
9. **The admin dashboard and Grafana** — adding users from a browser, and watching per-protocol throughput and per-user traffic: [admin commands](https://moav.sh/docs/CLI/#admin), [Monitoring](https://moav.sh/docs/MONITORING/)
---
# Client Setup Guide
This guide explains how to connect to MoaV from various devices.
Everything below arrives in the bundle you were sent. Opening its **`README.html`** is usually enough — it detects nothing, assumes nothing, and walks through one platform at a time in English and Farsi:
A bundle from the inside: connection guide, per-protocol configs, QR codes — 14s, 948 KB.
## Quick reference
### Protocol Support by Port
| Protocol | Port | Description |
|----------|------|-------------|
| [Reality (VLESS)](https://github.com/XTLS/REALITY) | 443/tcp | TLS camouflage, hard to detect with common techniques |
| [Trojan](https://trojan-gfw.github.io/trojan/) | 8443/tcp | HTTPS mimicry, battle-tested |
| [AnyTLS](https://github.com/anytls/anytls-go) | 8445/tcp | Resists TLS-in-TLS fingerprinting, high stealth against the filtering techniques it targets |
| [Shadowsocks-2022](https://github.com/shadowsocks/shadowsocks-org/blob/main/docs/doc/sip022.md) | 8388/tcp+udp | AEAD-2022 anti-active-probing; same protocol Outline VPN uses |
| [Hysteria2](https://v2.hysteria.network/) | 443/udp | QUIC-based, fast on lossy networks |
| CDN (VLESS+WS) | 443 via Cloudflare | When server IP is blocked |
| [TrustTunnel](https://trusttunnel.org/) | 4443/tcp+udp | HTTP/2 & QUIC, looks like HTTPS |
| [WireGuard](https://www.wireguard.com/) (Direct) | 51820/udp | Full VPN mode, simple setup |
| [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) | 51821/udp | Obfuscated WireGuard, designed to resist common DPI signatures |
| [WireGuard](https://www.wireguard.com/) + [wstunnel](https://github.com/erebe/wstunnel) | 8080/tcp | VPN wrapped in WebSocket |
| [DNS Tunnel (dnstt)](https://www.bamsoftware.com/software/dnstt/) | 53/udp | Last resort, slow but hard to block |
| [Slipstream](https://github.com/Mygod/slipstream-rust) | 53/udp | QUIC-over-DNS, 1.5-5x faster than dnstt |
| [Telegram MTProxy](https://github.com/telemt/telemt) | 993/tcp | Fake-TLS V2, direct Telegram access |
| [XHTTP (VLESS+XHTTP+Reality)](https://github.com/XTLS/Xray-core) | 2096/tcp | XHTTP transport with Reality TLS, experimental |
| [Psiphon](https://psiphon.ca/) | Various | Standalone app, uses Psiphon network |
| [Tor](https://www.torproject.org/) (Snowflake) | Various | Uses Tor network |
### Client Apps
#### iOS
| App | Protocols | Link |
|-----|-----------|------|
| [Shadowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118) | VLESS, VMess, Trojan, Hysteria2, AnyTLS (2.2.65+), WireGuard | [App Store ($2.99)](https://apps.apple.com/us/app/shadowrocket/id932747118) |
| [Streisand](https://apps.apple.com/us/app/streisand/id6450534064) | VLESS/Reality, VMess, Trojan, Hysteria2, WireGuard | [App Store (Free)](https://apps.apple.com/us/app/streisand/id6450534064) |
| [Hiddify](https://apps.apple.com/us/app/hiddify-proxy-vpn/id6596777532) | VLESS, VMess, Hysteria2, Trojan, AnyTLS, Reality, SSH | [App Store (Free)](https://apps.apple.com/us/app/hiddify-proxy-vpn/id6596777532) |
| [V2Box](https://apps.apple.com/ca/app/v2box-v2ray-client/id6446814690) | VLESS, VMess, Trojan, Hysteria2, Reality | [App Store](https://apps.apple.com/ca/app/v2box-v2ray-client/id6446814690) |
| [sing-box](https://apps.apple.com/us/app/sing-box-vt/id6673731168) | VLESS, VMess, Trojan, Hysteria2, AnyTLS, WireGuard | [App Store (Free)](https://apps.apple.com/us/app/sing-box-vt/id6673731168) |
| [Loon](https://apps.apple.com/us/app/loon/id1373567447) | VLESS/Reality, Hysteria2, Trojan, WireGuard | [App Store](https://apps.apple.com/us/app/loon/id1373567447) |
| [Pharos Pro](https://apps.apple.com/us/app/pharos-pro/id1456610173) | VLESS, Hysteria2, Trojan, TUIC | [App Store ($2.99)](https://apps.apple.com/us/app/pharos-pro/id1456610173) |
| [Onion Browser](https://apps.apple.com/us/app/onion-browser/id519296448) | Tor | [App Store (Free)](https://apps.apple.com/us/app/onion-browser/id519296448) |
| [Psiphon](https://apps.apple.com/us/app/psiphon-vpn-freedom-online/id1276263909) | Psiphon | [App Store (Free)](https://apps.apple.com/us/app/psiphon-vpn-freedom-online/id1276263909) |
| [WireGuard](https://apps.apple.com/us/app/wireguard/id1441195209) | WireGuard | [App Store (Free)](https://apps.apple.com/us/app/wireguard/id1441195209) |
| [AmneziaWG](https://apps.apple.com/app/amneziawg/id6478942365) | AmneziaWG | [App Store (Free)](https://apps.apple.com/app/amneziawg/id6478942365) |
| [TrustTunnel](https://apps.apple.com/app/trusttunnel/id6478890498) | TrustTunnel | [App Store (Free)](https://apps.apple.com/app/trusttunnel/id6478890498) |
#### Android
| App | Protocols | Link |
|-----|-----------|------|
| [v2rayNG](https://github.com/2dust/v2rayNG) | VLESS, VMess, Trojan, Shadowsocks | [GitHub](https://github.com/2dust/v2rayNG/releases) |
| [Hiddify](https://hiddify.com/) | VLESS, VMess, Hysteria2, Trojan, AnyTLS, Reality, SSH | [GitHub](https://github.com/hiddify/hiddify-app/releases) |
| [NekoBox](https://github.com/MatsuriDayo/NekoBoxForAndroid) | VLESS, VMess, Trojan, Hysteria2, AnyTLS (sing-box) | [GitHub](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases) |
| [V2Box](https://play.google.com/store/apps/details?id=dev.hexasoftware.v2box) | VLESS, VMess, Trojan, Hysteria2, Reality | [Play Store](https://play.google.com/store/apps/details?id=dev.hexasoftware.v2box) |
| [sing-box](https://github.com/SagerNet/sing-box) | VLESS, VMess, Trojan, Hysteria2, AnyTLS, WireGuard | [F-Droid](https://f-droid.org/packages/io.nekohasekai.sfa/) / [GitHub](https://github.com/SagerNet/sing-box/releases) |
| [HTTP Injector](https://play.google.com/store/apps/details?id=com.evozi.injector) | VLESS, Hysteria, DNS Tunnel, WireGuard, SSH | [Play Store](https://play.google.com/store/apps/details?id=com.evozi.injector) |
| [Clash Meta](https://github.com/MetaCubeX/ClashMetaForAndroid) | VLESS, VMess, Hysteria2, Trojan | [GitHub](https://github.com/MetaCubeX/ClashMetaForAndroid/releases) |
| [Tor Browser](https://www.torproject.org/download/) | Tor | [Play Store](https://play.google.com/store/apps/details?id=org.torproject.torbrowser) / [Official](https://www.torproject.org/download/) |
| [Psiphon](https://psiphon.ca/) | Psiphon | [Play Store](https://play.google.com/store/apps/details?id=com.psiphon3) / [APK](https://psiphon.ca/en/download.html) |
| [WireGuard](https://www.wireguard.com/) | WireGuard | [Play Store](https://play.google.com/store/apps/details?id=com.wireguard.android) |
| [AmneziaWG](https://play.google.com/store/apps/details?id=org.amnezia.awg) | AmneziaWG | [Play Store](https://play.google.com/store/apps/details?id=org.amnezia.awg) |
| [TrustTunnel](https://trusttunnel.org/) | TrustTunnel | [Play Store](https://play.google.com/store/apps/details?id=org.trusttunnel.app) / [GitHub](https://github.com/TrustTunnel/TrustTunnelClient) |
#### Windows
| App | Protocols | Link |
|-----|-----------|------|
| [v2rayN](https://github.com/2dust/v2rayN) | VLESS, VMess, Trojan, Hysteria2, TUIC | [GitHub](https://github.com/2dust/v2rayN/releases) |
| [Hiddify](https://hiddify.com/) | VLESS, VMess, Hysteria2, Trojan, AnyTLS, Reality | [GitHub](https://github.com/hiddify/hiddify-app/releases) |
| [NekoRay](https://github.com/MatsuriDayo/nekoray) | VLESS, VMess, Trojan, Hysteria2, AnyTLS (sing-box) | [GitHub](https://github.com/MatsuriDayo/nekoray/releases) ¹ |
| [Mihomo Party](https://github.com/mihomo-party-org/mihomo-party) | VLESS, VMess, Hysteria2, Trojan, AnyTLS | [GitHub](https://github.com/mihomo-party-org/mihomo-party/releases) |
| [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) | VLESS, VMess, Hysteria2, Trojan | [GitHub](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| [Tor Browser](https://www.torproject.org/download/) | Tor | [Official](https://www.torproject.org/download/) |
| [Psiphon](https://psiphon.ca/) | Psiphon | [Official](https://psiphon.ca/en/download.html) |
| [WireGuard](https://www.wireguard.com/) | WireGuard | [Official](https://www.wireguard.com/install/) |
| [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-windows-client/releases) | AmneziaWG | [GitHub](https://github.com/amnezia-vpn/amneziawg-windows-client/releases) |
| [TrustTunnel](https://trusttunnel.org/) | TrustTunnel | [GitHub](https://github.com/TrustTunnel/TrustTunnelClient/releases) |
#### macOS
| App | Protocols | Link |
|-----|-----------|------|
| [Streisand](https://apps.apple.com/us/app/streisand/id6450534064) | VLESS/Reality, VMess, Trojan, Hysteria2, WireGuard | [App Store (Free)](https://apps.apple.com/us/app/streisand/id6450534064) |
| [v2rayN](https://github.com/2dust/v2rayN) | VLESS, VMess, Trojan, Hysteria2 | [GitHub](https://github.com/2dust/v2rayN/releases) |
| [Hiddify](https://hiddify.com/) | VLESS, VMess, Hysteria2, Trojan, AnyTLS, Reality | [GitHub](https://github.com/hiddify/hiddify-app/releases) |
| [NekoRay](https://github.com/MatsuriDayo/nekoray) | VLESS, VMess, Trojan, Hysteria2, AnyTLS (sing-box) | [GitHub](https://github.com/MatsuriDayo/nekoray/releases) ¹ |
| [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) | VLESS, VMess, Hysteria2, Trojan | [GitHub](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| [sing-box](https://sing-box.sagernet.org/) | VLESS, VMess, Trojan, Hysteria2, AnyTLS, WireGuard | [Homebrew](https://formulae.brew.sh/formula/sing-box) / [GitHub](https://github.com/SagerNet/sing-box) |
| [Tor Browser](https://www.torproject.org/download/) | Tor | [Official](https://www.torproject.org/download/) |
| [Psiphon](https://psiphon.ca/) | Psiphon | [App Store (Apple Silicon)](https://apps.apple.com/us/app/psiphon/id1276263909) |
| [WireGuard](https://www.wireguard.com/) | WireGuard | [App Store](https://apps.apple.com/us/app/wireguard/id1451685025) |
| [AmneziaWG](https://apps.apple.com/app/amneziawg/id6478942365) | AmneziaWG | [App Store](https://apps.apple.com/app/amneziawg/id6478942365) |
| [TrustTunnel](https://trusttunnel.org/) | TrustTunnel | [GitHub](https://github.com/TrustTunnel/TrustTunnelClient/releases) |
#### Linux
| App | Protocols | Link |
|-----|-----------|------|
| [Hiddify](https://hiddify.com/) | VLESS, VMess, Hysteria2, Trojan, AnyTLS, Reality | [GitHub](https://github.com/hiddify/hiddify-app/releases) |
| [v2rayN](https://github.com/2dust/v2rayN) | VLESS, VMess, Trojan, Hysteria2 | [GitHub](https://github.com/2dust/v2rayN/releases) |
| [sing-box](https://sing-box.sagernet.org/) | VLESS, VMess, Trojan, Hysteria2, AnyTLS, WireGuard, DNS | [GitHub](https://github.com/SagerNet/sing-box) |
| [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) | VLESS, VMess, Hysteria2, Trojan | [GitHub](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| [Mihomo Party](https://github.com/mihomo-party-org/mihomo-party) | VLESS, VMess, Hysteria2, Trojan, AnyTLS | [GitHub](https://github.com/mihomo-party-org/mihomo-party/releases) |
| [Tor Browser](https://www.torproject.org/download/) | Tor | [Official](https://www.torproject.org/download/) |
| [WireGuard](https://www.wireguard.com/) | WireGuard | [Official](https://www.wireguard.com/install/) |
| [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) | AmneziaWG | `awg-quick` CLI (awg-tools) |
| [TrustTunnel](https://trusttunnel.org/) | TrustTunnel | [GitHub](https://github.com/TrustTunnel/TrustTunnelClient/releases) |
| **MoaV Client** | All MoaV protocols | Built-in (Docker) |
¹ NekoRay: Repository notes it is no longer actively maintained. Consider alternatives like Hiddify or Clash Verge.
**Notes:**
- Psiphon is not available via MoaV client - use [official Psiphon apps](https://psiphon.ca/download.html)
- iOS has no official Tor Browser; use [Onion Browser](https://apps.apple.com/us/app/onion-browser/id519296448) instead (Tor Project recommended)
- Psiphon for Linux is not officially available
## Protocol priority
Try these in order. If one doesn't work, try the next:
1. **Reality (VLESS)** - Primary, a strong first choice on networks where it currently works (port 443/tcp)
2. **Hysteria2** - Fast alternative, uses QUIC/UDP (port 443/udp)
3. **Trojan** - Backup, uses your domain's TLS cert (port 8443/tcp)
4. **CDN (VLESS+WS)** - When server IP is blocked, routes via Cloudflare (port 443 via CDN)
5. **TrustTunnel** - HTTP/2 & QUIC, looks like normal HTTPS (port 4443)
6. **WireGuard (Direct)** - Full VPN mode, simple setup (port 51820/udp)
7. **WireGuard (wstunnel)** - VPN wrapped in WebSocket, for restrictive networks (port 8080/tcp)
8. **Tor (Snowflake)** - Uses Tor network (no server needed)
9. **DNS Tunnel (dnstt)** - Last resort, very slow but hard to block (port 53/udp)
10. **Slipstream** - QUIC-over-DNS, 1.5-5x faster than dnstt (port 53/udp)
11. **Psiphon** - Standalone app only, uses Psiphon network (not via MoaV client)
**Telegram-specific:** Use the **Telegram MTProxy** link (in `telegram-proxy-link.txt`) directly in the Telegram app. This only works for Telegram traffic — it's not a general proxy.
---
## Set up your device
Pick your platform. Every path is the same three steps: install an app, import the bundle, connect.
=== "iOS"
### Shadowrocket (Recommended, $2.99)
The best all-in-one client for iOS.
**Download:** App Store (requires non-IR Apple ID)
**Import via QR Code:**
1. Open Shadowrocket
2. Tap the scanner icon (top-left)
3. Scan the QR code from your bundle (`reality-qr.png`)
4. Tap "Add" to save
**Import via Link:**
1. Copy the link from `reality.txt`
2. Open Shadowrocket
3. It auto-detects and asks to add - tap "Add"
**Import via Config File:**
1. AirDrop or share `reality-singbox.json` to your phone
2. Open with Shadowrocket
3. Import and save
**Connect:**
1. Toggle the switch ON
2. Allow VPN configuration when prompted
3. You're connected!
### Streisand (Free)
Good free alternative.
**Download:** App Store
**Setup:**
1. Open Streisand
2. Tap "+" to add server
3. Choose "Import from clipboard"
4. Paste the link from `reality.txt`
### Hiddify (Free, Iran-focused)
Specifically designed for Iran.
**Download:** App Store or https://hiddify.com
**Setup:**
1. Open Hiddify
2. Tap "Add Profile"
3. Paste or scan your Reality link
---
=== "Android"
### v2rayNG (Recommended, Free)
**Download:**
- Google Play: "v2rayNG"
- GitHub: https://github.com/2dust/v2rayNG/releases
**Import via QR Code:**
1. Open v2rayNG
2. Tap "+" button
3. Select "Import config from QRcode"
4. Scan `reality-qr.png`
**Import via Link:**
1. Copy link from `reality.txt`
2. Open v2rayNG
3. Tap "+" → "Import config from clipboard"
**Connect:**
1. Tap the server to select it
2. Tap the "V" button at bottom to connect
3. Allow VPN permission
### NekoBox (Free, sing-box based)
More advanced, uses sing-box core.
**Download:** GitHub: https://github.com/MatsuriDayo/NekoBoxForAndroid/releases
**Setup:**
1. Open NekoBox
2. Tap "+" → "Import from clipboard"
3. Paste your Reality link
4. Or import `reality-singbox.json` directly
### Hiddify (Free)
**Download:** https://hiddify.com or GitHub
**Setup:**
1. Open Hiddify
2. Add profile via link or QR code
---
=== "macOS"
### V2rayU (Free)
**Download:** https://github.com/yanue/V2rayU/releases
**Setup:**
1. Install and open V2rayU
2. Click menu bar icon → "Import"
3. Paste your Reality link
4. Click "Turn v2ray-core On"
### NekoRay (Free)
Cross-platform GUI client.
**Download:** https://github.com/MatsuriDayo/nekoray/releases
**Setup:**
1. Install and open NekoRay
2. Server → Add profile from clipboard
3. Paste your Reality link
### Command Line (sing-box)
For advanced users:
```bash
# Install sing-box
brew install sing-box
# Run with config
sing-box run -c reality-singbox.json
```
---
=== "Windows"
### v2rayN (Free)
**Download:** https://github.com/2dust/v2rayN/releases
**Setup:**
1. Extract and run v2rayN.exe
2. Click "Server" → "Add [VLESS]"
3. Or paste link: "Server" → "Import from clipboard"
4. Click "System Proxy" → "Set Global Proxy"
### NekoRay (Free)
Same as macOS version.
**Download:** https://github.com/MatsuriDayo/nekoray/releases
---
## Protocol-specific notes
Most protocols just work once the subscription is imported. These have quirks worth knowing.
??? note "WireGuard"
MoaV provides two WireGuard connection methods:
- **Direct Mode** (`wireguard.conf`) - Simple, fast, uses UDP port 51820
- **wstunnel Mode** (`wireguard-wstunnel.conf`) - Wrapped in WebSocket, uses TCP port 8080, for networks that block UDP
### Direct Mode (Recommended)
Use this when UDP traffic is allowed. Simple and fast.
**Your config file:** `wireguard.conf`
#### iOS / Android
1. Install "WireGuard" from App Store / Play Store
2. Tap "+" → "Create from QR code"
3. Scan `wireguard-qr.png`
4. Name it (e.g., "MoaV WG")
5. Toggle ON to connect
#### macOS / Windows / Linux
1. Install WireGuard from https://wireguard.com/install/
2. Click "Import tunnel(s) from file"
3. Select `wireguard.conf`
4. Click "Activate"
### wstunnel Mode (For Restrictive Networks)
Use this when UDP is blocked or heavily throttled. Wraps WireGuard in a WebSocket tunnel.
**Your config file:** `wireguard-wstunnel.conf`
#### Requirements
You need both WireGuard and wstunnel client:
- WireGuard: https://wireguard.com/install/
- wstunnel: https://github.com/erebe/wstunnel/releases
#### macOS / Linux Setup
> **Use the exact command from your bundle.** `wireguard-instructions.txt` in
> your user bundle has the ready-to-paste command — it uses `wss://YOUR_DOMAIN:8080`
> (TLS) when the server has a domain, plus a per-install `--http-upgrade-path-prefix`
> secret. The examples below are the generic (domainless) form.
```bash
# 1. Download wstunnel from GitHub releases
# https://github.com/erebe/wstunnel/releases
# 2. Start wstunnel client (connect to server's port 8080)
# Domain server: wss://YOUR_DOMAIN:8080 --http-upgrade-path-prefix
wstunnel client -L udp://127.0.0.1:51820:moav-wireguard:51820 ws://YOUR_SERVER_IP:8080
# 3. In another terminal, import WireGuard config
# The config points to 127.0.0.1:51820 (local wstunnel)
sudo wg-quick up ./wireguard-wstunnel.conf
```
#### Windows Setup
1. Download wstunnel.exe from GitHub releases
2. Open PowerShell/CMD and run:
```
wstunnel.exe client -L udp://127.0.0.1:51820:moav-wireguard:51820 ws://YOUR_SERVER_IP:8080
```
3. Keep this running
4. Import `wireguard-wstunnel.conf` in WireGuard app
5. Activate the tunnel
#### iOS / Android (Advanced)
wstunnel on mobile requires additional apps or rooted devices. For most users, try other protocols (Reality, Hysteria2) instead if direct WireGuard is blocked.
**Note:** Replace `YOUR_SERVER_IP` with your actual server IP address.
---
??? note "AmneziaWG"
AmneziaWG is a DPI-resistant fork of WireGuard that obfuscates packet headers and sizes to bypass deep packet inspection.
**Your config files:**
- `amneziawg.conf` - AmneziaWG client configuration (includes obfuscation parameters)
### Mobile Apps (iOS/Android)
1. Install **AmneziaWG** ([iOS](https://apps.apple.com/app/amneziawg/id6478942365) / [Android](https://play.google.com/store/apps/details?id=org.amnezia.awg))
2. Tap "+" and scan the QR code or import `amneziawg.conf`
3. Enable the connection
### Desktop
- **Windows:** Download [AmneziaWG Client](https://github.com/amnezia-vpn/amneziawg-windows-client/releases), import `amneziawg.conf`
- **macOS:** Install [AmneziaWG](https://apps.apple.com/app/amneziawg/id6478942365) or use `awg-quick up amneziawg.conf`
- **Linux:** Use `awg-quick up amneziawg.conf` (included in awg-tools)
---
??? note "Hysteria2"
### Using Shadowrocket / v2rayNG
Both support Hysteria2 links. Import `hysteria2.txt` the same way as Reality.
### Using Hysteria2 CLI
For desktop:
```bash
# Download from https://github.com/apernet/hysteria/releases
# Run with config
./hysteria -c hysteria2.yaml
```
This creates a local proxy on:
- SOCKS5: `127.0.0.1:1080`
- HTTP: `127.0.0.1:8080`
Configure your browser/apps to use this proxy.
---
??? note "AnyTLS"
AnyTLS is a password-authenticated TLS proxy designed to resist **TLS-in-TLS fingerprinting**, giving it high stealth against the filtering techniques it targets. It runs on the same sing-box engine as Trojan and reuses your domain's TLS certificate. It is **opt-in** (enabled with `ENABLE_ANYTLS=true` on the server) and uses the same per-user password as your Trojan/Hysteria2 entries.
**Your config file:** `anytls.txt`
**Important:** AnyTLS is a newer protocol with **narrower client support** than VLESS/Trojan. Use a recent build of one of these apps:
- **Hiddify** (iOS, Android, macOS, Windows)
- **sing-box** (SFA on Android, SFI on iOS, CLI on desktop)
- **NekoBox** (Android) / **NekoRay** (desktop)
- **Mihomo Party** (macOS, Windows)
- **Shadowrocket** 2.2.65 or newer (iOS)
Clients without AnyTLS support (e.g., v2rayNG, Streisand, V2Box, Clash Verge) will fail to import the link — switch to one of the apps above.
### Import the link
The `anytls.txt` link works in any of the supported apps:
1. Copy the link from `anytls.txt`
2. Import into your client app (paste from clipboard, or scan `anytls-qr.png` if present)
3. Connect
**Link format:**
```
anytls://password@yourdomain.com:8445?sni=yourdomain.com&insecure=0#MoaV-AnyTLS-username
```
### iOS (Shadowrocket 2.2.65+ / Hiddify)
1. Open Shadowrocket or Hiddify
2. Tap the scanner icon → scan `anytls-qr.png`, or paste the link from `anytls.txt`
3. Toggle ON to connect
### Android (NekoBox / Hiddify / sing-box)
1. Open NekoBox, Hiddify, or sing-box (SFA)
2. Tap "+" → "Import from clipboard"
3. Paste the link from `anytls.txt`
4. Connect
### Desktop (NekoRay / Mihomo Party / sing-box)
1. Open NekoRay, Mihomo Party, or the sing-box CLI
2. Import the link from `anytls.txt`
3. Connect
**Note:** AnyTLS requires a domain (TLS) and shares the Trojan certificate. If import fails, confirm your client actually supports AnyTLS (see the list above) and is up to date.
---
??? note "CDN VLESS+WS (When IP Blocked)"
Use this when direct connections to your server are blocked but Cloudflare IPs are accessible.
**Your config file:** `cdn-vless.txt`
CDN mode routes your traffic through Cloudflare's CDN, making it appear as regular HTTPS traffic to a CDN-hosted website.
### Using Any VLESS Client
The CDN link works in any app that supports VLESS with WebSocket transport:
1. Copy the link from `cdn-vless.txt`
2. Import into your client app (Shadowrocket, v2rayNG, Hiddify, etc.)
3. Connect
**Link format:**
```
vless://UUID@cdn.yourdomain.com:443?security=tls&type=httpupgrade&path=/auto-generated-path&sni=yourdomain.com&host=cdn.yourdomain.com&fp=random&alpn=http/1.1#MoaV-CDN-username
```
### iOS (Shadowrocket)
1. Open Shadowrocket
2. Tap scanner icon → scan `cdn-vless-qr.png`
3. Or paste the link from `cdn-vless.txt`
4. Toggle ON to connect
### Android (v2rayNG / Hiddify)
1. Open v2rayNG or Hiddify
2. Tap "+" → "Import from clipboard"
3. Paste the link from `cdn-vless.txt`
4. Connect
**Note:** CDN mode is slower than direct connections but works when your server's IP is blocked.
---
??? note "TrustTunnel"
TrustTunnel uses HTTP/2 and HTTP/3 (QUIC), making traffic look like regular HTTPS.
**Your config files:**
- `trusttunnel.txt` - Credentials and instructions
- `trusttunnel.toml` - CLI client configuration
- `trusttunnel.json` - JSON format for apps
### Mobile Apps (iOS/Android)
1. Download TrustTunnel from App Store or Play Store
2. Tap "+" to add a new VPN
3. Enter the settings from `trusttunnel.txt`:
- Server: `yourdomain.com:4443`
- Username: (from bundle)
- Password: (from bundle)
4. Connect
### Desktop (CLI Client)
```bash
# Download from https://github.com/TrustTunnel/TrustTunnelClient/releases
# Run with config file
trusttunnel_client --config trusttunnel.toml
```
The CLI client creates a TUN interface for full VPN functionality.
---
??? note "DNS Tunnel (Last Resort)"
Use this only when all other methods are blocked. DNS tunneling is slow but often works when everything else is blocked.
### dnstt
See `dnstt-instructions.txt` in your bundle for detailed steps.
**Summary:**
1. Download dnstt-client from https://www.bamsoftware.com/software/dnstt/
2. Run: `dnstt-client -doh https://1.1.1.1/dns-query -pubkey YOUR_KEY t.yourdomain.com 127.0.0.1:1080`
3. Configure apps to use SOCKS5 proxy `127.0.0.1:1080`
### Slipstream (Faster DNS Tunnel)
Slipstream is a QUIC-over-DNS tunnel that is 1.5-5x faster than dnstt. See `slipstream-instructions.txt` in your bundle.
**Summary:**
1. Download slipstream-client from https://github.com/net2share/slipstream-rust-build/releases
2. Copy the certificate file `slipstream-cert.pem` from your bundle
3. Run: `slipstream-client --domain s.yourdomain.com --cert slipstream-cert.pem --dns-server 1.1.1.1:53 --socks-listen 127.0.0.1:1080`
4. Configure apps to use SOCKS5 proxy `127.0.0.1:1080`
**Modes:**
- **Resolver mode** (default, stealthier): Uses public DNS resolvers (~60 KB/s)
- **Authoritative mode** (faster, less stealthy): Connects directly to server (~3-4 MB/s)
### XDNS (Xray mKCP DNS Tunnel)
XDNS encodes VPN traffic inside DNS-like packets using Xray-core's mKCP transport with FinalMask. Works when most protocols are blocked. Slow but reliable during heavy internet shutdowns — best for Telegram and chat apps, not web browsing.
**Important:** XDNS requires a client app with FinalMask support. Standard v2rayNG does not support this yet. Use Happ (Android beta) or Xray CLI.
**Setup:**
1. Find `xdns-config.json` in your user bundle
2. Import into an Xray-compatible client with FinalMask support
3. The config connects directly to the server IP on port 53
4. Use as SOCKS5 proxy: `127.0.0.1:7891`
5. **For Telegram:** Settings > Proxy > SOCKS5 > `127.0.0.1:7891`
**Tips:**
- Best for **Telegram only** — too slow for web browsing
- MTU 35 (default) is safest. Try 67 or 130 for faster speeds if your network allows
- MTU must match on both server and client
- All four DNS tunnels (dnstt, Slipstream, MasterDNS, XDNS) run simultaneously on port 53 — `dns-router` fans queries out by subdomain, so no conflict
- **Resolver choice matters.** The default `xdns-config.json` round-robins across multiple public DNS resolvers (set by `XDNS_RESOLVERS` in the server's `.env`); if it keeps dropping, swap in resolvers that actually answer from your network. See [protocols.md → Reachable DNS resolvers](https://moav.sh/docs/protocols/#reachable-dns-resolvers) for scanner tools ([findns](https://github.com/SamNet-dev/findns), [dns-mns](https://gitlab.com/E-Gurl/dns-mns)).
---
??? note "Psiphon"
Psiphon is a standalone circumvention tool that doesn't require your own server. It connects to the Psiphon network - a large, distributed system designed for censorship circumvention.
**When to use Psiphon:**
- You don't have access to a MoaV server
- Your MoaV server is blocked
- You need a quick, no-setup solution
### iOS
1. Download "Psiphon" from App Store (requires non-IR Apple ID)
2. Open the app
3. Tap "Start" to connect
4. The app automatically finds working servers
### Android
1. Download from:
- Google Play: "Psiphon"
- Direct APK: https://psiphon.ca/en/download.html
2. Open the app
3. Tap "Start" to connect
### Windows
1. Download from https://psiphon.ca/en/download.html
2. Run the executable (no installation needed)
3. Click "Connect"
4. Configure browser to use the local proxy shown in the app
### macOS
1. Download from https://psiphon.ca/en/download.html
2. Open the app
3. Click "Connect"
4. Configure system or browser proxy settings
**Note:** Psiphon uses various protocols internally (SSH, OSSH, etc.) and automatically switches between them to find working connections.
---
??? note "XHTTP"
XHTTP uses VLESS with XHTTP transport and Reality TLS camouflage, powered by Xray-core. No domain is required.
**Your config file:** `xhttp.txt`
### Compatible Client Apps
XHTTP requires Xray-based clients that support the XHTTP transport:
| Platform | App | Link |
|----------|-----|------|
| Android | [V2rayNG](https://github.com/2dust/v2rayNG) | [GitHub](https://github.com/2dust/v2rayNG/releases) |
| Android | [Hiddify](https://hiddify.com/) | [GitHub](https://github.com/hiddify/hiddify-app/releases) |
| Android | [NekoBox](https://github.com/MatsuriDayo/NekoBoxForAndroid) | [GitHub](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases) |
| iOS | [Streisand](https://apps.apple.com/us/app/streisand/id6450534064) | [App Store (Free)](https://apps.apple.com/us/app/streisand/id6450534064) |
| iOS | [Hiddify](https://apps.apple.com/us/app/hiddify-proxy-vpn/id6596777532) | [App Store (Free)](https://apps.apple.com/us/app/hiddify-proxy-vpn/id6596777532) |
| iOS | [V2Box](https://apps.apple.com/ca/app/v2box-v2ray-client/id6446814690) | [App Store](https://apps.apple.com/ca/app/v2box-v2ray-client/id6446814690) |
| Windows | [V2rayN](https://github.com/2dust/v2rayN) | [GitHub](https://github.com/2dust/v2rayN/releases) |
| macOS | [V2rayU](https://github.com/yanue/V2rayU) | [GitHub](https://github.com/yanue/V2rayU/releases) |
### Import
1. Copy the link from `xhttp.txt`
2. Import into your client app (V2rayNG, Hiddify, Streisand, etc.)
3. Connect
**Note:** XHTTP is enabled by default. Disable with `ENABLE_XHTTP=false` in `.env` if not needed.
---
## Testing from a server or CI
MoaV ships a built-in multi-protocol **client container**, used mainly to **verify a server's protocols actually work** — run straight from the server itself or in CI, without touching a phone or desktop. Typical uses:
- **Connectivity testing** — run `moav test ` on the server to check every protocol in a user's bundle
- **CI / automated testing** — the same container backs MoaV's end-to-end test suite (`tests/client-test.sh`)
- **Ad-hoc connections** — `moav client connect ` brings up a local SOCKS5/HTTP proxy for a quick check
!!! tip "Want a full client for everyday use?"
This container is optimized for testing. For a standalone client with a **web dashboard**, **load-balancing across all healthy endpoints**, **automatic failover**, and **live routing rules** — the kind you'd leave running on a home server or Linux desktop — see the dedicated **[MoaV Client](https://moav.sh/docs/client/)** page.
### Testing Connectivity
Test all protocols for a user to verify server is working:
```bash
# Test all protocols for user1
moav test user1
# Output results as JSON (for scripts/automation)
moav test user1 --json
```
The test checks: Reality, Trojan, Hysteria2, WireGuard (config validation), dnstt, and Slipstream.
**Sample output:**
```
═══════════════════════════════════════════════════════════════
MoaV Connection Test Results
═══════════════════════════════════════════════════════════════
Config: /bundles/user1
Time: Wed Jan 28 10:30:00 UTC 2026
───────────────────────────────────────────────────────────────
✓ reality Connected via VLESS/Reality
✓ trojan Connected via Trojan
✓ hysteria2 Connected via Hysteria2
✓ wireguard Config valid, endpoint reachable
○ dnstt No dnstt config found in bundle
═══════════════════════════════════════════════════════════════
```
### Client Mode (Connect Through Server)
!!! tip "For everyday use, prefer the MoaV Client"
`moav client connect` exists mainly to verify a server from the server side. If you
want a client to actually **use** — endpoint probing, load-balancing across healthy
endpoints, automatic failover, routing rules and a dashboard — that is
**[MoaV Client](https://moav.sh/docs/client/)**, a separate and more capable project. The built-in client
mode here is deliberately minimal and may be reduced to testing only.
Run MoaV as a local proxy client:
```bash
# Auto-detect best working protocol
moav client connect user1
# Force a specific protocol
moav client connect user1 --protocol reality
moav client connect user1 --protocol hysteria2
moav client connect user1 --protocol trojan
moav client connect user1 --protocol wireguard
moav client connect user1 --protocol dnstt
moav client connect user1 --protocol tor
```
**Local proxy endpoints:**
- SOCKS5: `127.0.0.1:1080`
- HTTP: `127.0.0.1:8080`
Configure these ports in `.env`:
```bash
CLIENT_SOCKS_PORT=1080
CLIENT_HTTP_PORT=8080
```
**Protocol fallback order (auto mode):**
1. Reality (VLESS) - A strong first choice on networks where it currently works
2. Hysteria2 - Fast, UDP-based
3. Trojan - TLS-based backup
4. WireGuard - Full VPN
5. Tor (Snowflake) - Uses Tor network (no server needed)
6. dnstt - Last resort, slow but hard to block
> **Note:** Psiphon is not available via MoaV client. Use the [official Psiphon apps](https://psiphon.ca/en/download.html) instead.
### Building the Client Image
The client image is built automatically when running `moav test` or `moav client`. To build manually:
```bash
moav client build
```
### Technical Details
The client container includes:
- **sing-box** - Handles Reality, Trojan, Hysteria2
- **wireguard-go** - Userspace WireGuard implementation
- **wstunnel** - WebSocket tunnel for WireGuard
- **dnstt-client** - DNS tunnel client
- **snowflake-client** - Tor Snowflake pluggable transport
- **tor** - Tor daemon
**Container capabilities:**
- Runs without privileged mode for most protocols
- WireGuard requires `--cap-add NET_ADMIN` for full functionality
- Uses Alpine Linux for minimal image size
---
## Highly censored networks
1. **Keep multiple configs** - Have Reality, Hysteria2, WireGuard, XDNS, and DNS tunnel ready
2. **Download client apps in advance** - Store APKs, wstunnel binaries, and Psiphon offline
3. **Use mobile data** as backup - Sometimes less filtered than home internet
4. **Avoid peak hours** - Filtering can be heavier during protests/events
5. **Update configs quickly** - If server is blocked, switch to backup
6. **Try wstunnel if UDP is blocked** - Some ISPs block UDP; wstunnel wraps WireGuard in TCP/WebSocket
7. **Reality is often best** - Mimics legitimate HTTPS traffic to common sites
8. **Keep Psiphon as backup** - No server needed, works independently of your MoaV setup
9. **Enable TLS Fragment and MUX** - See below for client-side optimizations
---
## Connection optimization (fragment & MUX)
MoaV's generated sing-box configs already include optimal Fragment and MUX settings. If you're using third-party apps (Hiddify, v2rayNG, NekoBox, etc.) or importing via share links, you can enable these manually for better performance in censored networks.
### TLS Fragment
TLS Fragment splits the TLS ClientHello message into smaller pieces, making it harder for DPI (Deep Packet Inspection) systems to detect the SNI (Server Name Indication) and block the connection. This is a **client-side only** feature — no server changes needed.
**When to use:** When connections are being blocked or reset during the TLS handshake, which is common in Iran and similar environments.
**Which protocols benefit:**
| Protocol | Fragment | Why |
|----------|----------|-----|
| Reality (VLESS) | Yes | Hides SNI from DPI during TLS handshake |
| Trojan | Yes | Same — TLS-based, benefits from fragment |
| CDN (VLESS+WS) | No | TLS terminates at Cloudflare, not your server |
| Hysteria2 | No | Uses QUIC/UDP, not TCP-based TLS |
| WireGuard / AmneziaWG | No | Not TLS-based |
#### sing-box JSON Config
MoaV's generated configs already include this. If you're building your own config:
```json
{
"outbounds": [
{
"type": "vless",
"tls": {
"enabled": true,
"server_name": "example.com",
"record_fragment": true
}
}
]
}
```
The `record_fragment` option (sing-box 1.12+) automatically splits TLS records. This is simpler than Xray-core's granular settings and works well for most scenarios.
#### Hiddify
1. Go to **Settings** → **Config Options**
2. Find **TLS Fragment** section
3. Enable it and set:
- **Size**: `10-100` (bytes per fragment)
- **Sleep**: `10-50` (ms delay between fragments)
#### v2rayNG
1. Go to **Settings** → **TLS/Reality**
2. Enable **TLS Fragment**
3. Recommended values:
- **Length**: `50-200`
- **Interval**: `10-50`
- **Packets**: `1-3`
#### Shadowrocket
Shadowrocket does not currently support TLS Fragment. Use the sing-box app or Hiddify if you need this feature on iOS.
### MUX (Multiplexing)
MUX multiplexes multiple connections over a single TCP connection, reducing the number of TLS handshakes and making traffic patterns harder to fingerprint.
**When to use:** When you experience frequent connection drops or slow initial connections. Also useful to reduce the number of observable connections to the server.
**Which protocols benefit:**
| Protocol | MUX | Why |
|----------|-----|-----|
| Reality (VLESS) | No | Incompatible with VLESS Vision flow (`xtls-rprx-vision`) |
| Trojan | Yes | Reduces handshakes, improves stability |
| CDN (VLESS+WS) | Yes | Fewer WebSocket connections through CDN |
| Hysteria2 | No | QUIC already multiplexes natively |
| WireGuard / AmneziaWG | No | Not applicable |
> **Important:** MUX is **not compatible** with Reality (VLESS Vision). Enabling MUX on a Reality connection will break it. MoaV's generated configs handle this correctly.
#### sing-box JSON Config
MoaV's generated Trojan and CDN configs already include this:
```json
{
"outbounds": [
{
"type": "trojan",
"multiplex": {
"enabled": true,
"protocol": "h2mux",
"max_connections": 2,
"padding": true
}
}
]
}
```
- `protocol`: `h2mux` is recommended (HTTP/2 multiplexing)
- `max_connections`: `2` balances speed and stealth
- `padding`: `true` adds random padding to obscure traffic patterns
#### Hiddify
1. Go to **Settings** → **Config Options**
2. Find **MUX** section
3. Enable and set:
- **Protocol**: `h2mux`
- **Max Connections**: `2`
- **Padding**: On
#### v2rayNG
1. Go to **Settings** → **MUX**
2. Enable **MUX**
3. Set **Concurrency**: `2-4`
### Summary: What to Enable Per Protocol
| Protocol | Fragment | MUX | Notes |
|----------|----------|-----|-------|
| Reality (VLESS) | Yes | **No** | Vision flow is incompatible with MUX |
| Trojan | Yes | Yes | Best with both enabled |
| CDN (VLESS+WS) | No | Yes | Fragment won't help (CDN terminates TLS) |
| Hysteria2 | No | No | QUIC handles both natively |
| WireGuard | No | No | Different protocol layer |
| AmneziaWG | No | No | Has its own obfuscation |
> **Note:** MoaV v1.3.7+ automatically includes these optimizations in generated sing-box JSON configs. If you import via share links (vless://, trojan://, hy2://), you may need to enable Fragment and MUX manually in your app settings.
## Donating bandwidth (server operators)
Psiphon Conduit and Tor Snowflake are **server** features, not something a user
installs — they donate your spare bandwidth to Psiphon and Tor users worldwide.
See [Protocols → Psiphon Conduit / Tor Snowflake](https://moav.sh/docs/protocols/#psiphon-conduit)
and enable them with `moav start conduit` / `moav start snowflake`.
## When it won't connect
Client-side symptoms — can't connect at all, TLS handshake timeouts, slow or dropping connections, "invalid config" — are covered in [Troubleshooting](https://moav.sh/docs/TROUBLESHOOTING/#nothing-connects).
### "Connection failed" or "Timeout"
1. Check your internet connection
2. Try a different protocol (Reality → Hysteria2 → Trojan)
3. Try a different DNS (1.1.1.1 or 8.8.8.8)
4. Restart the app
### "TLS handshake failed"
- Your ISP might be blocking the connection
- Try Hysteria2 (uses UDP instead of TCP)
- Try DNS tunnel as last resort
### "Certificate error"
- Check that your device's date/time is correct
- Try Reality protocol (doesn't use your domain's cert)
### Very slow connection
- Try Hysteria2 (optimized for lossy networks)
- Check if your ISP is throttling
- DNS tunnel is inherently slow - only for emergencies
### Nothing works
- The server IP might be blocked
- Contact admin for a new server/config
- Try using a different network (mobile data vs WiFi)
---
---
# MoaV Client
**[moav-client](https://github.com/MotherofallVPNs/moav-client)** is a standalone, self-hosted client for MoaV servers. You run it on your own Linux box, home server, or desktop; it ingests a MoaV subscription bundle, probes every endpoint end-to-end through its tunnel, load-balances across the healthy ones, and exposes a single local **SOCKS5 / HTTP CONNECT** proxy for the rest of your machine to use. A dark React dashboard — styled to match the MoaV admin panel — gives live visibility into endpoint health, per-protocol throughput, routing rules, and logs.

!!! note "How this differs from a connect-and-go app"
Phone/desktop apps like [Hiddify, NekoBox, sing-box, or Shadowrocket](https://moav.sh/docs/CLIENTS/#client-apps) are the simplest way to connect a single device. **moav-client** is for when you want an always-on local proxy that automatically **picks the best live endpoint**, **fails over** when one dies, and gives you a **dashboard + routing rules** — e.g. on a home server or Linux desktop that other devices route through. To simply **test** whether a server's protocols work — on the server itself or in CI — use the lightweight [MoaV Client Container](https://moav.sh/docs/CLIENTS/#testing-from-a-server-or-ci) instead.
## Install
One command installs Docker (if missing), clones the repo, lets you pick which sidecars to build, seeds config, and brings the stack up:
```bash
curl -fsSL moav.sh/client-install.sh | bash
```
It works interactively — even piped through `bash` — or fully headless, and installs a global `moavc` command.
Once it finishes, these are exposed on the local machine:
| What | Address |
|------|---------|
| Dashboard | `http://localhost:3001` |
| SOCKS5 proxy | `socks5h://localhost:1080` |
| HTTP CONNECT | `http://localhost:8081` |
| REST + WS API | `http://localhost:8088` |
Point your browser or system proxy at `socks5h://localhost:1080` — every connection then routes through the healthiest MoaV endpoint.
## Manage it with `moavc`
```bash
moavc status # service status + health + URLs
moavc info # just the dashboard / proxy / API URLs
moavc logs -f proxy-core # tail logs
moavc probe # trigger a latency probe
moavc sidecar add tor # enable + build + start a sidecar
moavc expose lan # network reach: loopback | lan | public
moavc update [-b ] # pull (optionally switch branch) + rebuild
moavc uninstall [--wipe] # remove the stack (--wipe deletes config/data)
```
## The dashboard
The web UI at `:3001` is organized into tabs:
- **Endpoints** — live status and latency per endpoint; toggle each on/off (sidecar toggles also stop/start its Docker container) and edit priority inline.
- **Configs** — import another MoaV server's bundle by dropping its `.zip`; list, remove, and reload sources.
- **Analytics** — per-protocol upload/download with a rolling 2-minute throughput chart, plus a per-endpoint table of dials, errors, and failovers.
- **Plugins** — first-match-wins routing rules (proxy / direct / block by domain, IP CIDR, GeoIP, port…), editable live from a curated template catalog.
- **Settings** — load-balancing strategy (latency / priority / weighted), network exposure, access URLs, SNI-spoofing, and config backup / restore.
- **Debug / Diagnostics** — streaming log tail and per-endpoint connectivity checks (TCP / DNS / traceroute, optionally *through* a chosen tunnel).
## Network exposure
By default the proxy binds to loopback only. Widen it from the Settings tab or the CLI:
```bash
moavc expose loopback # 127.0.0.1 — only this machine (default, safest)
moavc expose lan # 0.0.0.0 — every device on your LAN can use it
moavc expose public # LAN bind + you port-forward on your router
```
`lan` and `public` add optional SOCKS5 and dashboard authentication.
## Supported protocols
Protocol cryptography is delegated to **sing-box** (and **xray** for XHTTP), with optional sidecars for the rest:
VLESS / Reality · VLESS+WS+TLS (CDN) · Trojan · AnyTLS · Shadowsocks-2022 · Hysteria2 · VLESS+XHTTP+Reality · WireGuard · AmneziaWG · TrustTunnel · MasterDNS · Psiphon · Tor.
See the [Supported Protocols](https://moav.sh/docs/protocols/) reference for what each one is and when to use it.
## Learn more
Full documentation — configuration reference, architecture, plugin rules, and per-protocol dial details — lives in the repo:
- **Repository:** [github.com/MotherofallVPNs/moav-client](https://github.com/MotherofallVPNs/moav-client)
- **README:** install, config, and CLI reference (available in [English](https://github.com/MotherofallVPNs/moav-client/blob/main/README.md) and [فارسی](https://github.com/MotherofallVPNs/moav-client/blob/main/README-fa.md))
---
# Importing MoaV configs into MahsaNG
[MahsaNG](https://github.com/GFW-knocker/MahsaNG) is a hardened V2RayNG fork
built for Iran (2M+ users). It speaks standard V2Ray protocols (VLESS, Trojan,
Shadowsocks, Hysteria2) plus extra anti-censorship transports, and adds
client-side circumvention (Fragment, fake-SNI, rotating configs). MoaV already
generates everything MahsaNG needs — this guide shows the fastest way to get a
user connected.
> **TL;DR:** every user bundle already contains a base64 **V2Ray subscription**
> — in `subscription.txt` and as a click-to-copy block at the top of the
> bundle's `README.html`. Paste it once into MahsaNG to import all proxy
> protocols. (No separate command — it's generated with the bundle.)
---
## 1. Install MahsaNG
- **Android (recommended):** download the latest APK from the
[MahsaNG releases page](https://github.com/GFW-knocker/MahsaNG/releases)
and install it (allow "install from unknown sources"). It is not on Google
Play; only use the official GitHub releases.
- MahsaNG is Android-only. iOS/desktop users should use a standard V2Ray
client (Streisand, Hiddify, v2rayN, NekoBox) with the same URIs — see
[CLIENTS.md](https://moav.sh/docs/CLIENTS/).
Distribute the APK and configs over a channel the user can already reach
(Telegram, email, a USB drive). Treat config links as secrets.
---
## 2. Which MoaV protocols work in MahsaNG?
MahsaNG imports **standard V2Ray URIs**. MoaV's bundle generator automatically
includes only the compatible ones in the subscription, ordered by how well they
survive Iran's censorship:
| MoaV protocol | URI scheme | MahsaNG | Notes for Iran |
|---|---|---|---|
| **Reality (VLESS)** | `vless://` | ✅ | **Best default.** No domain, resembles real TLS. |
| **CDN (VLESS+WS)** | `vless://` | ✅ | **Best when your server IP is blocked** — rides Cloudflare. |
| **XHTTP (VLESS+XHTTP+Reality)** | `vless://` | ✅ | HTTP-camouflaged; good alternate. |
| **Trojan** | `trojan://` | ✅ | Solid; needs a domain + valid TLS cert. |
| **Shadowsocks-2022** | `ss://` | ✅ | Lightweight; decent fallback. |
| **Hysteria2** | `hysteria2://` | ✅ | Fast, but UDP is frequently throttled/blocked in Iran. |
| WireGuard / AmneziaWG | `.conf` | ❌ | Not a V2Ray URI — use the WireGuard/Amnezia app. |
| TrustTunnel | config file | ❌ | Use the TrustTunnel client. |
| dnstt / Slipstream / **MasterDNS** | DNS tunnel | ⚠️ | Not a subscription entry, but **MahsaNG v16 has a native MasterDNS tab** — see §5. |
| **GooseRelay** | — | ⚠️ | **MahsaNG v16 bundles the GooseRelay client** — see §6 below. Not a V2Ray URI; configured separately via `tunnel_key` + Apps Script URL. |
| Telegram MTProxy | `tg://proxy` | ❌ | Import directly into the Telegram app, not MahsaNG. |
**Recommended ordering for Iran:** Reality → CDN → XHTTP → Trojan →
Shadowsocks → Hysteria2. The bundle's subscription already includes them in
this order.
---
## 3. Where the subscription lives
MoaV builds the subscription into **every user bundle automatically** (on
`moav user add` / `moav regenerate-users`) — there's no separate command to run.
Each bundle in `outputs/bundles//` contains:
- **`subscription.txt`** — the base64 **V2Ray subscription body** (all
compatible configs in one string).
- **`README.html`** — opens with an **"Import everything at once"** card showing
the same subscription as a click-to-copy block (EN + FA).
- the individual config files (`reality.txt`, `trojan.txt`, `shadowsocks.txt`,
…) and a PNG QR per config (`reality-qr.png`, …).
The compatible configs (Reality, CDN, XHTTP, Trojan, Shadowsocks-2022,
Hysteria2 — IPv4 + IPv6) are selected automatically and ordered by reliability
for Iran. Download a user's bundle from the MoaV **admin dashboard**
(`https://your-server:9443`) or via SCP.
> If `subscription.txt` is missing (or the README's import card is hidden), the
> user has only non-V2Ray protocols enabled. Enable at least one of
> Reality/CDN/XHTTP/Trojan/Shadowsocks/Hysteria2 and regenerate the bundle
> (`moav user add ` or `moav regenerate-users`).
---
## 4. Three ways to import
### Method A — Subscription (one import, auto-updates)
A V2Ray subscription is just **base64 of a newline-separated URI list**.
`subscription.txt` *is* that body, and the README's import card holds the same
string. Two ways to use it:
- **Paste the text directly:** copy the subscription (from the README card or
`subscription.txt`) and in MahsaNG tap **≡ → Subscription / Group → +** and
paste it — MahsaNG, v2rayNG and Hiddify accept the base64 body directly. All
configs appear at once.
- **Host it as a URL:** put `subscription.txt` behind any HTTPS URL the user can
reach (a static host, a gist, an object-storage bucket), then add that URL as
a subscription in MahsaNG — configs refresh whenever you regenerate them
server-side.
Keep the subscription private — anyone with it gets all of that user's configs.
### Method B — Single URI (manual, no hosting)
Copy any individual config from the bundle (`reality.txt`, `trojan.txt`, …) or
from the README's per-protocol sections. In MahsaNG: tap **+ → Import config
from clipboard**. Start with the **Reality** URI; add **CDN** as a backup.
### Method C — QR code
Each config has a PNG QR in the bundle (`outputs/bundles//*-qr.png`, e.g.
`reality-qr.png`), also shown in the README. In MahsaNG: **+ → Scan QR code**.
Best for handing a config to someone in person without sending text.
---
## 5. Surviving total shutdowns: add a DNS-tunnel fallback
When Iran throttles to the point that even Reality/CDN fail, a DNS tunnel is
often the only thing that still moves data. **MahsaNG v16 ships a native
MasterDNS tab**, and MoaV can run the matching MasterDNS server:
1. MasterDNS is **enabled by default** (`ENABLE_MASTERDNS=true`) — just add
the `m` NS record (see
[DNS.md → NS Delegations](https://moav.sh/docs/DNS/#with-a-domain-the-records))
and rebootstrap. (Set `ENABLE_MASTERDNS=false` only if you want to opt out.)
2. The user's bundle gets `masterdns-instructions.txt` with the domain +
encryption key. Enter those in MahsaNG's MasterDNS section.
MasterDNS is faster and far more loss-tolerant than dnstt/Slipstream and was
battle-tested through Iran's 2025 70-day blackout — see the
[DNS-tunnel comparison](https://moav.sh/docs/protocols/#which-one-should-i-use). Keep a normal
proxy config (Reality/CDN) as the primary and MasterDNS as the emergency
fallback.
---
## 6. GooseRelay — SOCKS5 fronted through Google
MahsaNG v16 bundles the **GooseRelay** client; MoaV pins the server to
GooseRelay **v1.7.1** (fully interoperable with v1.6.x). GooseRelay tunnels
SOCKS5 through a Google Apps Script web app to your VPS — from Iran's
perspective the entire connection looks like HTTPS to `google.com`, which is
extremely hard to block.
**Server setup (operator):**
```bash
# .env
ENABLE_GOOSERELAY=true
PORT_GOOSE=8444 # must be reachable from Google's servers
```
Rerun bootstrap; the server generates a shared `tunnel_key` (AES-256-GCM).
Each user bundle then contains three ready-made GooseRelay files (nothing to
hand-edit except pasting in one ID):
- **`gooserelay-AppsScript.gs`** — the v1.7.1 Apps Script forwarder with the
`RELAY_URLS` array **already pointed at this server**. Paste as-is.
- **`gooserelay-client_config.json`** — a complete client config (tunnel_key,
SNI, tuning) with only the Deployment ID left to fill.
- **`gooserelay-instructions.txt`** — the short walkthrough below.
**User setup (one-time):**
1. Open → New project
2. Paste the **whole** of `gooserelay-AppsScript.gs` (no editing — the
`RELAY_URLS` array is already filled in)
3. Deploy → New deployment → Web app → Execute as: Me, Access: Anyone → copy the Deployment ID
4. In `gooserelay-client_config.json`, replace `REPLACE_WITH_YOUR_APPS_SCRIPT_DEPLOYMENT_ID` with that Deployment ID
5. Load `gooserelay-client_config.json` into the GooseRelay client, or paste it into MahsaNG v16's **GooseRelay tab**
**Notes:**
- No domain or DNS delegation needed — only the server IP + port 8444
- Google Apps Script quota: ~20,000 calls/day per Google account; add multiple accounts for higher capacity
- The `tunnel_key` is shared (not per-user); keep it secret
- GooseRelay is **opt-in** (`ENABLE_GOOSERELAY=false` by default) — existing deployments are unaffected until you enable it
---
## 7. Tips for Iran conditions
- **Lead with Reality, keep CDN ready.** If the server IP gets blocked, the
CDN config keeps working without any change on the server.
- **Enable MahsaNG's Fragment / fake-SNI** in its settings — these are
client-side and complement (don't replace) the MoaV protocol choice.
- **Hysteria2 last.** It's the fastest when it works, but UDP is the first
thing to get throttled during heavy censorship.
- **Rotate via subscription.** Using Method A means you can rotate a user's
configs server-side (`moav regenerate-users`) and they refresh on the next
subscription update — no need to resend links.
- **One user per person.** Per-user configs let you `moav user revoke` a
leaked identity without disrupting everyone else.
See also: [Supported Protocols](https://moav.sh/docs/protocols/) ·
[DNS Configuration](https://moav.sh/docs/DNS/) · [Client Apps](https://moav.sh/docs/CLIENTS/) ·
[CLI Reference](https://moav.sh/docs/CLI/).
---
# MoaV CLI Reference
Complete reference for the `moav` command-line interface.
!!! tip "`moav help` is the short version"
Every command below is listed by `moav help`, grouped the same way. This page adds
the flags, the behaviour, and worked examples. Running plain `moav` opens an
interactive menu over the same commands, so nothing here has to be memorised.
## Installation
```bash
# Install moav command globally (run from /opt/moav)
./moav.sh install
# Or using the script directly
./moav.sh [command]
# Uninstall global command
moav uninstall
```
After installation, `moav` is available from any directory.
---
## Quick Reference
```bash
moav # Interactive menu
moav help # Show help
moav start # Start all services
moav stop # Stop all services
moav status # Show service status
moav logs # View logs (follow mode)
moav doctor # Run diagnostics
moav user add NAME # Add new user
moav user add --batch 5 # Batch create 5 users
moav user revoke NAME # Revoke user
moav test USERNAME # Test connectivity
moav cert status # TLS cert expiry + renewal status
moav admin password # Reset admin password
moav donate # Donate configs to MahsaNet
```
---
## Commands
### General
#### `moav` (no command)
Launch the interactive menu (TUI) — the guided way to run everything, nothing to memorize. It shows your current status (running services, and the admin + Grafana URLs when they're up) and these options:
```bash
moav
```
```
Services
1) Start services # moav start — pick profiles, or "all"
2) Stop services # moav stop
3) Restart services # moav restart
4) View status # moav status
5) View logs # moav logs
Users & donations
6) User management # add / list / revoke / package users → client bundles
7) Donate configs # share configs to MahsaNet / Psiphon / Snowflake
System
8) Doctor # moav doctor — diagnose DNS, ports, certs, services
9) Admin password reset # moav admin password
10) Update MoaV # moav update
11) Build/rebuild # moav build
12) Export / Import # back up, or migrate to another server
0) Exit
```
Every item maps to a `moav` subcommand documented below — the menu is just a friendlier front door.
#### `moav help`
Display help message with all available commands.
```bash
moav help
moav --help
moav -h
```
#### `moav version`
Show MoaV version.
```bash
moav version
moav --version
```
#### `moav install`
Install `moav` command globally to `/usr/local/bin`.
```bash
./moav.sh install
```
#### `moav uninstall`
Remove MoaV containers and global command.
```bash
moav uninstall # Remove containers, keep data (.env, keys, bundles)
moav uninstall --wipe # Remove EVERYTHING (fresh install ready)
```
**Without `--wipe` (default):**
- Stops and removes all Docker containers
- Removes the global `moav` command
- Preserves: `.env`, keys, certificates, user bundles, Docker volumes
**With `--wipe`:**
- Removes all Docker containers AND volumes
- Removes `.env` and all generated configs
- Removes all keys and certificates
- Removes all user bundles
- Removes the global `moav` command
After `--wipe`, run `cp .env.example .env` and `./moav.sh` for a fresh setup.
---
### Setup & Configuration
#### `moav check`
Run prerequisites check (Docker, dependencies, ports).
```bash
moav check
```
#### `moav doctor`
Run diagnostic checks for common MoaV issues.
```bash
moav doctor # Run all checks
moav doctor docker # Docker and prerequisites
moav doctor memory # RAM availability
moav doctor disk # Disk space
moav doctor logs # Container log file sizes (offers to truncate oversized)
moav doctor dns # DNS records
moav doctor services # Enabled vs running services
moav doctor config # Config files and keys
moav doctor ports # Port availability
moav doctor conflicts # DNS-tunnel health (all 4 share port 53 via dns-router)
moav doctor env # Compare .env with .env.example
moav doctor updates # Check for MoaV updates
```
**Available checks:**
- `docker` — Docker daemon running, Compose available, Docker disk usage summary
- `memory` — Total RAM, available memory, warns if <1GB or <2GB with monitoring enabled
- `disk` — Disk space on root and Docker partition, warns if <2GB free
- `logs` — Scans `/var/lib/docker/containers/*/*-json.log` for files >100 MB; lists oversized files by container name and prompts to truncate in place. `truncate -s 0` keeps the FD live so Docker keeps writing — no service restart needed. Skips the prompt in non-interactive sessions (cron / piped runs) and prints the manual command instead. Pre-1.7.6 containers keep growing under Docker's unbounded default until they're recreated; this check is the fastest way to reclaim that space without a full restart
- `dns` — Verify DNS records for enabled protocols (A records, NS delegation, CDN)
- `services` — Compare enabled services in `.env` with running containers; flag crash-looping services
- `config` — Check bootstrap has been run and config files exist for enabled protocols
- `ports` — Verify required ports are listening; detect systemd-resolved on port 53
- `conflicts` — Check DNS-tunnel health. All four tunnels (dnstt, Slipstream, MasterDNS, XDNS) share port 53 via `dns-router`, fanned out by subdomain (`t.`/`s.`/`m.`/`x.`), so they coexist — this verifies enabled-vs-running state and that `dns-router` isn't crash-looping. Toggle individual tunnels with `ENABLE_*` or `moav switch-dns`
- `env` — Compare `.env` with `.env.example` for missing variables; flag critical missing vars
- `updates` — Check current version against latest GitHub release
#### `moav bootstrap`
Run first-time setup. Generates keys, obtains TLS certificates, creates initial users.
```bash
moav bootstrap
```
This command:
1. Checks prerequisites
2. Prompts for domain, email, admin password (if not in .env)
3. Generates Reality and dnstt keypairs
4. Obtains Let's Encrypt certificate
5. Creates initial users
6. Generates user bundles
#### `moav domainless`
Enable domainless mode for servers without a domain.
```bash
moav domainless
```
Available services in domainless mode:
- Reality (VLESS+Reality)
- XHTTP (VLESS+XHTTP+Reality)
- WireGuard (direct + wstunnel)
- AmneziaWG (obfuscated WireGuard)
- Telegram MTProxy (fake-TLS)
- Admin dashboard (self-signed certificate)
- Conduit (Psiphon bandwidth donation)
- Snowflake (Tor bandwidth donation)
#### `moav profiles`
Interactively change default services for `moav start`.
```bash
moav profiles
```
Saves selection to `DEFAULT_PROFILES` in `.env`.
#### `moav update`
Update MoaV from git repository.
```bash
moav update # Update from current branch
moav update -b dev # Switch to dev branch and update
moav update -b main # Switch back to main branch
```
**Options:**
- `-b BRANCH` - Switch to specified branch before updating
If local changes are detected, you'll be prompted to stash or discard them.
#### `moav setup-dns`
Free port 53 for dnstt by disabling systemd-resolved.
```bash
moav setup-dns
```
This command:
1. Stops systemd-resolved
2. Disables it from starting on boot
3. Configures /etc/resolv.conf with public DNS servers
---
### Service Management
#### `moav start`
Start services.
```bash
moav start # Start DEFAULT_PROFILES from .env
moav start all # Start all services whose ENABLE_* is true
moav start proxy # Start proxy profile only
moav start proxy admin # Start multiple profiles
moav start proxy wireguard admin # Start three profiles
```
**Arguments:**
- No arguments: uses `DEFAULT_PROFILES` from `.env`
- Profile names: start specific profiles (space-separated)
- `--force` / `-f`: bypass the profile-filtering prompt (see below)
##### Disabled profiles
`moav start` respects the `ENABLE_*` flags in `.env`:
- **No args** or **`moav start all`**: starts only the profiles whose `ENABLE_*` is `true`. Anything disabled is skipped (you'll see `Skipping disabled profiles: `).
- **`moav start `** for a profile that's disabled in `.env`: you get a prompt:
1. **Enable + start** — flips `ENABLE_*=true` in `.env`, then starts (persists for next time).
2. **Skip** — don't start; `.env` stays as-is.
3. **Start once** — start now without modifying `.env` (won't auto-start next time).
For profiles backed by multiple flags (`proxy`, `dnstunnel`), option 1 shows which flags to set manually. `--force` bypasses the prompt. Non-interactive shells default to skip.
#### `moav stop`
Stop services.
```bash
moav stop # Stop all running services
moav stop sing-box # Stop specific service
moav stop conduit snowflake # Stop multiple services
moav stop -r # Stop and remove containers
moav stop sing-box -r # Stop specific service and remove container
```
**Options:**
- `-r` - Remove containers after stopping (not just stop)
#### `moav restart`
Restart services.
```bash
moav restart # Restart all running services
moav restart sing-box # Restart specific service
moav restart sing-box admin # Restart multiple services
```
#### `moav status`
Show status of all services.
```bash
moav status
```
Displays:
- Container status (running/stopped)
- Health status
- Port mappings
- Uptime
#### `moav logs`
View service logs.
```bash
moav logs # All logs, follow mode (Ctrl+C to exit)
moav logs sing-box # Specific service logs
moav logs sing-box conduit # Multiple services
moav logs -n # Last 100 lines, no follow
moav logs sing-box -n # Specific service, no follow
moav logs -f conduit # Explicit follow mode
```
**Options:**
- `-n`, `--no-follow` - Show last 100 lines without following
- `-f`, `--follow` - Follow mode (default)
#### `moav build`
Build Docker images.
```bash
moav build # Build all images
moav build sing-box # Build specific image
moav build conduit snowflake # Build multiple images
```
---
### Network Tuning
#### `moav net [status|apply|revert]`
Kernel network tuning: BBR congestion control plus larger socket buffers. Circumvention
traffic takes long, often lossy paths out of censored networks, which is exactly where
the default (`cubic`) collapses and BBR holds throughput. Larger UDP buffers stop the
QUIC/UDP protocols (Hysteria2, WireGuard) dropping packets under load.
```bash
moav net # status: current vs recommended sysctl values (default)
moav net status
moav net apply # write the tuning bundle and reload sysctl
moav net revert # remove it and reload
```
Everything is written to one dedicated sysctl file, so `revert` is clean and leaves no
trace in your other sysctl config. The installer offers to apply this during setup.
### Certificates
TLS certificates for Trojan, Hysteria2, CDN VLESS, AnyTLS, and the admin UI come from Let's Encrypt (via the bundled `certbot` service) when a `DOMAIN` is set. They expire after 90 days, so MoaV manages renewal for you.
#### `moav cert status`
Show certificate expiry and whether the auto-renewal schedule is installed.
```bash
moav cert status
```
In domainless mode this reports that no Let's Encrypt certificates are in use and exits cleanly.
#### `moav cert renew`
Run a renewal check now. Renews only if the certificate is within Let's Encrypt's renewal window, and restarts the services that load certs at startup (sing-box, wstunnel, trusttunnel, admin, grafana, grafana-proxy) **only if the certificate actually changed**.
```bash
moav cert renew
```
> Do **not** use `docker compose run --rm certbot renew` directly — the compose `certbot` service overrides its entrypoint for one-shot issuance, so that command runs `/bin/sh renew` and never invokes certbot. `moav cert renew` forces the correct entrypoint.
#### `moav cert install`
Install the daily auto-renewal schedule. Uses a **systemd timer** (`moav-cert-renew.timer`) where available, falling back to `/etc/cron.d/moav-cert-renew` on hosts without systemd. The timer runs daily with a randomized delay, well within the renewal window.
```bash
moav cert install
```
This is installed **automatically** the first time `moav start` runs with a `DOMAIN` set. Opt out by setting `CERT_AUTORENEW=false` in `.env`.
#### `moav cert uninstall`
Remove the auto-renewal timer/cron job (certificates then expire in ≤90 days unless renewed manually).
```bash
moav cert uninstall
```
**Recovering an already-expired certificate:** `moav update` then `moav cert renew` — certbot reissues the cert in place (the webroot challenge on port 80 still works), and the timer self-installs on the next `moav start`.
---
### User Management
#### `moav users`
List all users.
```bash
moav users
moav user list # Same as above
```
#### `moav user add`
Add one or more users to all services. Users can also be created from the **Admin Dashboard** (User Bundles → + Create User).
```bash
# Single user
moav user add john # Add user 'john'
moav user add john --package # Add user and create zip bundle
moav user add john -p # Short form
# Multiple users
moav user add alice bob charlie # Add three users
moav user add alice bob charlie -p # Add three users with zip packages
# Batch mode (auto-numbered)
moav user add --batch 5 # Create user01, user02, ..., user05
moav user add --batch 10 --prefix team # Create team01, team02, ..., team10
moav user add --batch 5 --prefix dev -p # Create dev01..dev05 with packages
```
**Options:**
- `--package`, `-p` - Create distributable zip file with HTML guide
- `--batch N`, `-b N` - Create N users with auto-generated names
- `--prefix NAME` - Prefix for batch usernames (default: "user")
**Batch mode features:**
- Smart numbering: if user01-user03 exist, `--batch 2` creates user04, user05
- Services reload once at the end (not after each user)
- Shows progress for each user and summary at the end
Creates bundle in `outputs/bundles/USERNAME/` containing:
- Config files for all protocols
- QR codes for mobile import
- README.html with instructions
#### `moav user revoke`
Revoke a user from all services.
```bash
moav user revoke john # Revoke user 'john'
```
Removes user from:
- sing-box config (Reality, Trojan, Hysteria2, CDN)
- WireGuard config
- TrustTunnel credentials
- Deletes user bundle
#### `moav user package`
Create distributable zip for an existing user.
```bash
moav user package john # Creates outputs/bundles/john.zip
```
#### V2Ray subscription (in every bundle)
Every user bundle includes a standard base64 **V2Ray subscription** — both as
`outputs/bundles//subscription.txt` and as a click-to-copy block at the
top of the bundle's `README.html`. Paste it once into
[MahsaNG](https://github.com/GFW-knocker/MahsaNG), v2rayNG, Hiddify, Streisand,
or any V2Ray app to import all proxy protocols at once — Reality, CDN, XHTTP,
Trojan, Shadowsocks-2022, Hysteria2 (standard `vless://`/`trojan://`/`ss://`/
`hysteria2://` URIs, IPv4 + IPv6).
WireGuard/AmneziaWG/TrustTunnel/DNS-tunnel/GooseRelay/Telegram are intentionally
excluded (not subscription-importable; the DNS tunnels and GooseRelay are set up
in their own app tabs). Full walkthrough: [docs/mahsanet.md](https://moav.sh/docs/mahsanet/).
#### `moav user gooserelay`
Print GooseRelay setup instructions for a user (extracted from their bundle).
```bash
moav user gooserelay john # Print tunnel_key + Apps Script setup guide
```
GooseRelay is opt-in (`ENABLE_GOOSERELAY=true` in `.env`). When enabled, each
user bundle includes `gooserelay-instructions.txt` with the shared `tunnel_key`
and a step-by-step guide for deploying the Google Apps Script forwarder. See
[docs/protocols.md → GooseRelay](https://moav.sh/docs/protocols/#gooserelay) for full details.
---
### Testing & Client
#### `moav test`
Test connectivity for a user across all protocols.
```bash
moav test john # Test all protocols
moav test john --json # Output results as JSON
moav test john -v # Verbose output for debugging
moav test john --verbose # Same as above
```
**Options:**
- `--json` - Output results in JSON format
- `-v`, `--verbose` - Show detailed debug output
Tests: Reality, Trojan, Hysteria2, TrustTunnel, WireGuard, dnstt, Slipstream, MasterDNS
**Sample output:**
```
═══════════════════════════════════════════════════════════════
MoaV Connection Test Results
═══════════════════════════════════════════════════════════════
Config: /bundles/john
Time: Wed Jan 28 10:30:00 UTC 2026
───────────────────────────────────────────────────────────────
✓ reality Connected via VLESS/Reality
✓ trojan Connected via Trojan
✓ hysteria2 Connected via Hysteria2
✓ wireguard Config valid, endpoint reachable
○ dnstt No dnstt config found in bundle
○ slipstream No slipstream config found in bundle
○ masterdns No masterdns config found in bundle
═══════════════════════════════════════════════════════════════
```
#### `moav client`
Client mode commands.
```bash
moav client # Show client help
moav client build # Build client Docker image
moav client test john # Same as 'moav test john'
moav client connect john # Connect as user (exposes proxy)
```
#### `moav client connect`
Connect through your MoaV server and expose local proxy.
```bash
moav client connect john # Auto-detect best protocol
moav client connect john --protocol reality # Use specific protocol
moav client connect john -p hysteria2 # Short form
```
**Options:**
- `--protocol`, `-p` - Specify protocol (default: auto)
**Protocols:** `auto`, `reality`, `trojan`, `hysteria2`, `wireguard`, `tor`, `dnstt`, `slipstream`, `masterdns`
**Proxy endpoints (configurable in .env):**
- SOCKS5: `localhost:10800` (CLIENT_SOCKS_PORT)
- HTTP: `localhost:18080` (CLIENT_HTTP_PORT)
#### `moav client build`
Build the client Docker image.
```bash
moav client build
```
---
### Admin
#### `moav admin password`
Reset the admin dashboard password.
```bash
moav admin password # Prompts for new password (or generates random)
```
---
### Config Donation
#### `moav donate`
Donate VPN configs and bandwidth to help people bypass censorship. Supports three donation services:
- **MahsaNet** — Donate VPN config links to Mahsa VPN (2M+ users in Iran)
- **Psiphon Conduit** — Donate bandwidth to Psiphon's relay network (millions of users worldwide)
- **Tor Snowflake** — Donate bandwidth as a Tor Snowflake proxy
```bash
# Interactive donation wizard (shows all services, donates MahsaNet configs)
moav donate
# Configure donation services (MahsaNet API key, Conduit/Snowflake bandwidth)
moav donate setup
# Show all donation services status with live stats
moav donate status
# List donated MahsaNet configs
moav donate list
# Select and delete specific MahsaNet configs
moav donate delete
# Remove all donated MahsaNet configs
moav donate remove
# Show Conduit Ryve deep link and QR code
moav donate info
```
**Subcommands:**
- `setup` — Configure any donation service (menu: MahsaNet / Conduit / Snowflake)
- `status` — Show all 3 services: MahsaNet config stats, Conduit connected clients and bandwidth, Snowflake people served and bandwidth
- `list` — List all donated MahsaNet configs with status and health
- `delete` — Select and delete specific MahsaNet configs interactively
- `remove` — Remove all donated MahsaNet configs (with confirmation)
- `info` — Show Psiphon Conduit Ryve deep link and QR code for claiming in the Ryve app
**Configuration in `.env`:**
```bash
# MahsaNet
MAHSANET_API_KEY= # API key from mahsaserver.com/user/api
MAHSANET_PROTOCOLS="reality hysteria2" # Protocols to donate
MAHSANET_POOL=mahsa # Pool: mahsa, warp, popup, telegram
# Psiphon Conduit
CONDUIT_BANDWIDTH=100 # Bandwidth limit in Mbps
CONDUIT_MAX_COMMON_CLIENTS=200 # Max concurrent clients
# Tor Snowflake
SNOWFLAKE_BANDWIDTH=5 # Bandwidth limit in Mbps
SNOWFLAKE_CAPACITY=50 # Max concurrent clients
```
#### `moav conduit`
Show the Psiphon Conduit claim link, QR code, and sharing guide.
```bash
moav conduit # Same as 'moav conduit link'
moav conduit link # Ryve claim deep link + QR + sharing walkthrough
moav conduit status # Running state + connected clients / bandwidth
moav conduit help # Usage
```
While Conduit runs it already serves Psiphon users (including in Iran) through
the **public pool** — nothing needs to be shared for that. To give specific
people a private path, use **Personal Pairing**: import the station into the
Ryve app with the claim link this command prints, then generate a pairing
link inside Ryve.
> **⚠ Security:** the claim link/QR embeds this Conduit's **private key** (for
> your own phone's Ryve app). Treat it like a password — do **not** post it
> publicly. The link you share with users is the Personal Pairing link
> generated inside Ryve, not the claim link. `moav donate info` is an alias.
#### `moav conduit-offsets`
Manage the watcher that keeps Conduit's **lifetime bandwidth** Grafana panels accurate across container restarts.
```bash
moav conduit-offsets install # Install the systemd watcher
moav conduit-offsets status # Show watcher state (enabled/disabled, last run)
moav conduit-offsets uninstall # Remove the watcher
```
The watcher installs itself automatically the first time Conduit and monitoring run together — you usually don't need to touch this. Set `CONDUIT_OFFSETS_AUTOUPDATE=false` in `.env` to opt out. Hosts without systemd skip the watcher; run `scripts/update-conduit-offsets.sh` from cron instead. See [Monitoring → Conduit lifetime bandwidth](https://moav.sh/docs/MONITORING/#conduit-lifetime-bandwidth).
---
### Migration
#### `moav export`
Export full configuration backup.
```bash
moav export # Creates moav-backup-TIMESTAMP.tar.gz
moav export mybackup.tar.gz # Custom filename
```
**Backup includes:**
- `.env` configuration
- All cryptographic keys (Reality, WireGuard, dnstt)
- User credentials
- Generated user bundles
- TLS certificates
**Security:** Backup contains private keys. Transfer securely and delete after import.
#### `moav import`
Import configuration from backup.
```bash
moav import moav-backup-20240128.tar.gz
moav import /path/to/backup.tar.gz
```
Restores:
- `.env` file
- Keys and certificates
- User credentials
- User bundles
#### `moav migrate-ip`
Update SERVER_IP and regenerate all user configs.
```bash
moav migrate-ip 203.0.113.50 # Set new IP
moav migrate-ip $(curl -s api.ipify.org) # Auto-detect current IP
```
This command:
1. Updates `SERVER_IP` in `.env`
2. Regenerates all user bundle configs
3. Updates QR codes (if qrencode installed)
#### `moav regenerate-users`
Regenerate all user bundles with current .env settings.
```bash
moav regenerate-users
```
Use this after:
- Changing domain
- Enabling/disabling protocols
- Adding CDN_DOMAIN
- Changing any configuration that affects client configs
---
## Command Aliases
The dispatcher accepts several spellings for the same command. Use whichever you
remember; they are identical.
| Canonical | Also accepted |
|---|---|
| `cert` | `certificate`, `certs` |
| `switch-dns` | `dns-switch`, `dnsswitch`, `switch_dns` |
| `setup-dns` | `dns-setup`, `setup_dns` |
| `migrate-ip` | `migrateip`, `migrate_ip` |
| `regenerate-users` | `regen-users`, `regenerate_users` |
| `domainless` | `domain-less`, `no-domain` |
| `uninstall` | `remove` |
| `net` | `net-tuning` |
| `conduit-offsets` | `conduit_offsets`, `conduit-lifetime` |
| `users` | `user list` |
| `help` | `-h`, `--help` |
| `version` | `-v`, `--version` |
Service names have their own short forms: `wg` → wireguard, `awg` → amneziawg,
`tg` → telegram.
## Profiles
Profiles group related services. Each maps to one or more `ENABLE_*` flags in `.env`; `moav start` filters disabled profiles automatically (see [Profile filtering](#moav-start)).
| Profile | Services | Controlled by |
|---------|----------|---------------|
| `proxy` | sing-box, decoy, certbot | `ENABLE_REALITY` / `ENABLE_TROJAN` / `ENABLE_HYSTERIA2` / `ENABLE_SS` (any) |
| `xhttp` | xray | `ENABLE_XHTTP` |
| `wireguard` | wireguard, wstunnel, decoy, certbot | `ENABLE_WIREGUARD` |
| `amneziawg` | amneziawg | `ENABLE_AMNEZIAWG` |
| `dnstunnel` | dns-router, dnstt, slipstream, masterdns, xray (XDNS) | `ENABLE_DNSTT` / `ENABLE_SLIPSTREAM` / `ENABLE_MASTERDNS` / `ENABLE_XDNS` (any) |
| `trusttunnel` | trusttunnel | `ENABLE_TRUSTTUNNEL` |
| `telegram` | telemt | `ENABLE_TELEMT` |
| `admin` | admin, docker-proxy | `ENABLE_ADMIN_UI` |
| `conduit` | psiphon-conduit | `ENABLE_CONDUIT` |
| `snowflake` | snowflake, snowflake-exporter | `ENABLE_SNOWFLAKE` |
| `gooserelay` | gooserelay | `ENABLE_GOOSERELAY` (opt-in) |
| `monitoring` | prometheus, grafana, grafana-proxy, node-exporter, cadvisor + per-protocol exporters | `ENABLE_MONITORING` (opt-in) |
| `setup` | bootstrap, geoip-updater | (lifecycle, not user-toggled) |
| `client` | client | (for local testing) |
| `all` | All services above | (used by `moav start all`, `moav build`, `moav logs`, etc.) |
**Usage:**
```bash
moav start proxy admin # Start proxy and admin profiles
moav start all # Expands to every profile whose ENABLE_* is true
```
---
## Service Names & Aliases
| Service | Aliases |
|---------|---------|
| sing-box | `proxy`, `singbox`, `reality` |
| wireguard | `wg` |
| dnstt | `dns` |
| slipstream | `slip` |
| masterdns | `mdns` |
| psiphon-conduit | `conduit` |
**Usage:**
```bash
moav logs singbox # Same as 'moav logs sing-box'
moav restart wg # Same as 'moav restart wireguard'
moav stop conduit # Same as 'moav stop psiphon-conduit'
```
---
## Environment Variables
Key variables in `.env` that affect CLI behavior:
| Variable | Description | Default |
|----------|-------------|---------|
| `DEFAULT_PROFILES` | Profiles started by `moav start` | empty — `moav start` prompts, and `moav profiles` saves your choice here |
| `CLIENT_SOCKS_PORT` | SOCKS5 port for client mode | `10800` |
| `CLIENT_HTTP_PORT` | HTTP port for client mode | `18080` |
| `INITIAL_USERS` | Users created during bootstrap | `1` |
| `MAHSANET_API_KEY` | MahsaNet API key for config donation | (empty) |
| `MAHSANET_PROTOCOLS` | Protocols to donate to MahsaNet | `reality hysteria2` |
| `MAHSANET_POOL` | MahsaNet pool for donated configs | `mahsa` |
---
## Examples
### Complete Setup Flow
```bash
# 1. Install MoaV
curl -fsSL moav.sh/install.sh | bash
# 2. Configure environment
cd /opt/moav
cp .env.example .env
nano .env # Set DOMAIN, ACME_EMAIL, ADMIN_PASSWORD
# 3. Run bootstrap
moav bootstrap
# 4. Start services
moav start
# 5. Add a user
moav user add john --package
# 6. Download bundle
# Visit https://your-server:9443 or use SCP
```
### Daily Operations
```bash
# Check status
moav status
# View logs
moav logs sing-box
# Add new user
moav user add alice
# Add multiple users at once
moav user add alice bob charlie
# Batch create users (auto-numbered)
moav user add --batch 10 --prefix team --package
# Test user connectivity
moav test alice
# Update MoaV
moav update
```
### Server Migration
```bash
# On old server
moav export
# Copy backup to new server
scp moav-backup-*.tar.gz root@new-server:/opt/moav/
# On new server
cd /opt/moav
moav import moav-backup-*.tar.gz
moav migrate-ip $(curl -s api.ipify.org)
moav start
```
### Testing Development Branch
```bash
# Switch to dev branch
moav update -b dev
# Test changes
moav restart
# Return to stable
moav update -b main
```
### Bandwidth & Config Donation
```bash
# Set up donation services (MahsaNet API key, Conduit/Snowflake bandwidth)
moav donate setup
# Donate 5 VPN configs to MahsaNet
moav donate
# Enter: 5 for count, "mahsa" for prefix
# See all donation stats (MahsaNet configs + Conduit clients + Snowflake served)
moav donate status
# Configure Conduit bandwidth limit
moav donate setup # Select option 2
# Get Conduit Ryve deep link (for claiming in Ryve app)
moav donate info
# List/delete MahsaNet configs
moav donate list
moav donate delete
```
### Domain-less Quick Setup
```bash
# For servers without a domain
moav domainless
moav start wireguard admin conduit
moav user add john
```
---
# MoaV Monitoring Stack
Real-time observability for your MoaV deployment with Grafana dashboards.
A tour of the ten dashboards — 22s, 668 KB.
## Overview
The monitoring profile provides:
- **Prometheus** - Time-series database for metrics storage (15-day retention)
- **Grafana** - Beautiful dashboards for visualization
- **Node Exporter** - System metrics (CPU, RAM, disk, network)
- **cAdvisor** - Container metrics per service
- **Clash Exporter** - sing-box proxy metrics via Clash API
- **sing-box Exporter** - Per-user connections, protocol breakdown, GeoIP country stats
- **Xray Exporter** - Per-user connections and traffic (upload/download), GeoIP country stats
- **WireGuard Exporter** - VPN peer and traffic metrics, GeoIP country labels
- **AmneziaWG Exporter** - Per-peer traffic metrics, GeoIP country labels
- **Telemt Exporter** - MTProxy pool health, DC availability, upstream quality
- **Snowflake Exporter** - Tor donation metrics (people served, bandwidth donated)
- **GeoIP Database** - [DB-IP Lite](https://db-ip.com/db/lite.php) country database for offline IP-to-country lookups
## Quick Start
```bash
# Start with monitoring profile
moav start monitoring proxy admin
# Or add to existing deployment
moav start monitoring
```
## Access
Grafana is on **port 9444** over HTTPS: `https://your-domain:9444` (or the server IP). Log in as **`admin`** with your `ADMIN_PASSWORD` — the same one as the admin dashboard.
For a faster, CDN-fronted route see [Cloudflare CDN for Grafana](#cloudflare-cdn-for-faster-grafana-recommended) below.
## Pre-built dashboards
Ten dashboards are provisioned automatically — nothing to import or build. Grafana groups them under the **MoaV** folder.
| Dashboard | What it shows |
|---|---|
| **System** | CPU, memory, disk, network throughput for the host |
| **Containers** | Per-container CPU, memory, restarts (via cAdvisor) |
| **sing-box** | Per-user connections and traffic for Reality, Trojan, AnyTLS, Hysteria2, Shadowsocks, CDN |
| **WireGuard** | Peer handshakes, transfer per peer, last-seen |
| **AmneziaWG** | The same, for the obfuscated interface |
| **Xray (XHTTP)** | XHTTP / XDNS connections and throughput |
| **Telegram MTProxy** | telemt connections, pool health, config-change activity |
| **DNS Tunnels & GooseRelay** | Traffic across dnstt, Slipstream, MasterDNS, XDNS and GooseRelay. dnstt has no protocol-level metrics API, so its panels show container and network telemetry only |
| **Conduit** | Bandwidth donated to Psiphon users, lifetime totals |
| **Snowflake** | Bandwidth donated to Tor users, client connections |
Per-user series come from the Clash API via `clash-exporter`; WireGuard and AmneziaWG read interface state published by their containers, so the exporters never need the Docker socket.
## Conduit lifetime bandwidth
Conduit's live bandwidth gauges reset every time the container restarts. The **Lifetime Download / Lifetime Upload** panels work around that by adding a persistent offset back — so your cumulative donation totals keep growing across restarts.
This is automatic once monitoring and Conduit are both running. A systemd watcher banks the pre-restart total and reloads Prometheus the moment Conduit comes back up. Manage it with [`moav conduit-offsets`](https://moav.sh/docs/CLI/#moav-conduit-offsets); set `CONDUIT_OFFSETS_AUTOUPDATE=false` in `.env` to opt out. Hosts without systemd can run `scripts/update-conduit-offsets.sh` from cron.
## GeoIP Country Distribution
All four protocol dashboards (sing-box, Xray, WireGuard, AmneziaWG) include a "Geographic Distribution" row showing user connections by country.
### How it works
- A shared [DB-IP Lite Country](https://db-ip.com/db/lite.php) database (~5MB) provides offline IP-to-country lookups — no external API calls at runtime
- **sing-box**: polls the Clash API (`/connections`) for source IPs of active connections
- **Xray**: extracts source IPs from Xray access logs
- **WireGuard / AmneziaWG**: reads endpoint IPs from `wg show` / `awg show`
- Country codes are ISO 3166-1 alpha-2 (e.g., `IR`, `DE`, `US`)
### Setup
The GeoIP database must be downloaded once before country metrics appear:
```bash
# Download the GeoIP database (run once, or monthly to refresh)
docker compose --profile setup run --rm geoip-updater
```
The database is stored in a Docker volume (`moav_geoip`) and shared read-only with all exporters.
### Refreshing the database
DB-IP Lite is updated monthly. To refresh:
```bash
docker compose --profile setup run --rm geoip-updater
docker compose restart singbox-exporter xray-exporter wireguard-exporter amneziawg-exporter
```
### Using MaxMind GeoLite2 instead
The database format is MMDB — compatible with both DB-IP and MaxMind. To use MaxMind GeoLite2-Country instead:
```bash
# Download GeoLite2-Country.mmdb from maxmind.com (requires free account)
# Copy into the volume:
docker run --rm -v moav_geoip:/geoip -v /path/to/GeoLite2-Country.mmdb:/src/db.mmdb alpine \
cp /src/db.mmdb /geoip/dbip-country-lite.mmdb
```
### Graceful degradation
If the GeoIP database is not downloaded, all country lookups return `"XX"` (unknown) and existing metrics continue to work normally. The exporters log a warning on startup:
```
GeoIP: could not load /geoip/dbip-country-lite.mmdb: [Errno 2] No such file or directory
```
## Configuration
### Port Configuration
```bash
# .env
PORT_GRAFANA=9444 # External Grafana port (default: 9444)
```
### Cloudflare CDN for Faster Grafana (Recommended)
Grafana can be slow to load over high-latency connections due to large JS/CSS assets. You can use Cloudflare's CDN to cache static assets for much faster loading.
**Step 1: Add DNS Record**
In Cloudflare Dashboard, add:
| Type | Name | Content | Proxy |
|------|------|---------|-------|
| A | grafana | YOUR_SERVER_IP | **Proxied** (orange cloud) |
**Step 2: Configure Environment**
Add to your `.env` file:
```bash
GRAFANA_ROOT_URL=https://grafana.yourdomain.xyz:2083
```
**Step 3: Restart Services**
```bash
moav restart grafana grafana-proxy
```
**Step 4: Access Grafana**
Access via `https://grafana.yourdomain.xyz:2083` instead of `:9444`.
> **Note:** Port 2083 is used because Cloudflare only proxies specific HTTPS ports (443, 2053, 2083, 2087, 2096, 8443). The `grafana-proxy` service handles SSL termination and caching headers.
**Benefits:**
- Static assets (JS, CSS, images) cached at Cloudflare edge
- Gzip compression
- Faster global access
- WebSocket support for live dashboard updates
### Retention
Prometheus retains data for **15 days** by default. To change this, modify the `--storage.tsdb.retention.time` flag in `docker-compose.yml`:
```yaml
prometheus:
command:
- '--storage.tsdb.retention.time=30d' # 30 days
```
### Enabling/Disabling
```bash
# .env
ENABLE_MONITORING=true # Set to false to disable
```
## Resource Usage
> **Warning**: The monitoring stack nearly doubles MoaV's resource requirements. While MoaV alone runs on 1 vCPU / 1GB RAM, adding monitoring requires at least **2 vCPU / 2GB RAM** for stable operation.
Approximate additional resources when monitoring is enabled:
| Component | CPU | RAM | Disk |
|-----------|-----|-----|------|
| Prometheus | 0.1-0.3 cores | 200-500 MB | ~50 MB/day |
| Grafana | 0.1-0.2 cores | 100-200 MB | ~50 MB |
| Node Exporter | <0.1 cores | ~20 MB | - |
| cAdvisor | 0.1-0.3 cores | 50-150 MB | - |
| Clash Exporter | <0.1 cores | ~30 MB | - |
| WireGuard Exporter | <0.1 cores | ~10 MB | - |
| Snowflake Exporter | <0.1 cores | ~10 MB | - |
| **Total** | **~0.5-1 cores** | **~400-900 MB** | **~1 GB/15 days** |
### Minimum Requirements
| Configuration | vCPU | RAM | Notes |
|---------------|------|-----|-------|
| MoaV only | 1 | 1 GB | Bare minimum |
| MoaV + Monitoring | 2 | 2 GB | Recommended (for stats & monitoring) |
| MoaV + Monitoring + All Profiles | 2 | 4 GB | Production recommended |
**If you have only 1GB RAM**, do not enable monitoring - it could cause hangs and crashes.
## Security
- **Prometheus** is internal only (no external port exposed)
- **Grafana** requires authentication via `ADMIN_PASSWORD`
- All exporters expose metrics only to the internal Docker network
## What's Not Included
The following services do not currently expose metrics:
| Service | Reason |
|---------|--------|
| **TrustTunnel** | No metrics API available |
| **dnstt** | No metrics API available |
Container-level metrics (CPU, memory, network) are still available for these services via cAdvisor.
## Troubleshooting
Run `moav doctor services` to quickly check if monitoring services are running. For detailed issues, see [TROUBLESHOOTING.md](https://moav.sh/docs/TROUBLESHOOTING/#monitoring).
Common issues covered:
- System hangs after starting monitoring (RAM issues)
- Grafana shows "No Data"
- High memory usage from cAdvisor
- Snowflake metrics showing zeros
- WireGuard exporter not starting
## Blocked Registries (Iran, Russia, etc.)
If you're in a region where container registries are blocked (gcr.io, ghcr.io, Docker Hub), you can build images locally:
```bash
# Build commonly blocked images (cAdvisor from gcr.io, clash-exporter from ghcr.io)
moav build --local
# Build a specific image
moav build --local prometheus
# Build EVERYTHING locally (all services + all external images, no registry pulls)
moav build --local all
```
Available images for local build:
- `cadvisor` - Container metrics (gcr.io) **← commonly blocked**
- `clash-exporter` - Clash API exporter (ghcr.io) **← commonly blocked**
- `prometheus` - Time-series database (Docker Hub)
- `grafana` - Visualization dashboards (Docker Hub)
- `node-exporter` - System metrics (Docker Hub)
- `nginx` - Web server (Docker Hub)
- `certbot` - Let's Encrypt client (Docker Hub)
The build command automatically updates your `.env` to use the local images.
Alternatively, configure mirror registries manually in `.env`:
```bash
IMAGE_CADVISOR=mirror.example.com/cadvisor/cadvisor:latest
IMAGE_PROMETHEUS=mirror.example.com/prom/prometheus:latest
```
## CLI Commands
```bash
# Start monitoring only
moav start monitoring
# Start with other profiles
moav start monitoring proxy admin
# Build images locally (for blocked registries)
moav build --local
# View monitoring logs
moav logs prometheus
moav logs grafana
# Stop monitoring
moav stop prometheus grafana node-exporter cadvisor clash-exporter
```
## Customization
### Adding Custom Dashboards
Place JSON dashboard files in:
```
configs/monitoring/grafana/provisioning/dashboards/
```
Grafana automatically loads new dashboards within 30 seconds.
### Custom Prometheus Scrape Targets
Edit `configs/monitoring/prometheus.yml` to add new targets:
```yaml
scrape_configs:
- job_name: 'my-custom-exporter'
static_configs:
- targets: ['my-service:9999']
```
Then reload Prometheus:
```bash
docker exec moav-prometheus wget -qO- --post-data='' http://localhost:9091/-/reload
```
## Architecture
```
┌─────────────────┐
│ Cloudflare CDN │ (optional, for faster loading)
└────────┬────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│grafana-proxy│ │ Grafana │
│ :2083 (CDN) │ │ :9444 direct│
└──────┬──────┘ └──────┬──────┘
│ │
└───────┬────────┘
│
┌──────▼──────┐
│ Prometheus │ :9091 (internal)
│ (time-series│
│ storage) │
└──────┬──────┘
┌─────────┬──┴─────┬─────────┬─────────┐
│ │ │ │ │
┌──▼────┐ ┌──▼───┐ ┌──▼────┐ ┌──▼────┐ ┌──▼──────┐
│ node │ │cAdvsr│ │ clash │ │ wg │ │snowflke │
│export │ │(cntr)│ │(sngbx)│ │(vpn) │ │ (tor) │
└───────┘ └──────┘ └───────┘ └───────┘ └─────────┘
```
---
# Troubleshooting
Find your symptom below. Almost everything starts with one command, so run that first — it checks the things that are wrong most often and usually names the problem outright.
## Start Here: `moav doctor`
Before diving into specific issues, run the built-in diagnostics:
```bash
moav doctor
```
This runs all checks automatically and tells you exactly what's wrong:
- **docker** — Docker daemon running, Compose installed, disk usage
- **memory** — Available RAM, warns if too low for your config
- **disk** — Free disk space, warns before you run out
- **logs** — Container `json-file` log sizes; prompts to truncate any over 100 MB (useful for clearing pre-1.7.6 unbounded logs)
- **dns** — DNS records for your domain and enabled protocols
- **services** — Running containers vs enabled config, crash-looping detection
- **config** — Bootstrap status, config files exist for enabled protocols
- **ports** — Required ports listening, systemd-resolved conflicts
- **conflicts** — DNS-tunnel port-group collisions on port 53
- **env** — Missing `.env` variables compared to `.env.example`
- **updates** — New MoaV version available
You can also run individual checks:
```bash
moav doctor dns # Just DNS
moav doctor services # Just service status
moav doctor config # Just config files
```
If `moav doctor` identifies the issue, follow its hints. If not, continue below.
---
## Nothing connects
Start here if no protocol works at all. The pattern of what fails tells you where the problem is: the server, the network, or the client.
### Can't connect at all
1. **Verify server is reachable:**
```bash
ping YOUR_SERVER_IP
curl -I https://yourdomain.com
```
2. **Check if IP is blocked:**
- Try from a different network (mobile data)
- Use online tools to check if IP is accessible from Iran
3. **Try different protocols:**
Reality → Hysteria2 → Trojan → WireGuard → DNS tunnel
### TLS handshake timeout
**Causes:**
- Server certificate issue
- Deep packet inspection blocking
- Server overloaded
**Solutions:**
1. Try Reality protocol (doesn't use your cert)
2. Try Hysteria2 (uses UDP)
3. Check server certificate is valid
### All protocols blocked
When ISP blocks everything:
1. **DNS Tunnel** - Often still works as it's hard to block all DNS
2. **Different Reality targets** — Choose a domain that your ISP can't easily block (e.g., domestic banking or fintech sites). See [Choosing a Reality Target](https://moav.sh/docs/SETUP/#choosing-a-reality-target-sni) for how to pick and verify targets.
3. **Get a new server** - Your IP may be specifically blocked
### Protocol detected and blocked
Signs your protocol is detected:
- Works for a few minutes then dies
- Works initially then stops
- Specific protocol fails but others work
**Solutions:**
1. Switch protocols immediately
2. Change Reality target domain
3. Update to latest sing-box version (better anti-detection)
### Total internet shutdown
During major events, Govs sometimes shuts internet entirely:
1. DNS tunnel might still work (if any DNS works)
2. Satellite internet (Starlink) if available
3. Wait for restoration
---
### Works on WiFi but not mobile data
Mobile carriers may have different filtering:
- Try Hysteria2 (UDP-based)
- Try DNS tunnel
- Some carriers block all VPN signatures
### Works on mobile data but not WiFi
Home ISPs often have stricter filtering:
- Try Reality protocol
- Try different Reality target sites
- Try port 80 or other ports (if configured)
## Connected, but something is wrong
The tunnel establishes but traffic doesn't flow, or flows badly.
### WireGuard connected but no traffic
**Check if peer is loaded:**
```bash
docker compose exec wireguard wg show
```
Look for your peer's public key. It should show:
- `latest handshake: X seconds ago`
- `transfer: X received, X sent`
If there's no handshake, check for **key mismatch**:
```bash
# What the client config expects (server public key)
cat configs/wireguard/server.pub
# What's actually running
docker compose exec wireguard wg show wg0 public-key
```
**If keys don't match**, regenerate the bundles — the sync happens automatically:
```bash
moav regenerate-users
```
Since v2 every peer add reads the running container's public key and writes `server.pub` inline, so this mismatch heals itself on the next add or regenerate. (The old `scripts/wg-sync-keys.sh` that used to be needed here was removed in v2 — it had no callers left and the mismatch it patched no longer occurs.)
**Check NAT/masquerade:**
```bash
docker compose exec wireguard iptables -t nat -L -n | grep MASQUERADE
```
**Check IP forwarding:**
```bash
docker compose exec wireguard cat /proc/sys/net/ipv4/ip_forward
# Should return 1
```
**Check firewall allows WireGuard port:**
```bash
ufw allow 51820/udp
```
**Update MoaV if issue persists:**
Older versions had missing iptables rules for return traffic. Update and rebuild:
```bash
cd /opt/moav
moav update
docker compose --profile wireguard build --no-cache wireguard
moav restart wireguard
```
### Slow connection
**Hysteria2 often helps** - it's optimized for lossy networks.
**For sing-box clients:**
- Enable multiplexing
- Try different congestion control
**Check server resources:**
```bash
docker stats
htop
```
### Very slow despite connection working
1. **Check if throttled:**
- Speed test without VPN
- Speed test with VPN
- If VPN is significantly slower, you're being throttled
2. **Try Hysteria2:**
- Uses UDP which is sometimes less throttled
- Has built-in congestion control
3. **Try different times:**
- Filtering may be heavier during peak hours
---
### Frequent disconnections
1. **Enable keep-alive:**
- In client app, look for "persistent connection" or "keep-alive"
2. **Check server uptime:**
```bash
docker compose ps
uptime
```
3. **Check for IP blocks:**
- ISP may be actively disrupting connections
- Try rotating to a new server IP
### "Invalid config" errors
1. Ensure you're using the correct link for your app
2. Check for extra spaces or newlines in the link
3. Try importing the JSON file instead of the link
---
## One protocol won't connect
Everything else works, one protocol doesn't. Each entry below is that protocol's usual cause.
### sing-box crashes
**Check the logs:**
```bash
docker compose logs -f sing-box
```
**Common fixes:**
```bash
# Rebuild container
docker compose build --no-cache sing-box
docker compose up -d sing-box
# Reset configuration
docker compose --profile setup run --rm bootstrap
```
### Shadowsocks not working
- **Inbound missing from sing-box config** — confirm `ENABLE_SS=true` in `.env`, then `moav restart sing-box` (the inbound is templated in at bootstrap; flipping the flag without re-bootstrapping won't add it):
```bash
jq '.inbounds[] | select(.tag == "shadowsocks-in")' configs/sing-box/config.json
# if empty, the flag was off when bootstrap ran
moav bootstrap # or rerun bootstrap to splice it in
```
- **Server PSK missing** — `moav doctor config` will flag missing state keys. The server PSK lives at `state/keys/shadowsocks-server.psk`; per-user PSK at `state/users//shadowsocks.env`.
- **Outline app says "invalid key"** — Outline's iOS/Android app expects the standard `ss://` URI with SS-2022 multi-user encoding (`method:server_psk:user_psk` base64-encoded). The bundle's `shadowsocks.txt` has this format. NekoBox / Hiddify / Streisand handle the same URI.
- **Port 8388 blocked by ISP** — change `PORT_SS` in `.env` to a less-fingerprinted port (e.g., 4443, 8443 if Trojan is off) and rerun `moav restart sing-box`.
### WireGuard handshake timeout
If you see:
```
Handshake for peer 1 (SERVER:51820) did not complete after 5 seconds, retrying
```
This means UDP packets aren't reaching the server. Common causes:
1. **UDP port 51820 blocked** - Most common in restrictive networks
- Try WireGuard-wstunnel mode instead (tunnels over TCP/WebSocket)
2. **Server firewall:**
```bash
ufw allow 51820/udp
```
3. **Server WireGuard not running:**
```bash
docker compose --profile wireguard ps
# Should show wireguard as "running"
```
### WireGuard-wstunnel not connecting
If you see errors like:
```
Cannot connect to tcp endpoint SERVER:8080 due to timeout
```
1. **Open port 8080 on server firewall:**
```bash
ufw allow 8080/tcp
```
2. **Check wstunnel is running:**
```bash
docker compose --profile wireguard ps
# Both wireguard and wstunnel should be running
```
3. **Check wstunnel logs:**
```bash
docker compose logs wstunnel
```
4. **Rebuild after update** (if you updated MoaV):
```bash
docker compose --profile wireguard build wstunnel
docker compose --profile wireguard up -d
```
### AmneziaWG not connecting
**Check container is running:**
```bash
docker compose --profile amneziawg ps
docker compose logs amneziawg
```
**Common issues:**
1. **Port not open:**
```bash
ufw allow 51821/udp
```
2. **Config mismatch:**
- Obfuscation parameters (S1, S2, H1-H4) must match between server and client
- Re-download the user bundle if parameters are wrong
3. **awg-quick not found (client):**
- Install awg-tools from https://github.com/amnezia-vpn/amneziawg-tools/releases
- Or use the Amnezia VPN app which includes built-in support
### Hysteria2 not working
Hysteria2 uses **UDP port 443**. If it's not working but Reality/Trojan work:
1. **UDP is likely blocked** by your network - this is common in restrictive environments
2. Hysteria2 is designed for networks where TCP is throttled but UDP works
3. **Try other protocols** - Reality and Trojan use TCP and are more likely to work
**Verify server-side:**
```bash
# Check Hysteria2 is listening
docker compose logs sing-box | grep -i hysteria
# Test UDP connectivity (from another machine)
nc -vuz YOUR_SERVER_IP 443
```
### AnyTLS not connecting
AnyTLS runs inside the existing **sing-box** container on **TCP port 8445** and shares the Trojan TLS certificate for your `DOMAIN`. It is opt-in — make sure `ENABLE_ANYTLS=true` is set in `.env` and that you have re-run `moav restart sing-box` (or `moav bootstrap`).
**Check the container is running:**
```bash
docker compose ps sing-box
```
**Check the logs for the AnyTLS inbound:**
```bash
docker compose logs sing-box | grep -i anytls
# Look for the "anytls-in" inbound starting without errors
```
**Common issues:**
1. **Port not open:**
```bash
ufw allow 8445/tcp
```
Verify from another machine: `nc -vz YOUR_SERVER_IP 8445`
2. **Certificate issue:**
- AnyTLS uses the same Let's Encrypt certificate as Trojan (for your `DOMAIN`)
- If the cert is missing or expired, see [Certificate issues](#certificate-issues) and re-run `moav bootstrap`
3. **Client config error:**
- Verify the password matches `anytls.txt` in the user bundle (same password as Trojan/Hysteria2)
- Confirm the link points to your domain on port 8445 with `sni=` and `insecure=0`
4. **Client does not support AnyTLS (most common):**
- AnyTLS has narrower client support than VLESS/Trojan. Use a recent build of **Hiddify**, **sing-box (SFA/SFI)**, **NekoBox/NekoRay**, **Mihomo Party**, or **Shadowrocket 2.2.65+**
- Clients such as v2rayNG, Streisand, V2Box, and Clash Verge do **not** support AnyTLS and will fail to import the link — switch to a supported client or use another protocol (Reality, Trojan, Hysteria2)
### TrustTunnel not connecting
**Check container is running:**
```bash
docker compose --profile trusttunnel ps
docker compose logs trusttunnel
```
**Common issues:**
1. **Port not open:**
```bash
ufw allow 4443/tcp
ufw allow 4443/udp
```
2. **Certificate issue:**
- TrustTunnel uses the same Let's Encrypt certificate as other services
- If cert is missing, run `moav bootstrap` again
3. **Client config error:**
- Verify credentials match `trusttunnel.txt` in user bundle
- Check `trusttunnel.toml` has correct domain/IP
### XHTTP not connecting
**Check container is running:**
```bash
docker compose --profile xhttp ps
docker compose logs xray # the xhttp profile runs the xray service
```
**Common issues:**
1. **Port not open:**
```bash
ufw allow 2096/tcp
```
2. **Service not enabled:**
- XHTTP is experimental but enabled by default. Confirm `ENABLE_XHTTP=true` in `.env`
- Restart after enabling: `moav restart`
3. **Verify port is listening:**
```bash
ss -tlnp | grep 2096
```
4. **Client compatibility:**
- XHTTP requires Xray-compatible clients: V2rayNG, Hiddify, Streisand, V2Box, V2rayN, V2rayU, NekoBox
- Ensure your client app is updated to a version that supports XHTTP transport
### CDN VLESS+WS not working
> The `521` / `525` / `1016` response-code diagnosis lives in [DNS → Troubleshooting](https://moav.sh/docs/DNS/#troubleshooting); the Cloudflare settings it refers to are in [DNS → CDN mode](https://moav.sh/docs/DNS/#cdn-mode).
**No CDN config in the bundle at all?** Check `ENABLE_CDN` in `.env` — it is `false` by default, and no CDN links are generated until it is `true`. `moav doctor dns` reports whether CDN is enabled, whether the record resolves, and whether it is actually proxied.
**DNS lookup failure:**
If you see `lookup cdn.yourdomain.com: operation was canceled`:
1. Verify `cdn` subdomain exists in Cloudflare DNS
2. Check it's set to **Proxied** (orange cloud)
3. Wait for DNS propagation (up to 5 minutes)
**Connection refused:**
1. Verify port 2082 is open: `ufw allow 2082/tcp`
2. Check sing-box is listening: `docker compose logs sing-box | grep vless-ws`
**Cloudflare 521 "Web server is down":**
This usually means Cloudflare can't reach your origin on the correct port.
1. **Check Origin Rule exists** (most common cause):
- Go to Cloudflare → Rules → Origin Rules
- You need a rule that redirects `cdn.yourdomain.com` to port 2082
- Without this, Cloudflare connects to port 80 (wrong port)
- See [DNS.md Cloudflare section](https://moav.sh/docs/DNS/#provider-setup) for setup instructions
2. **Verify port 2082 is reachable:**
```bash
# From another machine, test direct access to your server
curl -s -o /dev/null -w "%{http_code}" http://YOUR_SERVER_IP:2082/test
# Should return 400 or 404 (sing-box responding)
```
3. **Check firewall:**
```bash
ufw allow 2082/tcp
```
4. **Verify sing-box is listening:**
```bash
docker compose logs sing-box | grep -i "vless-ws"
```
**Cloudflare 525 "SSL Handshake Failed":**
This means Cloudflare is trying HTTPS to your origin, but MoaV's CDN inbound on port 2082 is plain HTTP.
1. **Set SSL/TLS mode to Flexible** in Cloudflare dashboard:
- Go to **SSL/TLS** → **Overview** → Set to **Flexible**
- **Full** and **Full (Strict)** will NOT work — they make Cloudflare connect via HTTPS, but port 2082 doesn't speak TLS
2. **If you need Full SSL for other subdomains**, create a Configuration Rule:
- Go to **Rules** → **Configuration Rules** → **Create rule**
- Match: **Hostname** equals `cdn.yourdomain.com`
- Setting: **SSL** → **Flexible**
- This overrides the zone-wide SSL mode for just the CDN subdomain
**Cloudflare 520 "Unknown error":**
1. Set SSL/TLS mode to **Flexible** in Cloudflare dashboard (see 525 section above)
2. Verify sing-box container is running
3. Check sing-box config has `vless-ws-in` inbound on port 2082
### CloudFront CDN: `bad "Sec-WebSocket-Key" header`
```
inbound/vless[vless-ws-in]: process connection from 15.158.x.x: upgrade websocket connection: handshake error: bad "Sec-WebSocket-Key" header
```
CloudFront is stripping WebSocket upgrade headers before forwarding to your server. Two things to fix:
**1. CDN_TRANSPORT must be `ws`** (not `httpupgrade`):
```bash
# Check current sing-box config
docker exec moav-sing-box cat /etc/sing-box/config.json | jq '.inbounds[] | select(.tag == "vless-ws-in") | .transport.type'
# Must return "ws". If it returns "httpupgrade", fix .env and re-bootstrap:
# Set CDN_TRANSPORT=ws in .env, then: moav bootstrap && moav restart sing-box
```
**2. CloudFront must have `AllViewer` Origin Request Policy:**
```bash
# Check current policies
aws cloudfront get-distribution --id YOUR_DIST_ID \
--query 'Distribution.DistributionConfig.DefaultCacheBehavior.{Cache: CachePolicyId, OriginRequest: OriginRequestPolicyId}' \
--output table
```
Expected:
- CachePolicyId: `4135ea2d-6df8-44a3-9df3-4b5a84be39ad` (CachingDisabled)
- OriginRequestPolicyId: `216adef6-5c7f-47e4-b989-5492eafa07d3` (AllViewer)
If `OriginRequestPolicy` is `None`, CloudFront drops the `Sec-WebSocket-Key`, `Upgrade`, and `Connection` headers. Fix:
```bash
aws cloudfront get-distribution-config --id YOUR_DIST_ID > /tmp/cf-config.json
jq '.DistributionConfig.DefaultCacheBehavior.OriginRequestPolicyId = "216adef6-5c7f-47e4-b989-5492eafa07d3" | .DistributionConfig.DefaultCacheBehavior.CachePolicyId = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" | .DistributionConfig' /tmp/cf-config.json > /tmp/cf-update.json
ETAG=$(jq -r '.ETag' /tmp/cf-config.json)
aws cloudfront update-distribution --id YOUR_DIST_ID --if-match "$ETAG" --distribution-config file:///tmp/cf-update.json
```
Wait 5-10 minutes for CloudFront deployment, then test:
```bash
curl -s -o /dev/null -w "%{http_code}" https://d1234abcd.cloudfront.net/test
# Should return 400 (sing-box responding to non-WebSocket request)
```
### DNS tunnel not working
> Record setup and the four NS delegations are in [DNS Configuration](https://moav.sh/docs/DNS/#with-a-domain-the-records); what each tunnel is and which to choose is in [Protocols → DNS Tunnels](https://moav.sh/docs/protocols/#dns-tunnels).
> **Quick check:** Run `moav doctor dns` to verify NS delegation for DNS tunnel subdomains, and `moav doctor ports` to check port 53 conflicts.
**Enabling/disabling individual tunnels:** All four DNS tunnels share port 53 via `dns-router` (queries fanned out by subdomain suffix). Enable or disable each independently in `.env`:
```bash
# All four DNS tunnels are on by default:
ENABLE_DNSTT=true
ENABLE_SLIPSTREAM=true
ENABLE_MASTERDNS=true
ENABLE_XDNS=true # needs FinalMask-aware client; set false to opt out
PORT_DNS=53 # dns-router public port (owns port 53)
PORT_XDNS=5356 # xray XDNS secondary host port
```
Or use `moav switch-dns` to manage tunnel daemons: `moav switch-dns dnstt+slipstream+masterdns+xdns` (all four) or `moav switch-dns off`.
**Check logs for domain issues:**
```bash
docker compose logs dnstt # dnstt
docker compose logs xray # XDNS (runs inside xray container)
docker compose logs dns-router # DNS routing (all tunnels)
```
If you see `NXDOMAIN: not authoritative for example.com`, the domain wasn't set correctly during bootstrap:
```bash
# Check the config file
cat configs/dnstt/server.conf
# Should show: DNSTT_DOMAIN=t.yourdomain.com (not example.com)
# If wrong, update it
sed -i 's/example.com/yourdomain.com/g' configs/dnstt/server.conf
# Rebuild and restart dnstt
docker compose build dnstt
docker compose --profile dnstunnel up -d dnstt
```
**Verify NS delegation:**
```bash
dig NS t.yourdomain.com
# Should return dns.yourdomain.com (or your server)
```
**Test dnstt server:**
```bash
docker compose logs dnstt
# Should show "listening on :5353" and your correct domain
```
**Check firewall:**
```bash
# Ensure UDP 53 is open
ufw allow 53/udp
# or
iptables -A INPUT -p udp --dport 53 -j ACCEPT
```
#### dnstt connects but no traffic flows (`begin session` but no `begin stream`)
**Symptom:** the client (especially **MahsaNG v16**) reports `TLS handshake timeout` / "failed to detect internet" and never actually passes traffic. `docker compose logs dnstt` shows `begin session ` repeating with a **new id each time** and **no** `begin stream`.
**Cause:** this is almost always a **client-side MTU that's too high**, not a server problem. The tiny session-handshake packets get through (so the session opens), but the larger stream-open packets exceed what the DNS path/resolver will carry and get dropped — so a stream never opens and the client keeps retrying with fresh sessions.
**First, confirm the server is fine** — run a stock `dnstt-client` against your own server, bypassing your app entirely. If it fetches your server's IP, the whole server chain (NS → resolver → dns-router → dnstt → sing-box egress) is correct and the problem is purely the client:
```bash
# Replace PUBKEY (from outputs/dnstt/server.pub) and t.yourdomain.com
docker run --rm --network host \
-e GOPROXY='https://proxy.golang.org|https://goproxy.cn|direct' -e GOSUMDB=off \
golang:1.24-alpine sh -c '
apk add --no-cache git curl >/dev/null
git clone https://www.bamsoftware.com/git/dnstt.git /src >/dev/null 2>&1 || git clone https://repo.or.cz/dnstt.git /src >/dev/null 2>&1
cd /src/dnstt-client && go build -o /usr/local/bin/dnstt-client .
dnstt-client -udp 8.8.8.8:53 -pubkey PUBKEY t.yourdomain.com 127.0.0.1:7000 &
sleep 8
curl -s --socks5-hostname 127.0.0.1:7000 -m 40 https://api.ipify.org; echo
'
```
Watch `moav logs -f dnstt` alongside it: if you see `begin stream` (not just `begin session`) and curl returns your server IP, the server is **verified good** — the standalone client works because it negotiates a small `effective MTU` (~132).
**Fix (client side):** lower the client's dnstt MTU.
- The **standalone dnstt-client** picks a safe MTU automatically — it works out of the box.
- **MahsaNG v16 does not expose a dnstt MTU control**, so dnstt often opens a session but never a stream there. On MahsaNG, prefer **MasterDNS** (the native DNS tunnel in v16 — it manages its own small MTU, e.g. upload 109 / download 500, and works without tuning) or **Slipstream**; use the standalone dnstt-client when you specifically need dnstt with a tunable MTU.
> The same "session opens, nothing flows" logic applies to any DNS tunnel: the server side is almost never the culprit if the isolation test above passes — check the client's MTU/transport (UDP vs DoH) settings.
#### dnstt stream opens but resets immediately (`connection reset by peer` on `:1080`)
**Symptom:** the dnstt log shows `begin stream` followed immediately by `copy stream←upstream: … ->…:1080: read: connection reset by peer` and `end stream`, repeating. The client app reports something like `SSH Tunnel established` then `Handshake timeout`.
**Cause — client-type mismatch, by design (not a bug):** the client is an **SSH-over-DNS app** (HTTP Injector / Dark Tunnel / "SSH + DNSTT" style) that expects an **SSH server** at the far end of the tunnel. MoaV's DNS tunnels forward to **sing-box's SOCKS5 inbound** (`sing-box:1080`), *not* an SSH host — so sing-box receives SSH handshake bytes it can't parse and resets the connection.
**MoaV's DNS tunnels (dnstt, Slipstream, MasterDNS) are a SOCKS5 transport, not an SSH host.** Use a client that speaks SOCKS5 to the tunnel endpoint:
- the standalone **dnstt-client**, with your app pointing SOCKS5 at its local listener;
- **MahsaNG v16** (use its native **MasterDNS** tunnel);
- **v2ray / sing-box / Xray** clients using the local dnstt port as a SOCKS proxy.
SSH-tunnel apps that require an SSH account behind dnstt are **not supported** — there is no SSH server behind MoaV's DNS tunnels.
---
## Server and services
The stack itself: containers, certificates, disk, the dashboard.
### Services won't start
> **Quick check:** Run `moav doctor services` to see which services are enabled vs running.
**Check logs:**
```bash
docker compose logs sing-box
docker compose logs certbot
```
**Common causes:**
1. **Certificate not obtained:**
```bash
# Check if cert exists
docker compose exec sing-box ls -la /certs/live/
# Re-run certbot
docker compose run --rm certbot certonly --standalone \
--non-interactive --agree-tos \
--email YOUR_EMAIL --domains YOUR_DOMAIN
```
2. **Port already in use:**
```bash
# Check what's using port 443
ss -tlnp | grep 443
# Stop conflicting service
systemctl stop nginx # or apache2
```
3. **Port 53 already in use (for dnstt):**
This is usually caused by systemd-resolved:
```bash
# Check what's using port 53
ss -ulnp | grep 53
# Stop and disable systemd-resolved
systemctl stop systemd-resolved
systemctl disable systemd-resolved
# Set up direct DNS resolution
echo -e "nameserver 1.1.1.1\nnameserver 8.8.8.8" > /etc/resolv.conf
```
4. **Configuration error:**
```bash
# Validate sing-box config
docker compose exec sing-box sing-box check -c /etc/sing-box/config.json
```
5. **Docker network error ("network not found"):**
This happens when Docker networks get corrupted from failed runs:
```bash
# Stop all containers and remove networks
docker compose down
docker network prune -f
# Start fresh
docker compose --profile all up -d
```
6. **Only some images built:**
All services require `--profile` to be specified:
```bash
# Build ALL images including optional services
docker compose --profile all build --no-cache
# Build only proxy services
docker compose --profile proxy build
# Available profiles: proxy, wireguard, dnstt, trusttunnel, admin, conduit, snowflake, monitoring, all
```
7. **Port already in use (8443 for Trojan):**
Change the Trojan port in your .env file:
```bash
# In .env
PORT_TROJAN=9443 # Or any available port
```
### Disk space full
If your server runs out of disk space, services may fail to start or behave unexpectedly.
**Quick check:**
```bash
df -h /
```
**Find what's using space with `ncdu`** (interactive disk usage analyzer):
```bash
# Install ncdu
apt install -y ncdu
# Scan from root (shows largest directories first, navigate with arrow keys)
ncdu /
# Scan just Docker data
ncdu /var/lib/docker
```
**Common space hogs:**
```bash
# Docker: remove unused images, containers, volumes
docker system prune -a --volumes
# Build cache (separate from image cache; can be many GB after failed builds)
docker builder prune -af
# Prometheus data (if monitoring enabled, ~50MB/day)
# Reduce retention in docker-compose.yml: --storage.tsdb.retention.time=7d
# Old log files
journalctl --vacuum-size=100M
# Docker container logs (can grow large)
docker system df -v
```
**Truncate oversized container logs (immediate reclaim, no restart needed):**
MoaV ≥ 1.7.6 caps each container's `json-file` log at 10 MB × 3 files via the
`x-logging` anchor in `docker-compose.yml`. The cap applies at *container
creation time* — containers that pre-date the upgrade keep growing under the
old (unbounded) policy until they're recreated.
To inspect and truncate in place:
```bash
# Top 10 largest container log files
sudo du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail
# Zero them in place — Docker keeps writing to the same FD, no service restart
# required. Kernel reclaims the disk pages immediately.
sudo truncate -s 0 /var/lib/docker/containers/*/*-json.log
# Verify rotation is enforced going forward
docker inspect moav-sing-box --format '{{json .HostConfig.LogConfig}}'
# expect: {"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
# If the inspect shows empty/default config, force-recreate so the new
# rotation policy gets applied:
docker compose up -d --force-recreate
```
`moav doctor logs` (also part of `moav doctor`) detects oversized files
automatically and prompts to truncate them interactively.
### Certificate issues
> **Quick check:** Run `moav doctor dns` to verify DNS records point to your server, and `moav doctor config` to verify certificate files exist.
**Certificate not renewing:**
```bash
# Check expiry + whether the auto-renewal timer is installed
moav cert status
# Run a renewal check now (restarts TLS services if the cert changed)
moav cert renew
# (Re)install the daily auto-renewal timer — installed automatically on
# `moav start` since v1.8.5; pre-v1.8.5 installs need this once
moav cert install
```
> **Note:** plain `docker compose run --rm certbot renew` does NOT work — the
> compose service overrides the entrypoint for one-shot issuance and would run
> `/bin/sh renew` instead of certbot. Use `moav cert renew`, which forces
> `--entrypoint certbot` and restarts the services that load certs at startup.
**Certificate acquisition failed:**
- Ensure DNS A record points to this server
- Ensure port 80 is open (temporarily)
- Check rate limits: https://letsencrypt.org/docs/rate-limits/
### Admin dashboard not accessible
**Check if container is running:**
```bash
docker compose --profile admin ps
docker compose --profile admin logs admin
```
**Verify port is listening:**
```bash
# Inside container
docker exec moav-admin ss -tlnp
# On host
ss -tlnp | grep 9443
```
**Test locally first:**
```bash
curl -k https://localhost:9443/api/health
# Should return: {"status":"ok","timestamp":"..."}
```
**Open firewall:**
```bash
ufw allow 9443/tcp
# or
iptables -A INPUT -p tcp --dport 9443 -j ACCEPT
```
**Browser shows security warning (domainless mode):**
In domainless mode, admin uses a self-signed certificate. This is expected:
1. Click "Advanced" or "Show Details"
2. Click "Proceed to site" or "Accept the Risk"
**Access URLs:**
- With domain: `https://yourdomain.com:9443/`
- Domain-less mode: `https://YOUR_SERVER_IP:9443/`
**Admin runs on port 9443 by default** (not 8443). The internal container port is 8443, but it's mapped to 9443 externally.
### Delete config returns 404
**Symptom:** Clicking "del" on a donated config in the admin dashboard shows:
```
MahsaNet API returned 404: {"detail":"No Config matches the given query."}
```
**Cause:** The MahsaNet API uses `id` (not `hash`) as the delete identifier. The config list endpoint returns `hash` but some API versions may not include `id`. MoaV now automatically falls back to looking up the config by hash to find the `id` for deletion.
**Fixes:**
1. Update MoaV to the latest version (includes the fallback logic)
2. If the error persists, the config may have already been deleted on MahsaNet's side — click "Refresh" to reload the list
**MahsaNet API reference:** [https://www.mahsaserver.com/backend/api/schema/redoc/](https://www.mahsaserver.com/backend/api/schema/redoc/)
Key endpoints:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/backend/api/v1/config/` | List configs (supports `?hash=`, `?alias=`, `?is_active=` filters) |
| POST | `/backend/api/v1/config/` | Create (donate) a config |
| DELETE | `/backend/api/v1/config/{id}/` | Delete a config by `id` |
Authentication: `Authorization: Token ` header on all requests.
---
## Install, update and build
Getting MoaV on the box, and moving between versions.
### Update fails with "local changes would be overwritten"
When running `moav update` or the installer, you may see:
```
error: Your local changes to the following files would be overwritten by merge:
scripts/client-test.sh
Please commit your changes or stash them before you merge.
Aborting
```
**Why this happens:**
- You edited files while testing a fix or feature
- You manually modified configuration scripts
- You tested a development branch and switched back
**Solution 1: Use the interactive prompt (recommended)**
The latest MoaV versions detect this and offer options:
```bash
moav update
# Will show:
# ⚠ Local changes detected:
# M scripts/client-test.sh
# Options:
# 1) Stash changes (save temporarily, can restore later)
# 2) Discard changes (reset to clean state)
# 3) Abort
```
Choose option 1 to save your changes, or option 2 to discard them.
**Solution 2: Manual stash**
```bash
cd /opt/moav
# Save your changes temporarily
git stash
# Now update
moav update
# or: git pull
# Restore your changes (may cause conflicts)
git stash pop
```
**Solution 3: Discard changes**
If you don't need your local changes:
```bash
cd /opt/moav
# Discard all local modifications
git checkout -- .
# Remove untracked files
git clean -fd
# Now update
moav update
```
### Recovering from failed updates
If an update fails partway through:
```bash
cd /opt/moav
# Check current state
git status
# If there are merge conflicts
git merge --abort
# Reset to last known good state
git reset --hard HEAD
# Try updating again
moav update
```
**If you need to completely reset:**
```bash
cd /opt/moav
# Fetch latest from remote
git fetch origin
# Hard reset to remote main
git reset --hard origin/main
# Verify
git status
```
### Breaking changes after update
Some updates include breaking changes (marked in [CHANGELOG](https://github.com/MotherofallVPNs/moav/blob/main/CHANGELOG.md)) that require regenerating configs. Symptoms include:
- Clients can't connect after update
- Services crash on startup
- Protocol-specific errors (e.g., "invalid obfuscation password")
**Option 1: Rebuild configs (keeps users)**
```bash
moav config rebuild
moav restart
```
This regenerates server config while preserving user credentials. You must redistribute new config bundles to all users.
**Option 2: Fresh start (new keys, new users)**
If Option 1 doesn't work or you want a clean slate:
```bash
# Complete wipe and fresh install
moav uninstall --wipe
# Reconfigure
cp .env.example .env
nano .env # Set DOMAIN, ACME_EMAIL, ADMIN_PASSWORD
# Bootstrap fresh
./moav.sh
```
**After any breaking change update:**
1. Download new user bundles from admin dashboard or `outputs/bundles/`
2. Distribute to all users
3. Users must delete old configs and import new ones
### "unknown UUID" — existing users can't connect after an update
**Symptom:** after `moav update` or `moav bootstrap`, users created with `moav user add` can no longer connect via the proxy protocols (Reality, Trojan, AnyTLS, Hysteria2, CDN, XHTTP). The TLS handshake succeeds, then the connection is rejected — `moav logs sing-box` shows:
```
inbound/vless[vless-reality-in]: ... unknown UUID:
```
**Cause:** bootstrap regenerates the sing-box/xray configs from templates, which dropped the per-user entries that `moav user add` inserts incrementally. Bundles and credentials were never lost — only the server config forgot the users.
**Fix:** reconcile the server config from state. Credentials stay the same, so already-distributed bundles keep working:
```bash
moav regenerate-users
```
On **1.9.1+** this reconcile runs automatically at the end of every bootstrap, so an update can no longer orphan users.
There is a second, unrelated cause with the same symptom: sing-box runs from a copy of its config taken when the container started, so a **newly added user** only exists for the running process after a restart. v2.0.0 restarts and then verifies this automatically, warning if the user did not go live. If you see `unknown UUID` for a user you just created, `moav restart sing-box` applies them.
### Switching branches
**Switch to a feature/test branch:**
```bash
cd /opt/moav
git fetch origin
git checkout feature-branch-name
git pull
moav build # Rebuild containers if needed
```
**Switch back to stable (main):**
```bash
cd /opt/moav
git checkout main
git pull
moav build
```
**If switching fails due to local changes:**
```bash
# Stash changes first
git stash
git checkout main
git pull
# Optionally restore changes
git stash pop
```
### Common scenarios
**Testing a bug fix from GitHub:**
```bash
# Save current state
cd /opt/moav
git stash
# Get the fix
git fetch origin
git checkout fix-branch-name
moav build
moav restart
# After testing, return to main
git checkout main
git stash pop # Restore your changes if needed
```
**Accidentally edited files:**
```bash
# See what changed
git diff
# If you want to keep changes, stash them
git stash
# If you want to discard
git checkout -- filename.sh
# Or discard all changes
git checkout -- .
```
**View stashed changes:**
```bash
# List all stashes
git stash list
# Show what's in the most recent stash
git stash show -p
# Apply a specific stash
git stash apply stash@{0}
# Delete a stash
git stash drop stash@{0}
```
---
### Build fails on low-memory VPS (≤ 1 GB RAM)
**Symptoms:** During `moav start` or `moav build --profile all` on a small VPS, the parallel build dies with one of these errors (they have the same root cause):
```
target amneziawg-exporter: NotFound: forwarding Ping: no such job mxjreqi1urjzqlsbvdw622pdk
```
or:
```
target xray: failed to solve: process "/bin/sh -c apk add --no-cache bash ca-certificates tzdata"
did not complete successfully: failed to create endpoint fs4tn8... on network bridge:
failed to find host side interface vethf6f8751: resource temporarily unavailable
```
or: build hangs at ~1200s (20 min) before failing.
**Cause:** Recent Docker Compose (v2.22+) defaults to "bake" mode, which builds all images in parallel via BuildKit. With 19+ MoaV images attempting to build concurrently on a tight VPS:
- **OOM in BuildKit daemon** → `NotFound: no such job ...` (job registry corrupts when memory pressure kills internal goroutines)
- **Kernel/network resource exhaustion** → `failed to find host side interface vethN: resource temporarily unavailable` (bridge networking can't allocate veth pairs fast enough when many containers spawn concurrently)
- **Heavy swapping** → build appears stuck for 15-20+ minutes before eventually failing
All three are the same underlying problem — too many parallel operations on a machine without the RAM to serve them.
**Fix — reset buildx state and force sequential builds:**
> ⚠️ Compose v2 removed the `--parallel N` flag — it will be interpreted as a service name. Use one of the patterns below instead.
```bash
# 1. Clear the broken buildx state
docker buildx prune -af
docker buildx rm --force default 2>/dev/null || true
cd /opt/moav
# 2a. RECOMMENDED — loop one service at a time (most reliable on ≤ 1 GB RAM)
for svc in $(COMPOSE_BAKE=false docker compose --profile all config --services); do
echo "=== Building $svc ==="
COMPOSE_BAKE=false docker compose build "$svc" || { echo "FAILED: $svc"; break; }
done
# 2b. ALTERNATIVE — env-var based serialization (Compose v2.30+)
# COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=1 docker compose --profile all build
# 2c. LAST RESORT — disable BuildKit entirely (classic builder, always serial)
# May fail if any Dockerfile uses BuildKit-specific syntax
# DOCKER_BUILDKIT=0 COMPOSE_DOCKER_CLI_BUILD=0 COMPOSE_BAKE=false \
# docker compose --profile all build
# 3. Once build succeeds, start normally
moav start
```
**Prevent recurrence on low-RAM hosts:** Add these to your shell profile (`~/.bashrc` or `~/.zshrc`):
```bash
export COMPOSE_BAKE=false
export COMPOSE_PARALLEL_LIMIT=1
```
With bake disabled and parallelism pinned to 1, Compose builds images sequentially. Each build still succeeds — they just happen one at a time instead of 19 concurrently.
**When to worry about this:**
| RAM | Parallel build | Sequential build |
|-----|----------------|------------------|
| ≥ 2 GB | Usually OK | Always OK |
| 1 GB | Frequently crashes (this issue) | OK but slow (~15-20 min) |
| < 1 GB | Not supported | Try, but may OOM anyway |
### Container registry blocked (gcr.io, ghcr.io)
In some regions (Iran, Russia, China), certain container registries are blocked:
| Registry | Images Affected | Status |
|----------|-----------------|--------|
| `gcr.io` | cAdvisor | Often blocked |
| `ghcr.io` | clash-exporter | Often blocked |
| `docker.io` | Most base images | Usually works (mirrors available) |
**Symptoms:**
- `docker pull` hangs or times out
- Build fails with "connection refused" or "timeout"
- Monitoring stack won't start
**Solution:** Build blocked images locally using `moav build --local`:
```bash
# Build commonly blocked images (gcr.io, ghcr.io)
moav build --local
# Build specific image
moav build --local cadvisor
moav build --local clash-exporter
# Build ALL external images locally
moav build --local all
```
### Building images locally
MoaV can build monitoring stack images from source when registries are blocked.
**Available images for local build:**
| Image | Registry | Build Command |
|-------|----------|---------------|
| cAdvisor | gcr.io | `moav build --local cadvisor` |
| clash-exporter | ghcr.io | `moav build --local clash-exporter` |
| Prometheus | docker.io | `moav build --local prometheus` |
| Grafana | docker.io | `moav build --local grafana` |
| Node Exporter | docker.io | `moav build --local node-exporter` |
| Nginx | docker.io | `moav build --local nginx` |
| Certbot | docker.io | `moav build --local certbot` |
**How it works:**
1. Downloads pre-built binaries from GitHub releases (not blocked)
2. Creates a local Docker image
3. Updates `.env` to use the local image
**Version control:**
Set versions in `.env` before building:
```bash
# In .env
PROMETHEUS_VERSION=3.5.1
GRAFANA_VERSION=12.3.3
NODE_EXPORTER_VERSION=1.10.2
CADVISOR_VERSION=0.56.2
CLASH_EXPORTER_VERSION=0.0.4
```
**Force rebuild:**
```bash
moav build --local --no-cache cadvisor
```
**Build everything locally (no registry pulls):**
```bash
moav build --local all
```
This builds both MoaV services and all external monitoring images.
---
## Monitoring
Grafana, Prometheus and the exporters. None of these affect whether users can connect.
### System hangs after starting monitoring
If your server hangs or becomes unresponsive after starting monitoring (especially the first time), you're likely running out of RAM.
**Symptoms:**
- SSH connection freezes
- Commands stop responding
- Server becomes unreachable
**Solution 1: Recover and disable monitoring**
If you can still SSH in (wait a few minutes):
```bash
# Stop all monitoring services
docker compose --profile monitoring stop
# Or stop individual heavy services
docker stop moav-prometheus moav-grafana moav-cadvisor
```
If SSH is frozen, reboot via your VPS control panel, then:
```bash
cd /opt/moav
# Don't start monitoring on boot
moav start proxy admin # Without monitoring
```
**Solution 2: Upgrade your server**
Monitoring requires at least 2GB RAM. Upgrade your VPS to 2GB+ RAM before enabling monitoring.
**Solution 3: Run lighter monitoring**
If you must have metrics on 1GB RAM, disable the heaviest components:
```bash
# Start only essential monitoring (skip cAdvisor)
docker compose --profile monitoring up -d prometheus grafana node-exporter clash-exporter
# Stop cAdvisor if running (uses ~150MB)
docker stop moav-cadvisor
```
### Grafana shows "No Data"
> **Quick check:** Run `moav doctor services` to verify monitoring services are running.
1. Check Prometheus is running:
```bash
docker logs moav-prometheus
```
2. Verify targets are up - access Prometheus internally:
```bash
docker exec moav-grafana wget -qO- http://prometheus:9091/api/v1/query?query=up
```
3. Ensure services are on the same Docker network (`moav_net`)
### Clash-exporter authentication error (401)
**Symptoms:**
```
failed to dial: failed to WebSocket dial: expected handshake response status code 101 but got 401
```
This means `CLASH_API_SECRET` in `.env` doesn't match the secret in sing-box's config. This typically happens after a re-bootstrap where the state volume has a different secret than `.env`.
**Diagnose:**
```bash
# What .env has (used by clash-exporter)
grep CLASH_API_SECRET .env
# What sing-box actually uses (source of truth)
docker compose exec sing-box cat /etc/sing-box/config.json | python3 -m json.tool | grep -A2 clash_api
```
**Fix:**
```bash
# Sync .env with the actual sing-box secret
SECRET=$(docker compose exec sing-box cat /etc/sing-box/config.json | python3 -c "import sys,json; print(json.load(sys.stdin)['experimental']['clash_api']['secret'])")
sed -i "s/^CLASH_API_SECRET=.*/CLASH_API_SECRET=$SECRET/" .env
docker compose restart clash-exporter
```
Or use `moav restart monitoring` — the `ensure_clash_api_secret()` function now auto-syncs stale secrets from the state volume on startup.
### High memory usage from cAdvisor
Limit cAdvisor resources in `docker-compose.yml`:
```yaml
cadvisor:
deploy:
resources:
limits:
memory: 256M
```
### Snowflake metrics showing zeros
The Snowflake exporter parses log files for summary statistics. Summaries are logged periodically. If you just started Snowflake, wait for the first summary to appear:
```bash
# Check if summaries exist
docker exec moav-snowflake cat /var/log/snowflake/snowflake.log | grep "In the"
```
### WireGuard exporter not starting
The exporter needs read access to WireGuard config. Check:
```bash
docker logs moav-wireguard-exporter
ls -la configs/wireguard/wg0.conf
```
### GeoIP "Geographic Distribution" shows No Data
The GeoIP feature requires the DB-IP Lite database to be downloaded first.
**Step 1: Download the GeoIP database**
```bash
docker compose --profile setup run --rm geoip-updater
```
**Step 2: Verify the database is in the volume**
```bash
docker run --rm -v moav_geoip:/geoip alpine ls -la /geoip/
# Should show: dbip-country-lite.mmdb (~5MB)
```
**Step 3: Restart the exporters**
```bash
docker compose restart singbox-exporter xray-exporter wireguard-exporter amneziawg-exporter
```
**Step 4: Verify GeoIP is loaded**
```bash
docker logs moav-singbox-exporter 2>&1 | grep GeoIP
# Should show: GeoIP: loaded database from /geoip/dbip-country-lite.mmdb
```
**Step 5: Check metrics are being emitted**
```bash
# For sing-box:
docker exec moav-grafana wget -qO- http://singbox-exporter:9102/metrics | grep country
# For xray:
docker exec moav-grafana wget -qO- http://xray-exporter:9103/metrics | grep country
```
**Common issues:**
| Symptom | Cause | Fix |
|---------|-------|-----|
| `GeoIP: could not load ...` | Database not downloaded | Run `geoip-updater` (see step 1) |
| `GeoIP: maxminddb not installed` | Exporter image outdated | Rebuild: `docker compose build singbox-exporter` |
| Metrics show only `country="XX"` | Database loaded but IPs not resolving | DB may be corrupt — re-run `geoip-updater` |
| sing-box geo works but xray doesn't | Xray log format issue | Check `docker logs moav-xray` for `accepted` lines with IPs |
| WireGuard shows geo but sing-box doesn't | Clash API not reachable | Check `docker logs moav-singbox-exporter` for API errors |
For complete monitoring documentation, see [MONITORING.md](https://moav.sh/docs/MONITORING/).
---
## moav test and the built-in client
`moav test` runs the real client against your own server, so its failures are usually about the test environment rather than the protocols.
### `moav test` fails to build
**Docker build errors:**
```bash
# Rebuild with no cache
moav client build --no-cache
# Or manually:
docker build --no-cache -t moav-client -f Dockerfile.client .
```
**Network issues during build:**
- Pre-built binaries are downloaded from GitHub/GitLab
- If downloads fail, the build falls back to compiling from source (slower)
- Check your server has internet access
### `moav test` shows all protocols as "skip"
**User bundle not found:**
```bash
# Check if bundle exists
ls -la outputs/bundles/user1/
# Regenerate user bundle
moav user add user1
```
**Bundle path issue:**
```bash
# Verify the bundle contains config files
ls outputs/bundles/user1/
# Should contain: reality.txt, trojan.txt, hysteria2.yaml, etc.
```
### `moav test` shows "sing-box failed to start"
**Configuration format issue:**
- sing-box 1.12+ requires `route.final` instead of deprecated special outbounds
- Check sing-box version: `docker run --rm moav-client sing-box version`
**Debug with verbose output:**
```bash
# Run test container interactively
docker run --rm -it \
-v "$(pwd)/outputs/bundles/user1:/config:ro" \
moav-client /bin/bash
# Inside container, manually test
VERBOSE=true CONFIG_DIR=/config /app/client-test.sh
```
### `moav client connect` can't establish connection
**Check server is running:**
```bash
moav status
# Ensure sing-box and other services show as "running"
```
**Try different protocols:**
```bash
moav client connect user1 --protocol hysteria2
moav client connect user1 --protocol trojan
```
**Check firewall on server:**
```bash
# Server-side
ufw status
ss -tlnp # TCP ports
ss -ulnp # UDP ports
```
### Client proxy ports already in use
**Change ports in .env:**
```bash
# In .env
CLIENT_SOCKS_PORT=10800
CLIENT_HTTP_PORT=18080
```
**Or stop conflicting service:**
```bash
# Check what's using port 1080
ss -tlnp | grep 1080
```
### WireGuard test shows "endpoint not reachable"
This is expected if:
- UDP port 51820 is blocked by firewall
- Server WireGuard container is not running
**Check WireGuard is running:**
```bash
docker compose --profile wireguard ps
```
**Check UDP is not blocked:**
```bash
# From client machine
nc -vuz YOUR_SERVER_IP 51820
```
### Tor/Snowflake fallback not working
**Tor is standalone and doesn't require your server:**
```bash
# Test Snowflake independently
docker run --rm moav-client snowflake-client --help
```
**If binaries are missing:**
- Some optional binaries may fail to download during build
- Check build logs for "not available (optional)" messages
**For Psiphon:**
- Psiphon is not available via MoaV client
- Use the [official Psiphon apps](https://psiphon.ca/en/download.html) instead
---
## Reset and recovery
Escalating, most-conservative first. **Keys and users survive all of these** unless you explicitly choose a full reset.
### Full reset (start fresh)
If things are broken beyond repair, reset everything:
```bash
# Complete wipe - removes all containers, volumes, configs, keys, bundles
moav uninstall --wipe
# Reconfigure
cp .env.example .env
nano .env # Set DOMAIN, ACME_EMAIL, ADMIN_PASSWORD
# Fresh bootstrap
./moav.sh
```
This gives you a completely clean installation with new keys and certificates.
### Partial reset (keep data)
Remove containers but keep your configuration for quick reinstall:
```bash
# Remove containers only, keep .env, keys, bundles
moav uninstall
# Reinstall and start
./moav.sh install
moav start
```
### Re-bootstrap only
To regenerate server config without removing anything:
```bash
# Remove only the bootstrap flag
docker run --rm -v moav_moav_state:/state alpine rm /state/.bootstrapped
# Re-run bootstrap
moav bootstrap
# Restart services
moav restart
```
### Reset only WireGuard
```bash
# Remove WireGuard config
rm configs/wireguard/wg0.conf configs/wireguard/server.pub
# Remove WireGuard keys from state
docker run --rm -v moav_moav_state:/state alpine rm -f /state/keys/wg-server.key /state/keys/wg-server.pub
# Remove bootstrap flag and re-run
docker run --rm -v moav_moav_state:/state alpine rm /state/.bootstrapped
docker compose --profile setup run --rm bootstrap
# Restart WireGuard
docker compose --profile wireguard up -d wireguard
```
### Reset only dnstt
```bash
# Remove dnstt config and keys
rm configs/dnstt/server.conf configs/dnstt/server.pub
docker run --rm -v moav_moav_state:/state alpine rm -f /state/keys/dnstt-*
# Remove bootstrap flag and re-run
docker run --rm -v moav_moav_state:/state alpine rm /state/.bootstrapped
docker compose --profile setup run --rm bootstrap
# Restart dnstt
docker compose --profile dnstunnel up -d dnstt
```
---
## Useful commands
### View logs
```bash
# All services
docker compose logs
# Specific service
docker compose logs sing-box
docker compose logs -f sing-box # Follow
# Last 100 lines
docker compose logs --tail=100 sing-box
```
### Restart services
```bash
# Restart all (specify the profile you're using)
docker compose --profile all restart
# Restart specific service
docker compose --profile proxy restart sing-box
# Full rebuild
docker compose --profile all down
docker compose --profile all up -d --build
```
### Apply .env changes
**Important:** Docker caches environment variables at container creation time. Simply restarting a service does NOT pick up `.env` changes.
```bash
# WRONG - does NOT apply .env changes
docker compose restart snowflake
# CORRECT - recreates container with new .env values
docker compose up -d --force-recreate snowflake
# Or use moav (handles this automatically)
moav stop snowflake && moav start snowflake
```
### Check resource usage
```bash
docker stats
```
### Test connectivity
```bash
# Test from server
curl -I https://google.com
# Test TLS
openssl s_client -connect yourdomain.com:443
# Test specific protocol
# (run from a client that works)
```
### Reload configuration
```bash
# sing-box hot reload
docker compose exec sing-box sing-box reload
# Or restart container
docker compose restart sing-box
```
---
## Getting help
If issues persist:
1. **Collect logs:**
```bash
docker compose logs > logs.txt
```
2. **Check configuration:**
```bash
docker compose exec sing-box sing-box check -c /etc/sing-box/config.json
```
3. **Verify network:**
```bash
curl -I https://yourdomain.com
dig yourdomain.com
```
4. **Document:**
- What protocol you're trying
- What client app and version
- Error messages
- When it started failing
---
# Operational Security Guide
Security recommendations for running and using MoaV safely.
## For Server Operators
### Server Security
1. **Keep system updated:**
```bash
apt update && apt upgrade -y
# Enable automatic security updates
apt install unattended-upgrades
dpkg-reconfigure unattended-upgrades
```
2. **Use SSH keys, disable password auth:**
```bash
# In /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin prohibit-password
```
3. **Change SSH port** (optional but recommended):
```bash
# 1. Add firewall rule for new port FIRST
ufw allow 2222/tcp
# 2. Then change SSH config
# In /etc/ssh/sshd_config:
Port 2222
# 3. Restart SSH
systemctl restart sshd
# 4. Test new port works (from another terminal), then remove old rule
ufw delete allow 22/tcp
```
### Firewall & Docker
> **Important: Docker bypasses UFW/iptables.** Docker publishes ports by inserting iptables rules *before* UFW's chains. This means `ufw deny 9443` does NOT block access to port 9443 if Docker is publishing it. Ports listed in `docker-compose.yml` under `ports:` are publicly accessible regardless of UFW rules.
>
> Ports listed under `expose:` (without `ports:`) are Docker-internal only and NOT affected.
>
> See: [Docker packet filtering docs](https://docs.docker.com/engine/network/packet-filtering-firewalls/)
**Basic UFW setup** (for non-Docker ports like SSH):
```bash
ufw allow 22/tcp # SSH (IMPORTANT: always allow SSH first!)
ufw enable
```
> **Warning:** UFW rules only effectively control non-Docker services (SSH, system services). For Docker-published ports, use the methods below.
**Protocol ports** (published by Docker — UFW rules are informational only):
| Port | Service | Notes |
|------|---------|-------|
| 443/tcp | Reality (VLESS) | Required |
| 443/udp | Hysteria2 | Required if enabled |
| 8443/tcp | Trojan | Required if enabled |
| 4443/tcp+udp | TrustTunnel | Required if enabled |
| 2082/tcp | CDN WebSocket | Required if enabled |
| 51820/udp | WireGuard | Required if enabled |
| 51821/udp | AmneziaWG | Required if enabled |
| 8080/tcp | wstunnel | Required if enabled |
| 993/tcp | Telegram MTProxy | Required if enabled |
| 2096/tcp | XHTTP | Required if enabled |
| 53/udp | DNS tunnels — dnstt, Slipstream, MasterDNS, XDNS (all 4 via dns-router) | Required if DNS tunnels enabled |
| 8444/tcp | GooseRelay (SOCKS5-over-Google-Apps-Script exit) | Only if `ENABLE_GOOSERELAY=true` |
| 80/tcp | Let's Encrypt | Required during cert renewal |
| 9443/tcp | Admin dashboard | See access control below |
| 9444/tcp | Grafana | See access control below |
| 2083/tcp | Grafana CDN proxy | See access control below |
Protocol ports (Reality, Hysteria2, etc.) are designed to be public — they require authentication. The concern is admin/monitoring ports.
#### Option 1: Use `ADMIN_IP_WHITELIST` (Recommended)
MoaV's admin dashboard has built-in IP whitelisting. Set in `.env`:
```bash
# Allow only your IP (comma-separated for multiple)
ADMIN_IP_WHITELIST=YOUR_HOME_IP,YOUR_OFFICE_IP
# Then restart admin
moav restart admin
```
This blocks all other IPs at the application level, regardless of Docker/UFW.
#### Option 2: Bind to localhost + SSH tunnel
For maximum security, bind admin/monitoring to `127.0.0.1` so they're only accessible via SSH tunnel:
```bash
# In .env — bind to localhost only
PORT_ADMIN=127.0.0.1:9443
PORT_GRAFANA=127.0.0.1:9444
```
Then access via SSH tunnel:
```bash
# From your local machine
ssh -L 9443:127.0.0.1:9443 -L 9444:127.0.0.1:9444 root@YOUR_SERVER
# Then open https://localhost:9443 in your browser
```
#### Option 3: ufw-docker (Advanced)
[ufw-docker](https://github.com/chaifeng/ufw-docker) patches UFW to work with Docker by adding rules to the `DOCKER-USER` iptables chain. This makes `ufw` commands effective for Docker ports.
```bash
# Install
sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw
# Allow specific ports from any IP
sudo ufw-docker allow moav-sing-box 443/tcp
sudo ufw-docker allow moav-sing-box 443/udp
# Allow admin only from your IP
sudo ufw-docker allow moav-admin 8443/tcp from YOUR_IP
```
> **Trade-offs:** Requires modifying UFW config files (`/etc/ufw/after.rules`). Rules use `ufw route` syntax. Needs reload after container restarts. Works well but adds complexity. For most users, `ADMIN_IP_WHITELIST` is simpler.
### Admin & Monitoring Access Control
**Admin dashboard** (`https://server:9443`):
- Username: any value — only the password is checked
- Password: set during install (in `.env` as `ADMIN_PASSWORD`)
- Reset: `moav admin password`
- IP whitelist: `ADMIN_IP_WHITELIST` in `.env`
> **Fail-closed authentication.** The dashboard **refuses to serve** (HTTP 503) if `ADMIN_PASSWORD` is empty, unset, or one of the known-insecure defaults (`admin`, `change_me_to_something_secure`), rather than silently allowing access. This closes an edge case where an empty password would otherwise accept an empty `Authorization` header. Password comparison is constant-time. If you see a 503 with a remediation message, set a real `ADMIN_PASSWORD` in `.env` and `moav restart admin`.
**Grafana** (`https://server:9444`):
- Username: `admin`
- Password: same as `ADMIN_PASSWORD`
- Accessible from any IP by default (password-protected)
**Internal services** (not publicly accessible):
- Prometheus (9091) — `expose:` only, Docker-internal
- All exporters — `expose:` only, Docker-internal
- cAdvisor — `expose:` only, Docker-internal
- Docker socket proxy — `expose:` only, Docker-internal
### Domain Security
1. **Use WHOIS privacy** — hide personal information in domain registration
2. **Use a neutral registrar** — avoid country-specific registrars
3. **Keep registration details accurate** — use registrar-provided WHOIS privacy or a reputable privacy/proxy registration service rather than false information
4. **Match payment to your threat model** — don't assume cryptocurrency is anonymous; many blockchains create permanent public transaction records
5. **Separate domain from identity** — don't use a domain linked to your name
### Reality fallback target (`REALITY_TARGET`)
> How to pick one, with candidate targets and the trade-offs, is in [Setup → Choosing a Reality Target](https://moav.sh/docs/SETUP/#choosing-a-reality-target-sni). This section covers why the choice matters for your threat model.
Reality's `REALITY_TARGET` (and `XHTTP_REALITY_TARGET`) is the public TLS site the inbound proxies non-Reality TLS hellos to. Every probe and every misauthenticated client gets that site's real ServerHello back — so an outside observer sees a normal connection to a real CDN, not a closed port.
**The hostname MUST resolve in public DNS and be reachable from your server's network.** If it doesn't, the inbound RSTs every TLS hello — including ones from your own users — and your `:443` looks visibly dead to passive scanners. This is exactly what issue #115 reported (`update.samsung.com` is not a real public hostname; Samsung's update infrastructure uses opaque internal names).
Bootstrap now validates this and `moav doctor reality` re-checks it post-deployment.
**Vetted targets:**
| Audience | Hostname | Why it works |
|---|---|---|
| Global | `www.cloudflare.com:443` | Real TLS 1.3, ECH-capable, anycast, won't go dark |
| Global | `www.apple.com:443` | Corporate, stable, real ECH support |
| Global | `cdn.kernel.org:443` | Linux kernel CDN, real, neutral |
| Iran users | `www.aparat.com:443` | Iranian video CDN — looks like normal domestic traffic to Iran DPI |
| Iran users | `digikala.com:443` | Iran e-commerce, high-volume traffic profile |
| Iran users | `taghche.com:443` | Iranian book platform, plausible browsing target |
**Avoid:**
- `dl.google.com:443` — throttled by Iran DPI; works elsewhere but bad for Iran-bound clients
- Hostnames that *sound* like an update server but aren't real public DNS names (`update.samsung.com`, `swl.samsung.com`, `update.windows.com`, `cdn.tesla.com`) — verify with `getent hosts ` first
- Hostnames behind country-specific blocks from your VPS region (some VPS providers throttle major CDNs)
- Anything you wouldn't expect a real browser to talk to on a quiet Tuesday
**If you change `REALITY_TARGET` after bootstrap**, you must re-issue client bundles so the SNI in their config matches:
```bash
docker compose restart sing-box xray
moav regenerate-users
```
### Credential Management
1. **Never share master credentials** — each user gets unique credentials
2. **Revoke compromised users immediately:**
```bash
moav user revoke compromised_user
```
3. **Keep dnstt private keys private** — `state/keys/dnstt-server.key.hex` is the persistent dnstt server private key. It should be readable only by the dnstt container user (`100:101`, mode `0600`). If MoaV cannot set that ownership during bootstrap, fix the host permissions and re-run bootstrap rather than making the key world-readable.
4. **Rotate server keys periodically** — re-bootstrap if concerned
5. **Keep backups:**
```bash
moav export # Creates moav-backup-TIMESTAMP.tar.gz
```
6. **Use strong admin password** — at least 16 characters, generated randomly
### Monitoring
1. **Use `moav doctor`** to check for configuration issues:
```bash
moav doctor # All checks
moav doctor dns # DNS only
moav doctor ports # Port conflicts
```
2. **Check logs regularly:**
```bash
moav logs sing-box # Proxy logs
moav logs admin # Admin dashboard logs
```
3. **Inspect connections** to see who's connecting and what they're accessing:
```bash
./scripts/inspect-connections.sh # All connections (last 6h)
./scripts/inspect-connections.sh IR 24h # Filter by country
./scripts/inspect-connections.sh --csv # CSV export
```
4. **Grafana dashboards** (if monitoring enabled):
- Per-user traffic and connections
- GeoIP country distribution
- Protocol breakdown
- System health (CPU, RAM, disk)
5. **Watch for unusual patterns:**
- Sudden traffic spikes from unexpected countries
- Single IPs with very high error counts (scanning/probing)
- Connections to suspicious destinations
### Docker Security Hardening
MoaV applies these hardening measures to all containers (since v1.7.2):
- `cap_drop: ALL` — drops all Linux capabilities, adds back only what's needed
- `read_only: true` — read-only root filesystem with targeted `tmpfs` mounts
- `no-new-privileges: true` — prevents privilege escalation
- `mem_limit` and `cpus` — resource limits per container
- Non-root users — containers run as unprivileged `moav` user where possible
- Docker socket proxy — admin uses `tecnativa/docker-socket-proxy` instead of mounting the raw Docker socket
### Network tuning
The installer offers a one-time kernel-level network tuning bundle aimed at the long-RTT / lossy paths that proxy traffic to censored regions actually traverses. Real-world Portugal→Vilnius testing (Time4VPS box, ~400 ms RTT with burst loss) showed BBR roughly 3× single-flow TCP throughput compared to the Linux default (CUBIC 5.45 Mbps → BBR 14.8 Mbps), and dramatically faster recovery from packet loss. Bigger UDP buffers help Hysteria2 and WireGuard even though they don't use BBR — quic-go alone needs ≥7.5 MiB to avoid drops at high throughput.
**What it tunes** (written to a single dedicated file, `/etc/sysctl.d/99-moav-net.conf`, for clean rollback):
| Knob | Value | Why |
|---|---|---|
| `net.ipv4.tcp_congestion_control` | `bbr` | RTT/bandwidth-based, recovers from loss faster than CUBIC |
| `net.core.default_qdisc` | `fq` | Required for BBR's pacing |
| `net.core.{r,w}mem_max` | 32 MiB (16 MiB if RAM < 2 GB) | Headroom for high-BDP TCP + QUIC |
| `net.ipv4.tcp_{r,w}mem` | `4096 / 131072 / max` | TCP buffer auto-tune range |
| `net.core.{r,w}mem_default` | 1 MiB | Default UDP socket buffer (Hysteria2 / WireGuard) |
| `net.core.netdev_max_backlog` | 16384 | Queue depth — UDP drops hurt circumvention more than TCP drops |
| `net.core.somaxconn` | 8192 | Listen backlog for high-concurrency inbounds |
| `net.ipv4.tcp_max_syn_backlog` | 8192 | SYN queue depth — prevents drops on coordinated reconnect bursts |
| `net.ipv4.tcp_slow_start_after_idle` | 0 | Avoid restarting at congestion-window 1 on idle long-lived proxies |
| `net.ipv4.tcp_mtu_probing` | 1 | Recover gracefully if a path silently has a smaller MTU |
| `net.ipv4.tcp_notsent_lowat` | 131072 | Smaller send buffers to reduce HOL latency for interactive flows |
**What it deliberately does NOT set:** `net.ipv4.tcp_fastopen`. TFO server-side *adds* latency in heavily-censored networks because middleboxes (notably China Mobile) drop SYN+data on ~5% of paths and the client has to retry. The same reason MoaV v1.8.4 removed `tcp_fast_open: true` from the sing-box Reality and Trojan inbounds.
**Commands:**
```bash
moav net status # show current vs recommended values
moav net apply # write 99-moav-net.conf + reload sysctl + print verification
moav net revert # remove the file + reload sysctl (clean rollback)
moav doctor net # sysctl + packet drops + PMTU + CGNAT + per-interface MTU
```
`moav doctor net` extends the sysctl check with:
- **Packet drops**: reads `/proc/net/snmp` + `/proc/net/netstat` for TCP `ListenDrops` / `ListenOverflows` (SYN queue overflows) and UDP `RcvbufErrors` / `SndbufErrors` (Hysteria2/WireGuard buffer overflows). Counters are since-boot; non-zero values name the matching sysctl knob to raise.
- **PMTU**: confirms `tcp_mtu_probing` is enabled so silent black-hole paths recover instead of stalling.
- **CGNAT / NAT**: looks at the default route's source address. CGNAT (100.64/10) is reported as a hard failure for inbound proxy traffic; RFC1918 addresses are flagged as "behind NAT" with the public `SERVER_IP` from `.env` for context.
- **MTU**: prints egress MTU + WireGuard `wg0` MTU (recommends 1420). Informational.
**When it skips silently:** kernel <4.9 (no BBR), OpenVZ guests (shared kernel, no sysctl writes), or already-applied installs.
**Compatibility notes:**
- `net.ipv4.tcp_*` sysctls are network-namespaced, so they apply per-container. MoaV's sing-box runs in `network_mode: host` and inherits the host's BBR directly. Bridge-network containers (xray, telemt) use the host's settings as their default and pick up BBR on container restart after `moav net apply`.
- BBR + fq are mainline since kernel 4.9 — Ubuntu 20.04+, Debian 11+, RHEL 9+, every supported modern distro.
- Existing custom sysctl tweaks in `/etc/sysctl.conf` or other `/etc/sysctl.d/*.conf` files are not modified. If a later-numbered file (`99-moav-net.conf` is at 99) overrides something, it wins by sysctl-load order.
### If Server is Blocked
1. **Try different protocols first** — switch from Reality to Hysteria2, XHTTP, or CDN mode
2. **CDN mode** — routes through Cloudflare/CloudFront, works when server IP is blocked
3. **DNS tunnels** — XDNS/dnstt/Slipstream work when most traffic is blocked
4. **If IP is burned:**
```bash
# On old server: export
moav export
# On new server: import and update IP
moav import moav-backup-*.tar.gz
moav migrate-ip NEW_IP
moav start
```
5. **Donate bandwidth** — even if your server is blocked for your users, it can still serve millions through Psiphon Conduit, Tor Snowflake, and MahsaNet
---
## For Users
### Device Security
1. **Use a separate profile/user** for circumvention apps on shared devices
2. **Don't screenshot QR codes** — or delete immediately after import
3. **Delete bundle files** after importing to your apps
4. **Use device encryption** — enable full disk encryption
5. **Set strong device PIN/password**
### Connection Security
1. **Verify you're connected:**
- Check your IP: https://whatismyip.com
- Should show server IP, not your real IP
2. **Use HTTPS everywhere** even over tunnel:
- The tunnel encrypts transport, HTTPS encrypts content
- Protects against compromised tunnel endpoints
3. **Don't trust public WiFi** even with VPN:
- Your device can still be attacked locally
- Tunnel doesn't protect against local network attacks
### App Security
1. **Keep apps updated** — updates often fix detection bypasses
2. **Download from official sources:**
- iOS: App Store (Happ, Streisand, Hiddify)
- Android: GitHub releases (Happ, v2rayNG, Hiddify)
- Avoid random APK sites
3. **Backup your configs** — export from apps, store securely
### Behavior Security
1. **Don't share your credentials** — each person should have their own
2. **Don't share screenshots** showing server addresses or QR codes
3. **Don't mention specific servers** in public forums
4. **Use secure messaging** to receive configs (Signal, encrypted email)
### If You Suspect Compromise
1. **Stop using that config immediately**
2. **Contact admin** for new credentials
3. **Check your device** for malware
4. **Change passwords** for any accounts accessed over that connection
---
## Distribution Security
### Sharing Bundles Safely
**DO:**
- Use end-to-end encrypted messaging (Signal, Telegram secret chat)
- Share in person when possible (scan QR code directly)
- Use encrypted file sharing (OnionShare)
- Delete messages after recipient confirms receipt
**DON'T:**
- Email unencrypted configs
- Post links in public channels
- Share via unencrypted cloud storage
- Send screenshots of QR codes to groups
### Recommended Distribution Methods
1. **In Person** — safest, scan QR code directly
2. **Signal** — send configs as files, enable disappearing messages
3. **Telegram (Secret Chat only)** — NOT regular chats, use self-destruct timer
4. **Admin Dashboard** — share download links directly (HTTPS, password-protected)
---
## Legal Considerations
**Disclaimer:** This is not legal advice.
- Laws vary by country — running or using circumvention tools may carry legal risks
- Assess your personal risk level
- The decoy website reduces obvious disclosure during casual or unauthenticated probing; it is not protection against targeted investigation
### Data Retention
MoaV is configured for minimal logging:
- No URLs logged
- No request content
- Basic connection stats only (for admin dashboard)
- IP addresses are in memory only (not persisted to disk)
To minimize logging further:
```bash
# In .env
LOG_LEVEL=error
```
---
## Emergency Procedures
### If You Think You're Monitored
1. Stop using current credentials
2. Contact admin through alternate channel
3. Get fresh credentials
4. Consider using a different device
5. Assess whether to continue using service
### If Server is Seized
User data exposure is limited:
- No content is logged
- IP addresses are in memory only
- User identifiers are usernames (not real names)
But assume:
- Server IP is known
- User identifiers are known
- Active connections at time of seizure are known
### If User is Compromised
As admin:
1. Revoke user immediately: `moav user revoke username`
2. Monitor for unusual activity
3. Consider rotating server if credentials were extracted
4. Do NOT contact compromised user through normal channels
---
## Checklist
### Server Operator
- [ ] SSH keys only, no password auth
- [ ] SSH port changed from default 22
- [ ] System auto-updates enabled
- [ ] Admin IP whitelist configured (`ADMIN_IP_WHITELIST`)
- [ ] Strong admin password (16+ characters)
- [ ] Unique user credentials for everyone
- [ ] `moav doctor` passes all checks
- [ ] Backup plan if blocked (new IP or migration ready)
- [ ] Secure distribution channel established
- [ ] Monitoring enabled (Grafana) or logs checked regularly
### User
- [ ] Device encrypted
- [ ] App from official source
- [ ] Config imported securely
- [ ] Bundle files deleted after import
- [ ] Knows which protocol to try if one fails
- [ ] Knows how to contact admin securely
---
# Support MoaV
MoaV is free software built for people who can't rely on the internet being open. There are four ways to help, and the first one is worth more than the rest.
## Run a server
**This is the highest-leverage thing you can do.** Every MoaV server is capacity that didn't exist before — for your family, your colleagues, or people you'll never meet. The project doesn't run infrastructure on anyone's behalf; the network *is* the people running servers.
A $5/month VPS or a Raspberry Pi in a spare room is enough. [Quick Start](https://moav.sh/docs/quick-start/) takes about ten minutes.
If you already run one, the most useful next step is **giving bundles to people who need them** — see [sharing safely](https://moav.sh/docs/OPSEC/#sharing-bundles-safely) first, because how you distribute matters as much as that you do.
### Donate bandwidth instead of users
You can help even without users of your own, by relaying traffic for existing circumvention networks. Both are opt-in, capped, and run alongside everything else:
```bash
moav start conduit # relay for Psiphon users worldwide
moav start snowflake # relay for Tor users
```
- **[Psiphon Conduit](https://moav.sh/docs/protocols/#psiphon-conduit)** — your spare bandwidth carries Psiphon traffic for people in censored regions.
- **[Tor Snowflake](https://moav.sh/docs/protocols/#tor-snowflake)** — your server becomes a Snowflake proxy, helping people reach Tor.
You can also **donate configs to [MahsaNet](https://moav.sh/docs/mahsanet/)**, which distributes them to users in Iran who have no server of their own. That turns spare capacity on your server into access for someone who can't set one up.
## Contribute code
Bugs, protocols, docs, packaging — all of it. [Development & Testing](https://moav.sh/docs/development/) covers running from source, how CI and the end-to-end suite work, and the one rule that shapes the project: **every bug fixed ships a regression test in the same PR**.
**Bug reports count.** A reproducible report with `moav doctor` output is often more work than the fix, and it's the thing that turns "it's broken for me" into something that stays fixed for everyone.
> Never paste bundles, `.env` contents or share links into an issue — they contain live keys.
## Translate
Documentation only in English is a real barrier for exactly the people MoaV exists for. A Persian setup guide is worth more than another protocol.
**The single highest-value target is the user bundle guide** — the `README.html` template that ships inside every bundle. It is the one document a person reads while trying to get online, often on a phone, often in a hurry. It already ships English and Farsi; every additional language there reaches people who never touch this site.
After that, in order:
- **The bundle guide template** (`scripts/lib/bundle-readme.sh` / its template in the MoaV repo)
- **The project READMEs** — [MoaV](https://github.com/MotherofallVPNs/MoaV) and [moav-client](https://github.com/MotherofallVPNs/moav-client). MoaV already has `README-fa.md` as the pattern to follow.
- **The docs pages read under pressure**: [Quick Start](https://moav.sh/docs/quick-start/), [Setup](https://moav.sh/docs/SETUP/), [DNS Configuration](https://moav.sh/docs/DNS/), [Client Apps](https://moav.sh/docs/CLIENTS/), [Troubleshooting](https://moav.sh/docs/TROUBLESHOOTING/), [OPSEC](https://moav.sh/docs/OPSEC/)
Deep developer docs can stay English.
**Partial translations are welcome** — a translated Quick Start on its own is useful. Two things we ask:
- **Human-reviewed only.** A machine-translated DNS or OPSEC instruction that's subtly wrong is worse than an English one that's right. Unreviewed drafts get marked as drafts.
- **English stays the source of truth**, so a translated page can be checked against it when things change.
!!! abstract "The scaffolding is ready — start here"
**[Translating the Docs](https://moav.sh/docs/TRANSLATING/)** is the full walkthrough: which page to pick, where the file goes, what must stay untranslated, and how to preview your work before opening a pull request.
Farsi and Russian are open now. Copy one English page into `docs/fa/` or `docs/ru/`, translate it, and open a PR — untranslated pages fall back to English, so **a single page is a complete contribution.** For another language, open an issue and we'll add it.
## Donate money
Funds go to infrastructure the project actually needs — test servers for the end-to-end suite, domains, and build capacity. Not salaries.
!!! tip "Cards, PayPal, recurring"
| Platform | Link |
|---|---|
| **GitHub Sponsors** | [github.com/sponsors/shayanb](https://github.com/sponsors/shayanb) |
| **Buy Me a Coffee** | [buymeacoffee.com/pangana](https://buymeacoffee.com/pangana) |
!!! abstract "Crypto — click an address to copy it"
**Bitcoin (BTC)**
```text
bc1p6rpwzkgrlvpkre0n94fqayafpw47kl2j5lmvhvl0rfrtzm94wvvsmd3w5s
```
**Ethereum (ETH)** — works on **all EVM chains** at this same address: Ethereum mainnet and every L2 (Arbitrum, Optimism, Base, Polygon, Gnosis, zkSync, and so on), plus **any ERC-20 token** such as USDC, USDT or DAI.
```text
0xB4D06BDb0C2f1D81E0b0b805Ed813F4ffe960aE2
```
**Monero (XMR)**
```text
8BmduJgZLok9xiaX8FboSWBBbzYAugqLxUts7eZNsF2x9QDhk3Ua7iwQufBBNB8VFzcMEMAE1Uo6PjQvAYNYHmXsBRbqQqG
```
**Zcash (ZEC)**
```text
u1pclheucppc87qlffh9m8wjfw87w2nka40w9nxjuqnyppj0kx9xp7z9rg6wx556662y5f8dtfyeynmm2lnz5aqvaqzmnpajlq0mnmkntdqzqqegk8lwv09cnudf3ttzm3878p3030j3lwupj257rmmv9p3ea32hgwsuf3jdh8ycv7q587
```
**Lightning**
```text
lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0q0zdgjjahpdv7tnd9vstumyrw43snsmfmlzv0pgkqrjkgy48tsne6qsr0k64d8rz4k394pmre2rgnmstdxqsfj0w4dsmq2ec73ssek5wzqtqqv7argu9ptk09h9vfvvvham5xnwe306zjw6lptxx0d2yfk5rlvznjwefmsrmmpu8qnkqmghe0v96c8qy3m3nqgm977ay8f5p6k2d2ll2j3knnc8c4s6haufe203jx4ufy8z25tsscqqseg8jzh2qykejnc9sp2v4qm3z2q
```
**Lightning Address**
```text
shayan@bitrefill.me
```
**Tron** — accepts TRX and **TRC-20 tokens** (USDT, USDC). Tron has its own address format, so this one is not interchangeable with the EVM address above.
```text
TBSCbnTZCELrMnioobZMkah5r9qS6B1tC6
```
!!! warning "Verify the address"
Addresses are long and easy to spoof. Take them from this page or the repository over HTTPS, and check the first and last characters after pasting. **We will never DM you an address.**
The addresses above are generated at build time from [`.github/FUNDING.yml`](https://github.com/MotherofallVPNs/MoaV/blob/main/.github/FUNDING.yml) in the MoaV repository, which is the single source of truth — so what you see here is whatever that file says, and it cannot drift from the repo.
---
## Just here to use it?
That's fine — that's the point. If you want to help without running anything, the most useful things are: tell someone who needs it, report what breaks, and don't share bundles in public channels where they'll be collected and blocked.
---
# Development & Testing
How to run MoaV from source, how it's tested, and how to contribute a change that lands.
## Installing a specific version
The one-liner on `moav.sh` always installs the **latest stable release**. To run a development build or a specific release candidate, fetch the installer *from that ref* and tell it to clone the same one — the two must match, or you'll get an installer from one version setting up another.
=== "Latest dev"
```bash
curl -fsSL https://raw.githubusercontent.com/MotherofallVPNs/MoaV/dev/install.sh | bash -s -- -b dev
```
=== "A specific tag"
```bash
curl -fsSL https://raw.githubusercontent.com/MotherofallVPNs/MoaV/v2.0.0/install.sh | bash -s -- -b v2.0.0
```
=== "Already installed"
```bash
moav update -b dev # or a tag
moav build # images must be rebuilt when the code moves
moav start
```
!!! warning "`dev` is where work in progress lands"
It gets the fixes first and the regressions first. Run it on a test server, not on the one people depend on.
## Running from a clone
```bash
git clone https://github.com/MotherofallVPNs/MoaV.git && cd MoaV
cp .env.example .env # set DOMAIN, ACME_EMAIL, ADMIN_PASSWORD
./moav.sh build && ./moav.sh bootstrap && ./moav.sh start all
./moav.sh doctor
```
`./moav.sh` is the same dispatcher the installed `moav` command runs — the global install is just a symlink.
!!! note "bash 4+ required"
MoaV uses associative arrays, and macOS ships bash 3.2. The dispatcher refuses to run and tells you so rather than failing obscurely. On macOS: `brew install bash`, or work on a Linux box.
## How the code is laid out
| Path | What lives there |
|---|---|
| `moav.sh` | The CLI dispatcher — argument parsing, then straight into a `cmd_*` function |
| `lib/*.sh` | Host-side modules: `service`, `users`, `bootstrap`, `doctor`, `cert`, `migrate`, `donate`, `nettune`, `dns`, `menu`, … |
| `scripts/*-entrypoint.sh` | Container entrypoints, one per service |
| `scripts/lib/*.sh` | Shared provisioning libraries, mounted into containers as `/app/lib` |
| `configs/` | `*.template` files (tracked) rendered into `*.json` / `*.conf` (gitignored) |
| `dockerfiles/`, `exporters/`, `admin/` | Image builds, Prometheus exporters, the FastAPI dashboard |
| `tests/` | The regression suite |
The deep guide for anyone — human or AI agent — working in the repo is [`AGENTS.md`](https://github.com/MotherofallVPNs/MoaV/blob/main/AGENTS.md), and [`llms.txt`](https://moav.sh/llms.txt) is the one-fetch index.
## How testing works
Two layers, and they answer different questions.
### CI — fast, on every push
Runs on GitHub-hosted runners in a couple of minutes: `shellcheck`, a parse check on every script, `docker compose config`, Go tests for the DNS router, and **22 bash suites**.
Each suite is named after the bug class it pins rather than the file it tests — `reality-desync-test`, `wg-keygen-fallback-test`, `entrypoint-strict-test`, `state-perms-test`, `uuid-capture-test`. That naming is deliberate: the suite reads as a list of ways MoaV has broken before and can't break again.
```bash
bash tests/-test.sh # any suite, locally, no install needed
```
### End-to-end — slow, before anything ships
A self-hosted workflow builds the entire stack on a real server with a real domain, provisions a user, and **connects through every protocol**, checking the exit IP is the server. It's the merge bar for anything touching provisioning, because it's the only thing that proves a bundle actually carries traffic.
Tiers: `default` (domain mode), `full` (domain + domainless), `mega` (adds local image builds and image removal on uninstall). The server and domain come from repository secrets, so contributors don't need infrastructure of their own — the maintainers run it on the PR.
Two scripts in `tests/` are **integration harnesses**, not CI suites: `cli-smoke-test.sh` drives a live stack, and `client-test.sh` *is* what `moav test` runs. Both need a real install, and both exit `2` rather than pretending to pass when preconditions are missing.
### The rule
**Every bug fixed ships a regression test in the same PR.** Not as bureaucracy — several of those tests earned their place immediately by catching mistakes made *while* fixing something else: a render guard that could return a false PASS, a name that came out with a double dash, a probe that proved nothing because its control case also passed.
If you fix something, add the test that would have caught it. If the fix is a one-liner and the test is thirty lines, that's usually the right ratio.
## Contributing
1. **Branch off `dev`.** Never commit to `main` — it tracks releases.
2. **Open a PR into `dev`.** CI runs automatically; a maintainer triggers e2e for anything touching provisioning, protocols or permissions.
3. **Include the regression test** for whatever you fixed.
4. **Explain the failure, not just the change.** The most useful PR descriptions say what broke, how it presented, and why the fix addresses the cause rather than the symptom.
Bug reports are genuinely valuable, especially with `moav doctor` output and the relevant `moav logs`. A reproducible report is often more work than the fix.
Details on style and review: [`CONTRIBUTING.md`](https://github.com/MotherofallVPNs/MoaV/blob/main/CONTRIBUTING.md).
!!! danger "Never paste real credentials into an issue"
Bundles, `.env` contents, share links and QR codes contain live keys. Redact server IPs and domains too if your setup is in use. See [OPSEC](https://moav.sh/docs/OPSEC/).
## Conventions that bite
Hard-won, and easy to trip over:
- **Strict mode everywhere.** Entrypoints run `set -eu` with `pipefail` probed in a subshell — `set -o pipefail` is fatal in `dash`, so the usual `|| true` guard doesn't save you. `((x++))` returns 1 when `x` is 0. A plain `VAR=$(cmd)` propagates the failure, so an unguarded assignment can kill a script silently.
- **Never hand a TTY to a container exec.** `docker exec -i` attaches stdin; against a terminal it blocks until the timeout. This is why some commands worked in scripts and CI but failed interactively.
- **`get_env_val` is the only `.env` accessor.** Don't hand-roll `grep | cut` — it breaks on base64 values containing `=`.
- **Generated configs are gitignored**; only `*.template` is tracked, so a `git pull` can never clobber a rendered config.
- **Secrets live in state, not `.env`.** Renders re-source state immediately before writing, so an empty `.env` value can't blank a live secret.
## Contributing to these docs
The docs are mkdocs-material in [moav-site](https://github.com/MotherofallVPNs/moav-site).
```bash
pip install mkdocs-material
mkdocs serve # http://127.0.0.1:8000, live-reloads as you edit
mkdocs build --strict # what CI runs — warnings are failures
```
**Always review from a build, never from the diff.** GitHub's file view doesn't render tabs (`=== "X"`) or collapsibles (`??? note`) — they appear as literal text with their content turned into code blocks, so a correct page looks broken and a broken one can look fine.
Two ways to see a real render:
- **`mkdocs serve` locally** — instant, live-reloading, and the only option if the hosted preview is unavailable.
- **The PR preview** — every PR touching `docs/` gets a hosted URL posted as a comment (`pr-N.moav-docs-preview.pages.dev`). It's stable for the life of the PR, rebuilds on each push, and is deleted when the PR closes.
`--strict` is the gate: a link to a renamed heading, or one pointing into a tab or collapsible (neither generates an anchor), fails the build rather than shipping dead. If the hosted preview is skipped — missing secrets, Cloudflare down — the strict build still runs, so correctness is never gated on the preview being available.
---
# Translating the docs
MoaV exists for people who are being cut off from the internet, and most of them don't read English. A translated Quick Start is worth more to them than any feature we could ship.
This page is everything you need to translate a page. No prior work on MoaV required, and you don't need to understand the software — if you can read the English page and write your language well, you can do this.
**Currently open:** Farsi (فارسی) and Russian (Русский). Want to add another language? [Open an issue](https://github.com/MotherofallVPNs/moav-site/issues) and we'll set it up.
## Pick a page
Start at the top. These are ordered by how many people they reach.
| # | Page | What it is | Farsi | Russian |
|---|---|---|---|---|
| 1 | [Quick Start](https://moav.sh/docs/quick-start/) | Install to first user in ten minutes | *open* | *open* |
| 2 | [Client Setup](https://moav.sh/docs/CLIENTS/) | How someone connects with their bundle | *open* | *open* |
| 3 | [Troubleshooting](https://moav.sh/docs/TROUBLESHOOTING/) | Symptom-first fixes | *open* | *open* |
| 4 | [Home](https://moav.sh/docs/) | Landing page | *open* | *open* |
| 5 | [Supported Protocols](https://moav.sh/docs/protocols/) | What each protocol is and when it survives | *open* | *open* |
| 6 | [Mission](https://moav.sh/docs/mission/) | Why the project exists | *open* | *open* |
!!! tip "Pages 1 and 2 matter most"
Quick Start is read by the person setting up a server. Client Setup is read by the person receiving a bundle — often on a phone, often in a hurry, often on a censored connection. If you only ever translate one page, translate Client Setup.
The remaining pages (CLI reference, Setup, Monitoring, Deploy, OPSEC, Architecture, Development) stay **English-only for now**. They're operator references that change often, and a stale translation of a command reference is worse than no translation — someone will run the old flag. Readers viewing another language see the English version of these automatically.
## Claim it first
So two people don't translate the same page:
1. [Open an issue](https://github.com/MotherofallVPNs/moav-site/issues/new) titled `Translate: → `, e.g. `Translate: CLIENTS → Farsi`.
2. We'll mark it in the table above and it's yours.
If you go quiet for a few weeks we'll release the claim — no hard feelings, and pick it back up whenever.
## Where the file goes
Translations mirror the English tree inside a folder named for your language. **Copy the English file, then translate in place** — don't start from a blank page, because the structure and links need to survive.
```text
docs/
quick-start.md <- English, the source of truth. Never edit this.
CLIENTS.md
fa/
quick-start.md <- your Farsi translation
CLIENTS.md
ru/
quick-start.md <- Russian
```
Language folders: **`fa`** for Farsi, **`ru`** for Russian.
```bash
# Farsi translation of Client Setup:
mkdir -p docs/fa
cp docs/CLIENTS.md docs/fa/CLIENTS.md
# now edit docs/fa/CLIENTS.md
```
A page you haven't translated yet falls back to English automatically, so **one page is a complete, useful contribution.** Nothing is broken by the other pages not existing.
!!! tip "There is a worked example"
[`docs/fa/quick-start.md`](https://github.com/MotherofallVPNs/moav-site/blob/main/docs/fa/quick-start.md) is a complete translation of Quick Start, following every rule on this page — including the video blocks, the tabs, and the links. Read it next to the English original and the pattern should be obvious.
## What to translate, and what to leave alone
Translate all the prose: headings, paragraphs, list items, table cells, admonition titles, tab labels, image alt text.
Leave these **exactly as they are** — they're machine-read, and translating them turns working instructions into broken ones:
| Leave alone | Example |
|---|---|
| Everything inside a code block | `curl -fsSL moav.sh/install.sh \| bash` |
| Commands, subcommands and flags in prose | `moav user add`, `--config`, `moav doctor dns` |
| File paths and environment variable names | `/opt/moav`, `.env`, `REALITY_TARGET`, `DOMAIN` |
| Protocol and product names | Reality, Hysteria2, WireGuard, AmneziaWG, sing-box, Grafana |
| URLs, domains and ports | `moav.sh`, `9443`, `dl.google.com` |
| The admonition keyword | `!!! tip` stays `!!! tip`; its **title** gets translated |
| Link filenames | `[متن](https://moav.sh/docs/CLIENTS/)` — the `CLIENTS.md` part stays |
One nuance on code blocks: the **command** must survive untouched, but a trailing
`#` comment is prose and is worth translating. It renders correctly in
right-to-left languages — the command stays left-to-right and the comment reads
naturally after it:
```bash
moav user add alice # یک کاربر
moav user list # چه کسانی هستند
```
Two syntax details worth seeing side by side:
=== "Admonitions"
The keyword after `!!!` is a type, not text. Translate only the quoted title.
```markdown
!!! tip "Point your DNS before installing"
```
becomes
```markdown
!!! tip "قبل از نصب، DNS را تنظیم کنید"
```
=== "Tabs"
Tab labels *are* prose and should be translated.
```markdown
=== "Web dashboard"
=== "Command line"
```
becomes
```markdown
=== "پنل وب"
=== "خط فرمان"
```
### Links keep their English filename
Leave the `.md` filename exactly as it is, with no `../`:
```markdown
[اپلیکیشنهای کاربر](https://moav.sh/docs/CLIENTS/)
```
From `docs/fa/quick-start.md` that resolves to `/fa/CLIENTS/`, which serves the
English page inside your language's tree, so the reader keeps the language
switcher and the Persian navigation. Adding `../` also builds, but it is
unnecessary — copy the English link and translate only the text in brackets.
### Links with a `#anchor`
This is the one thing that trips people up. An anchor is generated from the *heading text of the target page*, so if you translate a heading, every link pointing at it must change too.
- Linking **within your own translated page**: translate the anchor to match your translated heading. `#do-i-need-a-domain` → the slug of your translated heading.
- Linking to a page that is **still English** (most of them): leave the anchor exactly as-is. It still points at the English heading, which is what the reader will land on.
If you're unsure, leave the anchor alone. The build will tell you if it's wrong — see below.
## Check your work before opening a PR
Two ways, and the first one catches almost everything.
=== "Open the PR and read the preview"
Every pull request gets a hosted preview posted as a comment within a couple of minutes. Yours will be at `…/fa/CLIENTS/`. This is the easiest route and needs nothing installed.
The build also **fails** if a link points at a heading that doesn't exist, so a broken anchor is caught for you rather than shipping dead.
=== "Build it locally"
```bash
pip install -r requirements.txt
mkdocs serve
```
Open `http://127.0.0.1:8000/fa/CLIENTS/`. It live-reloads as you save.
!!! warning "Don't review your translation in GitHub's file view"
GitHub doesn't render tabs (`=== "…"`) or collapsible blocks (`??? note`) — they show up as literal text with their contents turned into code blocks. A correct page looks broken there, and a broken one can look fine. Always read it from a build.
Farsi renders right-to-left automatically, and the interface strings (search box, navigation) are already translated by the theme. You don't need to do anything for either.
## Opening the pull request
1. Fork the repo, and branch off `main`.
2. Add your file under `docs/fa/` or `docs/ru/`.
3. Open a PR titled `docs(fa): translate Client Setup` (or `docs(ru): …`).
4. Read the preview link when it appears, and fix anything that looks off.
We review for broken syntax and links, not for your language — we mostly can't judge that, which is exactly why we need you. If a reviewer suggests a wording change and you disagree, you're the one who speaks the language. Say so.
## A note on tone
These pages are read by people under pressure, sometimes during a shutdown, often on a phone with a bad connection. Prefer the plain, direct wording of your language over the formal register. If an English sentence is long and hedged, it's fine to make it shorter and clearer — a translation that reads naturally is better than one that mirrors the English clause by clause.
If a passage assumes context your readers won't have, add a short clarifying phrase. You know the audience better than we do.
---
Questions, or want a language added? [Open an issue](https://github.com/MotherofallVPNs/moav-site/issues), or see the other ways to help on [Support MoaV](https://moav.sh/docs/support/).