NEWS Earn Money with Onidel Cloud! Affiliate Program Details - Check it out

Build Your Own Anycast DNS Infrastructure: Complete Guide to Setting Up BIND 9 with BGP Failover Across Amsterdam and New York VPS (2025)

Introduction

Building a robust DNS infrastructure is crucial for ensuring high availability and optimal performance for your applications. An anycast DNS setup allows you to deploy multiple DNS servers across different geographic locations while using the same IP addresses, automatically routing queries to the nearest or healthiest server.

In this comprehensive tutorial, we’ll walk you through creating a production-ready anycast DNS infrastructure using BIND 9 with FRR BGP routing and health-checked failover. You’ll learn how to deploy this setup across Onidel VPS in Amsterdam and New York VPS with full IPv4/IPv6 dual-stack support.

This tutorial covers BGP route advertisement, health monitoring, automatic failover mechanisms, and DNS zone synchronization to create a resilient DNS service that can handle both planned maintenance and unexpected outages.

Prerequisites

Before starting this tutorial, ensure you have:

  • Two VPS instances running Ubuntu 24.04 LTS (minimum 2GB RAM, 2 vCPU each)
  • BGP ASN and IP address assignments from your upstream providers
  • Root access to both servers
  • Basic understanding of DNS, BGP routing, and Linux networking
  • Upstream BGP sessions configured with your hosting providers

System Requirements:

  • Ubuntu 24.04 LTS
  • BIND 9.18+
  • FRRouting 8.4+
  • Python 3.10+ for health check scripts

Note: This tutorial assumes you have proper BGP connectivity established with your hosting provider. Amsterdam VPS vs New York VPS can help you understand the networking implications of each location.

Step-by-Step Tutorial

Step 1: Install BIND 9 and FRRouting

First, update your system and install the required packages on both servers:

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install BIND 9, FRRouting, and monitoring tools
sudo apt install -y bind9 bind9-utils bind9-doc frr frr-pythontools \
    python3-pip htop iftop tcpdump

# Install Python packages for health checking
pip3 install dnspython requests psutil

Enable and configure FRRouting daemons:

# Enable BGP daemon in FRR
sudo sed -i 's/bgpd=no/bgpd=yes/' /etc/frr/daemons
sudo sed -i 's/zebra=no/zebra=yes/' /etc/frr/daemons

# Start and enable FRR
sudo systemctl enable frr
sudo systemctl start frr

Step 2: Configure BIND 9 DNS Server

Configure BIND 9 on your primary server (Amsterdam):

# Create main configuration
sudo tee /etc/bind/named.conf.local <<EOF
// Anycast DNS Zone Configuration
zone "example.com" {
    type master;
    file "/etc/bind/db.example.com";
    allow-transfer { 10.0.1.2; }; // Secondary server IP
    notify yes;
    also-notify { 10.0.1.2; };
};

zone "1.0.10.in-addr.arpa" {
    type master;
    file "/etc/bind/db.10.0.1";
    allow-transfer { 10.0.1.2; };
};
EOF

Create the zone file:

# Create forward DNS zone
sudo tee /etc/bind/db.example.com <<EOF
\$TTL    300
@       IN      SOA     ns1.example.com. admin.example.com. (
                        2024010101      ; Serial
                        3600           ; Refresh
                        1800           ; Retry
                        1209600        ; Expire
                        300 )          ; Negative Cache TTL

@       IN      NS      ns1.example.com.
@       IN      NS      ns2.example.com.
@       IN      A       203.0.113.10    ; Anycast IP
@       IN      AAAA    2001:db8::10    ; Anycast IPv6

ns1     IN      A       203.0.113.10
ns2     IN      A       203.0.113.10
www     IN      A       203.0.113.100
mail    IN      A       203.0.113.101
EOF

# Set proper permissions
sudo chown bind:bind /etc/bind/db.example.com
sudo chmod 644 /etc/bind/db.example.com

Step 3: Configure BGP with FRRouting

Configure BGP to announce your anycast prefixes:

# Configure FRR BGP on Amsterdam server
sudo vtysh -c 'configure terminal' -c '
router bgp 65001
 bgp router-id 10.0.1.1
 neighbor 10.0.0.1 remote-as 64512
 neighbor 10.0.0.1 description "Upstream Provider"
 !
 address-family ipv4 unicast
  network 203.0.113.0/24
  neighbor 10.0.0.1 activate
 exit-address-family
 !
 address-family ipv6 unicast
  network 2001:db8::/48
  neighbor 10.0.0.1 activate
 exit-address-family
!
ip route 203.0.113.10/32 Null0
ipv6 route 2001:db8::10/128 Null0
'

# Save configuration
sudo vtysh -c 'write memory'

Configure the loopback interface for anycast IPs:

# Add anycast IPs to loopback interface
sudo tee /etc/systemd/network/99-anycast.netdev <<EOF
[NetDev]
Name=lo:anycast
Kind=dummy
EOF

sudo tee /etc/systemd/network/99-anycast.network <<EOF
[Match]
Name=lo:anycast

[Network]
Address=203.0.113.10/32
Address=2001:db8::10/128
EOF

# Enable and restart networking
sudo systemctl enable systemd-networkd
sudo systemctl restart systemd-networkd

Step 4: Implement Health Checking and Failover

Create a comprehensive health checker script:

#!/usr/bin/env python3
# /usr/local/bin/dns-health-checker.py

import subprocess
import time
import logging
import dns.resolver
import socket

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class DNSHealthChecker:
    def __init__(self, test_queries=['example.com'], check_interval=30):
        self.test_queries = test_queries
        self.check_interval = check_interval
        self.consecutive_failures = 0
        self.max_failures = 3
        self.routes_withdrawn = False
        
    def check_dns_response(self):
        """Test DNS resolution locally"""
        try:
            resolver = dns.resolver.Resolver()
            resolver.nameservers = ['127.0.0.1']
            resolver.timeout = 5
            
            for query in self.test_queries:
                resolver.resolve(query, 'A')
            return True
        except Exception as e:
            logger.error(f"DNS check failed: {e}")
            return False
            
    def check_bind_process(self):
        """Verify BIND process is running"""
        try:
            subprocess.run(['systemctl', 'is-active', '--quiet', 'bind9'], 
                         check=True)
            return True
        except subprocess.CalledProcessError:
            return False
            
    def withdraw_routes(self):
        """Withdraw BGP routes on failure"""
        if not self.routes_withdrawn:
            logger.warning("Withdrawing BGP routes due to health check failure")
            subprocess.run([
                'vtysh', '-c', 'configure terminal',
                '-c', 'router bgp 65001',
                '-c', 'address-family ipv4 unicast',
                '-c', 'no network 203.0.113.0/24'
            ])
            self.routes_withdrawn = True
            
    def advertise_routes(self):
        """Advertise BGP routes when healthy"""
        if self.routes_withdrawn:
            logger.info("Re-advertising BGP routes - service healthy")
            subprocess.run([
                'vtysh', '-c', 'configure terminal',
                '-c', 'router bgp 65001',
                '-c', 'address-family ipv4 unicast',
                '-c', 'network 203.0.113.0/24'
            ])
            self.routes_withdrawn = False
            
    def run(self):
        """Main health check loop"""
        while True:
            dns_ok = self.check_dns_response()
            bind_ok = self.check_bind_process()
            
            if dns_ok and bind_ok:
                logger.info("Health check passed")
                self.consecutive_failures = 0
                self.advertise_routes()
            else:
                self.consecutive_failures += 1
                logger.error(f"Health check failed ({self.consecutive_failures}/{self.max_failures})")
                
                if self.consecutive_failures >= self.max_failures:
                    self.withdraw_routes()
                    
            time.sleep(self.check_interval)

if __name__ == '__main__':
    checker = DNSHealthChecker()
    checker.run()

Make the script executable and create a systemd service:

# Make script executable
sudo chmod +x /usr/local/bin/dns-health-checker.py

# Create systemd service
sudo tee /etc/systemd/system/dns-health-checker.service <<EOF
[Unit]
Description=DNS Health Checker for Anycast
After=network.target bind9.service frr.service
Requires=bind9.service frr.service

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/dns-health-checker.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable dns-health-checker
sudo systemctl start dns-health-checker

Step 5: Configure Secondary Server (New York)

Repeat the installation steps on your New York server, then configure it as a secondary:

# Configure BIND as secondary on New York server
sudo tee /etc/bind/named.conf.local <<EOF
zone "example.com" {
    type secondary;
    file "db.example.com";
    masters { 10.0.1.1; }; // Primary server IP
};
EOF

# Configure BGP with different router-id
sudo vtysh -c 'configure terminal' -c '
router bgp 65001
 bgp router-id 10.0.1.2
 neighbor 10.0.2.1 remote-as 64512
 neighbor 10.0.2.1 description "Upstream Provider NY"
 !
 address-family ipv4 unicast
  network 203.0.113.0/24
  neighbor 10.0.2.1 activate
 exit-address-family
'

Best Practices

Security Hardening:

  • Configure BIND response rate limiting to prevent DNS amplification attacks
  • Implement access control lists (ACLs) for zone transfers and queries
  • Use TSIG keys for secure zone transfers between servers
  • Configure firewall rules to restrict BGP and DNS access

Performance Optimization:

  • Tune BIND cache sizes based on your query patterns
  • Configure appropriate TTL values for different record types
  • Monitor BGP convergence times and adjust timers if needed
  • Implement DNS over HTTPS (DoH) for encrypted queries

Monitoring and Alerting:

  • Set up comprehensive monitoring for DNS response times and BGP route announcements
  • Configure alerts for health check failures and route withdrawals
  • Monitor BGP peer states and route table consistency
  • Track DNS query patterns and potential abuse

For enterprise deployments, consider the full observability stack to monitor your anycast DNS infrastructure effectively.

Conclusion

You’ve successfully built a production-ready anycast DNS infrastructure using BIND 9 and FRRouting BGP across two geographic locations. This setup provides automatic failover, health checking, and optimal routing for DNS queries.

Key achievements include:

  • Redundant DNS resolution with automatic failover capabilities
  • Geographic load distribution through anycast routing
  • Health monitoring with intelligent route withdrawal
  • IPv4/IPv6 dual-stack support for modern networking requirements

This anycast DNS setup significantly improves your service reliability and performance. For businesses requiring even more robust infrastructure, explore our Amsterdam VPS and New York VPS offerings with high-availability features and AMD EPYC Milan processors for optimal DNS query processing performance.

Consider expanding this setup with additional geographic locations or implementing active-active architectures for even greater resilience and performance optimization.

Share your love