summaryrefslogtreecommitdiff
blob: 5621075fbae2edd1eafaa7d6cb5012139dbb43a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
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()