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. 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.
| 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 you expose |
| A | cdn |
server IP | Proxied | CDN mode |
| A | www |
server IP | Proxied | 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 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.
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.
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.
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):
- Origin Rule (Rules → Origin Rules): when hostname =
cdn.yourdomain.com, rewrite Destination Port → 2082. MoaV's CDN listener doesn't bind 80/443. - 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.
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).
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:
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):
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¶
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.
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 (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 — 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 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 so it stays in one place.