Geo DNS in BIND with ECS support

You will be able in 3 minutes:
  1. Setup your own GEO DNS server with BIND.
  2. Primary and a Secondary (in old terms: Mater and a Slave).
  3. Keep own master records and fetch Geodata acting as s slave.
  4. ECS compatible so your users will be able to dig your DNS via 8.8.8.8 (etc) and get the right GEO-aware answers.
  5. You will be able to setup separate zone files for different counties.
Fully explained automated shell script following. You can copy-paste it to root terminal to get the goal.
We're using Debian 9 but fell free to use any other Debian or CentOS.
Just substitute apt-get install with yum install for Red Hat based OS.

Installing BIND

Now installing the most recent BIND version:
cat <<'EOF' > /etc/apt/preferences.d/jessie-backports-bind.pref
Package: bind9* bind9utils bind9-host libbind9*
Pin: release a=stretch-backports
Pin-Priority: 777

EOF


cat <<'EOF' > /etc/apt/sources.list.d/stretch-backports.list
deb http://http.debian.net/debian stretch-backports main

EOF

apt-get update
apt-get -y -t stretch-backports install bind9 bind9-host libbind9*

Downloading GEO data with ECS

Following example will download MaxMind geographical database and convert it to BIND-like format.
Also the code is producing ECS enabled output.

cd /
cat <<'EOF' > /GeoIP-ecs.sh_
#!/bin/bash

[ -f GeoIPCountryCSV.zip ] || wget -T 5 -t 1 http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip

echo -n "Creating CBE (Country,Begin,End) CSV file..."
unzip -p GeoIPCountryCSV.zip GeoIPCountryWhois.csv | gawk -F \" '{print $10","$6","$8}' > cbe.csv
echo -ne "DONE\nGenerating BIND GeoIP.acl file..."

(for c in $(gawk -F , '{print $1}' cbe.csv | sort -u)
do
  echo "acl \"$c\" { "
  grep "^$c," cbe.csv | gawk -F , 'function s(b,e,l,m,n) {l = int(log(e-b+1)/log(2)); m = 2^32-2^l; n = and(m,e); if (n == and(m,b)) printf "\t%u.%u.%u.%u/%u;\n",b/2^24%256,b/2^16%256,b/2^8%256,b%256,32-l; else {s(b,n-1); s(n,e)}} s($2,$3)'
  grep "^$c," cbe.csv | gawk -F , 'function s(b,e,l,m,n) {l = int(log(e-b+1)/log(2)); m = 2^32-2^l; n = and(m,e); if (n == and(m,b)) printf "\tecs %u.%u.%u.%u/%u;\n",b/2^24%256,b/2^16%256,b/2^8%256,b%256,32-l; else {s(b,n-1); s(n,e)}} s($2,$3)'
  echo -e "};\n"
done) > GeoIP-ecs.acl

rm -f cbe.csv
echo "DONE"

exit 0

EOF
cd -
chmod 755 /GeoIP-ecs.sh_
/GeoIP-ecs.sh_

Turning ON GEO and ECS BIND options

We're creating separate options file for our configuration and includes it in main configuration.
cd /
cat <<'EOF' > /etc/bind/named.conf.local.geodns.options

#following directives inside options {..
#geoip-directory "/usr/share/GeoIP/";
	geoip-use-ecs yes;
	
allow-transfer {trusted;};
allow-query {any;};
notify yes;

EOF
cd -

cp -ia /etc/bind/named.conf.options /etc/bind/named.conf.options.orig
sed -i 's/^};/\n\ninclude "\/etc\/bind\/named.conf.local.geodns.options"; \n};/g' /etc/bind/named.conf.options

Excluding default zones

RFC require that every DNS instance will provides default zones, but they don't wrapped with 'view' statements.
#/etc/bind/named.conf.default-zones:2: when using 'view' statements, all zones must be in views
cp -ia /etc/bind/named.conf /etc/bind/named.conf.orig
sed -i 's/^include "\/etc\/bind\/named.conf.default-zones";/#include "\/etc\/bind\/named.conf.default-zones";/g' /etc/bind/named.conf

Adding ECS and GEO configuration

"trusted" ACL will be defined later in this article.
#==> named.conf <==
cd /
cat <<'EOF' > /etc/bind/named.conf.local.geodns.conf

#include "/GeoIP.acl";
include "/GeoIP-ecs.acl";

acl "trusted" {
localhost;
8.8.8.8;
};

EOF
cd -

echo 'include "/etc/bind/named.conf.local.geodns.conf";' >> /etc/bind/named.conf

Adding BIND logging (optional)

Logging very useful to debug GEO instance.
#==> named.conf <==
cd /
cat <<'EOF' > /etc/bind/named.conf.local.geodns.conf.logging


logging {
    channel default_file {
        file "/var/log/named/default.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel general_file {
        file "/var/log/named/general.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel database_file {
        file "/var/log/named/database.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel security_file {
        file "/var/log/named/security.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel config_file {
        file "/var/log/named/config.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel resolver_file {
        file "/var/log/named/resolver.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel xfer-in_file {
        file "/var/log/named/xfer-in.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel xfer-out_file {
        file "/var/log/named/xfer-out.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel notify_file {
        file "/var/log/named/notify.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel client_file {
        file "/var/log/named/client.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel unmatched_file {
        file "/var/log/named/unmatched.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel queries_file {
        file "/var/log/named/queries.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel network_file {
        file "/var/log/named/network.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel update_file {
        file "/var/log/named/update.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel dispatch_file {
        file "/var/log/named/dispatch.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel dnssec_file {
        file "/var/log/named/dnssec.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel lame-servers_file {
        file "/var/log/named/lame-servers.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };

    category default { default_file; };
    category general { general_file; };
    category database { database_file; };
    category security { security_file; };
    category config { config_file; };
    category resolver { resolver_file; };
    category xfer-in { xfer-in_file; };
    category xfer-out { xfer-out_file; };
    category notify { notify_file; };
    category client { client_file; };
    category unmatched { unmatched_file; };
    category queries { queries_file; };
    category network { network_file; };
    category update { update_file; };
    category dispatch { dispatch_file; };
    category dnssec { dnssec_file; };
    category lame-servers { lame-servers_file; };
};


EOF
cd -

echo 'include "/etc/bind/named.conf.local.geodns.conf.logging";' >> /etc/bind/named.conf

mkdir /var/log/named ; chmod 775 /var/log/named ; chown root:bind /var/log/named

Adding example GEO zones configuration

Note of match-clients directive and the /GeoIP-ecs.acl file content.
You can add your own IP ranges to file.
#==> named.conf <==

mkdir /etc/bind/zones ; chmod 755 /etc/bind/zones ; chown bind:bind /etc/bind/zones ;
mkdir /etc/bind/zones/master ; chmod 755 /etc/bind/zones/master ; chown bind:bind /etc/bind/zones/master ;
mkdir /etc/bind/zones/slave ; chmod 755 /etc/bind/zones/slave ; chown bind:bind /etc/bind/zones/slave ;

cd /
cat <<'EOF' > /etc/bind/named.conf.local.geodns.conf.zones


view "Europa" {
//match-clients { BE; DE; NL; GB; PL; SL;};
match-clients { RU; BE; DE; NL; GB; PL; SL;};
recursion no;
zone "example.com" IN {
    type master;
    file "/etc/bind/zones/master/example.com.zone.eu";
};
};


view "RU-UA-BY" {
//match-clients { RU; UA; BY; KZ;};
match-clients { UA; BY; KZ;};
recursion no;
zone "example.com" IN {
    type master;
    file "/etc/bind/zones/master/example.com.zone.ua";
};
};

view "NotMatched" {
match-clients { any; };
recursion no;
zone "example.com" IN {
    type master;
    file "/etc/bind/zones/master/example.com.zone";
};
};

EOF
cd -

echo 'include "/etc/bind/named.conf.local.geodns.conf.zones";' >> /etc/bind/named.conf

And the GEO zones itself

#==> example.com.zone <==
cd /
cat <<'EOF' > /etc/bind/zones/master/example.com.zone
$ORIGIN example.com.
$TTL 60
@       IN      SOA     ns1.example.com. hostmaster.example.com. 1511161583 300 350 400 2
@               NS      ns1.example.com.
@               NS      ns2.example.com.
ns1 		A 	151.80.149.182
ns2 		A 	145.239.95.188
@               A       192.168.4.1

EOF
cd -

#==> example.com.zone.eu <==
cd /
cat <<'EOF' > /etc/bind/zones/master/example.com.zone.eu
$ORIGIN example.com.
$TTL 60
@       IN      SOA     ns1.example.com. hostmaster.example.com. 1511161583 300 350 400 2
@               NS      ns1.example.com.
@               NS      ns2.example.com.
ns1 		A 	151.80.149.182
ns2 		A 	145.239.95.188
@               A       192.168.4.2

EOF
cd -

#==> example.com.zone.ua <==
cd /
cat <<'EOF' > /etc/bind/zones/master/example.com.zone.ua
$ORIGIN example.com.
$TTL 60
@       IN      SOA     ns1.example.com. hostmaster.example.com. 1511161583 300 350 400 2
@               NS      ns1.example.com.
@               NS      ns2.example.com.
ns1 		A 	151.80.149.182
ns2 		A 	145.239.95.188
@               A       192.168.4.3

EOF
cd -

All done

Now you're all set. Just reload BIND and test it.

Now try to add your own network to GEO file

We need it also after /GeoIP-ecs.acl file changed.
Edit /GeoIP-ecs.acl with: vi or nano.
Find line staring with: acl "UA" {
add following:
vi /GeoIP-ecs.acl
...
acl "UA" {
        127.0.0.3/32;
...

Restarting BIND

/etc/init.d/bind9 restart

Testing GEO DNS from localhost

Note of -b 127.0.0.3, we get the view "RU-UA-BY" { .. match-clients { UA; BY; KZ;}; match
from the /etc/bind/zones/master/example.com.zone.ua file.

And the view "NotMatched" { .. match-clients { any; }; match
from the /etc/bind/zones/master/example.com.zonefile in the other case.
dig -b 127.0.0.3  example.com @localhost 
dig -b 127.0.0.1  example.com @localhost

Request us to setup your GEO DNS

Go to main website page to find contacts.