]> granicus.if.org Git - pdns/commitdiff
auth: test GeoIP related features of LUA Records
authorCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Tue, 17 Apr 2018 05:32:44 +0000 (07:32 +0200)
committerCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Tue, 17 Apr 2018 05:32:44 +0000 (07:32 +0200)
modules/geoipbackend/regression-tests/GeoLiteCity.mmdb.b64
modules/geoipbackend/regression-tests/write-mmdb.pl
regression-tests.auth-py/authtests.py
regression-tests.auth-py/clientsubnetoption.py [new file with mode: 0644]
regression-tests.auth-py/runtests
regression-tests.auth-py/test_LuaRecords.py

index bee561f26fb273eab5d31b5ca2b5d2cb2c48d000..60da132432bbe51df905a9c82beb2a822d18978c 100644 (file)
@@ -13,21 +13,23 @@ AACgAABbAAAAoAAAXAAAAKAAAF0AAACgAABeAAAAoAAAXwAAAKAAAGAAAACgAABhAAAAoAAAYgAA
 AIEAAGMAAACgAABkAAAAoAAAZQAAAKAAAGYAAACgAABnAAAAoAAAoAAAAGgAAGkAAACgAABqAAAA
 oAAAawAAAKAAAGwAAACgAABtAAAAoAAAbgAAAKAAAG8AAAB4AACgAAAAcAAAcQAAAKAAAHIAAACg
 AABzAAAAoAAAdAAAAKAAAHUAAACgAAB2AAAAoAAAdwAAAKAAAKAAAACwAAB5AAAAoAAAegAAAKAA
-AHsAAACgAAB8AAAAoAAAfQAAAKAAAH4AAACgAAB/AAAAoAAAoAAAAIAAAKAAAAFsAACgAAAAggAA
+AHsAAACgAAB8AAAAoAAAfQAAAKAAAH4AAACgAAB/AAAAoAAAoAAAAIAAAKAAAAG1AACgAAAAggAA
 oAAAAIMAAKAAAACEAACgAAAAhQAAoAAAAIYAAKAAAACHAACIAAAAoAAAiQAAAKAAAIoAAACgAACL
 AAAAoAAAjAAAAKAAAI0AAACgAACOAAAAoAAAjwAAAKAAAJAAAACgAACRAAAAoAAAkgAAAKAAAJMA
 AACgAACUAAAAoAAAlQAAAKAAAJYAAACgAACXAAAAoAAAmAAAAKAAAJkAAACgAACaAAAAoAAAmwAA
-AKAAAJwAAACgAACdAAAAoAAAngAAAJ8AAKAAAAHmAAIwAAACbwAAAAAAAAAAAAAAAAAAAADkRGNp
-dHniSmdlb25hbWVfaWTDINUmRW5hbWVz4UJlbkhSZXNlYXJjaEljb250aW5lbnTjRGNvZGVCT0Mg
-B8Nfci8gFuFCZW5HT2NlYW5pYUdjb3VudHJ54yAHwx+zEEhpc29fY29kZUJBVSAW4UJlbklBdXN0
-cmFsaWFIbG9jYXRpb27jT2FjY3VyYWN5X3JhZGl1c6EBSGxhdGl0dWRlaD/wAAAAAAAASWxvbmdp
-dHVkZWg/8AAAAAAAAOQgAeIgB8NYkRIgFuFCZW5ITXVraWx0ZW8gKeMgNEJOQSAHw19yLSAW4UJl
-bk1Ob3J0aCBBbWVyaWNhIFDjIAfDX2XhIF9CVVMgFuFCZW5NVW5pdGVkIFN0YXRlcyB74yCFoQEg
-l2hAR/TdLxqfviCpaMBek3gDRtxd4yAB4iAHwQEgFuFCZW5CQzEgUOMgB8EBIF9CTzEgFuFCZW5D
-TyAxTHN1YmRpdmlzaW9ucwEE4yAHwQEgX0JMMSAW4UJlbkNMIDHjIAHiIAfBAiAW4UJlbkJDMiBQ
-4yAHwQIgX0JPMSAW4UJlbkNPIDIhXQEE4yAHwQIgX0JMMiAW4UJlbkNMIDLjIAHiIAfBAyAW4UJl
-bkJDMyBQ4yAHwQMgX0JPMSAW4UJlbkNPIDMhXQEE4yAHwQMgX0JMMyAW4UJlbkNMIDOrze9NYXhN
-aW5kLmNvbelbYmluYXJ5X2Zvcm1hdF9tYWpvcl92ZXJzaW9uoQJbYmluYXJ5X2Zvcm1hdF9taW5v
-cl92ZXJzaW9uoEtidWlsZF9lcG9jaAQCWk1MMk1kYXRhYmFzZV90eXBlTEdlb0NpdHktTGl0ZUtk
-ZXNjcmlwdGlvbuFCZW5TTW9jayBnZW9pcCBkYXRhYmFzZUppcF92ZXJzaW9uoQZJbGFuZ3VhZ2Vz
-AQRCZW5Kbm9kZV9jb3VudMGgS3JlY29yZF9zaXploRw=
+AKAAAJwAAACgAACdAAAAoAAAngAAAJ8AAKAAAAJEAAKOAAACzQAAAAAAAAAAAAAAAAAAAADmWGF1
+dG9ub21vdXNfc3lzdGVtX251bWJlcsIQkl0BYXV0b25vbW91c19zeXN0ZW1fb3JnYW5pemF0aW9u
+TFRlc3QgVGVsZWtvbURjaXR54kpnZW9uYW1lX2lkwyDVJkVuYW1lc+FCZW5IUmVzZWFyY2hJY29u
+dGluZW5040Rjb2RlQk9DIFDDX3IvIF/hQmVuR09jZWFuaWFHY291bnRyeeMgUMMfsxBIaXNvX2Nv
+ZGVCQVUgX+FCZW5JQXVzdHJhbGlhSGxvY2F0aW9u409hY2N1cmFjeV9yYWRpdXOhAUhsYXRpdHVk
+ZWg/8AAAAAAAAElsb25naXR1ZGVoP/AAAAAAAADmIAHCDPggHU1UZXN0IE5ldHdvcmtzIEriIFDD
+WJESIF/hQmVuSE11a2lsdGVvIHLjIH1CTkEgUMNfci0gX+FCZW5NTm9ydGggQW1lcmljYSCZ4yBQ
+w19l4SCoQlVTIF/hQmVuTVVuaXRlZCBTdGF0ZXMgxOMgzqEBIOBoQEf03S8an74g8mjAXpN4A0bc
+XeMgSuIgUMEBIF/hQmVuQkMxIJnjIFDBASCoQk8xIF/hQmVuQ08gMUxzdWJkaXZpc2lvbnMBBOMg
+UMEBIKhCTDEgX+FCZW5DTCAx4yBK4iBQwQIgX+FCZW5CQzIgmeMgUMECIKhCTzEgX+FCZW5DTyAy
+IbsBBOMgUMECIKhCTDIgX+FCZW5DTCAy4yBK4iBQwQMgX+FCZW5CQzMgmeMgUMEDIKhCTzEgX+FC
+ZW5DTyAzIbsBBOMgUMEDIKhCTDMgX+FCZW5DTCAzq83vTWF4TWluZC5jb23pW2JpbmFyeV9mb3Jt
+YXRfbWFqb3JfdmVyc2lvbqECW2JpbmFyeV9mb3JtYXRfbWlub3JfdmVyc2lvbqBLYnVpbGRfZXBv
+Y2gEAlrVhPpNZGF0YWJhc2VfdHlwZUxHZW9DaXR5LUxpdGVLZGVzY3JpcHRpb27hQmVuU01vY2sg
+Z2VvaXAgZGF0YWJhc2VKaXBfdmVyc2lvbqEGSWxhbmd1YWdlcwEEQmVuSm5vZGVfY291bnTBoEty
+ZWNvcmRfc2l6ZaEc
index 9a7d1c1c66917b725036c7135e7205aa51a1fd3c..0b90fbedc6c5983ab42bc755d630b50343c4d530 100644 (file)
@@ -1,20 +1,22 @@
 use MaxMind::DB::Writer::Tree;
 
 my %types = (
-        city          => 'map',
-        names         => 'map',
-        en            => 'utf8_string',
-        geoname_id    => 'uint32',
-        location      => 'map',
-        latitude      => 'double',
-        longitude     => 'double',
-        accuracy_radius => 'uint16',
-        continent     => 'map',
-        country       => 'map',
-        code          => 'utf8_string',
-        iso_code      => 'utf8_string',
-        subdivisions  => ['array', 'map'],
-);
+    city                             => 'map',
+    names                            => 'map',
+    en                               => 'utf8_string',
+    geoname_id                       => 'uint32',
+    location                         => 'map',
+    latitude                         => 'double',
+    longitude                        => 'double',
+    accuracy_radius                  => 'uint16',
+    continent                        => 'map',
+    country                          => 'map',
+    code                             => 'utf8_string',
+    iso_code                         => 'utf8_string',
+    subdivisions                     => ['array', 'map'],
+    autonomous_system_number         => 'uint32',
+    autonomous_system_organization   => 'utf8_string',
+    );
 
 my $tree = MaxMind::DB::Writer::Tree->new(
     ip_version            => 6,
@@ -33,6 +35,8 @@ $tree->insert_network(
       'continent' => { "code" => "OC", "geoname_id" => 6255151, "names" => { "en" => "Oceania" } },
       'country' => { "geoname_id" => 2077456, "iso_code" => "AU", "names" => { "en" => "Australia" } },
       'location' => { "latitude" => 1.0, "longitude" => 1.0, accuracy_radius => 1 },
+      'autonomous_system_number' => 4242,
+      'autonomous_system_organization' => "Test Telekom",
     }
 );
 
@@ -43,6 +47,8 @@ $tree->insert_network(
       'continent' => { "code" => "NA", "geoname_id" => 6255149, "names" => { "en" => "North America" } },
       'country' => { "geoname_id" => 6252001, "iso_code" => "US", "names" => { "en" => "United States" } },
       'location' => { "latitude" => 47.913000, "longitude" => -122.304200, accuracy_radius => 1 },
+      'autonomous_system_number' => 3320,
+      'autonomous_system_organization' => "Test Networks",
     }
 );
 
index 9e55a32ceda2a2ea87fb97e4f0c6a9c8a83b903d..6594c46db34f399cc7e450ca0a1422c2bb57fe35 100644 (file)
@@ -106,8 +106,8 @@ query-cache-ttl=0
 log-dns-queries=yes
 log-dns-details=yes
 loglevel=9
-geoip-zones-file=../modules/geoipbackend/regression-tests/geo.yaml
-geoip-database-files=../modules/geoipbackend/regression-tests/GeoLiteCity.dat
+geoip-database-files=../modules/geoipbackend/regression-tests/GeoLiteCity.mmdb
+edns-subnet-processing=yes
 distributor-threads=1""".format(confdir=confdir,
                                 bind_dnssec_db=bind_dnssec_db))
 
diff --git a/regression-tests.auth-py/clientsubnetoption.py b/regression-tests.auth-py/clientsubnetoption.py
new file mode 100644 (file)
index 0000000..c4f78f2
--- /dev/null
@@ -0,0 +1,301 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2012 OpenDNS, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#    * Redistributions of source code must retain the above copyright
+#      notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#    * Neither the name of the OpenDNS nor the names of its contributors may be
+#      used to endorse or promote products derived from this software without
+#      specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL OPENDNS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+""" Class to implement draft-ietf-dnsop-edns-client-subnet (previously known as
+draft-vandergaast-edns-client-subnet.
+
+The contained class supports both IPv4 and IPv6 addresses.
+Requirements:
+  dnspython (http://www.dnspython.org/)
+"""
+from __future__ import print_function
+from __future__ import division
+
+import socket
+import struct
+import dns
+import dns.edns
+import dns.flags
+import dns.message
+import dns.query
+
+__author__ = "bhartvigsen@opendns.com (Brian Hartvigsen)"
+__version__ = "2.0.0"
+
+ASSIGNED_OPTION_CODE = 0x0008
+DRAFT_OPTION_CODE = 0x50FA
+
+FAMILY_IPV4 = 1
+FAMILY_IPV6 = 2
+SUPPORTED_FAMILIES = (FAMILY_IPV4, FAMILY_IPV6)
+
+
+class ClientSubnetOption(dns.edns.Option):
+    """Implementation of draft-vandergaast-edns-client-subnet-01.
+
+    Attributes:
+        family: An integer indicating which address family is being sent
+        ip: IP address in integer notation
+        mask: An integer representing the number of relevant bits being sent
+        scope: An integer representing the number of significant bits used by
+            the authoritative server.
+    """
+
+    def __init__(self, ip, bits=24, scope=0, option=ASSIGNED_OPTION_CODE):
+        super(ClientSubnetOption, self).__init__(option)
+
+        n = None
+        f = None
+
+        for family in (socket.AF_INET, socket.AF_INET6):
+            try:
+                n = socket.inet_pton(family, ip)
+                if family == socket.AF_INET6:
+                    f = FAMILY_IPV6
+                    hi, lo = struct.unpack('!QQ', n)
+                    ip = hi << 64 | lo
+                elif family == socket.AF_INET:
+                    f = FAMILY_IPV4
+                    ip = struct.unpack('!L', n)[0]
+            except Exception:
+                pass
+
+        if n is None:
+            raise Exception("%s is an invalid ip" % ip)
+
+        self.family = f
+        self.ip = ip
+        self.mask = bits
+        self.scope = scope
+        self.option = option
+
+        if self.family == FAMILY_IPV4 and self.mask > 32:
+            raise Exception("32 bits is the max for IPv4 (%d)" % bits)
+        if self.family == FAMILY_IPV6 and self.mask > 128:
+            raise Exception("128 bits is the max for IPv6 (%d)" % bits)
+
+    def calculate_ip(self):
+        """Calculates the relevant ip address based on the network mask.
+
+        Calculates the relevant bits of the IP address based on network mask.
+        Sizes up to the nearest octet for use with wire format.
+
+        Returns:
+            An integer of only the significant bits sized up to the nearest
+            octect.
+        """
+
+        if self.family == FAMILY_IPV4:
+            bits = 32
+        elif self.family == FAMILY_IPV6:
+            bits = 128
+
+        ip = self.ip >> bits - self.mask
+
+        if (self.mask % 8 != 0):
+            ip = ip << 8 - (self.mask % 8)
+
+        return ip
+
+    def is_draft(self):
+        """" Determines whether this instance is using the draft option code """
+        return self.option == DRAFT_OPTION_CODE
+
+    def to_wire(self, file):
+        """Create EDNS packet as defined in draft-vandergaast-edns-client-subnet-01."""
+
+        ip = self.calculate_ip()
+
+        mask_bits = self.mask
+        if mask_bits % 8 != 0:
+                mask_bits += 8 - (self.mask % 8)
+
+        if self.family == FAMILY_IPV4:
+            test = struct.pack("!L", ip)
+        elif self.family == FAMILY_IPV6:
+            test = struct.pack("!QQ", ip >> 64, ip & (2 ** 64 - 1))
+        test = test[-(mask_bits // 8):]
+
+        format = "!HBB%ds" % (mask_bits // 8)
+        data = struct.pack(format, self.family, self.mask, self.scope, test)
+        file.write(data)
+
+    def from_wire(cls, otype, wire, current, olen):
+        """Read EDNS packet as defined in draft-vandergaast-edns-client-subnet-01.
+
+        Returns:
+            An instance of ClientSubnetOption based on the ENDS packet
+        """
+
+        data = wire[current:current + olen]
+        (family, mask, scope) = struct.unpack("!HBB", data[:4])
+
+        c_mask = mask
+        if mask % 8 != 0:
+            c_mask += 8 - (mask % 8)
+
+        ip = struct.unpack_from("!%ds" % (c_mask // 8), data, 4)[0]
+
+        if (family == FAMILY_IPV4):
+            ip = ip + b'\0' * ((32 - c_mask) // 8)
+            ip = socket.inet_ntop(socket.AF_INET, ip)
+        elif (family == FAMILY_IPV6):
+            ip = ip + b'\0' * ((128 - c_mask) // 8)
+            ip = socket.inet_ntop(socket.AF_INET6, ip)
+        else:
+            raise Exception("Returned a family other then IPv4 or IPv6")
+
+        return cls(ip, mask, scope, otype)
+
+    from_wire = classmethod(from_wire)
+
+    def __repr__(self):
+        if self.family == FAMILY_IPV4:
+            ip = socket.inet_ntop(socket.AF_INET, struct.pack('!L', self.ip))
+        elif self.family == FAMILY_IPV6:
+            ip = socket.inet_ntop(socket.AF_INET6,
+                                  struct.pack('!QQ',
+                                              self.ip >> 64,
+                                              self.ip & (2 ** 64 - 1)))
+
+        return "%s(%s, %s, %s)" % (
+            self.__class__.__name__,
+            ip,
+            self.mask,
+            self.scope
+        )
+
+    def __eq__(self, other):
+        """Rich comparison method for equality.
+
+        Two ClientSubnetOptions are equal if their relevant ip bits, mask, and
+        family are identical. We ignore scope since generally we want to
+        compare questions to responses and that bit is only relevant when
+        determining caching behavior.
+
+        Returns:
+            boolean
+        """
+
+        if not isinstance(other, ClientSubnetOption):
+            return False
+        if self.calculate_ip() != other.calculate_ip():
+            return False
+        if self.mask != other.mask:
+            return False
+        if self.family != other.family:
+            return False
+        return True
+
+    def __ne__(self, other):
+        """Rich comparison method for inequality.
+
+        See notes for __eq__()
+
+        Returns:
+            boolean
+        """
+        return not self.__eq__(other)
+
+
+dns.edns._type_to_class[DRAFT_OPTION_CODE] = ClientSubnetOption
+dns.edns._type_to_class[ASSIGNED_OPTION_CODE] = ClientSubnetOption
+
+if __name__ == "__main__":
+    import argparse
+    import sys
+
+    def CheckForClientSubnetOption(addr, args, option_code=ASSIGNED_OPTION_CODE):
+        print("Testing for edns-clientsubnet using option code", hex(option_code), file=sys.stderr)
+        cso = ClientSubnetOption(args.subnet, args.mask, option=option_code)
+        message = dns.message.make_query(args.rr, args.type)
+        # Tested authoritative servers seem to use the last code in cases
+        # where they support both. We make the official code last to allow
+        # us to check for support of both draft and official
+        message.use_edns(options=[cso])
+
+        try:
+            r = dns.query.udp(message, addr, timeout=args.timeout)
+            if r.flags & dns.flags.TC:
+                r = dns.query.tcp(message, addr, timeout=args.timeout)
+        except dns.exception.Timeout:
+            print("Timeout: No answer received from %s\n" % args.nameserver, file=sys.stderr)
+            sys.exit(3)
+
+        error = False
+        found = False
+        for options in r.options:
+            # Have not run into anyone who passes back both codes yet
+            # but just in case, we want to check all possible options
+            if isinstance(options, ClientSubnetOption):
+                found = True
+                print("Found ClientSubnetOption...", end=None, file=sys.stderr)
+                if not cso.family == options.family:
+                    error = True
+                    print("\nFailed: returned family (%d) is different from the passed family (%d)" % (options.family, cso.family), file=sys.stderr)
+                if not cso.calculate_ip() == options.calculate_ip():
+                    error = True
+                    print("\nFailed: returned ip (%s) is different from the passed ip (%s)." % (options.calculate_ip(), cso.calculate_ip()), file=sys.stderr)
+                if not options.mask == cso.mask:
+                    error = True
+                    print("\nFailed: returned mask bits (%d) is different from the passed mask bits (%d)" % (options.mask, cso.mask), file=sys.stderr)
+                if not options.scope != 0:
+                    print("\nWarning: scope indicates edns-clientsubnet data is not used", file=sys.stderr)
+                if options.is_draft():
+                    print("\nWarning: detected support for edns-clientsubnet draft code", file=sys.stderr)
+
+        if found and not error:
+            print("Success", file=sys.stderr)
+        elif found:
+            print("Failed: See error messages above", file=sys.stderr)
+        else:
+            print("Failed: No ClientSubnetOption returned", file=sys.stderr)
+
+    parser = argparse.ArgumentParser(description='draft-vandergaast-edns-client-subnet-01 tester')
+    parser.add_argument('nameserver', help='The nameserver to test')
+    parser.add_argument('rr', help='DNS record that should return an EDNS enabled response')
+    parser.add_argument('-s', '--subnet', help='Specifies an IP to pass as the client subnet.', default='192.0.2.0')
+    parser.add_argument('-m', '--mask', type=int, help='CIDR mask to use for subnet')
+    parser.add_argument('--timeout', type=int, help='Set the timeout for query to TIMEOUT seconds, default=10', default=10)
+    parser.add_argument('-t', '--type', help='DNS query type, default=A', default='A')
+    args = parser.parse_args()
+
+    if not args.mask:
+        if ':' in args.subnet:
+            args.mask = 48
+        else:
+            args.mask = 24
+
+    try:
+        addr = socket.gethostbyname(args.nameserver)
+    except socket.gaierror:
+        print("Unable to resolve %s\n" % args.nameserver, file=sys.stderr)
+        sys.exit(3)
+
+    CheckForClientSubnetOption(addr, args, DRAFT_OPTION_CODE)
+    print("", file=sys.stderr)
+    CheckForClientSubnetOption(addr, args, ASSIGNED_OPTION_CODE)
index 6c3fb67d4b8e1c2c994d4044be18f56b6008b2bc..a93132b11885bef5c7cadee253b8a789651c4db8 100755 (executable)
@@ -22,6 +22,11 @@ export RECCONTROL=${RECCONTROL:-${PWD}/../pdns/recursordist/rec_control}
 
 export PREFIX=127.0.0
 
+readonly GEOIP_TESTS_DIR=../modules/geoipbackend/regression-tests
+if [ ! -f ${GEOIP_TESTS_DIR}/GeoLiteCity.mmdb ] ; then
+    base64 -d ${GEOIP_TESTS_DIR}/GeoLiteCity.mmdb.b64 > ${GEOIP_TESTS_DIR}/GeoLiteCity.mmdb
+fi
+
 for bin in "$PDNS" "$PDNSUTIL" "$PDNSRECURSOR" "$RECCONTROL"; do
     if [ -n "$bin" -a ! -e "$bin" ]; then
         echo "E: Required binary $bin not found. Please install the binary and/or edit ./vars."
index e9ffdb7017892b361af69fda43197003dc240137..d93ec21cd378b41d815fa99ca16b424b94a6558e 100644 (file)
@@ -4,6 +4,7 @@ import requests
 import threading
 import dns
 import time
+import clientsubnetoption
 
 from authtests import AuthTest
 
@@ -52,13 +53,13 @@ class TestLuaRecords(AuthTest):
     * [x] test latlon()
     * [x] test latlonloc()
     * [x] test netmask()
+    * [x] test country()
+    * [x] test continent()
 
-    * [ ] test pickclosest()
-    * [ ] test country()
-    * [ ] test continent()
-    * [ ] test closestMagic()
+    * [x] test pickclosest()
+    * [x] test closestMagic()
     * [x] test view()
-    * [ ] test asnum()
+    * [x] test asnum()
     * [x] rename pickwhashed() and pickwrandom() ?
     * [x] unify pickrandom() pickwhashed() and pickwrandom() parameters (ComboAddress vs string)
     * [x] make lua errors SERVFAIL
@@ -86,7 +87,7 @@ whashed.example.org.         3600 IN LUA  A     "pickwhashed({{ {{15, '1.2.3.4'}
 rand.example.org.            3600 IN LUA  A     "pickrandom({{'{prefix}.101', '{prefix}.102'}})"
 v6-bogus.rand.example.org.   3600 IN LUA  AAAA  "pickrandom({{'{prefix}.101', '{prefix}.102'}})"
 v6.rand.example.org.         3600 IN LUA  AAAA  "pickrandom({{'2001:db8:a0b:12f0::1', 'fe80::2a1:9bff:fe9b:f268'}})"
-closest                      3600 IN LUA  A     "pickclosest({{'192.0.2.1','192.0.2.2','{prefix}.102', '198.51.100.1'}})"
+closest.geo                  3600 IN LUA  A     "pickclosest({{'1.1.1.2','1.2.3.4'}})"
 empty.rand.example.org.      3600 IN LUA  A     "pickrandom()"
 wrand.example.org.           3600 IN LUA  A     "pickwrandom({{ {{30, '{prefix}.102'}}, {{15, '{prefix}.103'}} }})"
 
@@ -111,6 +112,9 @@ eu-west      IN    LUA    A   ( ";include('config')                         "
 nl           IN    LUA    A   ( ";include('config')                                "
                                 "return ifportup(8081, NLips) ")
 latlon.geo      IN LUA    TXT "latlon()"
+continent.geo   IN LUA    TXT ";if(continent('NA')) then return 'true' else return 'false' end"
+asnum.geo       IN LUA    TXT ";if(asnum('4242')) then return 'true' else return 'false' end"
+country.geo     IN LUA    TXT ";if(country('US')) then return 'true' else return 'false' end"
 latlonloc.geo   IN LUA    TXT "latlonloc()"
 
 true.netmask     IN LUA   TXT   ( ";if(netmask({{ '{prefix}.0/24' }})) "
@@ -133,6 +137,8 @@ none.view        IN    LUA    A          ("view({{
                                           "{{ {{'192.168.0.0/16'}}, {{'192.168.1.54'}}}},"
                                           "{{ {{'1.2.0.0/16'}}, {{'1.2.3.4'}}}},         "
                                           " }})                                          " )
+*.magic          IN    LUA    A     "closestMagic()"
+www-balanced     IN           CNAME 1-1-1-3.17-1-2-4.1-2-3-5.magic.example.org.
         """,
     }
     _web_rrsets = []
@@ -225,21 +231,6 @@ none.view        IN    LUA    A          ("view({{
         self.assertRcodeEqual(res, dns.rcode.NOERROR)
         self.assertAnyRRsetInAnswer(res, expected)
 
-    @unittest.skip
-    def testClosest(self):
-        """
-        Basic pickClosest() test with a set of A records
-        """
-        expected = [dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A',
-                                        '{prefix}.103'.format(prefix=self._PREFIX)),
-                    dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A',
-                                        '{prefix}.102'.format(prefix=self._PREFIX))]
-        query = dns.message.make_query('closest.example.org', 'A')
-
-        res = self.sendUDPQuery(query)
-        self.assertRcodeEqual(res, dns.rcode.NOERROR)
-        self.assertAnyRRsetInAnswer(res, expected)
-
     def testIfportup(self):
         """
         Basic ifportup() test
@@ -357,10 +348,12 @@ none.view        IN    LUA    A          ("view({{
         """
         Basic latlon() test
         """
-        expected = dns.rrset.from_text('latlon.geo.example.org.', 0,
+        name = 'latlon.geo.example.org.'
+        ecso = clientsubnetoption.ClientSubnetOption('1.2.3.0', 24)
+        query = dns.message.make_query(name, 'TXT', 'IN', use_edns=True, payload=4096, options=[ecso])
+        expected = dns.rrset.from_text(name, 0,
                                        dns.rdataclass.IN, 'TXT',
-                                       '"0.000000 0.000000"')
-        query = dns.message.make_query('latlon.geo.example.org', 'TXT')
+                                       '"47.913000 -122.304200"')
 
         res = self.sendUDPQuery(query)
         self.assertRcodeEqual(res, dns.rcode.NOERROR)
@@ -370,15 +363,117 @@ none.view        IN    LUA    A          ("view({{
         """
         Basic latlonloc() test
         """
-        expected = dns.rrset.from_text('latlonloc.geo.example.org.', 0,
-                                       dns.rdataclass.IN, 'TXT',
-                                       '"0 0 -0 S 0 0 -0 W 0.00m 1.00m 10000.00m 10.00m"')
-        query = dns.message.make_query('latlonloc.geo.example.org', 'TXT')
+        name = 'latlonloc.geo.example.org.'
+        expected = dns.rrset.from_text(name, 0,dns.rdataclass.IN, 'TXT',
+                                       '"47 54 46.8 N 122 18 15.12 W 0.00m 1.00m 10000.00m 10.00m"')
+        ecso = clientsubnetoption.ClientSubnetOption('1.2.3.0', 24)
+        query = dns.message.make_query(name, 'TXT', 'IN', use_edns=True, payload=4096, options=[ecso])
 
         res = self.sendUDPQuery(query)
         self.assertRcodeEqual(res, dns.rcode.NOERROR)
         self.assertRRsetInAnswer(res, expected)
 
+    def testClosestMagic(self):
+        """
+        Basic closestMagic() test
+        """
+        name = 'www-balanced.example.org.'
+        cname = '1-1-1-3.17-1-2-4.1-2-3-5.magic.example.org.'
+        queries = [
+            ('1.1.1.0', 24,  '1.1.1.3'),
+            ('1.2.3.0', 24,  '1.2.3.5'),
+            ('17.1.0.0', 16, '17.1.2.4')
+        ]
+                
+        for (subnet, mask, ip) in queries:
+            ecso = clientsubnetoption.ClientSubnetOption(subnet, mask)
+            query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[ecso])
+
+            response = dns.message.make_response(query)
+
+            response.answer.append(dns.rrset.from_text(name, 0, dns.rdataclass.IN, dns.rdatatype.CNAME, cname))
+            response.answer.append(dns.rrset.from_text(cname, 0, dns.rdataclass.IN, 'A', ip))
+
+            res = self.sendUDPQuery(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertEqual(res.answer, response.answer)
+
+    def testAsnum(self):
+        """
+        Basic asnum() test
+        """
+        queries = [
+            ('1.1.1.0', 24,  '"true"'),
+            ('1.2.3.0', 24,  '"false"'),
+            ('17.1.0.0', 16, '"false"')
+        ]
+        name = 'asnum.geo.example.org.'
+        for (subnet, mask, txt) in queries:
+            ecso = clientsubnetoption.ClientSubnetOption(subnet, mask)
+            query = dns.message.make_query(name, 'TXT', 'IN', use_edns=True, payload=4096, options=[ecso])
+            expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'TXT', txt)
+
+            res = self.sendUDPQuery(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testCountry(self):
+        """
+        Basic country() test
+        """
+        queries = [
+            ('1.1.1.0', 24,  '"false"'),
+            ('1.2.3.0', 24,  '"true"'),
+            ('17.1.0.0', 16, '"false"')
+        ]
+        name = 'country.geo.example.org.'
+        for (subnet, mask, txt) in queries:
+            ecso = clientsubnetoption.ClientSubnetOption(subnet, mask)
+            query = dns.message.make_query(name, 'TXT', 'IN', use_edns=True, payload=4096, options=[ecso])
+            expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'TXT', txt)
+
+            res = self.sendUDPQuery(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testContinent(self):
+        """
+        Basic continent() test
+        """
+        queries = [
+            ('1.1.1.0', 24,  '"false"'),
+            ('1.2.3.0', 24,  '"true"'),
+            ('17.1.0.0', 16, '"false"')
+        ]
+        name = 'continent.geo.example.org.'
+        for (subnet, mask, txt) in queries:
+            ecso = clientsubnetoption.ClientSubnetOption(subnet, mask)
+            query = dns.message.make_query(name, 'TXT', 'IN', use_edns=True, payload=4096, options=[ecso])
+            expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'TXT', txt)
+
+            res = self.sendUDPQuery(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testClosest(self):
+        """
+        Basic pickclosest() test
+        """
+        queries = [
+            ('1.1.1.0', 24,  '1.1.1.2'),
+            ('1.2.3.0', 24,  '1.2.3.4'),
+            ('17.1.0.0', 16, '1.1.1.2')
+        ]
+        name = 'closest.geo.example.org.'
+        for (subnet, mask, ip) in queries:
+            ecso = clientsubnetoption.ClientSubnetOption(subnet, mask)
+            query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[ecso])
+            expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', ip)
+
+            res = self.sendUDPQuery(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
     def testNetmask(self):
         """
         Basic netmask() test