From: Bert Hubert Date: Sat, 28 Feb 2004 19:13:44 +0000 (+0000) Subject: initial load X-Git-Tag: pdns-2.9.16~3 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=76eeb3282b8eca1159145245f0543becf1471a11;p=pdns initial load git-svn-id: svn://svn.powerdns.com/pdns/trunk/pdns@236 d19b8d6e-7fed-0310-83ef-9ca221ded41b --- diff --git a/modules/geobackend/Makefile.am b/modules/geobackend/Makefile.am new file mode 100644 index 000000000..dbd97e005 --- /dev/null +++ b/modules/geobackend/Makefile.am @@ -0,0 +1,4 @@ +EXTRA_DIST=OBJECTFILES OBJECTLIBS +lib_LTLIBRARIES = libgeobackend.la +libgeobackend_la_SOURCES=geobackend.cc geobackend.hh ippreftree.cc ippreftree.hh +libgeobackend_la_LDFLAGS=-module diff --git a/modules/geobackend/OBJECTFILES b/modules/geobackend/OBJECTFILES new file mode 100644 index 000000000..aa081d30a --- /dev/null +++ b/modules/geobackend/OBJECTFILES @@ -0,0 +1 @@ +geobackend.o ippreftree.o diff --git a/modules/geobackend/OBJECTLIBS b/modules/geobackend/OBJECTLIBS new file mode 100644 index 000000000..e69de29bb diff --git a/modules/geobackend/README b/modules/geobackend/README new file mode 100644 index 000000000..cc53552d9 --- /dev/null +++ b/modules/geobackend/README @@ -0,0 +1,682 @@ +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 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 but please direct any questions to +the geo list thanks! + +http://lists.blitzed.org/listinfo/geo + diff --git a/modules/geobackend/geobackend.cc b/modules/geobackend/geobackend.cc new file mode 100644 index 000000000..88e78359f --- /dev/null +++ b/modules/geobackend/geobackend.cc @@ -0,0 +1,528 @@ +/* geobackend.cc + * Copyright (C) 2004 Mark Bergsma + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "geobackend.hh" + +// Static members + +IPPrefTree * GeoBackend::ipt; +vector GeoBackend::nsRecords; +map 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::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::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::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 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 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 newgrs; + + vector maps; + stringtok(maps, getArg("maps"), " ,"); + for (vector::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 &newgrs) { + map new_georecords; + + int mapcount = 0; + for (vector::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::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"); +} diff --git a/modules/geobackend/geobackend.hh b/modules/geobackend/geobackend.hh new file mode 100644 index 000000000..4f4353267 --- /dev/null +++ b/modules/geobackend/geobackend.hh @@ -0,0 +1,114 @@ +/* geobackend.hh + * Copyright (C) 2004 Mark Bergsma + * This software is licensed under the terms of the GPL, version 2. + * + * $Id: geobackend.hh,v 1.1 2004/02/28 19:13:44 ahu Exp $ + */ + + +#include +#include +#include + +#include +#include + +#include "ippreftree.hh" + +using namespace std; + +class GeoRecord { +public: + GeoRecord(); + + string qname; + string origin; + string directorfile; + map dirmap; +}; + +class GeoBackend : public DNSBackend{ +public: + + GeoBackend(const string &suffix); + ~GeoBackend(); + + virtual void lookup(const QType &qtype, const string &qdomain, DNSPacket *pkt_p=0, int zoneId=-1); + virtual bool list(const string &target, int domain_id); + virtual bool get(DNSResourceRecord &r); + virtual bool getSOA(const string &name, SOAData &soadata); + + virtual void reload(); + virtual void rediscover(string *status = 0); + +private: + // Static resources, shared by all instances + static IPPrefTree *ipt; + static vector nsRecords; + static map georecords; + static string soaMasterServer; + static string soaHostmaster; + static string zoneName; + static u_int32_t geoTTL; + static u_int32_t nsTTL; + static time_t lastDiscoverTime; + const static string logprefix; + + bool forceReload; + + // Locking + static bool first; + static int backendcount; + static pthread_mutex_t startup_lock; + static pthread_mutex_t ipt_lock; + + vector answers; + vector::const_iterator i_answers; + + void answerGeoRecord(const QType &qtype, const string &qdomain, DNSPacket *p); + void answerLocalhostRecord(const string &qdomain, DNSPacket *p); + void queueNSRecords(const string &qname); + void queueGeoRecords(); + void fillGeoResourceRecord(const string &qname, const string &target, DNSResourceRecord *rr); + const inline string resolveTarget(const GeoRecord &gr, short isocode) const; + + void loadZoneName(); + void loadTTLValues(); + void loadSOAValues(); + void loadNSRecords(); + void loadIPLocationMap(); + void loadGeoRecords(); + void loadDirectorMaps(const vector &newgrs); + void loadDirectorMap(GeoRecord &gr); +}; + +class GeoFactory : public BackendFactory{ +public: + GeoFactory() : BackendFactory("geo") {} + + void declareArguments(const string &suffix = "") { + declare(suffix, "zone", "zonename to be served", ""); + declare(suffix, "soa-values", "values of the SOA master nameserver and hostmaster fields, comma seperated", ""); + declare(suffix, "ns-records", "targets of the NS records, comma seperated.", ""); + declare(suffix, "ttl", "TTL value for geo records", "3600"); + declare(suffix, "ns-ttl", "TTL value for NS records", "86400"); + declare(suffix, "ip-map-zonefile", "path to the rbldnsd format zonefile", "zz.countries.nerd.dk.rbldnsd"); + declare(suffix, "maps", "list of paths to director maps or directories containing director map files", ""); + } + + DNSBackend *make(const string &suffix) { + return new GeoBackend(suffix); + } +}; + +class GeoLoader { +public: + GeoLoader() { + BackendMakers().report(new GeoFactory); + + L << Logger::Info << "[GeoBackend] This is the geobackend (" + __DATE__", "__TIME__" - $Revision: 1.1 $) reporting" << endl; + } +}; + +static GeoLoader geoloader; diff --git a/modules/geobackend/ippreftree.cc b/modules/geobackend/ippreftree.cc new file mode 100644 index 000000000..f038cdcbf --- /dev/null +++ b/modules/geobackend/ippreftree.cc @@ -0,0 +1,146 @@ +/* ippreftree.cc + * Copyright (C) 2004 Mark Bergsma + * This software is licensed under the terms of the GPL, version 2. + * + * $Id: ippreftree.cc,v 1.1 2004/02/28 19:13:44 ahu Exp $ + */ + +#include + +#include "ippreftree.hh" + +IPPrefTree::IPPrefTree(): nodecount(0) { + root = allocateNode(); + nodecount++; +} + +IPPrefTree::~IPPrefTree() { + removeNode(root); +} + +void IPPrefTree::add(const string &prefix, const short value) { + u_int32_t ip; + int preflen; + parsePrefix(prefix, ip, preflen); + + add(ip, preflen, value); +} + +void IPPrefTree::add(const u_int32_t ip, const int preflen, const short value) { + addNode(root, ip, preflenToNetmask(preflen), value); +} + +short IPPrefTree::lookup(const string &prefix) const { + u_int32_t ip; + int preflen; + parsePrefix(prefix, ip, preflen); + + return lookup(ip, preflen); +} + +short IPPrefTree::lookup(const u_int32_t ip, const int preflen) const { + const node_t *node = findDeepestFilledNode(root, ip, preflenToNetmask(preflen)); + return (node == NULL ? 0 : node->value); +} + +void IPPrefTree::clear() { + // Remove all children of the root node, but not the root node itself (reallocate it) + removeNode(root); + root = allocateNode(); + nodecount++; +} + +int IPPrefTree::getNodeCount() const { + return nodecount; +} + +int IPPrefTree::getMemoryUsage() const { + return nodecount * sizeof(node_t); +} + +// Private methods + +inline u_int32_t IPPrefTree::preflenToNetmask (const int preflen) const { + return ~( (1 << (32 - preflen)) - 1); +} + +inline void IPPrefTree::parsePrefix(const string &prefix, u_int32_t &ip, int &preflen) const { + // Parse the prefix string (with format 131.155.230.139/25) + istringstream is(prefix); + ip = 0; preflen = 32; + char c; + + for (int i = 0; i < 4; i++) { + int octet = 0; + is >> octet; + ip = (ip << 8) | octet; + is.get(c); + if (c != '.' && c != '/') + throw ParsePrefixException("Invalid format: expected '.' or '/'"); + } + + if (is.good() && c == '/') { + // Read the prefix length + is >> preflen; + } +} + +void IPPrefTree::addNode(node_t *node, const u_int32_t ip, const u_int32_t mask, const short value) { + if (mask == 0) { + // We are at the correct depth in the tree + node->value = value; + } + else { /* mask > 0 */ + // We need to walk deeper into the tree, and extend it if needed + int b = (ip >> 31); + + if (node->child[b] == NULL) { + node->child[b] = allocateNode(); + nodecount++; + } + + // Recursively add + addNode(node->child[b], ip << 1, mask << 1, value); + } +} + +node_t * IPPrefTree::allocateNode() { + node_t *node = new node_t; + + // Initialize + node->child[0] = node->child[1] = NULL; + node->value = 0; + + return node; +} + +const node_t * IPPrefTree::findDeepestFilledNode(const node_t *node, const u_int32_t ip, const u_int32_t mask) const { + if (node == NULL) return NULL; + + if (mask == 0) { + return (node->value == 0 ? NULL : node); + } + else { /* mask > 0 */ + int b = (ip >> 31); + const node_t *descendant = findDeepestFilledNode(node->child[b], ip << 1, mask << 1); + if (descendant == NULL) { + if (node->value != 0) // Children have no (more) explicit information, do we? + return node; + else + return NULL; + } + else + return descendant; + } +} + +void IPPrefTree::removeNode(node_t *node) { + if (node == NULL) return; + + // Recursively remove and deallocate all descendants + removeNode(node->child[0]); + removeNode(node->child[1]); + nodecount--; + + delete node; +} diff --git a/modules/geobackend/ippreftree.hh b/modules/geobackend/ippreftree.hh new file mode 100644 index 000000000..5d5be68b2 --- /dev/null +++ b/modules/geobackend/ippreftree.hh @@ -0,0 +1,57 @@ +/* ippreftree.hh + * Copyright (C) 2004 Mark Bergsma + * This software is licensed under the terms of the GPL, version 2. + * + * $Id: ippreftree.hh,v 1.1 2004/02/28 19:13:44 ahu Exp $ + */ + +#include +#include +#include + +using namespace std; + +// Use old style C structs for efficiency +typedef struct node_t { + node_t *child[2]; + short value; +} node_t; + +class IPPrefTree{ + +public: + IPPrefTree(); + ~IPPrefTree(); + + void add(const string &prefix, const short value); + void add(const u_int32_t ip, const int preflen, const short value); + + short lookup(const string &prefix) const; + short lookup(const u_int32_t ip, const int preflen) const; + + void clear(); + + int getNodeCount() const; + int getMemoryUsage() const; + +private: + node_t *root; // root of the tree + int nodecount; // total number of nodes in the tree + + void addNode(node_t * node, const u_int32_t ip, const u_int32_t mask, const short value); + node_t * allocateNode(); + const node_t * IPPrefTree::findDeepestFilledNode(const node_t *root, const u_int32_t ip, const u_int32_t mask) const; + void removeNode(node_t * node); + + inline u_int32_t preflenToNetmask(const int preflen) const; + inline void parsePrefix(const string &prefix, u_int32_t &ip, int &preflen) const; +}; + +class ParsePrefixException +{ +public: + ParsePrefixException() { reason = ""; }; + ParsePrefixException(string r) { reason = r; }; + + string reason; +};