commit 5e748acc7f787d37274cbdab9a57b317919f2fa9 Author: Isis Lovecruft isis@patternsinthevoid.net Date: Sat Jun 16 09:21:53 2012 -0700
Added subclassed t.n.client.Resolver() to overcome query port randomization. --- ooni/plugins/dnstamper.py | 75 ++++++++++++++++++++++++++++++++++++++++---- 1 files changed, 68 insertions(+), 7 deletions(-)
diff --git a/ooni/plugins/dnstamper.py b/ooni/plugins/dnstamper.py index 34aaa01..ca0e2e3 100644 --- a/ooni/plugins/dnstamper.py +++ b/ooni/plugins/dnstamper.py @@ -28,8 +28,9 @@
import os
-from twisted.names import client +from twisted.names import client, dns from twisted.internet import reactor +from twisted.internet.error import CannotListenError from twisted.internet.protocol import Factory, Protocol from twisted.python import usage from twisted.plugin import IPlugin @@ -39,7 +40,7 @@ from ooni.plugoo.assets import Asset from ooni.plugoo.tests import ITest, OONITest from ooni import log
-class Top1MAsset(Asset): +class AlexaAsset(Asset): """ Class for parsing the Alexa top-1m.txt as an asset. """ @@ -54,9 +55,60 @@ class DNSTamperArgs(usage.Options): optParameters = [['asset', 'a', None, 'Asset file of hostnames to resolve'], ['controlserver', 'c', '8.8.8.8', 'Known good DNS server'], ['testservers', 't', None, 'Asset file of DNS servers to test'], + ['localservers', 'l', False, 'Also test local servers'], + ['port', 'p', None, 'Local UDP port to send queries over'], ['usereverse', 'r', False, 'Also try reverse DNS resolves'], ['resume', 's', 0, 'Resume at this index in the asset file']]
+class DNSTamperResolver(client.Resolver): + """ + Twisted by default issues DNS queries over cryptographically random + UDP ports to mitigate the Berstein/Kaminsky attack on limited DNS + Transaction ID numbers.[1][2][3] + + This is fine, unless the client has external restrictions which require + DNS queries to be conducted over UDP port 53. Twisted does not provide + an easy way to change this, ergo subclassing client.Resolver.[4] It + would perhaps be wise to patch twisted.names.client and request a merge + into upstream. + + [1] https://twistedmatrix.com/trac/ticket/3342 + [2] http://blog.netherlabs.nl/articles/2008/07/09/ \ + some-thoughts-on-the-recent-dns-vulnerability + [3] http://www.blackhat.com/presentations/bh-dc-09/Kaminsky/ \ + BlackHat-DC-09-Kaminsky-DNS-Critical-Infrastructure.pdf + [4] http://comments.gmane.org/gmane.comp.python.twisted/22794 + """ + def __init__(self): + super(DNSTamperResolver, self).__init__() + #client.Resolver.__init__(self) + + if self.local_options['port']: + self.port = self.local_options['port'] + else: + self.port = '53' + + def _connectedProtocol(self): + """ + Return a new DNSDatagramProtocol bound to a specific port + rather than the default cryptographically-random port. + """ + if 'protocol' in self.__dict__: + return self.protocol + proto = dns.DNSDatagramProtocol(self) + + ## XXX We may need to remove the while loop, which was + ## originally implemented to safeguard against attempts to + ## bind to the same random port twice...but then the code + ## would be blocking... + while True: + try: + self._reactor.listenUDP(self.port, proto) + except error.CannotListenError: + pass + else: + return proto + class DNSTamperTest(OONITest): implements(IPlugin, ITest)
@@ -66,13 +118,20 @@ class DNSTamperTest(OONITest): options = DNSTamperArgs blocking = False
+ if self.local_options['localservers']: + ## client.createResolver() turns None into '/etc/resolv.conf' + ## on posix systems, ignored on Windows. + self.resolvconf = None + else: + self.resolvconf = '' + def load_assets(self): assets = {} if self.local_options: if self.local_options['asset']: assetf = self.local_options['asset'] if assetf == 'top-1m.txt': - assets.update({'asset': Top1MAsset(assetf)}) + assets.update({'asset': AlexaAsset(assetf)}) else: assets.update({'asset': Asset(assetf)}) elif self.local_options['testservers']: @@ -88,7 +147,7 @@ class DNSTamperTest(OONITest): def got_result(result): log.msg('Resolved %s through %s to %s' % (hostname, nameserver, result)) - reactor.stop() + #reactor.stop() return {'resolved': True, 'domain': hostname, 'nameserver': nameserver, @@ -96,13 +155,14 @@ class DNSTamperTest(OONITest):
def got_error(err): log.msg(err.printTraceback()) - reactor.stop() + #reactor.stop() return {'resolved': False, 'domain': hostname, 'nameserver': nameserver, 'address': err}
- res = client.createResolver(servers=[(nameserver, 53)]) + res = client.createResolver(resolvconf=self.resolvconf, + servers=[(nameserver, 53)]) d = res.getHostByName(hostname) d.addCallbacks(got_result, got_error) return d @@ -116,7 +176,8 @@ class DNSTamperTest(OONITest): sets from a positive result resolve to the same domain, in order to remove false positives due to GeoIP load balancing. """ - res = client.createResolver(servers=[(nameserver, 53)]) + res = client.createResolver(resolvconf=self.resolvconf, + servers=[(nameserver, 53)]) ptr = '.'.join(addr.split('.')[::-1]) + '.in-addr.arpa' d = res.lookupPointer(ptr) d.addCallback(lambda (ans, auth, add): util.println(ans[0].payload.name))
tor-commits@lists.torproject.org