Files
homelab-optimized/docs/infrastructure/split-horizon-dns.md
Gitea Mirror Bot 851e2132ce
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m1s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-03-20 09:03:20 UTC
2026-03-20 09:03:20 +00:00

11 KiB

Split-Horizon DNS Implementation Guide

Last updated: 2026-03-20

Problem

All DNS queries for *.vish.gg, *.thevish.io, and *.crista.love currently resolve to Cloudflare proxy IPs (104.21.x.x), even when the client is on the same LAN as the services. This means:

  1. Hairpin NAT — LAN traffic goes out to Cloudflare and back in through the router
  2. Internet dependency — if the WAN link goes down, LAN services are unreachable by domain
  3. Added latency — ~50ms roundtrip through Cloudflare vs ~1ms on LAN
  4. Cloudflare bottleneck — all traffic proxied through CF even when unnecessary

Solution

Use AdGuard Home on Calypso as a split-horizon DNS resolver that returns local IPs for homelab domains when queried from the LAN, while external clients continue to use Cloudflare.

                    ┌──────────────────────────────────┐
                    │         DNS Query for             │
                    │         nb.vish.gg                │
                    └───────────────┬──────────────────┘
                                    │
                    ┌───────────────▼──────────────────┐
                    │      Where is the client?         │
                    └───────┬───────────────┬──────────┘
                            │               │
                   LAN Client          External Client
                            │               │
                            ▼               ▼
                 ┌──────────────┐   ┌──────────────┐
                 │ AdGuard Home │   │  Cloudflare  │
                 │ (Calypso)   │   │   DNS        │
                 │              │   │              │
                 │ Returns:    │   │ Returns:     │
                 │ 192.168.0.250│   │ 104.21.73.214│
                 │ (NPM local) │   │ (CF proxy)   │
                 └──────┬───────┘   └──────┬───────┘
                        │                  │
                        ▼                  ▼
                 ┌──────────────┐   ┌──────────────┐
                 │ NPM (local)  │   │  Cloudflare  │
                 │ calypso:443  │   │  → WAN IP    │
                 │ ~1ms         │   │  → NPM       │
                 └──────┬───────┘   │  ~50ms       │
                        │           └──────┬───────┘
                        ▼                  ▼
                 ┌─────────────────────────────────┐
                 │        Backend Service           │
                 │   (same result, faster path)     │
                 └─────────────────────────────────┘

Prerequisites

Before implementing split-horizon DNS, NPM must listen on standard ports (80/443) so that LAN clients can reach it without specifying a port. Currently NPM uses temporary ports from the migration:

Current Target
8880:80 80:80
8443:443 443:443
81:81 81:81 (unchanged)

Implementation Steps

Step 1: Move NPM to Standard Ports

The NPM compose file at hosts/synology/calypso/nginx-proxy-manager.yaml has a comment noting the ports are temporary. To change them:

  1. Stop Synology's built-in nginx from binding port 80/443 (if active):

    • DSM → Control Panel → Login Portal → Web Services → change port from 80/443 to 5000/5001
    • Or via SSH: sudo synosystemctl stop nginx
  2. Update the compose file:

    ports:
      - "80:80"     # HTTP
      - "443:443"   # HTTPS
      - "81:81"     # Admin UI
    
  3. Update the router port forwarding:

    • Change WAN:443 → 192.168.0.250:8443 to WAN:443 → 192.168.0.250:443
    • Change WAN:80 → 192.168.0.250:8880 to WAN:80 → 192.168.0.250:80
  4. Redeploy NPM — push the compose change to git, CI auto-deploys.

Step 2: Configure AdGuard DNS Rewrites

In AdGuard Home on Calypso (http://192.168.0.250:9080), go to Filters → DNS rewrites and add wildcard entries:

Domain Answer Notes
*.vish.gg 192.168.0.250 All vish.gg domains → NPM on Calypso
*.thevish.io 192.168.0.250 All thevish.io domains → NPM on Calypso
*.crista.love 192.168.0.250 All crista.love domains → NPM on Calypso

These three wildcards cover all 36 proxy hosts. AdGuard resolves matching queries locally instead of forwarding to upstream DNS.

Exceptions — these domains need direct IPs (not NPM), add them as specific overrides:

Domain Answer Reason
mx.vish.gg 192.168.0.154 Matrix federation needs direct access on port 8448
derp.vish.gg 192.168.0.250 DERP relay — direct IP, no CF proxy
derp-atl.vish.gg 192.168.0.200 Atlantis DERP relay
headscale.vish.gg 192.168.0.250 Headscale control — direct access
turn.thevish.io 192.168.0.200 TURN/STUN needs direct UDP

Specific entries take priority over wildcards in AdGuard.

Step 3: Set AdGuard as LAN DNS Server

Configure the router (Archer BE800) to hand out AdGuard's IP as the DNS server via DHCP:

  1. Router admin → DHCP Settings → DNS Server
  2. Set Primary DNS: 192.168.0.250 (Calypso/AdGuard)
  3. Set Secondary DNS: 192.168.68.100 (NUC/AdGuard, backup)

Or per-device: point /etc/resolv.conf or network settings to 192.168.0.250.

Step 4: Configure NUC AdGuard (Backup DNS)

Add the same DNS rewrites to the NUC's AdGuard instance so it works as a backup:

  • Same wildcard rewrites as Calypso
  • Reachable at 192.168.68.100 or 100.72.55.21 (Tailscale)

Step 5: Test

# Verify local resolution
dig nb.vish.gg @192.168.0.250
# Expected: 192.168.0.250 (NPM local IP)

# Verify external resolution still works
dig nb.vish.gg @1.1.1.1
# Expected: 104.21.73.214 (Cloudflare proxy)

# Test HTTPS access via local DNS
curl -s --resolve "nb.vish.gg:443:192.168.0.250" https://nb.vish.gg/ -o /dev/null -w "%{http_code} %{time_total}s\n"
# Expected: 200 in ~0.05s (vs ~0.15s through Cloudflare)

# Test all domains resolve locally
for domain in nb.vish.gg gf.vish.gg git.vish.gg sso.vish.gg dash.vish.gg; do
    ip=$(dig +short $domain @192.168.0.250 | tail -1)
    echo "$domain$ip"
done

SSL Considerations

This works because:

  • NPM has the Cloudflare Origin Certificate for *.vish.gg (valid until 2041)
  • Browsers trust this cert because it's signed by Cloudflare's CA
  • The cert works whether traffic comes through Cloudflare or directly

However, the origin cert is only trusted by Cloudflare's proxy. If a browser connects directly to NPM (bypassing CF), it will see an untrusted cert warning because Cloudflare Origin CA is not in public trust stores.

Fix options:

  1. Use Let's Encrypt certs in NPM instead of Cloudflare Origin — trusted everywhere, works for both paths
  2. Accept the warning for LAN-only access (add exception in browser)
  3. Use Cloudflare in "Full" mode (not "Full Strict") — CF doesn't validate origin cert, and LAN clients would need to add the Cloudflare Origin CA to their trust store

Recommended: Switch to Let's Encrypt with DNS challenge (Cloudflare API) for the wildcard certs. NPM supports this natively. This gives you certs trusted by both Cloudflare and direct LAN connections.

What Changes for Each Path

LAN Client (after implementation)

Browser → nb.vish.gg
  → AdGuard DNS: 192.168.0.250
  → NPM (calypso:443) → SSL termination
  → Proxy to backend (192.168.0.210:8443)
  → Response (~1ms total DNS+proxy)

External Client (unchanged)

Browser → nb.vish.gg
  → Cloudflare DNS: 104.21.73.214
  → Cloudflare proxy → WAN IP → Router
  → NPM (calypso:443) → SSL termination
  → Proxy to backend (192.168.0.210:8443)
  → Response (~50ms total)

Internet Down (new capability)

Browser → nb.vish.gg
  → AdGuard DNS: 192.168.0.250 (cached/local)
  → NPM (calypso:443) → SSL termination
  → Proxy to backend
  → Response (services still work!)

Current NPM Proxy Hosts (for reference)

All 36 domains that would benefit from split-horizon:

vish.gg (27 domains)

Domain Backend
actual.vish.gg calypso:8304
cal.vish.gg atlantis:12852
dash.vish.gg atlantis:7575
dav.vish.gg calypso:8612
docs.vish.gg calypso:8777
gf.vish.gg homelab-vm:3300
git.vish.gg calypso:3052
headscale.vish.gg calypso:8085
kuma.vish.gg rpi5:3001
mastodon.vish.gg matrix-ubuntu:3000
mx.vish.gg matrix-ubuntu:8082
nb.vish.gg homelab-vm:8443
npm.vish.gg calypso:81
ntfy.vish.gg homelab-vm:8081
ollama.vish.gg atlantis:11434
ost.vish.gg calypso:3000
paperless.vish.gg calypso:8777
pt.vish.gg atlantis:10000
pw.vish.gg atlantis:4080
rackula.vish.gg calypso:3891
retro.vish.gg calypso:8025
rx.vish.gg calypso:9751
rxdl.vish.gg calypso:9753
scrutiny.vish.gg homelab-vm:8090
sf.vish.gg calypso:8611
sso.vish.gg calypso:9000
wizarr.vish.gg atlantis:5690

thevish.io (5 domains)

Domain Backend
binterest.thevish.io homelab-vm:21544
hoarder.thevish.io homelab-vm:3482
joplin.thevish.io atlantis:22300
matrix.thevish.io matrix-ubuntu:8081
meet.thevish.io atlantis:5443

crista.love (2 domains)

Domain Backend
crista.love guava:28888
cocalc.crista.love guava:8080
mm.crista.love matrix-ubuntu:8065

Rollback

If something breaks:

  1. Change router DHCP DNS back to 1.1.1.1 / 8.8.8.8
  2. Or remove the DNS rewrites from AdGuard
  3. All traffic reverts to Cloudflare path immediately