+++ /dev/null
-diff --git a/NEWS b/NEWS
-index 8d8bdd9..4d8c01d 100644
---- a/NEWS
-+++ b/NEWS
-@@ -1,6 +1,19 @@
- This file describes user-visible changes in rbldnsd.
- Newer news is at the top.
-
-+Next release
-+
-+ - fix tests for systems without ipv6 support, or when ipv6 is
-+ disabled in rbldnsd at compile-time
-+
-+ - fix tests for API change in pydns >= 2.3.6
-+
-+ - It is no longer an error to request binding to a particular
-+ address/port more than once. (The subsequent requests are simply
-+ ignored.) (This avoids confusion on certain systems/configurations
-+ where gethostbyname("localhost") can return 127.0.0.1 multiple
-+ times.)
-+
- 0.997a (23 Jul 2013)
-
- - minor fixes/changes in packaging, no code changes.
-diff --git a/rbldnsd.c b/rbldnsd.c
-index abf1d01..8322bdd 100644
---- a/rbldnsd.c
-+++ b/rbldnsd.c
-@@ -203,10 +203,79 @@ static volatile int signalled;
- #define SIGNALLED_ZSTATS 0x10
- #define SIGNALLED_TERM 0x20
-
-+static inline int sockaddr_in_equal(const struct sockaddr_in *addr1,
-+ const struct sockaddr_in *addr2)
-+{
-+ return (addr1->sin_port == addr2->sin_port
-+ && addr1->sin_addr.s_addr == addr2->sin_addr.s_addr);
-+}
-+
-+#ifndef NO_IPv6
-+static inline int sockaddr_in6_equal(const struct sockaddr_in6 *addr1,
-+ const struct sockaddr_in6 *addr2)
-+{
-+ if (memcmp(addr1->sin6_addr.s6_addr, addr2->sin6_addr.s6_addr, 16) != 0)
-+ return 0;
-+ return (addr1->sin6_port == addr2->sin6_port
-+ && addr1->sin6_flowinfo == addr2->sin6_flowinfo
-+ && addr1->sin6_scope_id == addr2->sin6_scope_id);
-+}
-+#endif
-+
-+static inline int sockaddr_equal(const struct sockaddr *addr1,
-+ const struct sockaddr *addr2)
-+{
-+ if (addr1->sa_family != addr2->sa_family)
-+ return 0;
-+ switch (addr1->sa_family) {
-+ case AF_INET:
-+ return sockaddr_in_equal((const struct sockaddr_in *)addr1,
-+ (const struct sockaddr_in *)addr2);
-+#ifndef NO_IPv6
-+ return sockaddr_in6_equal((const struct sockaddr_in6 *)addr1,
-+ (const struct sockaddr_in6 *)addr2);
-+#endif
-+ default:
-+ error(0, "unknown address family (%d)", addr1->sa_family);
-+ }
-+}
-+
-+/* already_bound(addr, addrlen)
-+ *
-+ * Determine whether we've already bound to a particular address.
-+ * This is here mostly to deal with the fact that on certain systems,
-+ * gethostbyname()/getaddrinfo() can return a duplicate 127.0.0.1
-+ * for 'localhost'. See
-+ * - http://sourceware.org/bugzilla/show_bug.cgi?id=4980
-+ * - https://bugzilla.redhat.com/show_bug.cgi?id=496300
-+ */
-+static int already_bound(const struct sockaddr *addr, socklen_t addrlen) {
-+#ifdef NO_IPv6
-+ struct sockaddr_in addr_buf;
-+#else
-+ struct sockaddr_in6 addr_buf;
-+#endif
-+ struct sockaddr *boundaddr = (struct sockaddr *)&addr_buf;
-+ socklen_t buflen;
-+ int i;
-+
-+ for (i = 0; i < numsock; i++) {
-+ buflen = sizeof(addr_buf);
-+ if (getsockname(sock[i], boundaddr, &buflen) < 0)
-+ error(errno, "getsockname failed");
-+ if (buflen == addrlen && sockaddr_equal(boundaddr, addr))
-+ return 1;
-+ }
-+ return 0;
-+}
-+
- #ifdef NO_IPv6
- static void newsocket(struct sockaddr_in *sin) {
- int fd;
- const char *host = ip4atos(ntohl(sin->sin_addr.s_addr));
-+
-+ if (already_bound((struct sockaddr *)sin, sizeof(*sin)))
-+ return;
- if (numsock >= MAXSOCK)
- error(0, "too many listening sockets (%d max)", MAXSOCK);
- fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
-@@ -223,6 +292,8 @@ static int newsocket(struct addrinfo *ai) {
- int fd;
- char host[NI_MAXHOST], serv[NI_MAXSERV];
-
-+ if (already_bound(ai->ai_addr, ai->ai_addrlen))
-+ return 1;
- if (numsock >= MAXSOCK)
- error(0, "too many listening sockets (%d max)", MAXSOCK);
- fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
-diff --git a/rbldnsd.py b/rbldnsd.py
-index 9300ef2..4b78dee 100644
---- a/rbldnsd.py
-+++ b/rbldnsd.py
-@@ -2,6 +2,7 @@
-
-
- """
-+import errno
- from itertools import count
- import subprocess
- from tempfile import NamedTemporaryFile, TemporaryFile
-@@ -12,6 +13,14 @@ try:
- import DNS
- except ImportError:
- raise RuntimeError("The pydns library is not installed")
-+try:
-+ from DNS import SocketError as DNS_SocketError
-+except ImportError:
-+ class DNS_SocketError(Exception):
-+ """ Dummy, never raised.
-+
-+ (Older versions of pydns before 2.3.6 do not raise SocketError.)
-+ """
-
- DUMMY_ZONE_HEADER = """
- $SOA 0 example.org. hostmaster.example.com. 0 1h 1h 2d 1h
-@@ -113,7 +122,6 @@ class Rbldnsd(object):
- stderr=self.stderr)
-
- # wait for rbldnsd to start responding
-- time.sleep(0.1)
- for retry in count():
- if daemon.poll() is not None:
- raise DaemonError(
-@@ -124,12 +132,18 @@ class Rbldnsd(object):
- break
- except QueryRefused:
- break
-+ except DNS_SocketError as ex:
-+ # pydns >= 2.3.6
-+ wrapped_error = ex.args[0]
-+ if wrapped_error.errno != errno.ECONNREFUSED:
-+ raise
- except DNS.DNSError as ex:
-+ # pydns < 2.3.6
- if str(ex) != 'no working nameservers found':
- raise
-- elif retries > 10:
-- raise DaemonError(
-- "rbldnsd does not seem to be responding")
-+ if retry > 10:
-+ raise DaemonError("rbldnsd does not seem to be responding")
-+ time.sleep(0.1)
-
- def _stop_daemon(self):
- daemon = self._daemon
-@@ -150,6 +164,22 @@ class Rbldnsd(object):
- raise DaemonError("rbldnsd exited with code %d"
- % daemon.returncode)
-
-+ @property
-+ def no_ipv6(self):
-+ """ Was rbldnsd compiled with -DNO_IPv6?
-+ """
-+ # If rbldnsd was compiled with -DNO_IPv6, the (therefore
-+ # unsupported) '-6' command-line switch will not be described
-+ # in the help message
-+ cmd = [self.daemon_bin, '-h']
-+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
-+ help_message = proc.stdout.readlines()
-+ if proc.wait() != 0:
-+ raise subprocess.CalledProcessError(proc.returncode, cmd)
-+ return not any(line.lstrip().startswith('-6 ')
-+ for line in help_message)
-+
-+
- class TestRbldnsd(unittest.TestCase):
- def test(self):
- rbldnsd = Rbldnsd()
-diff --git a/test_acl.py b/test_acl.py
-index d93ca0a..10bed1c 100644
---- a/test_acl.py
-+++ b/test_acl.py
-@@ -1,5 +1,8 @@
- """ Tests for the acl dataset
- """
-+from functools import wraps
-+import socket
-+import sys
- from tempfile import NamedTemporaryFile
- import unittest
-
-@@ -9,6 +12,35 @@ __all__ = [
- 'TestAclDataset',
- ]
-
-+try:
-+ from unittest import skipIf
-+except ImportError:
-+ # hokey replacement (for python <= 2.6)
-+ def skipIf(condition, reason):
-+ if condition:
-+ def decorate(f):
-+ @wraps(f)
-+ def skipped(*args, **kw):
-+ sys.stderr.write("skipped test: %s " % reason)
-+ return skipped
-+ return decorate
-+ else:
-+ return lambda f: f
-+
-+def _have_ipv6():
-+ # Check for IPv6 support
-+ if not getattr(socket, 'has_ipv6', False):
-+ return False # no python support for ipv6
-+ elif Rbldnsd().no_ipv6:
-+ return False # rbldnsd compiled with -DNO_IPv6
-+ try:
-+ socket.socket(socket.AF_INET6, socket.SOCK_DGRAM).close()
-+ except socket.error:
-+ return False # no kernel (or libc) support for ipv6?
-+ return True
-+
-+no_ipv6 = not _have_ipv6()
-+
- def daemon(acl, addr='localhost'):
- """ Create an Rbldnsd instance with given ACL
- """
-@@ -33,11 +65,13 @@ class TestAclDataset(unittest.TestCase):
- addr='127.0.0.1') as dnsd:
- self.assertEqual(dnsd.query('test.example.com'), 'Success')
-
-+ @skipIf(no_ipv6, "IPv6 unsupported")
- def test_refuse_ipv6(self):
- with daemon(acl=["::1 :refuse"],
- addr='::1') as dnsd:
- self.assertRaises(QueryRefused, dnsd.query, 'test.example.com')
-
-+ @skipIf(no_ipv6, "IPv6 unsupported")
- def test_pass_ipv6(self):
- with daemon(acl=[ "0/0 :refuse",
- "0::1 :pass" ],
-diff --git a/test_ip4trie.py b/test_ip4trie.py
-index fe9e78f..2cce09b 100644
---- a/test_ip4trie.py
-+++ b/test_ip4trie.py
-@@ -9,7 +9,7 @@ __all__ = [
- ]
-
- def ip4trie(zone_data):
-- """ Run rbldnsd with an ip6trie dataset
-+ """ Run rbldnsd with an ip4trie dataset
- """
- dnsd = Rbldnsd()
- dnsd.add_dataset('ip4trie', ZoneFile(zone_data))
-diff --git a/test_ip6trie.py b/test_ip6trie.py
-index d3600db..377c5dd 100644
---- a/test_ip6trie.py
-+++ b/test_ip6trie.py
-@@ -15,15 +15,6 @@ def ip6trie(zone_data):
- dnsd.add_dataset('ip6trie', ZoneFile(zone_data))
- return dnsd
-
--def rfc3152(ip6addr, domain='example.com'):
-- from socket import inet_pton, AF_INET6
-- from struct import unpack
--
-- bytes = unpack("16B", inet_pton(AF_INET6, ip6addr))
-- nibbles = '.'.join("%x.%x" % (byte & 0xf, (byte >> 4) & 0xf)
-- for byte in reversed(bytes))
-- return "%s.%s" % (nibbles, domain)
--
- class TestIp6TrieDataset(unittest.TestCase):
- def test_exclusion(self):
- with ip6trie(["dead::/16 listed",
-@@ -31,5 +22,35 @@ class TestIp6TrieDataset(unittest.TestCase):
- self.assertEqual(dnsd.query(rfc3152("dead::beef")), None)
- self.assertEqual(dnsd.query(rfc3152("dead::beee")), "listed")
-
-+
-+def rfc3152(ip6addr, domain='example.com'):
-+ return "%s.%s" % ('.'.join(reversed(_to_nibbles(ip6addr))), domain)
-+
-+def _to_nibbles(ip6addr):
-+ """ Convert ip6 address (in rfc4291 notation) to a sequence of nibbles
-+
-+ NB: We avoid the use of socket.inet_pton(AF_INET6, ip6addr) here
-+ because it fails (with 'error: can't use AF_INET6, IPv6 is
-+ disabled') when python has been compiled without IPv6 support. See
-+ http://www.corpit.ru/pipermail/rbldnsd/2013q3/001181.html
-+
-+ """
-+ def _split_words(addr):
-+ return [ int(w, 16) for w in addr.split(':') ] if addr else []
-+
-+ if '::' in ip6addr:
-+ head, tail = [ _split_words(s) for s in ip6addr.split('::', 1) ]
-+ nzeros = 8 - len(head) - len(tail)
-+ assert nzeros >= 0
-+ words = head + [ 0 ] * nzeros + tail
-+ else:
-+ words = _split_words(ip6addr)
-+
-+ assert len(words) == 8
-+ for word in words:
-+ assert 0 <= word <= 0xffff
-+
-+ return ''.join("%04x" % word for word in words)
-+
- if __name__ == '__main__':
- unittest.main()