When an authoritative DNS name server is queried it knows the address of the recursive caching server which queried it, and based on this information, it can return a different response depending on the source address. This is typically knows as GeoDNS or GeoIP-based DNS, and it is often used to return the address of a resource which is closest (network-wise) to the user’s resolver.

Aki Tuomi’s GeoIP back-end for PowerDNS does just that. The location information on a by-country basis can be obtained from MaxMind’s GeoLite database, or you can create your own.

GeoIP

We launch PowerDNS and configure the back-end with a few directives, basically giving it the path to the location data and to a YAML configuration file which defines the geo-enabled zones we provide on the server. (YAML is the same markup language we use in Ansible.)

launch=geoip
geoip-database-file=/usr/share/GeoIP/GeoIP.dat
geoip-database-file6=/usr/share/GeoIP/GeoIPv6.dat
geoip-zones-file=/home/jpm/src/powerdns/geo.yml
geoip-dnssec-keydir=/home/jpm/src/powerdns/geo.keys

There is an additional configuration directive called geoip-database-cache with which I specify what kind of caching should be done on the database.

  • standard, the default if unspecified, has the back-end read the database from the file system and consumes little memory.
  • memory loads the whole database into RAM which provides for high performance
  • index caches frequently accessed index portions of the database only, which is faster than standard and consumes less RAM than memory
  • mmap loads the database into memory-mapped RAM

Since I’m not in a position to test this at the moment from real addresses, or rather since I’m not willing to show you real addresses, I cobbled my own GeoIP.dat. Instead of using mmutils which ought to work, I compiled geoip-csv-to-dat and fed it this CSV based on a real one:

"192.168.1.196","192.168.1.196","34910976","34911231","ES","Spain"
"192.168.1.10","192.168.1.10","34621952","34622463","NL","Netherlands"
"127.0.0.1","127.0.0.255","34880512","34881535","DE","Germany"
"172.0.0.0","172.255.255.255","34938880","34947071","FR","France"

After getting that out of the way, we can create our geoip-zones-file. I add a single zone called geo.example.org with four records; one for each of the countries we support directly, and a wildcard country (note the quotes on that line – they’re an artifact of YAML syntax). Each of the records can have as many DNS resource records as I want, as long as they contain valid DNS rdata obviously.

domains:
- domain: geo.example.org
  ttl: 60
  records:
    geo.example.org:
       - soa: ns.example.org. geoman.example.org. 1 7200 3600 86400 60
       - ns:  ns.example.org.
    deu.geo.example.org:
       - a: 192.0.0.2
       - txt: Guten Tag
    esp.geo.example.org:
       - a: 192.0.0.10
       - txt: Muy buenos dias
       - loc: 40 8 43.041 N 3 21 42.539 W 714m 10m 100m 10m
    "*.geo.example.org":
       - a: 127.0.0.53
       - txt: I don't know exactly where you are
  services:
     www.geo.example.org: '%co.geo.example.org'

This configuration provides the server with answers for questions along the lines of what is the A record for esp.geo.example.org?, and what is its LOC record?. Additionally we create a service called www.geo.example.org which defines a service which people will query for. This will direct the server to return answers for the actual query of country.geo.example.org. The %co is expanded by the geoip back-end, as follows:

  • %co is the 3-letter ISO country code
  • %cn is the continent
  • %af is replaced by the address family, i.e. "v4" or "v6" depending on whether the query originated from an IPv4 or IPv6 address respectively
  • %hh, %dd, %mo, %wd are replaced by two digits of hour, day of the month, month, and weekday (UTC) respectively, whereas %mos and %wds are short strings which correspond to the month (jan, feb) and weekday (mon, tue) respectively. We can use this to direct clients to specific servers during, say, periodic maintenance times.

So, let’s try an IPv4 address query from localhost:

;; ANSWER SECTION:
www.geo.example.org.  60  IN   A  192.0.0.2

And an ANY query from “Spain”:

;; ANSWER SECTION:
www.geo.example.org.  60  IN  LOC    40 8 43.041 N 3 21 42.539 W 714.00m 10m 100m 10m
www.geo.example.org.  60  IN  A       192.0.0.10
www.geo.example.org.  60  IN  TXT    "Muy buenos dias"

And what happens if we come from an unconfigured country?

;; ANSWER SECTION:
www.geo.example.org.        60  IN  CNAME   unknown.geo.example.org.
unknown.geo.example.org.    60  IN  A       127.0.0.53
unknown.geo.example.org.    60  IN  TXT    "I don't know exactly where you are"

DNSSEC

You may have noticed the geoip-dnssec-keydir parameter in our configuration above; adding this will enable DNSSEC on this back-end, assuming the specified directory exists, is writeable by pdnssec, and readable by the server. This keydir stores keys in BIND’s Private-key-format: v1.2 (which IIRC hasn’t really been formally documented), and the filenames are built from zone name, key flags and active/disabled state encoded into them. To actually get our zone to produce DNSSEC data, we create at least one key for the zone:

$ pdnssec secure-zone geo.example.org
Securing zone with rsasha256 algorithm with default key size
Zone geo.example.org. secured
Erasing NSEC3 ordering since we are narrow, only setting 'auth' fields

$ ls -l geo.keys/
-rw-rw-r--. 1 jpm jpm  939 Nov 12 11:56 geo.example.org.256.2.1.key
-rw-rw-r--. 1 jpm jpm 1703 Nov 12 11:56 geo.example.org.257.1.1.key

(It’s probably worth pointing out at this time that the pdnssec utility will be renamed to pdnsutil very soon.)

I don’t have to restart PowerDNS to obtain DNSSEC-signed responses:

;; flags: qr aa; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1680
;; QUESTION SECTION:
;www.geo.example.org.   IN ANY

;; ANSWER SECTION:
www.geo.example.org.    60 IN RRSIG TXT 8 4 60 (
                                20151126000000 20151105000000 32029 geo.example.org.
                                lAtl9z/vVSOGpTX0XUHFyP1wgSofdslUB9KeLe5FlsBS
                                RaUBgNNgOOLPU56c3JuQWWT8zv9hlBNySMjJSFQ8OdbQ
                                CQj5gfLVgYc5GItztO72c8kJzpdEgodEhYKgF88QX7sB
                                oNwyIS6djdgX5NyfXSfa6Dd8fAVkjfIVzpgDsMc= )
www.geo.example.org.    60 IN A 192.0.0.2
www.geo.example.org.    60 IN TXT "Guten Tag"
www.geo.example.org.    60 IN RRSIG A 8 4 60 (
                                20151126000000 20151105000000 32029 geo.example.org.
                                S/jAfa5sIfDJRF+/GGOB4ZJDTc9vLHAfw6fio7mwkS00
                                RDpOqHJ/JizChHHwSU/xAdEme4+BmUMhcTxHP4M8NSY8
                                X7GSVcdvtXVG9rWl8ebnGNhaNRAKAmGnWVn2+s4Eemoa
                                BCIi0+GUtEt0QNioZkBlvL33N1Wf1HaZDSc2LrQ= )

I’ve said it before, and I’ll say it again, and you can’t stop me saying it: enabling DNSSEC doesn’t get easier than with PowerDNS.

Back on topic, the geoip back-end is easy to configure, and it is powerful. Our friends at PowerDNS are currently discussing adding a feature will will allow me to specify subnets directly in the YAML file.

If you want to see this live, fire off a TXT query to www.geo.powerdns.com.