commit 2c17bf159b448de42603d9904d95e3df154fd952 Author: Arturo Filastò art@fuffa.org Date: Tue Nov 20 19:37:25 2012 +0100
Create DNS Test template * Use such template for DNS Tamper test --- nettests/core/dnstamper.py | 217 +++++++------------------------------------- ooni/templates/dnst.py | 108 ++++++++++++++++++++++ 2 files changed, 142 insertions(+), 183 deletions(-)
diff --git a/nettests/core/dnstamper.py b/nettests/core/dnstamper.py index bc57f62..be3281a 100644 --- a/nettests/core/dnstamper.py +++ b/nettests/core/dnstamper.py @@ -18,11 +18,9 @@ import pdb
from twisted.python import usage - from twisted.internet import defer -from twisted.names import client, dns -from twisted.names.client import Resolver -from twisted.names.error import DNSQueryRefusedError + +from ooni.templates import dnst
from ooni import nettest from ooni.utils import log @@ -36,12 +34,12 @@ class UsageOptions(usage.Options): 'file containing list of DNS resolvers to test against'] ]
-class DNSTamperTest(nettest.NetTestCase): +class DNSTamperTest(dnst.DNSTest):
name = "DNS tamper" description = "DNS censorship detection test" - version = "0.2" - lookupTimeout = [1] + version = "0.3" + authors = "Arturo Filastò, Isis Lovecruft" requirements = None
inputFile = ['file', 'f', None, @@ -51,16 +49,6 @@ class DNSTamperTest(nettest.NetTestCase): requiredOptions = ['backend', 'backendport', 'file', 'testresolvers']
def setUp(self): - self.report['test_lookups'] = {} - self.report['test_reverse'] = {} - self.report['control_lookup'] = [] - self.report['a_lookups'] = {} - self.report['tampering'] = {} - - self.test_a_lookups = {} - self.control_a_lookups = [] - self.control_reverse = None - self.test_reverse = {}
if not self.localOptions['testresolvers']: self.test_resolvers = ['8.8.8.8'] @@ -74,74 +62,14 @@ class DNSTamperTest(nettest.NetTestCase):
self.test_resolvers = [x.strip() for x in fp.readlines()] fp.close() + self.control_dns_server = (self.localOptions['backend'], + int(self.localOptions['backendport']))
- def process_a_answers(self, message, resolver_address): - log.msg("Processing A answers for %s" % resolver_address) - log.debug("These are the answers I got %s" % message.answers) - - all_a = [] - a_a = [] - - for answer in message.answers: - if answer.type is 1: - # A type query - r = answer.payload.dottedQuad() - self.report['a_lookups'][resolver_address] = r - a_a.append(r) - lookup = str(answer.payload) - all_a.append(lookup) - - if resolver_address == 'control': - self.report['control_server'] = self.localOptions['backend'] - self.report['control_lookup'] = all_a - self.control_a_lookups = a_a - else: - self.test_a_lookups[resolver_address] = a_a - self.report['test_lookups'][resolver_address] = all_a - - log.msg("Done") - - def process_ptr_answers(self, answers, resolver): - log.msg("Processing PTR answers for %s" % resolver) - name = None - - for answer in answers[0]: - if answer.type is 12: - # PTR type - name = str(answer.payload.name) + self.report['test_resolvers'] = self.test_resolvers + self.report['control_resolver'] = self.control_dns_server
- if resolver == 'control': - self.control_reverse = name - self.report['control_reverse'] = name - else: - self.test_reverse[resolver] = name - self.report['test_reverse'][resolver] = name - - def ptr_lookup_error(self, failure, resolver): - log.msg("There was an error in PTR lookup %s" % resolver) - log.err(failure) - - if resolver == 'control': - self.report['control_reverse'] = None - else: - self.report['test_reverse'][resolver] = None - - def a_lookup_error(self, failure, resolver): - log.msg("There was an error in A lookup %s" % resolver) - log.err(failure) - - if failure.type is DNSQueryRefusedError: - self.report['tampering'][resolver] = 'connection-refused' - elif failure.type is defer.TimeoutError: - self.report['tampering'][resolver] = 'timeout' - - if resolver == 'control': - self.report['control_lookup'] = None - else: - self.report['test_lookups'][resolver] = None - self.test_a_lookups[resolver] = None - - def test_lookup(self): + @defer.inlineCallbacks + def test_a_queries(self): """ We perform an A lookup on the DNS test servers for the domains to be tested and an A lookup on the known good DNS server. @@ -163,115 +91,38 @@ class DNSTamperTest(nettest.NetTestCase): log.msg("Doing the test lookups on %s" % self.input) list_of_ds = [] hostname = self.input - dns_query = [dns.Query(hostname, dns.IN, dns.A)] - dns_server = [(self.localOptions['backend'], - self.localOptions['backendport'])]
- resolver = Resolver(servers=dns_server) + self.report['tampering'] = {}
- control_d = resolver.queryUDP(dns_query, timeout=self.lookupTimeout) - control_d.addCallback(self.process_a_answers, 'control') - control_d.addErrback(self.a_lookup_error, 'control') + control_answers = yield self.performALookup(hostname, self.control_dns_server)
for test_resolver in self.test_resolvers: log.msg("Going for %s" % test_resolver) - dns_server = [(test_resolver, 53)] - - resolver = Resolver(servers=dns_server) - - d = resolver.queryUDP(dns_query, timeout=self.lookupTimeout) - d.addCallback(self.process_a_answers, test_resolver) - d.addErrback(self.a_lookup_error, test_resolver) - - # This is required to cancel the delayed calls of the - # twisted.names.client resolver - list_of_ds.append(d) + test_dns_server = (test_resolver, 53)
- list_of_ds.append(control_d) - dl = defer.DeferredList(list_of_ds) - dl.addCallback(self.do_reverse_lookups) - dl.addBoth(self.compare_results) - return dl + experiment_answers = yield self.performALookup(hostname, test_dns_server) + log.debug("Got these answers %s" % experiment_answers)
- def reverse_lookup(self, address, resolver): - query = [dns.Query(hostname, dns.IN, dns.PTR)] - ptr = '.'.join(address.split('.')[::-1]) + '.in-addr.arpa' - r = resolver.queryUDP(query, timeout=self.lookupTimeout) - return r - - def do_reverse_lookups(self, result): - """ - Take a resolved address in the form "176.139.79.178.in-addr.arpa." and - attempt to reverse the domain with both the control and test DNS - servers to see if they match. - - :param result: - A resolved domain name. - """ - log.msg("Doing the reverse lookups %s" % self.input) - list_of_ds = [] - dns_server = [(self.localOptions['backend'], - self.localOptions['backendport'])] - - resolver = Resolver(servers=dns_server) - - test_reverse = self.reverse_lookup(self.control_a_lookups[0], resolver, - timeout=self.lookupTimeout) - - test_reverse.addCallback(self.process_ptr_answers, 'control') - test_reverse.addErrback(self.ptr_lookup_error, 'control') - - list_of_ds.append(test_reverse) - - for test_resolver in self.test_resolvers: - try: - ip = self.test_a_lookups[test_resolver][0] - except: - break - - d = self.reverse_lookup(ip, res) - d.addCallback(self.process_ptr_answers, test_resolver) - d.addErrback(self.ptr_lookup_error, test_resolver) - list_of_ds.append(d) - - dl = defer.DeferredList(list_of_ds) - return dl - - def compare_results(self, *arg, **kw): - """ - Take the set intersection of two test result sets. If the intersection - is greater than zero (there are matching addresses in both sets) then - the no censorship is reported. Else, if no IP addresses match other - addresses, then we mark it as a censorship event. - """ - log.msg("Comparing results for %s" % self.input) - log.msg(self.test_a_lookups) - - for test, test_a_lookups in self.test_a_lookups.items(): - log.msg("Now doing %s | %s" % (test, test_a_lookups)) - if not test_a_lookups: - self.report['tampering'][test] = 'unknown' + if not experiment_answers: + log.err("Got no response, perhaps the DNS resolver is down?") + self.report['tampering'][test_resolver] = 'no_answer' continue
- if set(test_a_lookups) & set(self.control_a_lookups): + log.debug("Comparing %s with %s" % (experiment_answers, control_answers)) + if set(experiment_answers) & set(control_answers): log.msg("Address has not tampered with on DNS server") - self.report['tampering'][test] = False - - elif self.control_reverse and set([self.control_reverse]) \ - & set([self.report['test_reverse'][test]]): - log.msg("Further testing has eliminated false positives") - self.report['tampering'][test] = 'reverse-match' - + self.report['tampering'][test_resolver] = False else: - log.msg("Reverse DNS on the results returned by returned") - log.msg("which does not match the expected domainname") - self.report['tampering'][test] = True - - if len(self.test_a_lookups) == len(self.test_resolvers): - return - else: - missing_tests = len(self.test_a_lookups) - missing_resolvers = len(self.test_resolvers) - log.msg("Still missing %s resolvers and %s tests" % - (missing_tests, missing_resolvers)) + log.msg("Trying to do reverse lookup") + + experiment_reverse = yield self.performPTRLookup(experiment_answers[0], test_dns_server) + control_reverse = yield self.performPTRLookup(control_answers[0], self.control_dns_server) + + if experiment_reverse == control_reverse: + log.msg("Further testing has eliminated false positives") + self.report['tampering'][test_resolver] = 'reverse_match' + else: + log.msg("Reverse DNS on the results returned by returned") + log.msg("which does not match the expected domainname") + self.report['tampering'][test_resolver] = True
diff --git a/ooni/templates/dnst.py b/ooni/templates/dnst.py new file mode 100644 index 0000000..7a6eb37 --- /dev/null +++ b/ooni/templates/dnst.py @@ -0,0 +1,108 @@ +# -*- encoding: utf-8 -*- +# +# :authors: Arturo Filastò +# :licence: see LICENSE + +from twisted.internet import defer +from twisted.names import client, dns +from twisted.names.client import Resolver + +from twisted.names.error import DNSQueryRefusedError + +from ooni.utils import log +from ooni.nettest import NetTestCase + +class DNSTest(NetTestCase): + name = "Base DNS Test" + version = 0.1 + + requiresRoot = False + queryTimeout = [1] + + def _setUp(self): + self.report['queries'] = [] + + def performPTRLookup(self, address, dns_server): + """ + Does a reverse DNS lookup on the input ip address + + :address: the IP Address as a dotted quad to do a reverse lookup on. + + :dns_server: is the dns_server that should be used for the lookup as a + tuple of ip port (ex. ("127.0.0.1", 53)) + """ + ptr = '.'.join(address.split('.')[::-1]) + '.in-addr.arpa' + query = [dns.Query(ptr, dns.IN, dns.PTR)] + def gotResponse(message): + answers = [] + name = None + for answer in message.answers: + if answer.type is 12: + name = answer.payload.name + + result = {} + result['query_type'] = 'PTR' + result['query'] = repr(query) + result['answers'] = answers + result['name'] = name + self.report['queries'].append(result) + return name + + def gotError(failure): + log.exception(failure) + result = {} + result['query_type'] = 'PTR' + result['query'] = repr(query) + result['error'] = str(failure) + return None + + resolver = Resolver(servers=[dns_server]) + d = resolver.queryUDP(query, timeout=self.queryTimeout) + d.addCallback(gotResponse) + d.addErrback(gotError) + return d + + def performALookup(self, hostname, dns_server): + """ + Performs an A lookup and returns an array containg all the dotted quad + IP addresses in the response. + + :hostname: is the hostname to perform the A lookup on + + :dns_server: is the dns_server that should be used for the lookup as a + tuple of ip port (ex. ("127.0.0.1", 53)) + """ + query = [dns.Query(hostname, dns.IN, dns.A)] + def gotResponse(message): + addrs = [] + answers = [] + for answer in message.answers: + if answer.type is 1: + addr = answer.payload.dottedQuad() + addrs.append(addr) + # We store the resource record and the answer payload in a + # tuple + r = (repr(answer), repr(answer.payload)) + answers.append(r) + result = {} + result['query_type'] = 'A' + result['query'] = repr(query) + result['answers'] = answers + result['addrs'] = addrs + self.report['queries'].append(result) + return addrs + + def gotError(failure): + log.exception(failure) + result = {} + result['query_type'] = 'A' + result['query'] = repr(query) + result['error'] = str(failure) + return None + + resolver = Resolver(servers=[dns_server]) + d = resolver.queryUDP(query, timeout=self.queryTimeout) + d.addCallback(gotResponse) + d.addErrback(gotError) + return d +