--- /dev/null
+PowerDNS geobackend setup notes
+===============================
+
+These are the steps I went through to set up geobackend for PowerDNS on FreeBSD
+-STABLE. By the time you read this maybe geobackend is part of the PowerDNS
+main CVS so perhaps not much of this will apply. In that case you should skip
+further down to the configuration part.
+
+Before I carry on I should probably point out that if you don't know how DNS
+works much, or if you have never installed PowerDNS ever before, then you
+probably won't understand any of this. In that case you should probably go and
+do some reading/practicing before trying to set this up for yourself. As a
+minimum of DNS knowledge I would say you need to understand:
+
+- Basically how DNS servers answer queries
+
+- What common DNS terms like "CNAME" "RR" and "SOA" mean
+
+- How to use diagnostic utilities such as "dig" to test your setup.
+
+So, how this should work on Linux or any general Unix system. This didn't work
+for me so I ended up having to do it another way, but anyhow..
+
+1. Download PowerDNS source from http://www.powerdns.com/downloads/index.php -
+ you want the GPL sources.
+
+2. Edit configure.in in the main directory so that where it has the list of
+ backends at the bottom, you add:
+
+ modules/geobackend/Makefile
+
+3. $ cd modules
+
+ and get the geobackend:
+
+ $ cvs -d :pserver:anon@cvs.blitzed.org:/data/cvs login
+ (press <return> at password prompt)
+ $ cvs -d :pserver:anon@cvs.blitzed.org:/data/cvs co -d geobackend geo-dns
+
+ $ cd ..
+
+ to return to top of build directory.
+
+4. Regenerate the autotools with:
+
+ $ aclocal
+ $ autoheader
+ $ automake --add-missing --copy --foreign
+ $ autoconf
+
+5. Do a ./configure with the flags you normally would use, but also
+ --with-dynmodules="geo"
+
+6. Install PowerDNS as normal for how you would normally use it.
+
+This did not work for me on FreeBSD: no matter what combination of autoconf,
+automake, libtool was installed I would always get one error or another at the
+stage where I was running those commands. So, after a few hours of messing
+around I decided to just compile the geobackend outside of the PowerDNS source
+tree. Probably if/when geobackend is made part of PowerDNS, this will "just
+work", but in the meantime here's what I did:
+
+1. Install PowerDNS from the port /usr/ports/dns/powerdns as normal. Do not do
+ a "make clean" yet though! Make sure your PowerDNS works as you'd expect
+ without any of this geobackend stuff before going further.
+
+2. Somewhere else, get the source of our geobackend as above in step 3.
+
+3. Compile geobackend:
+
+ $ c++ -I/usr/ports/dns/powerdns/work/pdns-2.9.15 -O2 -Wall -c geobackend.cc
+ $ c++ -I/usr/ports/dns/powerdns/work/pdns-2.9.15 -O2 -Wall -c ippreftree.cc
+ $ c++ ippreftree.o geobackend.o -Wl,-soname -Wl,libgeobackend.so.0 -shared -o libgeobackend.so
+
+ All of those should compile and link without error.
+
+4. Now you need to copy that shared library to the system library directory, as
+ root:
+
+ # cp libgeobackend.so /usr/local/lib/
+
+Now whichever way you managed to get libgeobackend.so compiled, you are now
+ready to configure PowerDNS. This is what Blitzed's configuration looks like
+right now (but this is very much an experiment so things are bound to get out
+of date quickly).
+
+By the way, I could not find a SysV-style startup script installed by the
+FreeBSD port so I had to copy one from debian and put it in
+/usr/local/etc/rc.d/pdns.sh. You can get that file here:
+http://nubian.blitzed.org/pdns.sh
+
+And another thing is that the FreeBSD port doesn't add any new users for
+PowerDNS's use. You probably don't want to run it as root even without our
+code in it! So be sure to add some sensible user and group like "pdns".
+
+PowerDNS Configuration
+======================
+
+Here is the relevant parts of my /usr/local/etc/pdns.conf file as running on
+FreeBSD -STABLE:
+
+# -------------------------------------------------------------------------
+
+# To make it run as user@group pdns:pdns instead of root:root
+setgid=pdns
+setuid=pdns
+
+# These totally disable query+packet caching for all zones. This is necessary
+# because otherwise when the exact same question is asked twice in a short
+# period of time (by default, 10 seconds), the same response will be given
+# without any backends getting involved.
+#
+# This is bad for geobackend because obviously every question can potentially
+# require a new answer based only on the IP of the user's nameserver. Now, it
+# should be noted that if you have other zones in PowerDNS then they will have
+# their query cache disabled as well. That's not ideal, so you probably want
+# to run a separate instance of PowerDNS just for geobackend. Maybe one day
+# there will be config options to set per-zone query caching time or something.
+query-cache-ttl=0
+cache-ttl=0
+
+# Log a lot of stuff. Logging is slow. We will disable this when we are happy
+# things are working. :)
+loglevel=7
+
+# But these logs are not interesting at the moment
+log-dns-details=no
+
+# This disables wildcards which is more efficient. geobackend doesn't use
+# them, so if none of your backends need them, set this, otherwise comment it
+# out.
+wildcards=no
+
+# The geobackend
+launch=geo
+
+# The zone that your geo-balanced RR is inside of. The whole zone has to be
+# delegated to the PowerDNS backend, so you will generally want to make up some
+# subzone of your main zone. We chose "geo.blitzed.org".
+#
+geo-zone=geo.blitzed.org
+
+# The only parts of the SOA for "geo.blitzed.org" that apply here are the
+# master server name and the contact address.
+geo-soa-values=ns0.blitzed.org,hostmaster@blitzed.org
+
+# List of NS records of the PowerDNS servers that are authoritative for your
+# GLB zone.
+geo-ns-records=ns0.blitzed.org,ns1.blitzed.org
+
+# The TTL of the CNAME records that geobackend will return. Since the same
+# resolver will always get the same CNAME (apart from if the director-map
+# changes) it is safe to return a reasonable TTL, so if you leave this
+# commented then a sane default will be chosen.
+#geo-ttl=3600
+
+# The TTL of the NS records that will be returned. Leave this commented if you
+# don't understand.
+#geo-ns-ttl=86400
+
+# This is the real guts of the data that drives this backend. This is a DNS
+# zone file for RBLDNSD, a nameserver specialised for running large DNS zones
+# typical of DNSBLs and such. We choose it for our data because it is easier
+# to parse than the BIND-format one.
+#
+# Anyway, it comes from http://countries.nerd.dk/more.html - there are details
+# there for how to rsync your own copy. You'll want to do that regularly,
+# every couple of days maybe. We believe the nerd.dk guys take the netblock
+# info from Regional Internet Registries (RIRs) like RIPE, ARIN, APNIC. From
+# that they build a big zonefile of IP/prefixlen -> ISO-country-code mappings.
+geo-ip-map-zonefile=/usr/local/etc/zz.countries.nerd.dk.rbldnsd
+
+# And finally this last directive tells the geobackend where to find the map
+# files that say a) which RR to answer for, and b) what actual resource record
+# to return for each ISO country code. The setting here is a comma-separated
+# list of paths, each of which may either be a single map file or a directory
+# that will contain map files. If you are only ever going to serve one RR then
+# a single file is probably better, but if you're going to serve many then a
+# directory would probably be better. The rest of this documentation will
+# assume you chose a directory.
+geo-maps=/usr/local/etc/geo-maps
+
+# -------------------------------------------------------------------------
+
+Map configuration
+=================
+
+Above you defined a directory which should contain one file for each RR you are
+going to serve. This section describes the format for those files.
+
+There is a perl script in the geo-dns module
+(http://cvs.blitzed.org/geo-dns/iso2region.pl) which will print out a useful
+template for starting with. There are just two lines you MUST add for your own
+setup. The one that Blitzed is using is here:
+http://nubian.blitzed.org/irc.geo.blitzed.org
+
+The first line you must add is the $RECORD line. This tells the geobackend
+which RR within the geo-zone the file is for, so for example the file above
+gives:
+
+ $RECORD irc
+
+meaning it is for irc.geo.blitzed.org.
+
+The second line that must be present is the $ORIGIN line:
+
+ $ORIGIN iso.blitzed.org.
+
+The rest of this file is a list of mappings of ISO country to RR, and the
+$ORIGIN line tells the geobackend how to qualify the RRs. Any relative RR with
+be qualified by adding a dot and then this $ORIGIN string onto it. So all the
+relative RRs that follow are actually in the "iso.blitzed.org." zone. If you want to refer to an RR outside the $ORIGIN, put a trailing dot.
+
+The final mandatory line is the 0 mapping:
+
+ 0 pool.blitzed.org.
+
+This is the "default" mapping. It's possible that you will get a query from an
+IP that is not represented in the nerd.dk zone. Maybe it is a new allocation
+by a RIR, or maybe something unexpected happened like you got a query from IPv6
+or from an RFC1918 address. Or, there could be some error elsewhere in the
+geobackend that makes it want to give up. In any of these cases it needs to
+return a CNAME to a useful default.
+
+The default chosen for blitzed is "pool.blitzed.org." (note the trailing dot
+puts it outside the $ORIGIN!). At the moment, pool.blitzed.org is a
+round-robin of A records of all our connected servers, but the best way to
+handle it is under debate. Since it is in one of our regular zone files we
+can change it later how we want.
+
+The entire rest of the file is optional, and takes the format of an ISO country
+code (number) and an RR to map it to. iso2region.pl will have helpfully added
+comments with the country name so you can see what is what:
+
+ # Belgium
+ 56 eu
+
+Lines starting with # are comments. That's mainly so you can tell what the ISO
+code corresponds to, and maybe later when you are tweaking where all these
+countries will go to you can add some documentation for why you did it.
+
+The "56" is the ISO country code (see
+http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/index.html)
+for Belgium. The "eu" tells the geobackend that if a query for
+irc.geo.blitzed.org should come in from someone in Belgium, then it should
+respond with a CNAME for "eu.iso.blitzed.org". That's it. That is the RR that
+gets sent back. Every other line in this file is the same, just code->RR maps.
+
+At this point you might be wondering when the user gets the actual IP address
+sent to them. The answer is that we have chosen to make our geobackend only
+respond with CNAMEs to other RRs that are assumed to be hosted elsewhere in
+DNS. Our main blitzed.org zone is hosted in bind servers like it has been for
+years. In that zone we have entries for every one of the RRs that appears on
+the right hand side in the director-map. A list of those RRs is as follows:
+
+an.iso.blitzed.org.
+af.iso.blitzed.org.
+as.iso.blitzed.org.
+eu.iso.blitzed.org.
+na.iso.blitzed.org.
+oc.iso.blitzed.org.
+sa.iso.blitzed.org.
+
+These correspond to the ISO names for the regions/continents (Antarctica,
+Africa, Asia, Europe, North America, Oceania, South America) and are
+represented in our DNS at the moment by either a list of A records of servers
+"near" there, or else a CNAME to a "nearby" region. For example we have no
+servers in Antarctica or Asia. We just CNAME those to "na.iso.blitzed.org." to
+send those users to North American servers instead. Doing it this way is just
+our first attempt, we are still experimenting and might decide to do it
+different. All you need to know is that the geobackend gives the CNAMEs you
+tell it to give, it's your business what those CNAMEs are and what they end up
+resolving to.
+
+DNS configuration
+=================
+
+Time to configure the things that go into your existing DNS setup:
+
+1. The geographically load-balanced zone needs to be delegated to your PowerDNS
+ servers. For Blitzed we chose "geo.blitzed.org.", so:
+
+ geo NS ns0.blitzed.org.
+ geo NS ns1.blitzed.org.
+ geo NS ns2.blitzed.org.
+
+2. The lists of servers that correspond to each CNAME that your director-map
+ can possibly come up with. The above configuration can only answer one of:
+
+ pool.blitzed.org.
+ an.iso.blitzed.org.
+ af.iso.blitzed.org.
+ as.iso.blitzed.org.
+ eu.iso.blitzed.org.
+ na.iso.blitzed.org.
+ oc.iso.blitzed.org.
+ sa.iso.blitzed.org.
+
+ We have no servers in Antarctica so we just send that to North America:
+
+ an.iso CNAME na.iso.blitzed.org.
+
+ Other regions that actually have servers light look like:
+
+ na.iso A 1.2.3.4 ; Some server in North America
+ na.iso A 2.3.4.1 ; Some other server in North America
+
+3. Eventually you probably will want to use a more friendly name than something
+ like "irc.geo.blitzed.org.". At that point you could just do the equivalent
+ of:
+
+ irc CNAME irc.geo.blitzed.org.
+
+ BEAR IN MIND THAT THERE MIGHT BE BUGS IN THIS BACKEND AND IF YOU DO THIS TO
+ YOUR MAIN POOL AND IT STOPS RESPONDING, SENDS YOUR USERS TO THE WRONG SERVERS,
+ OR EVEN TO THE WRONG NETWORKS, OR ANYTHING ELSE UNFORTUNATE HAPPENS AT ALL,
+ THEN THAT'S JUST TOUGH LUCK AS THIS CODE COMES WITH NO WARRANTY, GUARANTEE
+ OR ASSURANCE OF ANY KIND!
+
+Testing
+=======
+
+OK! If you're still awake after all that, it is ready to test.
+
+By the way, at the moment some of the logging from the geobackend is a severity
+"debug" (facility "daemon" if using FreeBSD port). The default FreeBSD -STABLE
+install does not log daemon.debug to any file. If you don't add daemon.debug
+to your /etc/syslog.conf then you might not see some of the logs I shall talk
+about later. Most logging will be removed or made optional anyway as it slows
+things down.
+
+- Start PowerDNS
+
+ # /usr/local/etc/rc.d/pdns.sh start
+
+- Check your logs! You should see something like this:
+
+ Feb 26 16:07:45 nubian pdns[4661]: PowerDNS 2.9.15 (C) 2001-2004 PowerDNS.COM BV (Feb 9 2004, 23:40:35) starting up
+ Feb 26 16:07:45 nubian pdns[4661]: PowerDNS comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it according to the terms of the GPL version 2.
+ Feb 26 16:07:45 nubian pdns[4661]: Set effective group id to 1023
+ Feb 26 16:07:45 nubian pdns[4661]: Set effective user id to 1023
+ Feb 26 16:07:45 nubian pdns[4661]: Creating backend connection for TCP
+ Feb 26 16:07:45 nubian pdns[4661]: [geobackend] Parsing IP map zonefile
+ Feb 26 16:07:47 nubian pdns[4661]: [geobackend] Finished parsing IP map zonefile: added 53072 prefixes, stored in 132525 nodes using 1590300 bytes of memory
+ Feb 26 16:07:47 nubian pdns[4661]: [geobackend] Parsing director map /usr/local/etc/geo-maps/irc.geo.blitzed.org
+ Feb 26 16:07:47 nubian pdns[4661]: [geobackend] Finished parsing 2 director map files, 0 failures
+ Feb 26 16:07:47 nubian pdns[4661]: About to create 3 backend threads
+ Feb 26 16:07:47 nubian pdns[4661]: Done launching threads, ready to distribute questions
+
+ As long as there were no errors, the server is ready, geobackend is probably
+ working. You should now test with a suitable diagnostic tool:
+
+ $ dig irc.geo.blitzed.org.
+
+ ; <<>> DiG 9.2.2 <<>> irc.geo.blitzed.org.
+ ;; global options: printcmd
+ ;; Got answer:
+ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59602
+ ;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 5, ADDITIONAL: 5
+
+ ;; QUESTION SECTION:
+ ;irc.geo.blitzed.org. IN A
+
+ ;; ANSWER SECTION:
+ irc.geo.blitzed.org. 0 IN CNAME eu.iso.blitzed.org.
+ eu.iso.blitzed.org. 1 IN A 195.92.253.3
+ eu.iso.blitzed.org. 1 IN A 213.193.225.252
+ eu.iso.blitzed.org. 1 IN A 62.80.124.155
+ eu.iso.blitzed.org. 1 IN A 80.196.158.72
+ eu.iso.blitzed.org. 1 IN A 195.22.74.199
+
+ ;; AUTHORITY SECTION:
+ blitzed.org. 3600 IN NS sou.nameserver.net.
+ blitzed.org. 3600 IN NS bos.nameserver.net.
+ blitzed.org. 3600 IN NS iad.nameserver.net.
+ blitzed.org. 3600 IN NS phl.nameserver.net.
+ blitzed.org. 3600 IN NS sjc.nameserver.net.
+
+ ;; ADDITIONAL SECTION:
+ bos.nameserver.net. 43200 IN A 203.20.52.5
+ iad.nameserver.net. 43200 IN A 192.148.252.171
+ phl.nameserver.net. 7200 IN A 203.56.139.102
+ sjc.nameserver.net. 43200 IN A 205.158.174.201
+ sou.nameserver.net. 34825 IN A 194.196.163.7
+
+ ;; Query time: 389 msec
+ ;; SERVER: 192.168.0.5#53(192.168.0.5)
+ ;; WHEN: Tue Feb 10 03:10:14 2004
+ ;; MSG SIZE rcvd: 322
+
+ What does this show? Well first of all it tells us that we looked for the A
+ record of "irc.geo.blitzed.org." (A records are the default RR for dig).
+ What we actually got back was a CNAME to "eu.iso.blitzed.org." At that point
+ the work of geobackend and our PowerDNS server as a whole is done. All it is
+ designed to do is return a CNAME based on the location of the server doing
+ the query. The server I did that from is in UK, so a response of
+ "eu.iso.blitzed.org." is correct.
+
+ The rest of the data comes from the normal BIND9 nameservers that are
+ authoritative for the "blitzed.org." zone, in this case a list of A records
+ corresponding to our EU servers. Finally the list of authoritative servers
+ for "blitzed.org." is given, those same BIND9 boxes.
+
+ Meanwhile in the syslog of nubian, we have:
+
+ Feb 10 03:07:02 nubian pdns[32106]: [geobackend] Serving irc.geo.blitzed.org CNAME eu.iso.blitzed.org to 82.195.224.5 (756)
+
+ Here you can see that 82.195.224.5 asked for "irc.geo.blitzed.org." and was
+ served the mapping for ISO code 756: "eu.iso.blitzed.org.". This log notice
+ will be useful for debugging and refining the director-map by hand, but
+ later it will probably be removed or made optional.
+
+Ongoing maintenance
+===================
+
+New IPs are regularly allocated, also there may end up being corrections to the
+nerd.dk zones, so you should arrange to rsync this file every so often. I'm
+guessing once a week would be adequate. You may not be satisfied with your
+first try at the director-map either, so from time to time you may make
+changes. You might also add more map files to your geo-maps directory.
+Anytime those changes happen you will need to tell the geobackend to reread
+them. At the moment the best way to do this is:
+
+# pdns_control rediscover
+
+Feb 26 16:10:57 nubian pdns[4661]: Rediscovery was requested
+Feb 26 16:10:57 nubian pdns[4661]: [geobackend] Parsing IP map zonefile
+Feb 26 16:10:58 nubian pdns[4661]: [geobackend] Finished parsing IP map zonefile: added 53072 prefixes, stored in 132525 nodes using 1590300 bytes of memory
+Feb 26 16:10:58 nubian pdns[4661]: [geobackend] Parsing director map /usr/local/etc/geo-maps/irc.geo.blitzed.org
+Feb 26 16:10:58 nubian pdns[4661]: [geobackend] Parsing director map /usr/local/etc/geo-maps/irc.strugglers.net
+Feb 26 16:10:58 nubian pdns[4661]: [geobackend] Finished parsing 2 director map files, 0 failures
+
+About recursive nameservers
+===========================
+
+There is a small but potentially confusing gotcha in all this regarding
+recursive nameservers.
+
+Normally the authoritative nameservers for your regular domain will not allow
+recursion, that is, they will return data only for the domains they are
+authoritative for, returning pointers to nameservers for everything else.
+
+Here's an example of an authoritative server for blitzed.org that does not
+allow recursion:
+
+$ dig www.bbc.co.uk @sou.nameserver.net
+
+; <<>> DiG 9.2.3 <<>> www.bbc.co.uk @sou.nameserver.net
+;; global options: printcmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38059
+;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 7, ADDITIONAL: 0
+
+;; QUESTION SECTION:
+;www.bbc.co.uk. IN A
+
+;; AUTHORITY SECTION:
+uk. 166680 IN NS NS1.NIC.uk.
+uk. 166680 IN NS NS2.NIC.uk.
+uk. 166680 IN NS NS3.NIC.uk.
+uk. 166680 IN NS NS4.NIC.uk.
+uk. 166680 IN NS NS5.NIC.uk.
+uk. 166680 IN NS NSA.NIC.uk.
+uk. 166680 IN NS NSB.NIC.uk.
+
+;; Query time: 56 msec
+;; SERVER: 194.196.163.7#53(sou.nameserver.net)
+;; WHEN: Mon Feb 23 02:22:16 2004
+;; MSG SIZE rcvd: 161
+
+Note in the flags part the "rd", which means "recursion desired". Since this
+server does not offer recursion to me, all it does is pass back the hostname of
+the nameservers that can further answer my question (in this case the list of
+nic.uk servers). My own resolver would then carry on asking questions, which
+is how it should be.
+
+Look what happens if I pick a server that does allow recursion:
+
+$ dig www.bbc.co.uk @bos.nameserver.net
+
+; <<>> DiG 9.2.3 <<>> www.bbc.co.uk @bos.nameserver.net
+;; global options: printcmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51313
+;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 2
+
+;; QUESTION SECTION:
+;www.bbc.co.uk. IN A
+
+;; ANSWER SECTION:
+www.bbc.co.uk. 899 IN CNAME www.bbc.net.uk.
+www.bbc.net.uk. 300 IN A 212.58.240.121
+
+;; AUTHORITY SECTION:
+bbc.net.uk. 148544 IN NS ns0.thny.bbc.co.uk.
+bbc.net.uk. 148544 IN NS ns0.thdo.bbc.co.uk.
+
+;; ADDITIONAL SECTION:
+ns0.thny.bbc.co.uk. 62144 IN A 38.160.150.20
+ns0.thdo.bbc.co.uk. 62144 IN A 212.58.224.20
+
+;; Query time: 321 msec
+;; SERVER: 203.20.52.5#53(bos.nameserver.net)
+;; WHEN: Mon Feb 23 02:27:14 2004
+;; MSG SIZE rcvd: 164
+
+Note the extra flag that appeared, "ra". This is "recursion available", and
+the server true to its word has gone and got the information for us.
+
+Normally this probably would not be noticeable. In most cases it does not
+matter if your own resolver does the work or if some other server does. This
+geo-dns project is based totally on the IP address of the server that asks the
+question, however, so for this application it is critical.
+
+As far as geo-dns is concerned, you cannot have any nameserver that allows
+recursion be authoritative for your main domain. If you do, then any query
+which hits this server will cause it to go out and get the answer itself. It
+will hand back answers that are based on its own location, instead of the
+location of the client. After wondering why too many people were getting
+answers based on the location of bos.nameserver.net instead of their own
+location, we finally worked out it had recursion enabled. This note is to save
+you the same hassle.
+
+It is generally recommended anyway that nameservers which are meant to be
+authoritative for domains do not have recursion enabled
+(http://www.isc.org/pubs/tn/isc-tn-2002-2.txt), but in this case it is an
+absolute requirement if you wish to get any sensible results. Check they are
+not allowing recursion by use of dig as above before setting up this backend.
+
+(The specific example of bos.nameserver.net has since been fixed (had recursion
+disabled) so you will not be able to repeat this example)
+
+Hosting multiple domains
+========================
+
+You may have noticed that all of the instructions so far have talked only of
+the single example domain, geo.blitzed.org, and may be wondering how to serve
+multiple zones. The answer is, you don't need to. The only thing you want to
+serve is individual RRs, and this geobackend does allow you to serve multiple
+of these just by adding files to the geo-maps directory.
+
+So, assume you now want to apply geo-dns to the RR irc.strugglers.net, You might
+add a new director map to your geo-maps directory named "irc.strugglers.net"
+(name doesn't matter, just an example). Inside this file you would have
+something like:
+
+ $RECORD irc.strugglers.net
+ $ORIGIN iso.strugglers.net.
+ 0 bar.example.com.
+ # List of code->RR maps follow, unqualified RRs are inside
+ # iso.strugglers.net
+
+Once you now do a "pdns_control rediscover", your geobackend will be configured
+to answer for irc.strugglers.net.geo.blitzed.org. Now the people who run
+strugglers.net's dns just need to add a handy CNAME in their example.com zone
+file:
+
+irc CNAME irc.strugglers.net.geo.blitzed.org.
+
+You can check it will work before they add the CNAME:
+
+ $ dig +norecurse irc.strugglers.net.geo.blitzed.org. @ns0.blitzed.org
+
+ ; <<>> DiG 9.2.3 <<>> +norecurse irc.strugglers.net.geo.blitzed.org. @ns0.blitzed.org
+ ;; global options: printcmd
+ ;; Got answer:
+ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3592
+ ;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
+
+ ;; QUESTION SECTION:
+ ;irc.strugglers.net.geo.blitzed.org. IN A
+
+ ;; ANSWER SECTION:
+ irc.strugglers.net.geo.blitzed.org. 3600 IN CNAME eu.iso.blitzed.org.
+
+ ;; Query time: 234 msec
+ ;; SERVER: 82.195.234.5#53(ns0.blitzed.org)
+ ;; WHEN: Thu Feb 26 15:45:36 2004
+ ;; MSG SIZE rcvd: 73
+
+so that is how you can geo-dnsify RRs in multiple zones.
+
+Other questions, corrections, etc.?
+===================================
+
+Please subscribe to our "geo" list from http://lists.blitzed.org/listinfo/geo
+and tell us about it. Tell us about how you use this stuff too, we're
+interested in people's experiences.
+
+All this is too much for you?
+=============================
+
+We're still beta-testing this ourselves. As long as it doesn't place too much
+load on our servers we are open to the idea of doing the PowerDNS part of this
+for you as well. We're not ready to do that yet; it will require more
+development and specification of how you would provide your own director-maps
+etc.. If you are interested then please subscribe to the geo list and help us
+work out the details.
+
+FAQ
+===
+
+Q1. My IRC network is based mostly in one country (e.g. Australia, Brazil, ...)
+ so all my users are from one ISO code and this isn't so useful to me, what
+ can I do?
+
+A1. Well, one of the assumptions made during the design of this backend is that
+ latency and geographical distance doesn't have a good relationship at small
+ scales, even within North America or Europe it probably does not *always*
+ follow that short distance == low latency.
+
+ To improve things further I think you will need to do actual measurements.
+ Or you could look at the amount of hops in the AS path from your client to
+ each of your servers. Maybe you can come up with more ideas, if so we'd
+ like to hear.
+
+ However, if you are able to get more detailed geographic info from
+ somewhere then you could still feed it into this backend, you'd just have
+ to give up on the ISO country code model. I can really only see this
+ working for very big countries like Australia and North America.
+
+ Remember also that your servers probably are not 100% reliable! Say you
+ have 2 US servers; one in California and one in New York. You find some
+ way to split the US up into two halves with one half going to California
+ and one to New York. Now suppose the New York server dies. Half your US
+ users are still being directed there because that's the nearest one! Worse
+ still, those users probably think that what they are connecting to is a
+ *random* server, yet every single time they get will directed to a dead
+ server. They may conclude your entire network is dead.
+
+ You would be better advised IMHO to just have one US pool with both servers
+ in. If either dies, the irc clients will make a few more attempts and get
+ the other. It might add a few more ms to the RTT, but it's better than not
+ being able to get on at all, and it's better than ending up on a non-US
+ server. Maybe one day we'll do a high-availability backend too, though. :)
+
+
+Q2. Why is this tied to IRC? Can I use it for other things?
+
+A2. Yes! The code itself is not restricted in usefulness to only IRC, it's
+ just that IRC is a good example of a situation where you have a service
+ that is commonly split across many widely-separated servers, lots of widely
+ spread clients, and a serious lack of money for "real" solutions.
+
+ It wouldn't be any harder to use it for things like HTTP or many other
+ protocols.
+
+
+Q3. I don't want to run PowerDNS, will you make a custom backend for BIND9?
+
+A3. Probably not unless you're willing to pay or induce us in other ways!
+ PowerDNS is fun, you should try it, maybe you can make it work with only
+ one IP address by use of a forward zone in BIND and putting PowerDNS on a
+ strange port.
+
+
+Q4. I have some ideas for other metrics you could use instead of origin
+ country, like server load or user count, or ... will you implement that
+ too?
+
+A4: In another backend maybe. Tell us about your ideas, if you can make them
+ sound interesting and useful then maybe we will.
+
+Q5. I serve lots of RRs out of my geobackend and with lots of nameservers I
+ find it hard to keep my geo-maps in sync between them all. Any hints?
+
+A5. At the moment we use rsync like this:
+
+ 07 04 * * 0 pdns rsync -t rsync://calzone.hestdesign.com/countries/zz.countries.nerd.dk.rbldnsd /usr/local/etc/powerdns/zz.countries.nerd.dk.rbldnsd && /usr/local/bin/pdns_control rediscover > /dev/null
+ */15 * * * * pdns NR=$(rsync -rt --delete --stats rsync://rsync.blitzed.org/geo/ /usr/local/etc/powerdns/directormaps | awk '/Number of files transferred/ { print $5 }'); [ $NR != "0" ] && /usr/local/bin/pdns_control rediscover > /dev/null
+
+ Which will take care of getting a new nerd.dk zone weekly and will sync the
+ geo-maps every 15 minutes, doing a rediscover if files were transferred.
+
+Contact
+=======
+
+Author: Andy Smith <grifferz@blitzed.org> but please direct any questions to
+the geo list thanks!
+
+http://lists.blitzed.org/listinfo/geo
+
--- /dev/null
+/* geobackend.cc
+ * Copyright (C) 2004 Mark Bergsma <mark@nedworks.org>
+ * This software is licensed under the terms of the GPL, version 2.
+ *
+ * $Id: geobackend.cc,v 1.1 2004/02/28 19:13:44 ahu Exp $
+ */
+
+#include <fstream>
+#include <sstream>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <pdns/misc.hh>
+#include <pdns/lock.hh>
+#include <pdns/dnspacket.hh>
+
+#include "geobackend.hh"
+
+// Static members
+
+IPPrefTree * GeoBackend::ipt;
+vector<string> GeoBackend::nsRecords;
+map<string, GeoRecord*> GeoBackend::georecords;
+string GeoBackend::soaMasterServer;
+string GeoBackend::soaHostmaster;
+string GeoBackend::zoneName;
+u_int32_t GeoBackend::geoTTL;
+u_int32_t GeoBackend::nsTTL;
+time_t GeoBackend::lastDiscoverTime = 0;
+const string GeoBackend::logprefix = "[geobackend] ";
+bool GeoBackend::first = true;
+int GeoBackend::backendcount = 0;
+pthread_mutex_t GeoBackend::startup_lock;
+pthread_mutex_t GeoBackend::ipt_lock;
+
+// Class GeoRecord
+
+GeoRecord::GeoRecord() : origin(".") {}
+
+// Class GeoBackend, public methods
+
+GeoBackend::GeoBackend(const string &suffix) : forceReload(false) {
+ setArgPrefix("geo" + suffix);
+
+ // Make sure only one (the first) backend instance is initializing static things
+ Lock lock(&startup_lock);
+
+ backendcount++;
+
+ if (!first)
+ return;
+ first = false;
+
+ ipt = NULL;
+
+ loadZoneName();
+ loadTTLValues();
+ loadSOAValues();
+ loadNSRecords();
+ reload();
+}
+
+GeoBackend::~GeoBackend() {
+ Lock lock(&startup_lock);
+ backendcount--;
+ if (backendcount == 0) {
+ for (map<string, GeoRecord*>::iterator i = georecords.begin(); i != georecords.end(); ++i)
+ delete i->second;
+
+ if (ipt != NULL) {
+ delete ipt;
+ ipt = NULL;
+ }
+ }
+}
+
+bool GeoBackend::getSOA(const string &name, SOAData &soadata) {
+ if (toLower(name) != toLower(zoneName))
+ return false;
+
+ soadata.nameserver = soaMasterServer;
+ soadata.hostmaster = soaHostmaster;
+ soadata.domain_id = 1; // We serve only one zone
+ soadata.db = this;
+
+ // These values are bogus for backends like this one
+ soadata.serial = 1;
+ soadata.refresh = 86400;
+ soadata.retry = 2*soadata.refresh;
+ soadata.expire = 7*soadata.refresh;
+ soadata.default_ttl = 3600;
+
+ return true;
+}
+
+void GeoBackend::lookup(const QType &qtype, const string &qdomain, DNSPacket *pkt_p, int zoneId) {
+ answers.clear();
+
+ if ((qtype.getCode() == QType::NS || qtype.getCode() == QType::ANY)
+ && toLower(qdomain) == toLower(zoneName))
+ queueNSRecords(qdomain);
+
+ if (qtype.getCode() == QType::ANY || qtype.getCode() == QType::CNAME)
+ answerGeoRecord(qtype, qdomain, pkt_p);
+
+ if ((qtype.getCode() == QType::ANY || qtype.getCode() == QType::A)
+ && toLower(qdomain) == "localhost." + toLower(zoneName))
+ answerLocalhostRecord(qdomain, pkt_p);
+
+ if (!answers.empty())
+ i_answers = answers.begin();
+}
+
+bool GeoBackend::list(const string &target, int domain_id) {
+ answers.clear();
+ queueNSRecords(zoneName);
+ answerLocalhostRecord("localhost."+zoneName, NULL);
+ queueGeoRecords();
+
+ if (!answers.empty())
+ i_answers = answers.begin();
+ return true;
+}
+
+bool GeoBackend::get(DNSResourceRecord &r) {
+ if (answers.empty()) return false;
+
+ if (i_answers != answers.end()) {
+ // FIXME DNSResourceRecord could do with a copy constructor
+ DNSResourceRecord *ir = *i_answers;
+ r.qtype = ir->qtype;
+ r.qname = ir->qname;
+ r.content = ir->content;
+ r.priority = ir->priority;
+ r.ttl = ir->ttl;
+ r.domain_id = ir->domain_id;
+ r.last_modified = ir->last_modified;
+
+ delete ir;
+ i_answers++;
+ return true;
+ }
+ else {
+ answers.clear();
+ return false;
+ }
+}
+
+void GeoBackend::reload() {
+ forceReload = true;
+ rediscover();
+ forceReload = false;
+}
+
+void GeoBackend::rediscover(string *status) {
+ // Store current time for use after discovery
+ struct timeval nowtime;
+ gettimeofday(&nowtime, NULL);
+
+ loadIPLocationMap();
+ loadGeoRecords();
+
+ // Use time at start of discovery for checking whether files have changed
+ // next time
+ lastDiscoverTime = nowtime.tv_sec;
+}
+
+// Private methods
+
+void GeoBackend::answerGeoRecord(const QType &qtype, const string &qdomain, DNSPacket *p) {
+ const string lqdomain = toLower(qdomain);
+
+ if (georecords.count(lqdomain) == 0)
+ return;
+
+ GeoRecord *gr = georecords[lqdomain];
+
+ // Try to find the isocode of the country corresponding to the source ip
+ // If that fails, use the default
+ short isocode = 0;
+ if (p != NULL && ipt != NULL) {
+ try {
+ isocode = ipt->lookup(p->getRemote());
+ }
+ catch(ParsePrefixException &e) { // Ignore
+ L << Logger::Notice << logprefix << "Unable to parse IP '"
+ << p->getRemote() << " as IPv4 prefix" << endl;
+ }
+ }
+
+ DNSResourceRecord *rr = new DNSResourceRecord;
+ string target = resolveTarget(*gr, isocode);
+ fillGeoResourceRecord(qdomain, target, rr);
+
+ L << Logger::Debug << logprefix << "Serving " << qdomain << " "
+ << rr->qtype.getName() << " " << target << " to " << p->getRemote()
+ << " (" << isocode << ")" << endl;
+
+ answers.push_back(rr);
+}
+
+void GeoBackend::answerLocalhostRecord(const string &qdomain, DNSPacket *p) {
+ short isocode = 0;
+ if (p != NULL) {
+ try {
+ isocode = ipt->lookup(p->getRemote());
+ }
+ catch(ParsePrefixException &e) {} // Ignore
+ }
+
+ ostringstream target;
+ target << "127.0." << ((isocode >> 8) & 0xff) << "." << (isocode & 0xff);
+
+ DNSResourceRecord *rr = new DNSResourceRecord;
+ rr->qtype = QType::A;
+ rr->qname = qdomain;
+ rr->content = target.str();
+ rr->priority = 0;
+ rr->ttl = geoTTL;
+ rr->domain_id = 1;
+ rr->last_modified = 0;
+
+ answers.push_back(rr);
+}
+
+void GeoBackend::queueNSRecords(const string &qname) {
+ for (vector<string>::const_iterator i = nsRecords.begin(); i != nsRecords.end(); ++i) {
+ DNSResourceRecord *rr = new DNSResourceRecord;
+ rr->qtype = QType::NS;
+ rr->qname = qname;
+ rr->content = *i;
+ rr->priority = 0;
+ rr->ttl = nsTTL;
+ rr->domain_id = 1;
+ rr->last_modified = 0;
+
+ answers.push_back(rr);
+ }
+}
+
+void GeoBackend::queueGeoRecords() {
+ for (map<string, GeoRecord*>::const_iterator i = georecords.begin(); i != georecords.end(); ++i) {
+ GeoRecord *gr = i->second;
+ DNSResourceRecord *rr = new DNSResourceRecord;
+
+ fillGeoResourceRecord(gr->qname, resolveTarget(*gr, 0), rr);
+ answers.push_back(rr);
+ }
+}
+
+void GeoBackend::fillGeoResourceRecord(const string &qdomain, const string &target, DNSResourceRecord *rr) {
+ rr->qtype = QType::CNAME;
+ rr->qname = qdomain;
+ rr->content = target;
+ rr->priority = 0;
+ rr->ttl = geoTTL;
+ rr->domain_id = 1;
+ rr->last_modified = 0;
+}
+
+const string GeoBackend::resolveTarget(const GeoRecord &gr, short isocode) const {
+ // If no mapping exists for this isocode, use the default
+ if (gr.dirmap.count(isocode) == 0)
+ isocode = 0;
+
+ // Append $ORIGIN only if target does not end with a dot
+ string target(gr.dirmap.find(isocode)->second);
+ if (target[target.size()-1] != '.')
+ target += gr.origin;
+ else
+ target.resize(target.size()-1);
+
+ return target;
+}
+
+void GeoBackend::loadZoneName() {
+ zoneName = getArg("zone");
+ if (zoneName.empty())
+ throw AhuException("zone parameter must be set");
+}
+
+void GeoBackend::loadTTLValues() {
+ geoTTL = getArgAsNum("ttl");
+ nsTTL = getArgAsNum("ns-ttl");
+}
+
+void GeoBackend::loadSOAValues() {
+ vector<string> values;
+ stringtok(values, getArg("soa-values"), " ,");
+
+ if (values.size() != 2)
+ throw AhuException("Invalid number of soa-values specified in configuration");
+
+ soaMasterServer = values[0];
+ soaHostmaster = values[1];
+}
+
+void GeoBackend::loadNSRecords() {
+ stringtok(nsRecords, getArg("ns-records"), " ,");
+
+ if (nsRecords.empty())
+ throw AhuException("No NS records specified in configuration");
+}
+
+void GeoBackend::loadIPLocationMap() {
+ string filename = getArg("ip-map-zonefile");
+
+ if (filename.empty())
+ throw AhuException("No IP map zonefile specified in configuration");
+
+ // Stat file to see if it has changed since last read
+ struct stat stbuf;
+ if (stat(filename.c_str(), &stbuf) != 0 || !S_ISREG(stbuf.st_mode)) {
+ const string errormsg = "stat() failed, or " + filename + "is no regular file.";
+ if (lastDiscoverTime == 0) // We have no older map, bail out
+ throw AhuException(errormsg);
+ else {
+ // Log, but continue
+ L << Logger::Error << logprefix << errormsg;
+ return;
+ }
+ }
+
+ if (stbuf.st_mtime < lastDiscoverTime && !forceReload) // File hasn't changed
+ return;
+
+ ifstream ifs(filename.c_str(), ios::in);
+ if (!ifs)
+ throw AhuException("Unable to open IP map zonefile for read: " + stringerror());
+
+ L << Logger::Info << logprefix << "Parsing IP map zonefile" << endl;
+
+ IPPrefTree *new_ipt = new IPPrefTree;
+ string line;
+ int linenr = 0, entries = 0;
+
+ while (getline(ifs, line)) {
+ linenr++;
+ chomp(line, " \t"); // Erase whitespace
+
+ if (line[0] == '#')
+ continue; // Skip comments
+
+ vector<string> words;
+ stringtok(words, line, " :");
+
+ if (words.empty() || words[0] == "$SOA")
+ continue;
+
+ // Assume words[0] is a prefix. Feed it to the ip prefix tree
+ try {
+ // Parse country code nr
+ if (words.size() < 2 || words[1].empty()) {
+ L << Logger::Warning << logprefix
+ << "Country code number is missing at line " << linenr << endl;
+ continue;
+ }
+
+ struct in_addr addr;
+ if (inet_aton(words[1].c_str(), &addr) < 0) {
+ L << Logger::Warning << logprefix << "Invalid IP address '"
+ << words[1] << " at line " << linenr << endl;
+ continue;
+ }
+ short value = ntohl(addr.s_addr) & 0x7fff;
+
+ new_ipt->add(words[0], value);
+ entries++;
+ }
+ catch(ParsePrefixException &e) {
+ L << Logger::Warning << logprefix << "Error while parsing prefix at line "
+ << linenr << ": " << e.reason << endl;
+ }
+ }
+ ifs.close();
+
+ L << Logger::Info << logprefix << "Finished parsing IP map zonefile: added "
+ << entries << " prefixes, stored in " << new_ipt->getNodeCount()
+ << " nodes using " << new_ipt->getMemoryUsage() << " bytes of memory"
+ << endl;
+
+ // Swap the new tree with the old tree
+ IPPrefTree *oldipt = NULL;
+ {
+ Lock iptl(&ipt_lock);
+
+ oldipt = ipt;
+ ipt = new_ipt;
+ }
+
+ // Delete the old ip prefix tree
+ if (oldipt != NULL)
+ delete oldipt;
+}
+
+void GeoBackend::loadGeoRecords() {
+ vector<GeoRecord*> newgrs;
+
+ vector<string> maps;
+ stringtok(maps, getArg("maps"), " ,");
+ for (vector<string>::const_iterator i = maps.begin(); i != maps.end(); ++i) {
+ struct stat stbuf;
+
+ if (stat(i->c_str(), &stbuf) != 0)
+ continue;
+
+ if (S_ISREG(stbuf.st_mode)) {
+ // Regular file
+ GeoRecord *gr = new GeoRecord;
+ gr->directorfile = *i;
+ newgrs.push_back(gr);
+ }
+ else if (S_ISDIR(stbuf.st_mode)) { // Directory
+ DIR *dir = opendir(i->c_str());
+ if (dir != NULL) {
+ struct dirent *dent;
+ while ((dent = readdir(dir)) != NULL) {
+ string filename(*i);
+ if (filename[filename.size()-1] != '/')
+ filename += '/';
+ filename += dent->d_name;
+
+ if (stat(filename.c_str(), &stbuf) != 0 || !S_ISREG(stbuf.st_mode))
+ continue;
+
+ GeoRecord *gr = new GeoRecord;
+ gr->directorfile = filename;
+ newgrs.push_back(gr);
+ }
+ closedir(dir);
+ }
+ }
+ }
+
+ loadDirectorMaps(newgrs);
+}
+
+void GeoBackend::loadDirectorMaps(const vector<GeoRecord*> &newgrs) {
+ map<string, GeoRecord*> new_georecords;
+
+ int mapcount = 0;
+ for (vector<GeoRecord*>::const_iterator i = newgrs.begin(); i != newgrs.end(); ++i) {
+ GeoRecord *gr = *i;
+ try {
+ loadDirectorMap(*gr);
+ if (new_georecords.count(gr->qname) == 0) {
+ new_georecords[gr->qname] = gr;
+ mapcount++;
+ }
+ else
+ throw AhuException("duplicate georecord " + gr->qname + ", skipping");
+ }
+ catch(AhuException &e) {
+ L << Logger::Error << logprefix << "Error occured while reading director file "
+ << gr->directorfile << ": " << e.reason << endl;
+ delete gr;
+ }
+ }
+
+ // Swap the new georecord map with the old one.
+ georecords.swap(new_georecords);
+
+ L << Logger::Notice << logprefix << "Finished parsing " << mapcount
+ << " director map files, " << newgrs.size() - mapcount << " failures" << endl;
+
+ // Cleanup old georecords
+ for (map<string, GeoRecord*>::iterator i = new_georecords.begin(); i != new_georecords.end(); ++i)
+ delete i->second;
+}
+
+void GeoBackend::loadDirectorMap(GeoRecord &gr) {
+ L << Logger::Info << logprefix << "Parsing director map " << gr.directorfile << endl;
+
+ ifstream ifs(gr.directorfile.c_str(), ios::in);
+ if (!ifs)
+ throw AhuException("Error opening file.");
+
+ string line;
+ while(getline(ifs, line)) {
+ chomp(line, " \t"); // Erase whitespace
+
+ if (line[0] == '#')
+ continue; // Skip comments
+
+ // Parse $RECORD
+ if (line.substr(0, 7) == "$RECORD") {
+ gr.qname = line.substr(8);
+ chomp(gr.qname, " \t");
+ if (gr.qname[gr.qname.size()-1] != '.')
+ gr.qname += '.' + zoneName;
+ else {
+ gr.qname.resize(gr.qname.size()-1);
+ // Check whether zoneName is a prefix of this FQDN
+ if (gr.qname.rfind(zoneName) == string::npos)
+ throw AhuException("georecord " + gr.qname + " is out of zone " + zoneName);
+ }
+ continue;
+ }
+
+ // Parse $ORIGIN
+ if (line.substr(0, 7) == "$ORIGIN") {
+ gr.origin = line.substr(8);
+ chomp(gr.origin, " \t.");
+ gr.origin.insert(0, ".");
+ continue;
+ }
+
+ istringstream ii(line);
+ short isocode;
+ string target;
+ ii >> isocode >> target;
+
+ gr.dirmap[isocode] = target;
+ }
+
+ // Do some checks on the validness of this director map / georecord
+
+ if (gr.qname.empty())
+ throw AhuException("$RECORD line empty or missing, georecord qname unknown");
+
+ if (gr.dirmap.count(0) == 0)
+ throw AhuException("No default (0) director map entry");
+}