Split-Horizon DNS Setup
Status: β Fully configured and operational as of 2026-02-03
Using shdwnet.cloud as both internal LAN domain and external domain via split-horizon (split-brain) DNS.
π§ The Concept
Split-horizon DNS = same domain resolves to different IPs depending on where youβre asking from.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β plex.shdwnet.cloud β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β INSIDE LAN (Pi-hole DNS) OUTSIDE (Cloudflare DNS) β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β
β Query: plex.shdwnet.cloud Query: plex.shdwnet.cloud β
β β β β
β Pi-hole answers: Cloudflare answers: β
β β 192.168.1.10 (NPM local) β 203.x.x.x (your public IP)β
β β β β
β Direct LAN traffic Traffic routes through β
β (fast, no hairpin NAT) Internet β Router β NPM β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why this is better than .local or .lan:
- β Same URLs work everywhere (home, mobile, VPN, coffee shop)
- β Real SSL certificates (Letβs Encrypt validates against Cloudflare)
- β No hairpin NAT issues when inside LAN
- β Proper domain you actually own
- β Works with apps that validate domain names
ποΈ Architecture
βββββββββββββββββββ
β INTERNET β
ββββββββββ¬βββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
β β
βΌ β
ββββββββββββββββββββ β
β Cloudflare β β
β DNS Servers β β
β β β
β *.shdwnet.cloud β β
β β Your Public IP β β
ββββββββββ¬ββββββββββ β
β β
β (external queries) β
βΌ β
ββββββββββββββββββββ ββββββββββ΄ββββββββ
β Your Router β β Your Router β
β (Public IP) βββββββββββββ€ (Port Fwd) β
ββββββββββ¬ββββββββββ ββββββββββββββββββ
β
β Port 80/443 forwarded
βΌ
ββββββββββββββββββββ
β NPM (.10) βββββββββββββββββββββββββββββββββ
β Reverse Proxy β β
ββββββββββ¬ββββββββββ β
β β
β Proxies to internal services β
βΌ β
ββββββββββββββββββββββββββββββββββββββββ β
β Internal Services β β
β β’ Plex (.156:32400) β β
β β’ Paperless (.156:8000) β β
β β’ Vaultwarden (docker1) β β
ββββββββββββββββββββββββββββββββββββββββ β
β
ββββββββββββββββββββββββββββββββββββββββ β
β
INSIDE LAN: β
β
ββββββββββββββββββββ β
β Pi-hole (.50) β β
β Local DNS β β
β β β
β *.shdwnet.cloud βββββββββββββββββββββββββββββββββ
β β 192.168.1.10 β (resolves to NPM directly)
β (local NPM IP) β
ββββββββββββββββββββ
β²
β
ββββββββββ΄ββββββββββ
β LAN Clients β
β (use Pi-hole) β
ββββββββββββββββββββ
π DNS Records Needed
Layer 1: Cloudflare (External/Public DNS)
These records are set in Cloudflare dashboard for shdwnet.cloud:
| Type | Name | Target | Proxy | TTL | Purpose |
|---|---|---|---|---|---|
| A | @ | YOUR_PUBLIC_IP | βοΈ Proxied | Auto | Root domain |
| A | * | YOUR_PUBLIC_IP | βοΈ Proxied | Auto | Wildcard (all subdomains) |
Or individual subdomains (more control):
| Type | Name | Target | Proxy | TTL |
|---|---|---|---|---|
| A | notes | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
| A | plex | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
| A | vault | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
| A | uptime | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
| A | paperless | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
| A | portainer | YOUR_PUBLIC_IP | βοΈ Proxied | Auto |
π‘ Wildcard vs Individual: Wildcard (
*) is easier but exposes that any subdomain exists. Individual records give you more control.
βοΈ Cloudflare Proxy: βProxiedβ (orange cloud) hides your real IP and adds DDoS protection. βDNS Onlyβ (grey cloud) exposes your IP.
Layer 2: Pi-hole (Internal/LAN DNS)
These records override Cloudflare for LAN clients. Set in Pi-hole Admin β Local DNS β DNS Records:
| Domain | IP Address | Purpose |
|---|---|---|
shdwnet.cloud | 192.168.1.10 | Root β NPM |
notes.shdwnet.cloud | 192.168.1.10 | Notes β NPM |
plex.shdwnet.cloud | 192.168.1.10 | Plex β NPM |
vault.shdwnet.cloud | 192.168.1.10 | Vault β NPM |
uptime.shdwnet.cloud | 192.168.1.10 | Uptime β NPM |
paperless.shdwnet.cloud | 192.168.1.10 | Paperless β NPM |
portainer.shdwnet.cloud | 192.168.1.10 | Portainer β NPM |
proxmox.shdwnet.cloud | 192.168.1.144 | Direct to Proxmox (no proxy) |
Why point to NPM (192.168.1.10)?
- NPM handles SSL termination
- NPM routes to the correct backend service
- You get HTTPS even on LAN
- Same URL path works everywhere
When to point directly (skip NPM)?
- Services with their own SSL (Proxmox WebUI)
- Services that donβt need external access
- Performance-critical internal-only services
Layer 3: NPM Proxy Hosts
For each subdomain, create a proxy host in NPM:
| Domain | Forward Host | Forward Port | SSL |
|---|---|---|---|
notes.shdwnet.cloud | 192.168.1.X | XXXX | β Letβs Encrypt |
plex.shdwnet.cloud | 192.168.1.156 | 32400 | β Letβs Encrypt |
vault.shdwnet.cloud | 192.168.1.20 | XXXX | β Letβs Encrypt |
uptime.shdwnet.cloud | 192.168.1.20 | XXXX | β Letβs Encrypt |
paperless.shdwnet.cloud | 192.168.1.156 | 8000 | β Letβs Encrypt |
portainer.shdwnet.cloud | 192.168.1.20 | 8080 | β Letβs Encrypt |
π§ Setup Steps
Step 1: Cloudflare DNS Records
- Login to Cloudflare Dashboard
- Select
shdwnet.clouddomain - Go to DNS β Records
- Add A records pointing to your public IP
- Enable Cloudflare proxy (orange cloud) for DDoS protection
Find your public IP:
curl -s ifconfig.meStep 2: Router Port Forwarding
Forward ports 80 and 443 to NPM:
| External Port | Internal IP | Internal Port | Protocol |
|---|---|---|---|
| 80 | 192.168.1.10 | 80 | TCP |
| 443 | 192.168.1.10 | 443 | TCP |
Step 3: NPM Proxy Hosts
- Login to NPM: http://192.168.1.10:81
- Proxy Hosts β Add Proxy Host
- For each subdomain:
- Domain:
plex.shdwnet.cloud - Forward Hostname:
192.168.1.156 - Forward Port:
32400 - Enable: Block Common Exploits
- SSL Tab: Request new SSL certificate, Force SSL, HTTP/2
- Domain:
Step 4: Pi-hole Local DNS
- Login to Pi-hole: http://192.168.1.50/admin
- Local DNS β DNS Records
- Add each subdomain pointing to
192.168.1.10(NPM)
Or via CLI:
ssh root@192.168.1.50
# Add to /etc/pihole/custom.list
echo "192.168.1.10 shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 notes.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 plex.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 vault.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 uptime.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 paperless.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.10 portainer.shdwnet.cloud" >> /etc/pihole/custom.list
echo "192.168.1.144 proxmox.shdwnet.cloud" >> /etc/pihole/custom.list
# Restart DNS
pihole restartdnsStep 5: Test
From inside LAN:
# Should resolve to local IP
nslookup plex.shdwnet.cloud
# Expected: 192.168.1.10
# Should work
curl -I https://plex.shdwnet.cloudFrom outside (mobile data, VPN off):
# Should resolve to public IP (or Cloudflare proxy IP)
nslookup plex.shdwnet.cloud 1.1.1.1
# Should work through the internet
curl -I https://plex.shdwnet.cloudπ SSL Certificates
How Letβs Encrypt works with this setup:
- You request cert for
plex.shdwnet.cloudin NPM - Letβs Encrypt does HTTP-01 challenge (or DNS-01)
- Challenge goes to Cloudflare β your public IP β NPM
- NPM responds, cert is issued
- Cert is valid for both internal and external access
DNS-01 Challenge (recommended for wildcards):
- Configure Cloudflare API token in NPM
- NPM can request
*.shdwnet.cloudwildcard cert - No port 80 needed during validation
π‘οΈ Security Considerations
- Firewall NPM: Only expose 80/443, not the admin port (81)
- Cloudflare Proxy: Hides your real IP from attackers
- Access Lists: NPM can restrict access by IP for sensitive services
- Internal-Only Services: Donβt add Cloudflare DNS for services that shouldnβt be external (like Proxmox)
πΊοΈ Final Domain Map (β Live 2026-02-03)
| Domain | External (Cloudflare) | Internal (Pi-hole) | Service |
|---|---|---|---|
shdwnet.cloud | β Public IP | β 192.168.1.10 | Root/Landing |
notes.shdwnet.cloud | β Public IP | β 192.168.1.10 | Quartz Notes (PM2 :3030) |
plex.shdwnet.cloud | β Public IP | β 192.168.1.10 | Plex Media Server |
vault.shdwnet.cloud | β Public IP | β 192.168.1.10 | Vaultwarden |
uptime.shdwnet.cloud | β Public IP | β 192.168.1.10 | Uptime Kuma |
pulse.shdwnet.cloud | β Public IP | β 192.168.1.10 | Alias β Uptime Kuma |
paperless.shdwnet.cloud | β Public IP | β 192.168.1.10 | Paperless-ngx |
portainer.shdwnet.cloud | β Public IP | β 192.168.1.10 | Portainer |
netdata.shdwnet.cloud | β Public IP | β 192.168.1.10 | VPS Netdata (via NPM β VPS:19998) |
proxmox.shdwnet.cloud | β None | β 192.168.1.144 | Proxmox (internal only) |
Pi-hole v6 Configuration
Pi-hole v6 uses pihole.toml for DNS records (not custom.list):
# /etc/pihole/pihole.toml (inside Docker container)
hosts = [
"192.168.1.10 shdwnet.cloud",
"192.168.1.10 notes.shdwnet.cloud",
"192.168.1.10 plex.shdwnet.cloud",
"192.168.1.10 vault.shdwnet.cloud",
"192.168.1.10 uptime.shdwnet.cloud",
"192.168.1.10 pulse.shdwnet.cloud",
"192.168.1.10 paperless.shdwnet.cloud",
"192.168.1.10 portainer.shdwnet.cloud",
"192.168.1.10 netdata.shdwnet.cloud",
"192.168.1.144 proxmox.shdwnet.cloud"
]Reload after changes: docker exec pihole pihole reloaddns