From 709ca59fa0a4d29970435169afa5d6dbd789c185 Mon Sep 17 00:00:00 2001 From: Aki Tuomi Date: Thu, 4 Sep 2014 17:06:09 +0300 Subject: [PATCH] GeoIP backend implementation --- .travis.yml | 10 +- configure.ac | 14 + m4/pdns_with_geo.m4 | 4 + modules/Makefile.am | 2 +- modules/geoipbackend/Makefile.am | 5 + modules/geoipbackend/OBJECTFILES | 1 + modules/geoipbackend/OBJECTLIBS | 1 + modules/geoipbackend/geoipbackend.cc | 650 ++++++++++++++++++ modules/geoipbackend/geoipbackend.hh | 66 ++ .../geoipbackend/regression-tests/.gitignore | 4 + .../00dnssec-grabkeys/command | 16 + .../00dnssec-grabkeys/description | 1 + .../00dnssec-grabkeys/expected_result | 0 .../regression-tests/basic-a-dnssec/command | 3 + .../basic-a-dnssec/description | 1 + .../basic-a-dnssec/expected_result | 7 + .../basic-a-dnssec/skip.nodnssec | 0 .../basic-a-resolution/command | 3 + .../basic-a-resolution/description | 2 + .../basic-a-resolution/expected_result | 4 + .../region-a-resolution/command | 2 + .../region-a-resolution/description | 2 + .../region-a-resolution/expected_result | 4 + .../static-any-resolution/command | 3 + .../static-any-resolution/description | 2 + .../static-any-resolution/expected_result | 6 + pdns/docs/pdns.xml | 126 ++++ pdns/iputils.hh | 8 + regression-tests/backends/common | 4 +- regression-tests/backends/geoip-master | 77 +++ regression-tests/start-test-stop | 1 + 31 files changed, 1024 insertions(+), 5 deletions(-) create mode 100644 m4/pdns_with_geo.m4 create mode 100644 modules/geoipbackend/Makefile.am create mode 100644 modules/geoipbackend/OBJECTFILES create mode 100644 modules/geoipbackend/OBJECTLIBS create mode 100644 modules/geoipbackend/geoipbackend.cc create mode 100644 modules/geoipbackend/geoipbackend.hh create mode 100644 modules/geoipbackend/regression-tests/.gitignore create mode 100755 modules/geoipbackend/regression-tests/00dnssec-grabkeys/command create mode 100644 modules/geoipbackend/regression-tests/00dnssec-grabkeys/description create mode 100644 modules/geoipbackend/regression-tests/00dnssec-grabkeys/expected_result create mode 100755 modules/geoipbackend/regression-tests/basic-a-dnssec/command create mode 100644 modules/geoipbackend/regression-tests/basic-a-dnssec/description create mode 100644 modules/geoipbackend/regression-tests/basic-a-dnssec/expected_result create mode 100644 modules/geoipbackend/regression-tests/basic-a-dnssec/skip.nodnssec create mode 100755 modules/geoipbackend/regression-tests/basic-a-resolution/command create mode 100644 modules/geoipbackend/regression-tests/basic-a-resolution/description create mode 100644 modules/geoipbackend/regression-tests/basic-a-resolution/expected_result create mode 100755 modules/geoipbackend/regression-tests/region-a-resolution/command create mode 100644 modules/geoipbackend/regression-tests/region-a-resolution/description create mode 100644 modules/geoipbackend/regression-tests/region-a-resolution/expected_result create mode 100755 modules/geoipbackend/regression-tests/static-any-resolution/command create mode 100644 modules/geoipbackend/regression-tests/static-any-resolution/description create mode 100644 modules/geoipbackend/regression-tests/static-any-resolution/expected_result create mode 100644 regression-tests/backends/geoip-master diff --git a/.travis.yml b/.travis.yml index 38e9223af..79f2bf829 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,12 +5,13 @@ compiler: before_script: - git describe --always --dirty=+ - sudo /sbin/ip addr add 10.0.3.0/24 dev lo + - sudo /sbin/ip addr add 1.2.3.4/32 dev lo - sudo rm /etc/apt/sources.list.d/travis_ci_zeromq3-source.list - sudo apt-get update - - sudo apt-get install --no-install-recommends libboost-all-dev libtolua-dev bc libcdb-dev libnet-dns-perl unbound-host ldnsutils dnsutils bind9utils libtool libcdb-dev xmlto links asciidoc ruby-json ruby-sqlite3 rubygems libcurl4-openssl-dev ruby1.9.1 socat time libzmq1 libzmq-dev pkg-config daemontools authbind liblua5.1-posix1 libopendbx1-dev libopendbx1-sqlite3 python-virtualenv libldap2-dev softhsm libp11-kit-dev p11-kit moreutils + - sudo apt-get install --no-install-recommends libboost-all-dev libtolua-dev bc libcdb-dev libnet-dns-perl unbound-host ldnsutils dnsutils bind9utils libtool libcdb-dev xmlto links asciidoc ruby-json ruby-sqlite3 rubygems libcurl4-openssl-dev ruby1.9.1 socat time libzmq1 libzmq-dev pkg-config daemontools authbind liblua5.1-posix1 libopendbx1-dev libopendbx1-sqlite3 python-virtualenv libldap2-dev softhsm libp11-kit-dev p11-kit moreutils libgeoip-dev geoip-database - sudo sh -c 'sed s/precise/trusty/g /etc/apt/sources.list > /etc/apt/sources.list.d/trusty.list' - sudo apt-get update - - sudo apt-get install liblmdb0 liblmdb-dev lmdb-utils + - sudo apt-get install liblmdb0 liblmdb-dev lmdb-utils libyaml-cpp-dev - sudo update-alternatives --set ruby /usr/bin/ruby1.9.1 - sudo touch /etc/authbind/byport/53 - sudo chmod 755 /etc/authbind/byport/53 @@ -31,7 +32,7 @@ before_script: - p11-kit -l # ensure it's ok script: - ./bootstrap - - ./configure --with-modules='bind gmysql gpgsql gsqlite3 mydns tinydns remote random opendbx ldap lmdb' --enable-unit-tests --enable-tools --enable-remotebackend-zeromq --enable-experimental-pkcs11 + - ./configure --with-modules='bind geoip gmysql gpgsql gsqlite3 mydns tinydns remote random opendbx ldap lmdb' --enable-unit-tests --enable-tools --enable-remotebackend-zeromq --enable-experimental-pkcs11 - make -k dist - make -k -j 4 - make -k install DESTDIR=/tmp/pdns-install-dir @@ -60,6 +61,7 @@ script: - touch tests/verify-dnssec-zone/allow-missing - touch tests/verify-dnssec-zone/skip.nsec3 # some (travis) tools in this test are unable to handle nsec3 zones - touch tests/verify-dnssec-zone/skip.optout + - export geoipregion=oc geoipregionip=1.2.3.4 - ./timestamp ./start-test-stop 5300 bind-both - ./timestamp ./start-test-stop 5300 bind-dnssec-both - ./timestamp ./start-test-stop 5300 bind-dnssec-pkcs11 @@ -67,6 +69,8 @@ script: - ./timestamp ./start-test-stop 5300 bind-dnssec-nsec3-optout-both - ./timestamp ./start-test-stop 5300 bind-dnssec-nsec3-narrow - ./timestamp ./start-test-stop 5300 bind-hybrid-nsec3 + - ./timestamp ./start-test-stop 5300 geoipbackend + - ./timestamp ./start-test-stop 5300 geoipbackend-nsec3-narrow - ./timestamp ./start-test-stop 5300 gmysql-nodnssec-both - ./timestamp ./start-test-stop 5300 gmysql-both - ./timestamp ./start-test-stop 5300 gmysql-nsec3-both diff --git a/configure.ac b/configure.ac index 0669154a7..e2814b5b4 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,16 @@ eval full_libdir="\"$libdir\"" # detect pkg-config explicitly PKG_PROG_PKG_CONFIG +AC_CHECK_HEADERS( + [sys/mman.h], + [AC_CHECK_FUNC( + [mmap], + [AC_DEFINE(HAVE_MMAP, [1], [Define to 1 if you have mmap])], + [have_mmap=no] + )], + [have_mmap=no] +) + PDNS_CHECK_RAGEL AC_CHECK_PROG([ASCIIDOC], [asciidoc], [asciidoc]) @@ -284,6 +294,9 @@ for a in $modules $dynmodules; do tinydns) PDNS_CHECK_CDB ;; + geoip) + PDNS_CHECK_GEOIP + ;; esac done @@ -358,6 +371,7 @@ AC_CONFIG_FILES([ modules/bindbackend/Makefile modules/db2backend/Makefile modules/geobackend/Makefile + modules/geoipbackend/Makefile modules/gmysqlbackend/Makefile modules/goraclebackend/Makefile modules/gpgsqlbackend/Makefile diff --git a/m4/pdns_with_geo.m4 b/m4/pdns_with_geo.m4 new file mode 100644 index 000000000..d0bcb6343 --- /dev/null +++ b/m4/pdns_with_geo.m4 @@ -0,0 +1,4 @@ +AC_DEFUN([PDNS_CHECK_GEOIP], [ + PKG_CHECK_MODULES([GEOIP], [geoip]) + PKG_CHECK_MODULES([YAML], [yaml-cpp >= 0.5]) +]) diff --git a/modules/Makefile.am b/modules/Makefile.am index 1a544a9fb..2af3f1799 100644 --- a/modules/Makefile.am +++ b/modules/Makefile.am @@ -1,2 +1,2 @@ SUBDIRS=@moduledirs@ -DIST_SUBDIRS=bindbackend db2backend geobackend gmysqlbackend goraclebackend gpgsqlbackend gsqlite3backend ldapbackend luabackend mydnsbackend opendbxbackend oraclebackend pipebackend tinydnsbackend remotebackend randombackend lmdbbackend +DIST_SUBDIRS=bindbackend db2backend geobackend gmysqlbackend goraclebackend gpgsqlbackend gsqlite3backend ldapbackend luabackend mydnsbackend opendbxbackend oraclebackend pipebackend tinydnsbackend remotebackend randombackend lmdbbackend geoipbackend diff --git a/modules/geoipbackend/Makefile.am b/modules/geoipbackend/Makefile.am new file mode 100644 index 000000000..240f4cd4b --- /dev/null +++ b/modules/geoipbackend/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS=$(THREADFLAGS) $(BOOST_CPPFLAGS) $(YAML_CFLAGS) $(GEOIP_CFLAGS) +EXTRA_DIST=OBJECTFILES OBJECTLIBS +pkglib_LTLIBRARIES = libgeoipbackend.la +libgeoipbackend_la_SOURCES=geoipbackend.cc geoipbackend.hh +libgeoipbackend_la_LDFLAGS=-module -avoid-version diff --git a/modules/geoipbackend/OBJECTFILES b/modules/geoipbackend/OBJECTFILES new file mode 100644 index 000000000..7173c166c --- /dev/null +++ b/modules/geoipbackend/OBJECTFILES @@ -0,0 +1 @@ +geoipbackend.lo diff --git a/modules/geoipbackend/OBJECTLIBS b/modules/geoipbackend/OBJECTLIBS new file mode 100644 index 000000000..d87d58b84 --- /dev/null +++ b/modules/geoipbackend/OBJECTLIBS @@ -0,0 +1 @@ +$(YAML_LIBS) $(GEOIP_LIBS) diff --git a/modules/geoipbackend/geoipbackend.cc b/modules/geoipbackend/geoipbackend.cc new file mode 100644 index 000000000..3dfc7a607 --- /dev/null +++ b/modules/geoipbackend/geoipbackend.cc @@ -0,0 +1,650 @@ +#include "geoipbackend.hh" +#include +#include +#include + +pthread_rwlock_t GeoIPBackend::s_state_lock=PTHREAD_RWLOCK_INITIALIZER; + +class GeoIPDomain { +public: + int id; + string domain; + int ttl; + map services; + map > records; +}; + +static vector s_domains; +static GeoIP *s_gi = 0; // geoip database +static GeoIP *s_gi6 = 0; // geoip database +static int s_rc = 0; // refcount + +GeoIPBackend::GeoIPBackend(const string& suffix) { + WriteLock wl(&s_state_lock); + d_dnssec = false; + setArgPrefix("geoip" + suffix); + if (getArg("dnssec-keydir").empty() == false) { + DIR *d = opendir(getArg("dnssec-keydir").c_str()); + if (d == NULL) { + throw PDNSException("dnssec-keydir " + getArg("dnssec-keydir") + " does not exist"); + } + d_dnssec = true; + closedir(d); + } + if (s_rc == 0) { // first instance gets to open everything + initialize(); + } + d_dbmode = GeoIP_database_edition(s_gi); + s_rc++; +} + +void GeoIPBackend::initialize() { + YAML::Node config; + vector tmp_domains; + GeoIP *gi; + + string mode = getArg("database-cache"); + int flags; + if (mode == "standard") + flags = GEOIP_STANDARD; + else if (mode == "memory") + flags = GEOIP_MEMORY_CACHE; + else if (mode == "index") + flags = GEOIP_INDEX_CACHE; +#ifdef HAVE_MMAP + else if (mode == "mmap") + flags = GEOIP_MMAP_CACHE; +#endif + else + throw PDNSException("Invalid cache mode " + mode + " for GeoIP backend"); + + if (getArg("database-file").empty() == false) { + gi = GeoIP_open(getArg("database-file").c_str(), flags); + if (gi == NULL) + throw PDNSException("Cannot open GeoIP database " + getArg("database-file")); + if (s_gi) GeoIP_delete(s_gi); + s_gi = gi; + } + if (getArg("database-file6").empty() == false) { + gi = GeoIP_open(getArg("database-file6").c_str(), flags); + if (gi == NULL) + throw PDNSException("Cannot open GeoIP database " + getArg("database-file6")); + if (s_gi6) GeoIP_delete(s_gi6); + s_gi6 = gi; + } + + if (s_gi == NULL && s_gi6 == NULL) + throw PDNSException("You need to specify one database at least"); + + config = YAML::LoadFile(getArg("zones-file")); + + BOOST_FOREACH(YAML::Node domain, config["domains"]) { + GeoIPDomain dom; + dom.id = s_domains.size(); + dom.domain = domain["domain"].as(); + std::transform(dom.domain.begin(), dom.domain.end(), dom.domain.begin(), dns_tolower); + dom.ttl = domain["ttl"].as(); + + for(YAML::const_iterator recs = domain["records"].begin(); recs != domain["records"].end(); recs++) { + string qname = recs->first.as(); + std::transform(qname.begin(), qname.end(), qname.begin(), dns_tolower); + vector rrs; + + BOOST_FOREACH(YAML::Node item, recs->second) { + YAML::const_iterator rec = item.begin(); + DNSResourceRecord rr; + rr.domain_id = dom.id; + rr.ttl = dom.ttl; + rr.qname = qname; + if (rec->first.IsNull()) { + rr.qtype = "NULL"; + } else { + string qtype = boost::to_upper_copy(rec->first.as()); + rr.qtype = qtype; + } + if (rec->second.IsNull()) { + rr.content = ""; + } else { + string content=rec->second.as(); + if (rr.qtype == QType::MX || rr.qtype == QType::SRV) { + // extract priority + rr.priority=atoi(content.c_str()); + string::size_type pos = content.find_first_not_of("0123456789"); + if(pos != string::npos) + boost::erase_head(content, pos); + trim_left(content); + } + rr.content = content; + } + + rr.auth = 1; + rr.d_place = DNSResourceRecord::ANSWER; + rrs.push_back(rr); + } + std::swap(dom.records[qname], rrs); + } + + for(YAML::const_iterator service = domain["services"].begin(); service != domain["services"].end(); service++) { + dom.services[service->first.as()] = service->second.as(); + } + + tmp_domains.push_back(dom); + } + + s_domains.clear(); + std::swap(s_domains, tmp_domains); +} + +GeoIPBackend::~GeoIPBackend() { + WriteLock wl(&s_state_lock); + s_rc--; + if (s_rc == 0) { // last instance gets to cleanup + if (s_gi) + GeoIP_delete(s_gi); + if (s_gi6) + GeoIP_delete(s_gi6); + s_gi = NULL; + s_gi6 = NULL; + s_domains.clear(); + } +} + +void GeoIPBackend::lookup(const QType &qtype, const string &qdomain, DNSPacket *pkt_p, int zoneId) { + ReadLock rl(&s_state_lock); + GeoIPDomain dom; + bool found = false; + + //cerr << qtype.getName() << " " << qdomain << " " << zoneId << std::endl; + + if (d_result.size()>0) + throw PDNSException("Cannot perform lookup while another is running"); + + string search = qdomain; + std::transform(search.begin(), search.end(), search.begin(), dns_tolower); + + d_result.clear(); + + if (zoneId > -1 && zoneId < static_cast(s_domains.size())) + dom = s_domains[zoneId]; + else { + BOOST_FOREACH(GeoIPDomain i, s_domains) { + if (endsOn(search, dom.domain)) { + dom = i; + found = true; + break; + } + } + if (!found) return; // not found + } + + if (dom.records.count(search)) { // return static value + map >::iterator i = dom.records.find(search); + BOOST_FOREACH(DNSResourceRecord rr, i->second) { + if (qtype == QType::ANY || rr.qtype == qtype) { + d_result.push_back(rr); + d_result.back().qname = qdomain; + } + } + return; + } + + if (!(qtype == QType::ANY || qtype == QType::CNAME)) return; + + string ip = "0.0.0.0"; + bool v6 = false; + if (pkt_p != NULL) { + ip = pkt_p->getRealRemote().toStringNoMask(); + v6 = pkt_p->getRealRemote().isIpv6(); + } + + if (dom.services.count(search) == 0) return; // no hit + map::const_iterator target = dom.services.find(search); + string format = target->second; + + format = format2str(format, ip, v6); + + DNSResourceRecord rr; + rr.domain_id = dom.id; + rr.qtype = QType::CNAME; + rr.qname = qdomain; + rr.content = format; + rr.auth = 1; + rr.ttl = dom.ttl; + rr.scopeMask = (v6 ? 128 : 32); + d_result.push_back(rr); +} + +bool GeoIPBackend::get(DNSResourceRecord &r) { + if (d_result.empty()) return false; + + r = d_result.back(); + d_result.pop_back(); + + //cerr << "get " << r.qname << " IN " << r.qtype.getName() << " " << r.content << endl; + + return true; +} + +string GeoIPBackend::queryGeoIP(const string &ip, bool v6, GeoIPQueryAttribute attribute) { + string ret = "unknown"; + const char *val = NULL; + GeoIPRegion *gir = NULL; + GeoIPRecord *gir2 = NULL; + int id; + + + if (v6 && s_gi6) { + if (attribute == Afi) { + return "v6"; + } else if (d_dbmode == GEOIP_ISP_EDITION_V6 || d_dbmode == GEOIP_ORG_EDITION_V6) { + if (attribute == Name) { + val = GeoIP_name_by_addr_v6(s_gi6, ip.c_str()); + } + } else if (d_dbmode == GEOIP_COUNTRY_EDITION_V6 || + d_dbmode == GEOIP_LARGE_COUNTRY_EDITION_V6 || + d_dbmode == GEOIP_COUNTRY_EDITION) { + id = GeoIP_id_by_addr_v6(s_gi6, ip.c_str()); + if (attribute == Country) { + val = GeoIP_code3_by_id(id); + } else if (attribute == Continent) { + val = GeoIP_continent_by_id(id); + } + } else if (d_dbmode == GEOIP_REGION_EDITION_REV0 || + d_dbmode == GEOIP_REGION_EDITION_REV1) { + gir = GeoIP_region_by_addr_v6(s_gi6, ip.c_str()); + if (gir) { + if (attribute == Country) { + id = GeoIP_id_by_code(gir->country_code); + val = GeoIP_code3_by_id(id); + } else if (attribute == Region) { + val = gir->region; + } else if (attribute == Continent) { + id = GeoIP_id_by_code(gir->country_code); + val = GeoIP_continent_by_id(id); + } + } + } else if (d_dbmode == GEOIP_CITY_EDITION_REV0_V6 || + d_dbmode == GEOIP_CITY_EDITION_REV1_V6) { + gir2 = GeoIP_record_by_addr_v6(s_gi6, ip.c_str()); + if (gir2) { + if (attribute == Country) { + val = gir2->country_code3; + } else if (attribute == Region) { + val = gir2->region; + } else if (attribute == Continent) { + id = GeoIP_id_by_code(gir2->country_code); + val = GeoIP_continent_by_id(id); + } else if (attribute == City) { + val = gir2->city; + } + } + } + } else if (!v6 && s_gi) { + if (attribute == Afi) { + return "v4"; + } else if (d_dbmode == GEOIP_ISP_EDITION || d_dbmode == GEOIP_ORG_EDITION) { + if (attribute == Name) { + val = GeoIP_name_by_addr_v6(s_gi, ip.c_str()); + } + } else if (d_dbmode == GEOIP_COUNTRY_EDITION || + d_dbmode == GEOIP_LARGE_COUNTRY_EDITION) { + id = GeoIP_id_by_addr(s_gi, ip.c_str()); + if (attribute == Country) { + val = GeoIP_code3_by_id(id); + } else if (attribute == Continent) { + val = GeoIP_continent_by_id(id); + } + } else if (d_dbmode == GEOIP_REGION_EDITION_REV0 || + d_dbmode == GEOIP_REGION_EDITION_REV1) { + gir = GeoIP_region_by_addr(s_gi, ip.c_str()); + if (gir) { + if (attribute == Country) { + id = GeoIP_id_by_code(gir->country_code); + val = GeoIP_code3_by_id(id); + } else if (attribute == Region) { + val = gir->region; + } else if (attribute == Continent) { + id = GeoIP_id_by_code(gir->country_code); + val = GeoIP_continent_by_id(id); + } + } + } else if (d_dbmode == GEOIP_CITY_EDITION_REV0 || + d_dbmode == GEOIP_CITY_EDITION_REV1) { + gir2 = GeoIP_record_by_addr(s_gi, ip.c_str()); + if (gir2) { + if (attribute == Country) { + val = gir2->country_code3; + } else if (attribute == Region) { + val = gir2->region; + } else if (attribute == Continent) { + id = GeoIP_id_by_code(gir2->country_code); + val = GeoIP_continent_by_id(id); + } else if (attribute == City) { + val = gir2->city; + } + } + } + } + if (val) { + ret = val; + if (ret == "--") ret = "unknown"; + std::transform(ret.begin(), ret.end(), ret.begin(), ::tolower); + } + return ret; +} + +string GeoIPBackend::format2str(string format, const string& ip, bool v6) { + string::size_type cur,last; + GeoIPQueryAttribute attr; + last=0; + while((cur = format.find("%", last)) != string::npos) { + if (!format.compare(cur,3,"%co")) { + attr = Country; + } else if (!format.compare(cur,3,"%cn")) { + attr = Continent; + } else if (!format.compare(cur,3,"%af")) { + attr = Afi; + } else if (!format.compare(cur,3,"%re")) { + attr = Region; + } else if (!format.compare(cur,3,"%na")) { + attr = Name; + } else if (!format.compare(cur,3,"%ci")) { + attr = City; + } else if (!format.compare(cur,2,"%%")) { + last = cur + 2; continue; + } else { + last = cur + 1; continue; + } + + string rep = queryGeoIP(ip, v6, attr); + + format.replace(cur, 3, rep); + last = cur + rep.size(); // move to next attribute + } + return format; +} + +void GeoIPBackend::reload() { + WriteLock wl(&s_state_lock); + + try { + initialize(); + } catch (PDNSException &pex) { + L<getSOA(domain, sd); + di.id = dom.id; + di.zone = dom.domain; + di.serial = sd.serial; + di.kind = DomainInfo::Native; + di.backend = this; + return true; + } + } + return false; +} + +bool GeoIPBackend::getAllDomainMetadata(const string& name, std::map >& meta) { + if (!d_dnssec) return false; + + ReadLock rl(&s_state_lock); + BOOST_FOREACH(GeoIPDomain dom, s_domains) { + if (pdns_iequals(dom.domain, name)) { + if (hasDNSSECkey(dom.domain)) { + meta[string("NSEC3NARROW")].push_back("1"); + meta[string("NSEC3PARAM")].push_back("1 0 1 f95a"); + } + return true; + } + } + return false; +} + +bool GeoIPBackend::getDomainMetadata(const std::string& name, const std::string& kind, std::vector& meta) { + if (!d_dnssec) return false; + + ReadLock rl(&s_state_lock); + BOOST_FOREACH(GeoIPDomain dom, s_domains) { + if (pdns_iequals(dom.domain, name)) { + if (hasDNSSECkey(dom.domain)) { + if (kind == "NSEC3NARROW") + meta.push_back(string("1")); + if (kind == "NSEC3PARAM") + meta.push_back(string("1 0 1 f95a")); + } + return true; + } + } + return false; +} + +bool GeoIPBackend::getDomainKeys(const std::string& name, unsigned int kind, std::vector& keys) { + if (!d_dnssec) return false; + ReadLock rl(&s_state_lock); + BOOST_FOREACH(GeoIPDomain dom, s_domains) { + if (pdns_iequals(dom.domain, name)) { + regex_t reg; + regmatch_t regm[5]; + regcomp(®, "(.*)[.]([0-9]+)[.]([0-9]+)[.]([01])[.]key$", REG_ICASE|REG_EXTENDED); + ostringstream pathname; + pathname << getArg("dnssec-keydir") << "/" << dom.domain << "*.key"; + glob_t glob_result; + if (glob(pathname.str().c_str(),GLOB_ERR,NULL,&glob_result) == 0) { + for(size_t i=0;i0) { + content << string(buffer, ifs.gcount()); + } + } + ifs.close(); + kd.content = content.str(); + keys.push_back(kd); + } + } + } + regfree(®); + globfree(&glob_result); + return true; + } + } + return false; +} + +bool GeoIPBackend::removeDomainKey(const string& name, unsigned int id) { + if (!d_dnssec) return false; + WriteLock rl(&s_state_lock); + ostringstream path; + + BOOST_FOREACH(GeoIPDomain dom, s_domains) { + if (pdns_iequals(dom.domain, name)) { + regex_t reg; + regmatch_t regm[5]; + regcomp(®, "(.*)[.]([0-9]+)[.]([0-9]+)[.]([01])[.]key$", REG_ICASE|REG_EXTENDED); + ostringstream pathname; + pathname << getArg("dnssec-keydir") << "/" << dom.domain << "*.key"; + glob_t glob_result; + if (glob(pathname.str().c_str(),GLOB_ERR,NULL,&glob_result) == 0) { + for(size_t i=0;i= nextid) nextid = kid+1; + } + } + } + regfree(®); + globfree(&glob_result); + pathname.str(""); + pathname << getArg("dnssec-keydir") << "/" << dom.domain << "." << key.flags << "." << nextid << "." << (key.active?"1":"0") << ".key"; + ofstream ofs(pathname.str().c_str()); + ofs.write(key.content.c_str(), key.content.size()); + ofs.close(); + return nextid; + } + } + return false; + +} + +bool GeoIPBackend::activateDomainKey(const string& name, unsigned int id) { + if (!d_dnssec) return false; + WriteLock rl(&s_state_lock); + BOOST_FOREACH(GeoIPDomain dom, s_domains) { + if (pdns_iequals(dom.domain, name)) { + regex_t reg; + regmatch_t regm[5]; + regcomp(®, "(.*)[.]([0-9]+)[.]([0-9]+)[.]([01])[.]key$", REG_ICASE|REG_EXTENDED); + ostringstream pathname; + pathname << getArg("dnssec-keydir") << "/" << dom.domain << "*.key"; + glob_t glob_result; + if (glob(pathname.str().c_str(),GLOB_ERR,NULL,&glob_result) == 0) { + for(size_t i=0;i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pdns/dnspacket.hh" +#include "pdns/dns.hh" +#include "pdns/dnsbackend.hh" +#include "pdns/lock.hh" + +class GeoIPDomain; + +class GeoIPBackend: public DNSBackend { +public: + GeoIPBackend(const std::string& suffix=""); + ~GeoIPBackend(); + + 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, bool include_disabled=false) { return false; } // not supported + virtual bool get(DNSResourceRecord &r); + virtual void reload(); + virtual void rediscover(string *status = 0); + virtual bool getDomainInfo(const string &domain, DomainInfo &di); + + // dnssec support + virtual bool doesDNSSEC() { return d_dnssec; }; + virtual bool getAllDomainMetadata(const string& name, std::map >& meta); + virtual bool getDomainMetadata(const std::string& name, const std::string& kind, std::vector& meta); + virtual bool getDomainKeys(const std::string& name, unsigned int kind, std::vector& keys); + virtual bool removeDomainKey(const string& name, unsigned int id); + virtual int addDomainKey(const string& name, const KeyData& key); + virtual bool activateDomainKey(const string& name, unsigned int id); + virtual bool deactivateDomainKey(const string& name, unsigned int id); + + enum GeoIPQueryAttribute { + Afi, + City, + Continent, + Country, + Name, + Region + }; + +private: + static pthread_rwlock_t s_state_lock; + + void initialize(); + void ip2geo(const GeoIPDomain& dom, const string& qname, const string& ip); + string queryGeoIP(const string &ip, bool v6, GeoIPQueryAttribute attribute); + string format2str(string format, const string& ip, bool v6); + int d_dbmode; + bool d_dnssec; + bool hasDNSSECkey(const string &domain); + + vector d_result; +}; diff --git a/modules/geoipbackend/regression-tests/.gitignore b/modules/geoipbackend/regression-tests/.gitignore new file mode 100644 index 000000000..638f32cb0 --- /dev/null +++ b/modules/geoipbackend/regression-tests/.gitignore @@ -0,0 +1,4 @@ +diff +real_result +*.out +geosec diff --git a/modules/geoipbackend/regression-tests/00dnssec-grabkeys/command b/modules/geoipbackend/regression-tests/00dnssec-grabkeys/command new file mode 100755 index 000000000..0d984d3b3 --- /dev/null +++ b/modules/geoipbackend/regression-tests/00dnssec-grabkeys/command @@ -0,0 +1,16 @@ +#!/bin/sh -e +set pipefail +rm -f trustedkeys +rm -f unbound-host.conf +for zone in example.com +do + drill -p $port -o rd -D dnskey $zone @$nameserver | grep -v '^;' | grep -v AwEAAarTiHhPgvD28WCN8UBXcEcf8f >> trustedkeys + echo "stub-zone:" >> unbound-host.conf + echo " name: $zone" >> unbound-host.conf + echo " stub-addr: $nameserver@$port" >> unbound-host.conf + echo "" >> unbound-host.conf +done + +echo "server:" >> unbound-host.conf +echo " do-not-query-address: 192.168.0.0/16" >> unbound-host.conf +echo ' trust-anchor-file: "trustedkeys"' >> unbound-host.conf diff --git a/modules/geoipbackend/regression-tests/00dnssec-grabkeys/description b/modules/geoipbackend/regression-tests/00dnssec-grabkeys/description new file mode 100644 index 000000000..431565079 --- /dev/null +++ b/modules/geoipbackend/regression-tests/00dnssec-grabkeys/description @@ -0,0 +1 @@ +Grab DNSKEY records for validation testing. diff --git a/modules/geoipbackend/regression-tests/00dnssec-grabkeys/expected_result b/modules/geoipbackend/regression-tests/00dnssec-grabkeys/expected_result new file mode 100644 index 000000000..e69de29bb diff --git a/modules/geoipbackend/regression-tests/basic-a-dnssec/command b/modules/geoipbackend/regression-tests/basic-a-dnssec/command new file mode 100755 index 000000000..f67a8549b --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-dnssec/command @@ -0,0 +1,3 @@ +#!/bin/sh + +cleandig www.geo.example.com A dnssec diff --git a/modules/geoipbackend/regression-tests/basic-a-dnssec/description b/modules/geoipbackend/regression-tests/basic-a-dnssec/description new file mode 100644 index 000000000..6cd423e24 --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-dnssec/description @@ -0,0 +1 @@ +Basic DNSSEC test diff --git a/modules/geoipbackend/regression-tests/basic-a-dnssec/expected_result b/modules/geoipbackend/regression-tests/basic-a-dnssec/expected_result new file mode 100644 index 000000000..63dad9cc8 --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-dnssec/expected_result @@ -0,0 +1,7 @@ +0 unknown.service.geo.example.com. IN A 30 127.0.0.1 +0 unknown.service.geo.example.com. IN RRSIG 30 A 8 5 30 [expiry] [inception] [keytag] geo.example.com. ... +0 www.geo.example.com. IN CNAME 30 unknown.service.geo.example.com. +0 www.geo.example.com. IN RRSIG 30 CNAME 8 4 30 [expiry] [inception] [keytag] geo.example.com. ... +2 . IN OPT 32768 +Rcode: 0, RD: 0, QR: 1, TC: 0, AA: 1, opcode: 0 +Reply to question for qname='www.geo.example.com.', qtype=A diff --git a/modules/geoipbackend/regression-tests/basic-a-dnssec/skip.nodnssec b/modules/geoipbackend/regression-tests/basic-a-dnssec/skip.nodnssec new file mode 100644 index 000000000..e69de29bb diff --git a/modules/geoipbackend/regression-tests/basic-a-resolution/command b/modules/geoipbackend/regression-tests/basic-a-resolution/command new file mode 100755 index 000000000..c8e28a0c1 --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-resolution/command @@ -0,0 +1,3 @@ +#!/bin/sh +cleandig www.geo.example.com A + diff --git a/modules/geoipbackend/regression-tests/basic-a-resolution/description b/modules/geoipbackend/regression-tests/basic-a-resolution/description new file mode 100644 index 000000000..51497ac48 --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-resolution/description @@ -0,0 +1,2 @@ +This test tries to resolve a straight A record that is directly available in +the database. diff --git a/modules/geoipbackend/regression-tests/basic-a-resolution/expected_result b/modules/geoipbackend/regression-tests/basic-a-resolution/expected_result new file mode 100644 index 000000000..eef51b2f4 --- /dev/null +++ b/modules/geoipbackend/regression-tests/basic-a-resolution/expected_result @@ -0,0 +1,4 @@ +0 unknown.service.geo.example.com. IN A 30 127.0.0.1 +0 www.geo.example.com. IN CNAME 30 unknown.service.geo.example.com. +Rcode: 0, RD: 0, QR: 1, TC: 0, AA: 1, opcode: 0 +Reply to question for qname='www.geo.example.com.', qtype=A diff --git a/modules/geoipbackend/regression-tests/region-a-resolution/command b/modules/geoipbackend/regression-tests/region-a-resolution/command new file mode 100755 index 000000000..432a2e9aa --- /dev/null +++ b/modules/geoipbackend/regression-tests/region-a-resolution/command @@ -0,0 +1,2 @@ +#!/bin/sh +nameserver=$geoipregionip cleandig www.geo.example.com A diff --git a/modules/geoipbackend/regression-tests/region-a-resolution/description b/modules/geoipbackend/regression-tests/region-a-resolution/description new file mode 100644 index 000000000..51497ac48 --- /dev/null +++ b/modules/geoipbackend/regression-tests/region-a-resolution/description @@ -0,0 +1,2 @@ +This test tries to resolve a straight A record that is directly available in +the database. diff --git a/modules/geoipbackend/regression-tests/region-a-resolution/expected_result b/modules/geoipbackend/regression-tests/region-a-resolution/expected_result new file mode 100644 index 000000000..dc3ff19c0 --- /dev/null +++ b/modules/geoipbackend/regression-tests/region-a-resolution/expected_result @@ -0,0 +1,4 @@ +0 oc.service.geo.example.com. IN A 30 62.236.200.4 +0 www.geo.example.com. IN CNAME 30 oc.service.geo.example.com. +Rcode: 0, RD: 0, QR: 1, TC: 0, AA: 1, opcode: 0 +Reply to question for qname='www.geo.example.com.', qtype=A diff --git a/modules/geoipbackend/regression-tests/static-any-resolution/command b/modules/geoipbackend/regression-tests/static-any-resolution/command new file mode 100755 index 000000000..abd0fdf43 --- /dev/null +++ b/modules/geoipbackend/regression-tests/static-any-resolution/command @@ -0,0 +1,3 @@ +#!/bin/sh +cleandig geo.example.com ANY + diff --git a/modules/geoipbackend/regression-tests/static-any-resolution/description b/modules/geoipbackend/regression-tests/static-any-resolution/description new file mode 100644 index 000000000..51497ac48 --- /dev/null +++ b/modules/geoipbackend/regression-tests/static-any-resolution/description @@ -0,0 +1,2 @@ +This test tries to resolve a straight A record that is directly available in +the database. diff --git a/modules/geoipbackend/regression-tests/static-any-resolution/expected_result b/modules/geoipbackend/regression-tests/static-any-resolution/expected_result new file mode 100644 index 000000000..4b35164cf --- /dev/null +++ b/modules/geoipbackend/regression-tests/static-any-resolution/expected_result @@ -0,0 +1,6 @@ +0 geo.example.com. IN MX 30 10 mx.example.com. +0 geo.example.com. IN NS 30 ns1.example.com. +0 geo.example.com. IN NS 30 ns2.example.com. +0 geo.example.com. IN SOA 30 ns1.example.com. hostmaster.example.com. 2014090125 7200 3600 1209600 3600 +Rcode: 0, RD: 0, QR: 1, TC: 0, AA: 1, opcode: 0 +Reply to question for qname='geo.example.com.', qtype=ANY diff --git a/pdns/docs/pdns.xml b/pdns/docs/pdns.xml index a13134aa3..f4ff4ccee 100644 --- a/pdns/docs/pdns.xml +++ b/pdns/docs/pdns.xml @@ -21441,6 +21441,132 @@ VALUES (:zoneid, :ip) modules/geobackend/README, part of the PowerDNS Authoritative Server distribution. + GeoIP backend + + + GeoIP backend capabilities + + + NativeYes + MasterNo + SlaveNo + SuperslaveNo + AutoserialNo + DNSSECYes + + +
+
+ + The GeoIP backend can be used to distribute queries globally using an MaxMind IP-address/country mapping table, currently avaible for debian and ubuntu for free. Other formats + are not yet supported but will be in the future. The only format supported at the moment is country listing. + + + This allows visitors to be sent to a server close to them, with no appreciable delay, as would otherwise be incurred with a protocol level redirect. + Additionally, the Geo Backend can be used to provide service over several clusters, any of which can be taken out of use easily, for example + for maintenance purposes. + + Prerequisites + + To compile the backend, you need libyaml-cpp 0.5 or later and libgeoip. + + + You must have geoip database available. As of writing, on debian/ubuntu systems, you can use apt-get install geoip-database to get one, and the backend is + configured to use the location where these files are installed as source. On other systems you might need to alter the database-file and database-file6 attribute. + If you don't need ipv4 or ipv6 support, set the respective setting to "". Leaving it unset leaves it pointing to default location, preventing the software from + starting up. + + + Configuration Parameters + + These are the configuration file parameters that are available for the GeoIP backend. geoip-zones-files is the only thing you must set, if the defaults suite you. + + + geoip-database-file + + Specifies the full path of the data file for IPv4 to use. + + + + geoip-database-file6 + + Specifies the full path of the data file for IPv6 to use. + + + + geoip-zones-file + + Specifies the full path of the zone configuration file to use. + + + + geoip-dnssec-keydir + + Specifies the full path of a directory that will contain DNSSEC keys. + + + + + + Zonefile format + + Zone configuration file uses YAML syntax. Here is simple example. Note that the ‐ before certain keys is part of the syntax. + +domains: +- domain: geo.example.com + ttl: 30 + records: + geo.example.com: + - soa: ns1.example.com hostmaster.example.com 2014090125 7200 3600 1209600 3600 + - ns: ns1.example.com + - ns: ns2.example.com + - mx: 10 mx.example.com + fin.eu.service.geo.example.com: + - a: 62.236.200.4 + - txt: hello world + services: + service.geo.example.com: '%co.%cn.service.geo.example.com' + + + + + Keys explained + + + domains + + Mandatory root key. All configuration is below this + + + + domain + + Defines a domain. You need ttl, records, services under this. + + + + ttl + + TTL value for all records + + + + records + + Put fully qualified name as subkey, under which you must define at least soa: key. Note that this is an array of records, so ‐ is needed for the values. + + + + services + + Defines one or more services for querying. The format supports following placeholders, %% = %, %co = 3-letter country, %cn = continent, %af = v4 or v6. There are also other specifiers that will only work with suitable database and currently are untested. These are %re = region, %na = Name (such as, organisation), %ci = City. + + + + + +
+ Lua Backend diff --git a/pdns/iputils.hh b/pdns/iputils.hh index 6b15f4b21..9d0aa9375 100644 --- a/pdns/iputils.hh +++ b/pdns/iputils.hh @@ -304,6 +304,14 @@ public: { return d_bits; } + bool isIpv6() const + { + return d_network.sin6.sin6_family == AF_INET6; + } + bool isIpv4() const + { + return d_network.sin4.sin_family == AF_INET; + } private: ComboAddress d_network; uint32_t d_mask; diff --git a/regression-tests/backends/common b/regression-tests/backends/common index d5d8bf930..6beec129a 100644 --- a/regression-tests/backends/common +++ b/regression-tests/backends/common @@ -4,7 +4,9 @@ start_master () bind*) source ./backends/bind-master ;; - + geoip*) + source ./backends/geoip-master + ;; gmysql*) source ./backends/gmysql-master ;; diff --git a/regression-tests/backends/geoip-master b/regression-tests/backends/geoip-master new file mode 100644 index 000000000..703a52095 --- /dev/null +++ b/regression-tests/backends/geoip-master @@ -0,0 +1,77 @@ +case $context in + geoipbackend|geoipbackend-nsec3-narrow) + set +e + if test "x$geoipregion" = "x"; then + echo "This test suite requires that you provide geoipregion which defines the region name produced by MaxMind database with geoipregionip address." + exit 1 + fi + if test "x$geoipregionip" = "x"; then + echo "This test suite requires that you have IP address bound to localhost interface and exported as variable geoipregionip" + exit 1 + fi + ping -q -c1 -W1 -t1 -Ilo $geoipregionip 2>&1 >/dev/null + if test $? -ne 0; then + echo "This test suite requires that you have $geoipregionip bound to localhost interface" + exit 1 + fi + set -e + testsdir=../modules/geoipbackend/regression-tests/ + if test "$context" = "geoipbackend-nsec3-narrow"; then + narrow="narrow" + extracontexts="dnssec nsec3 narrow" + skipreasons="narrow nsec3 nodyndns" + geoipdosec="yes" + geoipkeydir="geoip-dnssec-keydir=$testsdir/geosec" + rm -rf $testsdir/geosec + mkdir -p $testsdir/geosec + else + skipreasons="nonarrow nonsec3 nodyndns nodnssec" + fi + cat > $testsdir/geo.yaml < $testsdir/region-a-resolution/expected_result < pdns-geoip.conf <> pdns-geoip.conf + ../pdns/pdnssec --config-dir=. --config-name=geoip secure-zone geo.example.com + geoipkeydir="--geoip-dnssec-keydir=$testsdir/geosec" + fi + + $RUNWRAPPER $PDNS --daemon=no --local-port=$port --socket-dir=./ \ + --no-shuffle --launch=geoip \ + --cache-ttl=$cachettl --experimental-dname-processing --no-config \ + --send-root-referral --distributor-threads=1 \ + --geoip-zones-file=$testsdir/geo.yaml \ + $geoipkeydir & + ;; + + *) + nocontext=yes +esac diff --git a/regression-tests/start-test-stop b/regression-tests/start-test-stop index 6b4f2f30a..0c3009227 100755 --- a/regression-tests/start-test-stop +++ b/regression-tests/start-test-stop @@ -180,6 +180,7 @@ Usage: ./start-test-stop [] [wait|nowait] [] [