Files
homelab-optimized/docs/infrastructure/split-horizon-dns.md
Gitea Mirror Bot d0b00a1ac4
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m0s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-07 06:53:42 UTC
2026-04-07 06:53:42 +00:00

9.6 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

Status: IMPLEMENTED (2026-03-20)

Use AdGuard Home on Calypso (primary) and Atlantis (backup) as split-horizon DNS resolvers that return 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        │
                 │  Atlantis)  │   │              │
                 │ Returns:    │   │ Returns:     │
                 │100.85.21.51 │   │ 104.21.73.214│
                 │(NPM Tailscale)│  │ (CF proxy)   │
                 └──────┬───────┘   └──────┬───────┘
                        │                  │
                        ▼                  ▼
                 ┌──────────────┐   ┌──────────────┐
                 │ NPM (local)  │   │  Cloudflare  │
                 │ matrix-ubuntu│   │  → WAN IP    │
                 │ :443 ~1ms   │   │  → NPM       │
                 └──────┬───────┘   │  ~50ms       │
                        │           └──────┬───────┘
                        ▼                  ▼
                 ┌─────────────────────────────────┐
                 │        Backend Service           │
                 │   (same result, faster path)     │
                 └─────────────────────────────────┘

Prerequisites

NPM is now on matrix-ubuntu (192.168.0.154) listening on standard ports 80/443/81. The migration from Calypso was completed on 2026-03-20.

Port Status
80:80 Active
443:443 Active
81:81 Active (Admin UI)

Implementation Steps

Step 1: Move NPM to Standard Ports -- DONE

NPM migrated from Calypso to matrix-ubuntu (192.168.0.154) on 2026-03-20. Compose file: hosts/vms/matrix-ubuntu/nginx-proxy-manager.yaml. Host nginx on matrix-ubuntu has been disabled (systemctl disable nginx); NPM now handles mastodon.vish.gg, mx.vish.gg, and mm.crista.love directly.

Router port forwards updated:

  • WAN:443 → 192.168.0.154:443
  • WAN:80 → 192.168.0.154:80

Step 2: Configure AdGuard DNS Rewrites -- DONE

AdGuard DNS rewrites configured on both Calypso (http://192.168.0.250:9080) and Atlantis (http://192.168.0.200:9080). Wildcard entries point to NPM's Tailscale IP:

Domain Answer Notes
*.vish.gg 100.85.21.51 All vish.gg domains → NPM Tailscale IP
*.thevish.io 100.85.21.51 All thevish.io domains → NPM Tailscale IP
*.crista.love 100.85.21.51 All crista.love domains → NPM Tailscale IP

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), added 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

.tail.vish.gg overrides — specific rewrites to override the wildcard for Tailscale-specific subdomains.

Specific entries take priority over wildcards in AdGuard.

Step 3: Set AdGuard as LAN DNS Server -- DONE

Router (Archer BE800) DHCP configured with dual AdGuard DNS:

  1. Primary DNS: 192.168.0.250 (Calypso AdGuard)
  2. Secondary DNS: 192.168.0.200 (Atlantis AdGuard, backup)

Step 4: Configure Atlantis AdGuard (Backup DNS) -- DONE

Same DNS rewrites added to Atlantis's AdGuard instance (http://192.168.0.200:9080) as backup:

  • Same wildcard rewrites as Calypso (pointing to 100.85.21.51)
  • Reachable at 192.168.0.200

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

Resolved: NPM now uses Let's Encrypt wildcard certificates (DNS challenge via Cloudflare API) instead of Cloudflare Origin certs. This means:

  • Certs are trusted by all browsers, whether traffic comes through Cloudflare or directly via LAN
  • No browser warnings for split-horizon DNS LAN access
  • Certs auto-renew via NPM's built-in Let's Encrypt integration

What Changes for Each Path

LAN Client

Browser → nb.vish.gg
  → AdGuard DNS: 100.85.21.51 (NPM Tailscale IP)
  → NPM (matrix-ubuntu:443) → SSL termination (LE wildcard cert)
  → Proxy to backend (192.168.0.210:8443)
  → Response (~1ms total DNS+proxy)

External Client

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

Internet Down

Browser → nb.vish.gg
  → AdGuard DNS: 100.85.21.51 (cached/local)
  → NPM (matrix-ubuntu: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