Geographic Load Balancing with DNS

Geographic load balancing distributes traffic across globally distributed servers based on client location, optimizing latency and improving user experience. DNS-based geographic routing directs clients to the nearest or most appropriate datacenter based on their geographic location. This guide covers BIND GeoIP implementation, PowerDNS geographic routing, Route53-style latency-based routing, failover mechanisms, and testing strategies.

Geographic Load Balancing Overview

Geographic load balancing strategies:

  1. GeoIP-Based: Route based on client IP geolocation database
  2. Latency-Based: Route to nearest server by network latency
  3. Geolocation: Route based on geographic boundaries
  4. Weighted: Route based on percentage distribution
  5. Failover: Route to alternate locations on primary failure

Benefits:

  • Reduced latency for end users
  • Better user experience
  • Improved compliance for data residency
  • Load distribution across regions
  • DDoS mitigation through distribution

BIND with GeoIP Module

Install BIND with GeoIP support:

sudo apt update
sudo apt install bind9 bind9-utils bind9-dnsutils bind9-libs

# Install GeoIP database
sudo apt install libgeoip1 geoip-bin

# Download maxmind GeoIP database (GeoLite2)
cd /usr/share/GeoIP
sudo wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz
sudo tar xzf GeoLite2-Country.tar.gz

Configure BIND with GeoIP (BIND 9.10+):

cat > /etc/bind/named.conf.acl <<'EOF'
# Define GeoIP ACLs
geoip {
    country-db "/usr/share/GeoIP/GeoLite2-Country.mmdb";
};

acl us-clients {
    geoip country "US";
};

acl eu-clients {
    geoip country "DE" "FR" "GB" "IT" "ES";
};

acl asia-clients {
    geoip country "JP" "CN" "IN" "SG" "AU";
};

acl other-clients {
    any;
};
EOF

Configure zone-level geographic routing:

cat >> /etc/bind/named.conf.local <<'EOF'
zone "example.com" {
    type master;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 10.0.0.0/8; };
};

# Use CNAME or view-based routing for geographic distribution
view "us-view" {
    match-clients { us-clients; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/db.example.com.us";
    };
};

view "eu-view" {
    match-clients { eu-clients; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/db.example.com.eu";
    };
};

view "asia-view" {
    match-clients { asia-clients; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/db.example.com.asia";
    };
};

view "other-view" {
    match-clients { other-clients; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/db.example.com";
    };
};
EOF

Create region-specific zone files:

# US zone file
cat > /etc/bind/zones/db.example.com.us <<'EOF'
$TTL 300
@   IN  SOA ns1.example.com. admin.example.com. (
        2024010101  ; serial
        3600        ; refresh
        1800        ; retry
        604800      ; expire
        300         ; minimum
    )

    IN  NS  ns1.example.com.
    IN  NS  ns2.example.com.

ns1 IN  A   10.0.1.1
ns2 IN  A   10.0.1.2

@   IN  A   192.168.1.100  ; US datacenter
www IN  A   192.168.1.100  ; US datacenter
api IN  A   192.168.1.101  ; US API server
EOF

# EU zone file
cat > /etc/bind/zones/db.example.com.eu <<'EOF'
$TTL 300
@   IN  SOA ns1.example.com. admin.example.com. (
        2024010101  ; serial
        3600        ; refresh
        1800        ; retry
        604800      ; expire
        300         ; minimum
    )

    IN  NS  ns1.example.com.
    IN  NS  ns2.example.com.

ns1 IN  A   10.0.2.1
ns2 IN  A   10.0.2.2

@   IN  A   192.168.2.100  ; EU datacenter
www IN  A   192.168.2.100  ; EU datacenter
api IN  A   192.168.2.101  ; EU API server
EOF

Validate BIND configuration:

sudo named-checkconf /etc/bind/named.conf
sudo named-checkzone example.com /etc/bind/zones/db.example.com

Restart BIND:

sudo systemctl restart bind9
sudo systemctl status bind9

Test geographic routing:

# Test from different regions
nslookup example.com 127.0.0.1

# Test with specific client IP simulation (using dig)
dig @127.0.0.1 +subnet=1.2.3.4/32 example.com

PowerDNS Geographic Routing

Install PowerDNS with GeoIP backend:

sudo apt install pdns-server pdns-backend-mysql pdns-tools

# Install GeoIP library
sudo apt install libgeoip1 geoip-bin

Configure PowerDNS with Lua scripting for GeoIP:

cat > /etc/powerdns/pdns.lua <<'EOF'
-- GeoIP-based routing script
function preresolve(dq)
    if dq.qname:equal("example.com.") then
        local geoip = newGeoIP()
        local client_country = geoip:country(dq.remoteaddr:getaddr())
        
        if client_country == "US" then
            if dq.qtype == pdns.A then
                dq:addAnswer(pdns.A, "192.168.1.100")
                return true
            end
        elseif client_country == "GB" or client_country == "DE" or client_country == "FR" then
            if dq.qtype == pdns.A then
                dq:addAnswer(pdns.A, "192.168.2.100")
                return true
            end
        else
            if dq.qtype == pdns.A then
                dq:addAnswer(pdns.A, "192.168.3.100")  -- Default
                return true
            end
        end
    end
    return false
end
EOF

Enable Lua backend in PowerDNS config:

cat >> /etc/powerdns/pdns.conf <<'EOF'
# Enable Lua backend
launch=lua
lua-script=/etc/powerdns/pdns.lua

# GeoIP settings
geoip-database-files=/usr/share/GeoIP/GeoLite2-Country.mmdb
EOF

Restart PowerDNS:

sudo systemctl restart pdns
sudo systemctl status pdns

AWS Route53 Style Routing

Implement Route53-style latency-based and geolocation routing with open-source tools:

Using dnsdist (DNS query distributor):

# Install dnsdist
sudo apt install dnsdist

cat > /etc/dnsdist/dnsdist.conf <<'EOF'
-- Define backend servers by region
newServer({address="192.168.1.100:53", name="us-primary"})
newServer({address="192.168.1.101:53", name="us-secondary"})
newServer({address="192.168.2.100:53", name="eu-primary"})
newServer({address="192.168.2.101:53", name="eu-secondary"})
newServer({address="192.168.3.100:53", name="asia-primary"})

-- Load balancing policies
setServFailLimitParams(3, 60)  -- Allow 3 failures in 60 seconds

-- Configure latency-based routing
function latency_routing()
    -- Route based on server latency (dnsdist tracks this)
    return true
end

-- Default pool configuration
setPoolPolicy("roundrobin")

-- Add rules for geographic routing using client IP
addAction(AndRule({
    NetmaskGroupRule("1.0.0.0/8"),   -- US address space
    QTypeRule("A")
}), PoolAction("us-pool"))

addAction(AndRule({
    NetmaskGroupRule("2.0.0.0/8"),   -- EU address space
    QTypeRule("A")
}), PoolAction("eu-pool"))

addAction(AndRule({
    NetmaskGroupRule("3.0.0.0/8"),   -- Asia address space
    QTypeRule("A")
}), PoolAction("asia-pool"))

-- Bind to listen port
setLocal("0.0.0.0:53")

-- Enable stats
setConsoleInputKey("setKey(\"YOUR_SECRET_KEY\")")
controlSocket("127.0.0.1:5199")
EOF

Latency-Based Routing

Implement latency-based routing with health checks:

cat > /etc/powerdns/latency-routing.lua <<'EOF'
-- Latency-based routing

local latency_map = {
    ["192.168.1.100"] = 10,   -- US primary: 10ms
    ["192.168.1.101"] = 15,   -- US secondary: 15ms
    ["192.168.2.100"] = 50,   -- EU primary: 50ms
    ["192.168.2.101"] = 55,   -- EU secondary: 55ms
    ["192.168.3.100"] = 80,   -- Asia primary: 80ms
}

function selectLatencyServer(servers)
    local best_server = nil
    local min_latency = 999999
    
    for server, latency in pairs(latency_map) do
        if latency < min_latency then
            min_latency = latency
            best_server = server
        end
    end
    
    return best_server
end

function preresolve(dq)
    if dq.qname:equal("example.com.") and dq.qtype == pdns.A then
        local best_server = selectLatencyServer(latency_map)
        dq:addAnswer(pdns.A, best_server)
        return true
    end
    return false
end
EOF

Failover and Health Checks

Implement DNS failover with health checks:

cat > /etc/powerdns/failover-routing.lua <<'EOF'
-- DNS failover with health checking

local server_status = {
    ["192.168.1.100"] = true,   -- Primary US
    ["192.168.1.101"] = true,   -- Secondary US
    ["192.168.2.100"] = true,   -- Primary EU
    ["192.168.3.100"] = true,   -- Primary Asia
}

function checkServerHealth(server)
    -- In real implementation, query HTTP health endpoint
    -- For now, use status map
    return server_status[server] or false
end

function getHealthyServers(region)
    local healthy = {}
    
    if region == "us" then
        if checkServerHealth("192.168.1.100") then
            table.insert(healthy, "192.168.1.100")
        elseif checkServerHealth("192.168.1.101") then
            table.insert(healthy, "192.168.1.101")
        end
    elseif region == "eu" then
        if checkServerHealth("192.168.2.100") then
            table.insert(healthy, "192.168.2.100")
        end
    elseif region == "asia" then
        if checkServerHealth("192.168.3.100") then
            table.insert(healthy, "192.168.3.100")
        end
    end
    
    return healthy
end

function preresolve(dq)
    if dq.qname:equal("example.com.") and dq.qtype == pdns.A then
        local region = determineRegion(dq.remoteaddr)
        local servers = getHealthyServers(region)
        
        if #servers > 0 then
            dq:addAnswer(pdns.A, servers[1])
            return true
        else
            -- Failover to global server
            dq:addAnswer(pdns.A, "192.168.3.100")
            return true
        end
    end
    return false
end
EOF

Testing Geographic Routing

Test DNS routing from different locations:

# Test from local (simulated US)
dig @127.0.0.1 example.com
# Expected: 192.168.1.100

# Test with geolocation simulation
# Use VPN or proxy to simulate different locations
curl -s "https://api.ipify.org?format=json"

# Test with specific client subnet
dig +subnet=1.2.3.4/32 @ns1.example.com example.com

# Test with DNS performance tools
dnsbench -h ns1.example.com -q example.com -c 100

Test failover behavior:

# Simulate server down
sudo iptables -A OUTPUT -d 192.168.1.100 -j DROP

# Test DNS response (should failover)
nslookup example.com ns1.example.com

# Remove rule
sudo iptables -D OUTPUT -d 192.168.1.100 -j DROP

Monitoring and Logging

Enable DNS query logging:

# BIND logging configuration
cat >> /etc/bind/named.conf <<'EOF'
logging {
    channel query_log {
        file "/var/log/bind/query.log" versions 3 size 100M;
        print-time yes;
        print-severity yes;
        print-category yes;
    };
    
    category queries {
        query_log;
    };
};
EOF

Monitor DNS queries:

# Real-time query monitoring
tail -f /var/log/bind/query.log

# Count queries by client
grep -o "client.*#" /var/log/bind/query.log | sort | uniq -c | sort -rn

# Count queries by domain
grep "example.com" /var/log/query.log | awk '{print $NF}' | sort | uniq -c

Monitor server health:

# Create health check script
cat > /usr/local/bin/dns-health-check.sh <<'EOF'
#!/bin/bash
for server in 192.168.1.100 192.168.2.100 192.168.3.100; do
    response=$(curl -s -m 5 http://$server/health)
    if [ $? -eq 0 ]; then
        echo "$server: UP"
    else
        echo "$server: DOWN"
    fi
done
EOF

chmod +x /usr/local/bin/dns-health-check.sh
/usr/local/bin/dns-health-check.sh

Troubleshooting

Verify DNS resolution:

# Test specific nameserver
dig @ns1.example.com example.com

# Trace DNS resolution
dig +trace example.com

# Check DNS propagation
nslookup example.com 8.8.8.8
nslookup example.com 1.1.1.1
nslookup example.com ns1.example.com

Check GeoIP database:

# Test GeoIP lookup
geoiplookup -f /usr/share/GeoIP/GeoLite2-Country.mmdb 8.8.8.8

# Update GeoIP database
wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz
tar xzf GeoLite2-Country.tar.gz

Debug DNS routing decisions:

# Enable query logging
rndc querylog

# Monitor logs
tail -f /var/log/bind/query.log

# Check BIND configuration
named-checkconf -p /etc/bind/named.conf

# Test recursive lookup
dig +recurse @127.0.0.1 example.com
dig +norecurse @127.0.0.1 example.com

Verify TTL and caching:

# Check TTL on DNS response
dig example.com | grep -A 5 "ANSWER SECTION"

# Clear DNS cache (if using systemd-resolved)
sudo systemctl restart systemd-resolved

# Check cache with dig
dig example.com +nocmd +noall +answer

Conclusion

Geographic load balancing with DNS distributes traffic intelligently across global infrastructure, improving user experience and service resilience. BIND with GeoIP, PowerDNS with Lua scripting, and open-source tools like dnsdist provide flexible geographic routing capabilities. Combined with health checks, failover mechanisms, and careful monitoring, geographic DNS routing ensures low-latency, highly available services globally.