DNS Configuration Guide¶
This guide explains how to configure DNS records for MoaV.
Table of Contents¶
- Do I Need a Domain?
- Domain-less Mode
- Domain Setup
- Minimum Setup (Without DNS Tunnels)
- Full Setup (With DNS Tunnels)
- Provider-Specific Instructions
- Cloudflare
- Namecheap
- Google Domains / Squarespace
- Hetzner DNS
- Home Servers & Raspberry Pi
- Port Forwarding
- Dynamic DNS (DDNS)
- DuckDNS (Free)
- Cloudflare DDNS (Own Domain)
- Home Server Tips
- Verification
- Common Issues
- Domain Acquisition Tips
Do I Need a Domain?¶
No. MoaV can run without a domain in domain-less mode. A domain unlocks more protocols, but several work with just an IP address.
| Protocol | Requires Domain | Port |
|---|---|---|
| Reality (VLESS) | No | 443/tcp |
| WireGuard | No | 51820/udp |
| WireGuard (wstunnel) | No | 8080/tcp |
| AmneziaWG | No | 51821/udp |
| Telegram MTProxy (telemt) | No | 993/tcp |
| Admin Dashboard | No | 9443/tcp |
| Conduit (Psiphon donation) | No | — |
| Snowflake (Tor donation) | No | — |
| Trojan | Yes | 8443/tcp |
| Hysteria2 | Yes | 443/udp |
| TrustTunnel | Yes | 4443/tcp+udp |
| CDN (VLESS+WebSocket) | Yes (Cloudflare) | 2082/tcp |
| dnstt (DNS tunnel) | Yes (NS records) | 53/udp |
| Slipstream (QUIC-over-DNS) | Yes (NS records) | 53/udp |
Domain-dependent protocols need a valid TLS certificate (via Let's Encrypt) or NS delegation, which both require a domain.
Domain-less Mode¶
Leave DOMAIN= empty in your .env file. MoaV automatically detects this and runs only protocols that work without a domain:
- Reality — VLESS with TLS camouflage (uses
REALITY_TARGETlikedl.google.cominstead of your own domain) - WireGuard — Full VPN, direct UDP or tunneled over WebSocket (TCP) when UDP is blocked
- AmneziaWG — DPI-resistant WireGuard with packet-level obfuscation
- Telegram MTProxy — Direct Telegram access via fake-TLS, no VPN needed
- Admin Dashboard — Web UI with self-signed certificate
- Conduit / Snowflake — Bandwidth donation (optional)
This is ideal for: - Raspberry Pi or home servers without a registered domain - Quick deployments when you can't register a domain - Environments where only VPN-style protocols are needed
You can upgrade to a full domain setup later — just set DOMAIN= in .env and run moav bootstrap.
Port Forwarding (Domain-less)¶
If running on a home network, forward these ports on your router:
| Port | Protocol | Service |
|---|---|---|
| 443/tcp | TCP | Reality (VLESS) |
| 51820/udp | UDP | WireGuard |
| 8080/tcp | TCP | wstunnel (WireGuard over WebSocket) |
| 51821/udp | UDP | AmneziaWG |
| 993/tcp | TCP | Telegram MTProxy |
| 9443/tcp | TCP | Admin Dashboard |
No port 80 needed — domain-less mode doesn't use Let's Encrypt.
Domain Setup¶
If you have a domain, you unlock all 13+ protocols. How many DNS records you need depends on which features you enable.
Minimum Setup (Without DNS Tunnels)¶
If you don't need DNS tunnels (dnstt / Slipstream), you only need one record:
This enables: Reality, Trojan, Hysteria2, TrustTunnel, CDN mode, and all domain-less protocols.
Full Setup (With DNS Tunnels)¶
Step 1: Main A Record¶
Step 2: DNS Server A Record¶
This creates dns.yourdomain.com pointing to your server (used as the nameserver for tunnel subdomains).
Step 3: NS Delegation for dnstt¶
This tells DNS resolvers that queries for *.t.yourdomain.com should be sent to dns.yourdomain.com (your server). Used by dnstt.
Step 4: NS Delegation for Slipstream (Optional)¶
Same concept as dnstt, but for the Slipstream QUIC-over-DNS tunnel. Slipstream is 1.5-5x faster than dnstt. Only needed if ENABLE_SLIPSTREAM=true.
Optional: IPv6 Support¶
If your server has IPv6, you can also add an AAAA record for the nameserver:
More Info: For detailed dnstt documentation, see the official dnstt guide.
Summary of All DNS Records¶
| Record | Name | Value | Proxy | Purpose | Required? |
|---|---|---|---|---|---|
| A | @ |
Server IP | DNS only | Main domain (Trojan, Hysteria2, Reality) | Yes |
| A | dns |
Server IP | DNS only | Nameserver for DNS tunnels | Only for dnstt/Slipstream |
| NS | t |
dns.domain.com |
— | dnstt tunnel subdomain | Only for dnstt |
| NS | s |
dns.domain.com |
— | Slipstream tunnel subdomain | Only for Slipstream |
| A | cdn |
Server IP | Proxied | CDN-fronted VLESS | Only for CDN mode |
| A | www |
Server IP | Proxied | CDN stealth connect address | Optional (CDN stealth) |
| A | grafana |
Server IP | Proxied | Grafana via CDN | Optional (monitoring) |
Provider-Specific Instructions¶
Cloudflare¶
- Log into Cloudflare Dashboard
- Select your domain
- Go to DNS → Records
- Add records:
Important: Set proxy status to "DNS only" (gray cloud) for most records. Only CDN-related records should be "Proxied" (orange cloud).
| Type | Name | Content | Proxy status |
|---|---|---|---|
| A | @ | YOUR_IP | DNS only |
| A | dns | YOUR_IP | DNS only |
| NS | t | dns.yourdomain.com | — |
| NS | s | dns.yourdomain.com | — |
| A | cdn | YOUR_IP | Proxied (orange cloud) |
| A | www | YOUR_IP | Proxied (orange cloud) |
| A | grafana | YOUR_IP | Proxied (orange cloud) |
The
cdn,www, andgrafanarecords are optional: -cdn— Required if you want CDN-fronted VLESS (ENABLE_CDN=trueor CDN_SUBDOMAIN set) -www— Recommended for CDN stealth. Used as the CDN connect address so DNS queries don't reveal the "cdn" subdomain to DPI. SetCDN_ADDRESS=www.yourdomain.comin.env-grafana— Only needed if you want faster Grafana loading via CDN (see Monitoring Guide)All other records must be DNS only (gray cloud).
CDN Origin Rule (Required for CDN Mode)¶
If you added the cdn record above, you must also create an Origin Rule to redirect traffic to port 2082. By default, Cloudflare's Flexible SSL connects to origin port 80, but MoaV's CDN listener runs on port 2082.
Step 1: Go to Rules → Origin Rules
- In Cloudflare Dashboard, select your domain
- Navigate to Rules → Origin Rules
- Click Create rule
Step 2: Configure the Rule
| Field | Value |
|---|---|
| Rule name | CDN to port 2082 |
| When incoming requests match... | Hostname equals cdn.yourdomain.com |
| Then... | Destination Port → Rewrite to 2082 |
Step 3: Deploy
Click Deploy to activate the rule.
Verify it works:
# Should return HTTP 400 (sing-box responding, not Cloudflare 521)
# Use any path - the CDN WS path is auto-generated during bootstrap
curl -s -o /dev/null -w "%{http_code}" https://cdn.yourdomain.com/test
A 400 or 404 response means sing-box is receiving the request.
- 521 = Origin Rule is missing or misconfigured
- 525 = SSL mode is wrong — set Cloudflare SSL/TLS to Flexible (not Full/Strict), because MoaV's CDN port 2082 is plain HTTP
Important: Cloudflare SSL/TLS mode must be set to Flexible for CDN mode. MoaV's CDN inbound on port 2082 is plain HTTP (Cloudflare terminates TLS). If you need Full SSL for other subdomains, use a Configuration Rule to set Flexible for just
cdn.yourdomain.com.
See CDN Setup Guide for complete CDN configuration.
Namecheap¶
- Log into Namecheap
- Domain List → Manage → Advanced DNS
- Add records:
| Type | Host | Value | TTL |
|---|---|---|---|
| A Record | @ | YOUR_IP | Automatic |
| A Record | dns | YOUR_IP | Automatic |
| NS Record | t | dns.yourdomain.com. | Automatic |
| NS Record | s | dns.yourdomain.com. | Automatic |
Note: NS value may need trailing dot. The dns, t, and s records are only needed if using DNS tunnels.
Google Domains / Squarespace¶
- Go to DNS settings
- Add custom records:
| Host name | Type | TTL | Data |
|---|---|---|---|
| (blank) | A | 300 | YOUR_IP |
| dns | A | 300 | YOUR_IP |
| t | NS | 300 | dns.yourdomain.com |
| s | NS | 300 | dns.yourdomain.com |
Hetzner DNS¶
- Go to DNS Console
- Select your zone
- Add records:
Home Servers & Raspberry Pi¶
MoaV runs on Raspberry Pi 4+ (2GB+ RAM) and any ARM64/x64 Linux machine. Home servers typically have dynamic IPs and sit behind a router, so you need port forwarding and (if using a domain) Dynamic DNS.
Port Forwarding¶
Configure your router to forward the ports you need to your MoaV server's local IP.
Domain-less mode (minimum):
| Port | Protocol | Service |
|---|---|---|
| 443/tcp | TCP | Reality (VLESS) |
| 51820/udp | UDP | WireGuard |
| 8080/tcp | TCP | wstunnel (WireGuard over WebSocket) |
| 51821/udp | UDP | AmneziaWG |
| 993/tcp | TCP | Telegram MTProxy |
| 9443/tcp | TCP | Admin Dashboard |
With domain (add these):
| Port | Protocol | Service |
|---|---|---|
| 80/tcp | TCP | Let's Encrypt verification (only during certificate setup/renewal) |
| 443/udp | UDP | Hysteria2 |
| 8443/tcp | TCP | Trojan |
| 4443/tcp | TCP | TrustTunnel (HTTP/2) |
| 4443/udp | UDP | TrustTunnel (HTTP/3 / QUIC) |
| 53/udp | UDP | DNS tunnels (dnstt + Slipstream) |
Optional:
| Port | Protocol | Service |
|---|---|---|
| 9444/tcp | TCP | Grafana monitoring dashboard |
Only forward ports for protocols you actually enable. Check your
.envfile forENABLE_*toggles.
Before You Start¶
-
Check for CGNAT: Some ISPs use Carrier-Grade NAT which prevents incoming connections entirely. Test by comparing your router's WAN IP with
curl ipinfo.io/ip. If they differ, contact your ISP for a public IP or use a VPS instead. -
Static local IP: Assign a static IP to your MoaV server in your router's DHCP settings so port forwarding rules don't break when the local IP changes.
Dynamic DNS (DDNS)¶
If you're using a domain with a home server, your ISP likely assigns a dynamic public IP that changes periodically. Dynamic DNS services automatically update your domain to point to your current IP.
Domain-less mode does not need DDNS. Users connect via your public IP directly. You can find your current public IP with
curl ifconfig.meand share it manually. If your IP changes, update the configs you shared.
DuckDNS (Free)¶
DuckDNS is a free DDNS service that provides subdomains like yourname.duckdns.org. Let's Encrypt works with DuckDNS domains.
Step 1: Create Account¶
- Go to duckdns.org
- Sign in with Google, GitHub, Twitter, or Reddit
- Create a subdomain (e.g.,
myvpn→myvpn.duckdns.org) - Copy your token from the dashboard
Step 2: Install Update Script¶
On your MoaV server (Raspberry Pi or home server):
# Create update script
mkdir -p /opt/duckdns
cat > /opt/duckdns/duck.sh << 'EOF'
#!/bin/bash
DOMAIN="YOUR_SUBDOMAIN" # e.g., myvpn (without .duckdns.org)
TOKEN="YOUR_TOKEN"
curl -s "https://www.duckdns.org/update?domains=${DOMAIN}&token=${TOKEN}&ip=" | logger -t duckdns
EOF
# Replace with your values
nano /opt/duckdns/duck.sh
# Make executable
chmod +x /opt/duckdns/duck.sh
# Test it
/opt/duckdns/duck.sh
Step 3: Schedule Automatic Updates¶
# Add to crontab (runs every 5 minutes)
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/duckdns/duck.sh") | crontab -
Step 4: Configure MoaV¶
In your .env file:
Then run bootstrap as normal.
Note: DuckDNS subdomains don't support NS delegation, so DNS tunnels (dnstt, Slipstream) won't work with DuckDNS. All other domain-based protocols work fine.
Cloudflare DDNS (Own Domain)¶
If you have your own domain on Cloudflare, you can use the Cloudflare API to update DNS records automatically. This supports all features including DNS tunnels.
Step 1: Get API Token¶
- Go to Cloudflare Dashboard → My Profile → API Tokens
- Create a token with Zone:DNS:Edit permission for your domain
- Copy the token
Step 2: Get Zone ID¶
- Go to your domain in Cloudflare
- Scroll down on the Overview page
- Copy the Zone ID from the right sidebar
Step 3: Install Update Script¶
mkdir -p /opt/cloudflare-ddns
cat > /opt/cloudflare-ddns/update.sh << 'EOF'
#!/bin/bash
# Configuration
CF_API_TOKEN="YOUR_API_TOKEN"
CF_ZONE_ID="YOUR_ZONE_ID"
DOMAIN="yourdomain.com"
RECORD_NAME="@" # Use "@" for root domain or "subdomain" for subdomain
# Get current public IP
CURRENT_IP=$(curl -s https://api.ipify.org)
# Get current DNS record
RECORD_DATA=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
RECORD_ID=$(echo "$RECORD_DATA" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
RECORD_IP=$(echo "$RECORD_DATA" | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4)
# Update if IP changed
if [ "$CURRENT_IP" != "$RECORD_IP" ]; then
echo "IP changed from $RECORD_IP to $CURRENT_IP, updating..."
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${DOMAIN}\",\"content\":\"${CURRENT_IP}\",\"ttl\":300,\"proxied\":false}" | logger -t cloudflare-ddns
else
echo "IP unchanged ($CURRENT_IP)"
fi
EOF
# Edit with your values
nano /opt/cloudflare-ddns/update.sh
chmod +x /opt/cloudflare-ddns/update.sh
Step 4: Schedule Updates¶
# Run every 5 minutes
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/cloudflare-ddns/update.sh") | crontab -
Step 5: Configure MoaV¶
In your .env:
After DDNS Setup¶
- Wait for propagation: After the first update, wait 5-10 minutes
- Verify:
dig +short yourdomain.comshould show your home IP - Run MoaV setup:
moavto start the interactive setup - Test from outside: Use mobile data (not home WiFi) to test connectivity
Home Server Tips¶
- UPS recommended: Protect against power outages, especially for Raspberry Pi
- Monitor uptime: Use a free service like UptimeRobot to alert you if your server goes down
- Backup regularly:
moav exportto backup your configuration - Temperature: Ensure adequate cooling for Raspberry Pi under sustained VPN load
- SD card: Use a high-endurance microSD card or boot from USB/SSD for reliability
Verification¶
After configuring DNS, wait for propagation (usually 5-30 minutes, up to 48 hours).
Verify A Record¶
dig +short yourdomain.com
# Should return: YOUR_SERVER_IP
dig +short dns.yourdomain.com
# Should return: YOUR_SERVER_IP
Verify NS Delegation¶
dig NS t.yourdomain.com
# Should show: dns.yourdomain.com in AUTHORITY SECTION
# Test that queries reach your server
dig @YOUR_SERVER_IP test.t.yourdomain.com
# Should get a response (after dnstt is running)
Online Tools¶
- https://dnschecker.org - Check propagation worldwide
- https://mxtoolbox.com/DNSLookup.aspx - Detailed DNS lookup
Common Issues¶
"DNS not propagated yet"¶
Wait longer (up to 48 hours in rare cases). Check with multiple DNS servers:
"NS record not working"¶
- Ensure the A record for
dns.yourdomain.comexists - Some registrars require a trailing dot:
dns.yourdomain.com. - NS delegation can take longer to propagate
"Certificate acquisition failed"¶
- Verify A record is correct:
dig yourdomain.com - Ensure port 80 is open (temporarily, for ACME HTTP-01)
- Check that no other service is using port 80
- Not applicable in domain-less mode (no certificates needed)
"Can't connect from outside my home network"¶
- Verify port forwarding is configured on your router
- Check for CGNAT:
curl ifconfig.meshould match your router's WAN IP - Ensure your ISP doesn't block the ports you need
- Test from mobile data, not your home WiFi
Domain Acquisition Tips¶
For users in censored regions:
- Use privacy protection - Hide your personal info in WHOIS
- Pay with crypto if possible - For anonymity
- Choose a neutral TLD -
.com,.net,.orgare less suspicious than country-specific TLDs - Avoid "VPN" or "proxy" in the domain name - Keep it generic
- Consider multiple domains - Have backups ready if one gets blocked
Domain Naming Strategy¶
Your domain name is the first thing DPI systems see in the TLS SNI. A good domain blends with legitimate traffic:
Good examples:
- Names that look like business infrastructure: cloudops-services.com, cdn-platform.net
- Names that look like SaaS products: dataflow-sync.com, metrics-hub.net
- Generic tech names: stackbuilder.io, nodebridge.net
Bad examples:
- Anything with "vpn", "proxy", "tunnel", "free", "bypass" in the name
- Random strings: xk4m2p.com (suspicious to automated systems)
- Known circumvention patterns: v2ray-server.com
Subdomain naming also matters. MoaV's CDN subdomain defaults to cdn — consider changing it to something like assets, static, api, or app in your .env:
Recommended Registrars¶
- Namecheap - Good privacy, accepts crypto
- Porkbun - Cheap, good privacy
- Njalla - Maximum privacy (they own the domain for you)