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:

TypeNameTargetProxyTTLPurpose
A@YOUR_PUBLIC_IP☁️ ProxiedAutoRoot domain
A*YOUR_PUBLIC_IP☁️ ProxiedAutoWildcard (all subdomains)

Or individual subdomains (more control):

TypeNameTargetProxyTTL
AnotesYOUR_PUBLIC_IP☁️ ProxiedAuto
AplexYOUR_PUBLIC_IP☁️ ProxiedAuto
AvaultYOUR_PUBLIC_IP☁️ ProxiedAuto
AuptimeYOUR_PUBLIC_IP☁️ ProxiedAuto
ApaperlessYOUR_PUBLIC_IP☁️ ProxiedAuto
AportainerYOUR_PUBLIC_IP☁️ ProxiedAuto

πŸ’‘ 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:

DomainIP AddressPurpose
shdwnet.cloud192.168.1.10Root β†’ NPM
notes.shdwnet.cloud192.168.1.10Notes β†’ NPM
plex.shdwnet.cloud192.168.1.10Plex β†’ NPM
vault.shdwnet.cloud192.168.1.10Vault β†’ NPM
uptime.shdwnet.cloud192.168.1.10Uptime β†’ NPM
paperless.shdwnet.cloud192.168.1.10Paperless β†’ NPM
portainer.shdwnet.cloud192.168.1.10Portainer β†’ NPM
proxmox.shdwnet.cloud192.168.1.144Direct 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:

DomainForward HostForward PortSSL
notes.shdwnet.cloud192.168.1.XXXXXβœ… Let’s Encrypt
plex.shdwnet.cloud192.168.1.15632400βœ… Let’s Encrypt
vault.shdwnet.cloud192.168.1.20XXXXβœ… Let’s Encrypt
uptime.shdwnet.cloud192.168.1.20XXXXβœ… Let’s Encrypt
paperless.shdwnet.cloud192.168.1.1568000βœ… Let’s Encrypt
portainer.shdwnet.cloud192.168.1.208080βœ… Let’s Encrypt

πŸ”§ Setup Steps

Step 1: Cloudflare DNS Records

  1. Login to Cloudflare Dashboard
  2. Select shdwnet.cloud domain
  3. Go to DNS β†’ Records
  4. Add A records pointing to your public IP
  5. Enable Cloudflare proxy (orange cloud) for DDoS protection

Find your public IP:

curl -s ifconfig.me

Step 2: Router Port Forwarding

Forward ports 80 and 443 to NPM:

External PortInternal IPInternal PortProtocol
80192.168.1.1080TCP
443192.168.1.10443TCP

Step 3: NPM Proxy Hosts

  1. Login to NPM: http://192.168.1.10:81
  2. Proxy Hosts β†’ Add Proxy Host
  3. 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

Step 4: Pi-hole Local DNS

  1. Login to Pi-hole: http://192.168.1.50/admin
  2. Local DNS β†’ DNS Records
  3. 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 restartdns

Step 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.cloud

From 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:

  1. You request cert for plex.shdwnet.cloud in NPM
  2. Let’s Encrypt does HTTP-01 challenge (or DNS-01)
  3. Challenge goes to Cloudflare β†’ your public IP β†’ NPM
  4. NPM responds, cert is issued
  5. 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.cloud wildcard cert
  • No port 80 needed during validation

πŸ›‘οΈ Security Considerations

  1. Firewall NPM: Only expose 80/443, not the admin port (81)
  2. Cloudflare Proxy: Hides your real IP from attackers
  3. Access Lists: NPM can restrict access by IP for sensitive services
  4. Internal-Only Services: Don’t add Cloudflare DNS for services that shouldn’t be external (like Proxmox)

πŸ—ΊοΈ Final Domain Map (βœ… Live 2026-02-03)

DomainExternal (Cloudflare)Internal (Pi-hole)Service
shdwnet.cloud→ Public IP→ 192.168.1.10Root/Landing
notes.shdwnet.cloud→ Public IP→ 192.168.1.10Quartz Notes (PM2 :3030)
plex.shdwnet.cloud→ Public IP→ 192.168.1.10Plex Media Server
vault.shdwnet.cloud→ Public IP→ 192.168.1.10Vaultwarden
uptime.shdwnet.cloud→ Public IP→ 192.168.1.10Uptime Kuma
pulse.shdwnet.cloud→ Public IP→ 192.168.1.10Alias → Uptime Kuma
paperless.shdwnet.cloud→ Public IP→ 192.168.1.10Paperless-ngx
portainer.shdwnet.cloud→ Public IP→ 192.168.1.10Portainer
netdata.shdwnet.cloud→ Public IP→ 192.168.1.10VPS Netdata (via NPM → VPS:19998)
proxmox.shdwnet.cloud❌ Noneβ†’ 192.168.1.144Proxmox (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