Architecture¶
How MoaV is wired together. For protocol-level details see protocols.md; for CLI behavior see CLI.md; for DNS-tunnel mechanics see DNS.md.
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).
| 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, 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. 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
.txts, QR.pngs, a single base64subscription.txtimportable by MahsaNG / v2rayNG / Hiddify / Streisand. - L3 VPNs (WireGuard, AmneziaWG, TrustTunnel) —
.conf/.tomlconfigs + 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.
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: ALLand add back only what they need (e.g.NET_ADMINfor WireGuard,NET_BIND_SERVICEfor privileged ports). Most also setread_only: truewith a smalltmpfsfor/tmp,no-new-privileges: true, andmem_limit/cpuscaps. - 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
setprivbefore exec'ing the daemon. - No direct Docker socket — the admin dashboard reads container status through a read-only docker-socket-proxy scoped to
CONTAINERS/NETWORKSonly, never mounting/var/run/docker.sockinto the app. - Admin auth fails closed — the dashboard refuses to serve (HTTP 503) if
ADMIN_PASSWORDis empty or one of the known-insecure defaults, and uses constant-time comparison; an optional IP allow-list narrows access further. See OPSEC. - Secrets live under the
moav_stateDocker volume (state/keys/,state/users/<user>/), generated withopenssl 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). 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 — step-by-step deployment walkthrough
- DNS Configuration — NS records, resolver-mode vs direct-mode XDNS, port 53
- CLI Reference — every
moavcommand, including the disabled-profile prompt - Supported Protocols — protocol-level cipher, port, and client-compat detail
- Monitoring — dashboards, Conduit lifetime, GeoIP setup