You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
11 KiB
330 lines
11 KiB
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()
|