Split DNS Strategy¶
Overview¶
Split DNS (also called Split-Horizon DNS) allows internal network clients to resolve the same domain names to internal IP addresses while external clients resolve to public IP addresses. This is essential for seamless access to services from both inside and outside your network.
The Problem Split DNS Solves¶
Without Split DNS¶
```text Internal user browsing to https://passwords.skaggsfamily.us:
- DNS resolves to public IP (your WAN address)
- Traffic goes: Device → Router → Internet → Router → Internal Service
- Issues: Hairpin NAT problems, slower routing, potential failures
```text
```text
With Split DNS¶
```text
```text Internal user:
- Internal DNS resolves to 172.16.103.x (direct internal IP)
- Traffic goes: Device → Internal Service (fast, direct)
External user:
- Public DNS resolves to your WAN IP
- Traffic goes: Device → Internet → Your Router → Internal Service
```text
```text
Recommended Solution: Technitium DNS + AdGuard Home¶
Why This Combination¶
- Technitium: Excellent split DNS capabilities, authoritative server features
- AdGuard Home: Family-friendly ad blocking, easy management interface
- Complementary: Technitium handles split DNS, AdGuard handles filtering/blocking
Architecture¶
DNS Flow Design¶
```text
```text Family Devices (VLAN 100) ↓ AdGuard Home (Primary DNS - 172.16.90.10) ├── Ad blocking and filtering ├── Family-safe DNS policies └── Forwards to Technitium for local domains
↓
Technitium DNS (Secondary/Authoritative - 172.16.90.11) ├── Authoritative for *.skaggsfamily.us ├── Split DNS zones └── Forwards other queries to public DNS (1.1.1.1, 8.8.8.8)
External Clients ↓ Public DNS (CloudFlare, etc.) ├── Public DNS records for skaggsfamily.us └── Points to your WAN IP ```text
```text
Split DNS Zone Configuration¶
Internal Zone (Technitium)¶
```dns
```dns ; Internal zone for skaggsfamily.us $ORIGIN skaggsfamily.us. $TTL 300
@ IN SOA dns.skaggsfamily.us. admin.skaggsfamily.us. ( 2024092101 ; Serial 3600 ; Refresh 1800 ; Retry 1209600 ; Expire 300 ; Minimum TTL )
; Internal DNS records (resolve to internal IPs) @ IN A 172.16.103.100 ; Main website passwords IN A 172.16.103.101 ; VaultWarden media IN A 172.16.103.102 ; Media server files IN A 172.16.103.103 ; File sharing monitoring IN A 172.16.103.104 ; Grafana auth IN A 172.16.103.105 ; Authentik business IN A 172.16.103.106 ; Business WordPress
; Wildcard for dynamic services
- IN A 172.16.103.100 ; Default to main ingress
```text
```text
External Zone (Public DNS Provider)¶
```dns
dns
; Public DNS records (CloudFlare, etc.)
skaggsfamily.us. A YOUR_WAN_IP
passwords.skaggsfamily.us. A YOUR_WAN_IP
media.skaggsfamily.us. A YOUR_WAN_IP
files.skaggsfamily.us. A YOUR_WAN_IP
monitoring.skaggsfamily.us. A YOUR_WAN_IP
auth.skaggsfamily.us. A YOUR_WAN_IP
business.skaggsfamily.us. A YOUR_WAN_IPtext
```text
Implementation Strategy¶
Phase 1: Technitium DNS Server Setup¶
```yaml
```yaml
Kubernetes deployment for Technitium¶
apiVersion: apps/v1 kind: Deployment metadata: name: technitium-dns namespace: infrastructure spec: replicas: 1 selector: matchLabels: app: technitium-dns template: metadata: labels: app: technitium-dns spec: containers:
- name: technitium-dns
image: technitium/dns-server:latest
ports:
- containerPort: 53
protocol: UDP
- containerPort: 53
protocol: TCP
- containerPort: 5380
protocol: TCP # Web interface
volumeMounts:
- name: dns-config
mountPath: /etc/dns/config
- name: dns-zones
mountPath: /etc/dns/zones
env:
- name: DNS_SERVER_DOMAIN
value: "dns.skaggsfamily.us"
- name: DNS_SERVER_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: technitium-secret
key: admin-password
volumes:
- name: dns-config
persistentVolumeClaim:
claimName: technitium-config-pvc
- name: dns-zones
persistentVolumeClaim:
claimName: technitium-zones-pvc
apiVersion: v1 kind: Service metadata: name: technitium-dns-service namespace: infrastructure spec: selector: app: technitium-dns ports:
-
name: dns-udp
port: 53
protocol: UDP targetPort: 53
-
name: dns-tcp
port: 53
protocol: TCP targetPort: 53
-
name: web
port: 5380
protocol: TCP targetPort: 5380 type: LoadBalancer loadBalancerIP: 172.16.90.11 # Static internal IP ```text
```text
Phase 2: AdGuard Home Integration¶
```yaml
```yaml
AdGuard Home for ad blocking + DNS filtering¶
apiVersion: apps/v1 kind: Deployment metadata: name: adguard-home namespace: infrastructure spec: replicas: 1 selector: matchLabels: app: adguard-home template: metadata: labels: app: adguard-home spec: containers:
- name: adguard-home
image: adguard/adguardhome:latest
ports:
- containerPort: 53
protocol: UDP
- containerPort: 53
protocol: TCP
- containerPort: 3000
protocol: TCP # Web interface
volumeMounts:
- name: adguard-work
mountPath: /opt/adguardhome/work
- name: adguard-conf
mountPath: /opt/adguardhome/conf
volumes:
- name: adguard-work
persistentVolumeClaim:
claimName: adguard-work-pvc
- name: adguard-conf
persistentVolumeClaim:
claimName: adguard-conf-pvc
apiVersion: v1 kind: Service metadata: name: adguard-home-service namespace: infrastructure spec: selector: app: adguard-home ports:
-
name: dns-udp
port: 53
protocol: UDP targetPort: 53
-
name: dns-tcp
port: 53
protocol: TCP targetPort: 53
-
name: web
port: 3000
protocol: TCP targetPort: 3000 type: LoadBalancer loadBalancerIP: 172.16.90.10 # Static internal IP ```text
```text
Configuration Details¶
Technitium Split DNS Configuration¶
```json
json
{
"zones": [
{
"name": "skaggsfamily.us",
"type": "Primary",
"internal": true,
"records": [
{
"name": "@",
"type": "A",
"ttl": 300,
"rdata": "172.16.103.100"
},
{
"name": "passwords",
"type": "A",
"ttl": 300,
"rdata": "172.16.103.101"
},
{
"name": "auth",
"type": "A",
"ttl": 300,
"rdata": "172.16.103.105"
}
]
}
],
"forwarders": [
"1.1.1.1",
"8.8.8.8"
],
"recursion": true,
"allowRecursionOnlyForPrivateNetworks": true
}text
```text
AdGuard Home Configuration¶
```yaml
```yaml
AdGuard Home configuration¶
dns: bind_hosts:
- 0.0.0.0
port: 53
upstream_dns:
- 172.16.90.11 # Technitium for local domains
- 1.1.1.1
- 8.8.8.8
upstream_dns_file: ""
bootstrap_dns:
- 9.9.9.10
- 149.112.112.10
- 2620:fe::10
- 2620:fe::fe:10
Local DNS rewrites for split DNS¶
dns_rewrites:
-
domain: "*.skaggsfamily.us"
answer: "172.16.90.11" # Forward to Technitium
Filtering settings¶
filters:
-
enabled: true
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
name: AdGuard DNS filter
-
enabled: true
url: https://someonewhocares.org/hosts/zero/hosts
name: Dan Pollock's List
Custom filtering rules for family¶
user_rules:
- "||doubleclick.net^"
- "||googlesyndication.com^"
- "@@||skaggsfamily.us^$important" # Always allow your domain
```text
```text
Network Configuration¶
Router DNS Settings (EdgeRouter-X)¶
```bash
```bash
Configure DHCP to use AdGuard Home as primary DNS¶
set service dhcp-server shared-network-name LAN subnet 172.16.100.0/24 dns-server 172.16.90.10 set service dhcp-server shared-network-name SERVERS subnet 172.16.90.0/24 dns-server 172.16.90.10 set service dhcp-server shared-network-name LAB subnet 172.16.103.0/24 dns-server 172.16.90.10
Static DNS entries for infrastructure¶
set system static-host-mapping host-name adguard.skaggsfamily.us inet 172.16.90.10 set system static-host-mapping host-name dns.skaggsfamily.us inet 172.16.90.11
commit save ```text
```text
Kubernetes Ingress Configuration¶
```yaml
```yaml
Nginx Ingress with proper internal routing¶
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: vaultwarden-ingress namespace: vaultwarden annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: tls:
-
hosts:
- passwords.skaggsfamily.us
secretName: vaultwarden-tls
rules:
-
host: passwords.skaggsfamily.us
http:
paths:
-
path: /
pathType: Prefix
backend: service: name: vaultwarden-service port: number: 80 ```text
-
```text
Testing and Validation¶
Internal DNS Testing¶
```bash
```bash
Test from internal network¶
nslookup passwords.skaggsfamily.us 172.16.90.10
Should return: 172.16.103.101¶
dig @172.16.90.10 passwords.skaggsfamily.us
Should return internal IP¶
Test browsing¶
curl -k https://passwords.skaggsfamily.us
Should connect directly to internal service¶
```text
```text
External DNS Testing¶
```bash
```bash
Test from external network (mobile data, etc.)¶
nslookup passwords.skaggsfamily.us 8.8.8.8
Should return: YOUR_WAN_IP¶
Test external access¶
curl -k https://passwords.skaggsfamily.us
Should route through your public IP¶
```text
```text
Troubleshooting Commands¶
```bash
```bash
Check DNS resolution path¶
dig +trace passwords.skaggsfamily.us
Test specific DNS servers¶
dig @172.16.90.10 passwords.skaggsfamily.us # AdGuard dig @172.16.90.11 passwords.skaggsfamily.us # Technitium dig @1.1.1.1 passwords.skaggsfamily.us # Public DNS
Check DNS server logs¶
kubectl logs -n infrastructure deployment/technitium-dns kubectl logs -n infrastructure deployment/adguard-home ```text
```text
Benefits of This Setup¶
Performance Benefits¶
- Faster internal access: Direct routing to services
- Reduced WAN bandwidth: Internal traffic stays internal
- Lower latency: No hairpin NAT routing
Security Benefits¶
- Ad blocking: AdGuard Home blocks malicious domains
- Family filtering: Content filtering for children's devices
- DNS monitoring: Visibility into DNS queries and threats
Operational Benefits¶
- Single domain management: Same URLs work everywhere
- Easy SSL certificates: Let's Encrypt works for internal services
- Simplified networking: No special internal URLs to remember
Family IT Benefits¶
- Transparent operation: Services "just work" from anywhere
- No user education needed: Same URLs everywhere
- Mobile device support: Seamless switching between WiFi and cellular
Maintenance and Monitoring¶
Backup Strategy¶
```yaml
```yaml
DNS configuration backup¶
apiVersion: batch/v1 kind: CronJob metadata: name: dns-backup spec: schedule: "0 3 * * *" # Daily at 3 AM jobTemplate: spec: template: spec: containers:
- name: backup
image: alpine:latest
command:
- /bin/sh
- -c
- |
# Backup Technitium config
tar -czf /backup/technitium-$(date +%Y%m%d).tar.gz /etc/dns/
# Backup AdGuard config
tar -czf /backup/adguard-$(date +%Y%m%d).tar.gz /opt/adguardhome/conf/
volumeMounts:
- name: backup-storage
mountPath: /backup
- name: dns-config
mountPath: /etc/dns
readOnly: true
- name: adguard-conf
mountPath: /opt/adguardhome/conf
readOnly: true
```text
```text
Monitoring Integration¶
```yaml
```yaml
Prometheus monitoring for DNS services¶
- alert: DNSServiceDown
expr: up{job="dns-server"} == 0
for: 1m labels: severity: critical annotations: summary: "DNS service is down"
- alert: DNSHighQueryLatency
expr: dns_query_duration_seconds > 0.5
for: 5m labels: severity: warning annotations: summary: "DNS queries are slow" ```text
```text