def test_authority_domain(self): question = Question(Name("server1.gumpe"), Type.A, Class.IN) header = Header(1337, 0, 1, 0, 0, 0) query = Message(header, questions=[question]) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(query.to_bytes(), (SERVER, PORT)) data = s.recv(512) s.close() message = Message.from_bytes(data) self.assertCountEqual(message.answers, [ ResourceRecord( name=Name("server1.gumpe."), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("10.0.1.5"), ), ResourceRecord( name=Name("server1.gumpe."), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("10.0.1.4"), ), ])
def from_bytes(cls, packet): """Create Message from bytes. Args: packet (bytes): byte representation of the message. """ header, offset = Header.from_bytes(packet), 12 questions = [] for _ in range(header.qd_count): question, offset = Question.from_bytes(packet, offset) questions.append(question) answers = [] for _ in range(header.an_count): answer, offset = ResourceRecord.from_bytes(packet, offset) answers.append(answer) authorities = [] for _ in range(header.ns_count): authority, offset = ResourceRecord.from_bytes(packet, offset) authorities.append(authority) additionals = [] for _ in range(header.ar_count): additional, offset = ResourceRecord.from_bytes(packet, offset) additionals.append(additional) return cls(header, questions, answers, authorities, additionals)
def consult_cache(self, questions): answers = [] for q in questions: res, iplist, namelist = self.check_cache(q.qname) if res: for ip in iplist: answers.append(ResourceRecord(q.qname, Type.A, Class.IN, self.ttl, ARecordData(ip))) for n in namelist: answers.append(ResourceRecord(q.qname, Type.CNAME, Class.IN, self.ttl, CNAMERecordData(n))) return answers
def test_resource_to_bytes(self): name = MagicMock() name.to_bytes.return_value = b"\x07example\x03com\x00" rdata = MagicMock() rdata.to_bytes.return_value = b"\x04\x05\x06\x07" record = ResourceRecord(name, Type.A, Class.CS, 3, rdata) compress = {} self.assertEqual( record.to_bytes(0, compress), (b"\x07example\x03com\x00\x00\x01\x00\x02\x00\x00\x00\x03\x00" b"\x04\x04\x05\x06\x07"))
def test_resource_from_bytes(self, MockName, MockRData): MockName.from_bytes.return_value = (Name("example.com"), 13) MockRData.create_from_bytes.return_value = ARecordData("1.1.1.1") packet = (b"\x07example\x03com\x00\x00\x01\x00\x02\x00\x00\x00\x03\x00" b"\x04\x01\x01\x01\x01") record1, offset = ResourceRecord.from_bytes(packet, 0) record2 = ResourceRecord(Name("example.com"), Type.A, Class.CS, 3, ARecordData("1.1.1.1")) self.assertEqual(record1, record2) MockName.from_bytes.assert_called_with(packet, 0) MockRData.create_from_bytes.assert_called_with(Type.A, packet, 23, 4)
def test_concurrent_requests(self): queries = [] answers = [] question = Question(Name("gaia.cs.umass.edu"), Type.A, Class.IN) header = Header(1337, 0, 1, 0, 0, 0) header.rd = 1 queries.append(Message(header, questions=[question])) answers.append([ ResourceRecord(name=Name("gaia.cs.umass.edu"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("128.119.245.12")), ]) question = Question(Name("server2.gumpe"), Type.A, Class.IN) header = Header(420, 0, 1, 0, 0, 0) queries.append(Message(header, questions=[question])) answers.append([ ResourceRecord(name=Name("server2.gumpe"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("10.0.1.7")), ]) header = Header(69, 0, 1, 0, 0, 0) question = Question(Name("www.gumpe"), Type.A, Class.IN) queries.append(Message(header, questions=[question])) answers.append([ ResourceRecord(name=Name("www.gumpe"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("10.0.1.7")), ResourceRecord(name=Name("www.gumpe"), type_=Type.CNAME, class_=Class.IN, ttl=0, rdata=CNAMERecordData(Name("server2.gumpe"))), ]) sockets = [ socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in range(len(queries)) ] for i in range(len(sockets)): sockets[i].sendto(queries[i].to_bytes(), (SERVER, PORT)) responses = [] for i in range(len(sockets)): responses.append(Message.from_bytes(sockets[i].recv(1024))) for i in range(len(sockets)): sockets[i].close() for i in range(len(queries)): self.assertCountEqual(responses[i].answers, answers[i])
def testCache(self): cac = RecordCache(0) self.assertEqual(cac.lookup("abcdefghqqqq.com", Type.A, Class.IN), []) test = ResourceRecord("blabla.com", Type.A, Class.IN, 0, ARecordData("111.111.111.111")) cac.add_record(test) self.assertEqual(cac.lookup("blabla.com", Type.A, Class.IN), test)
def read_master_file(self, filename): """Read the zone from a master file See section 5 of RFC 1035. Args: filename (str): the filename of the master file """ temp = [] """Read the cache file from disk""" with open("roothints.md") as file_: lines = file_.readlines() for l in lines: #remove comments b = re.sub(r';[^\n]*', "", l) te = l.split() if (te[0] != ';'): temp.append(te) if ("." in te[0]): type_ = self.getType(te[2]) if (type_ == Type.A): rdata = ARecordData(te[3]) elif (type_ == Type.NS): rdata = NSRecordData(te[3]) elif (type_ == Type.CNAME): rdata = CNAMERecordData(te[3]) rr = ResourceRecord(te[0], type_, Class.IN, te[1], rdata) self.records.update({te[0]: rr})
def read_cache_file(self, cache_file=Consts.CACHE_FILE): """ Read the cache file from disk """ #Empty current cache self.records = [] #Load from file try: with open(cache_file + ".timestamp") as infile: self.lastCleanup = int(infile.readline()) with open(cache_file, "r") as infile: curTime = int(time.time()) dcts = json.load(infile) self.records = [ResourceRecord.from_dict(dct) for dct in dcts] #Don't add the entries whose TTL is expired and update the ttls self.cleanup() except (ValueError, IOError, FileNotFoundError) as e: print("An error has occured while loading cache from disk: " + str(e)) with open(cache_file, 'w') as outfile: json.dump([], outfile, indent=2) with open(cache_file + ".timestamp", 'w') as outfile: outfile.write(str(self.lastCleanup)) if isinstance(e, FileNotFoundError): print("Missing files were created") self.records = []
def read_master_file(self, filename): """Read the zone from a master file See section 5 of RFC 1035. Args: filename (str): the filename of the master file """ records = re.finditer( Zone.record_re, open(filename, "r").read(), flags=re.MULTILINE ) for r in records: domain, ttl, class_, type_, rdata = r.groups() domain = domain or self.last_domain self.last_domain = domain record = ResourceRecord( name=Name(domain), type_=Type[type_], class_=Class[class_], ttl=int(ttl) if ttl is not None else Zone.default_ttl, rdata=RecordData.create_from_str(Type[type_], rdata) ) if domain in self.records: self.records[domain].append(record) else: self.records[domain] = [record]
def serve(self): """Start serving requests""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("", self.port)) while not self.done: data, address = sock.recvfrom(65565) message = Message.from_bytes(data) rd = message.header.rd rcode = 0 aa = 1 self.log("REQUEST RECIEVED:", address) answers, authorities, additionals = self.zone_resolution(message.questions) if answers == []: if authorities == [] and additionals == []: self.log("\tZONE RESOLUTION FAILED") answers = self.consult_cache(message.questions) if answers == []: self.log("\tCACHE LOOKUP FAILED") rcode = 3 else: aa = 0 if rcode == 3 and rd == 1: rcode = 0 self.log("\tCALLING RESOLVER") resolver = Resolver(5, True, 0) resolver.rd = 0 resolver.rootip = "198.41.0.4" for q in message.questions: self.log("\t\tRESOLVING:", q.qname) hostname, namelist, iplist = resolver.gethostbyname(str(q.qname)) if hostname == str(q.qname): for ip in iplist: answers.append(ResourceRecord(q.qname, Type.A, Class.IN, self.ttl, ARecordData(ip))) for n in namelist: answers.append(ResourceRecord(q.qname, Type.CNAME, Class.IN, self.ttl, CNAMERecordData(n))) self.log("SENDING RESPONSE:", rcode, "\n") mess = self.build_message(message.header.ident, rd, aa, rcode, message.questions, answers, authorities, additionals) sock.sendto(mess.to_bytes(), address)
def setUp(self): # put invalid record in cache file record_data = RecordData.create(Type.A, "192.168.123.456") self.rr = ResourceRecord("invalid.invalid", Type.A, Class.IN, 3, record_data) cache = RecordCache() cache.add_record(self.rr) cache.write_cache_file()
def read_cache_file(self): """Read the cache file from disk""" dcts = [] try: with open("cache", "r") as file_: dcts = json.load(file_) except: print("could not read cache") self.records = [ResourceRecord.from_dict(dct) for dct in dcts]
def test_cache_lookup(self): """ Add a record to the cache and look it up """ rr = ResourceRecord("wiki.nl", Type.A, Class.IN, self.ttl, RecordData.create(Type.A, "192.168.123.456")) cache = RecordCache() cache.add_record(rr) lookup_vals = cache.lookup("wiki.nl", Type.A, Class.IN) self.assertEqual([rr], lookup_vals)
def analyze_response(self, response): additionals = list( ) # this list makes sure no cache lookups for additional information are necessary. for additional_rr in response.additionals: try: self.CACHE.add_record(additional_rr) additionals.append(additional_rr) except CacheException: pass for answer_rr in response.answers: if answer_rr.type_ == Type.A: self.CACHE.add_record(answer_rr) self.addresses.append(answer_rr.rdata.data) elif answer_rr.type_ == Type.CNAME: self.CACHE.add_record(answer_rr) new_sname = answer_rr.rdata.data try: cname_resolver = Resolver(self.caching, self.CACHE) self.SNAME, self.addresses, self.aliases = cname_resolver.gethostbyname( new_sname) except ResolverException: pass return else: raise ResolverException(RCode.FormErr) if response.answers: for additional in additionals: self.aliases.append(additional.rdata.data) return new_slist = list() for authority in response.authorities: rr = ResourceRecord(authority.name, authority.type_, authority.class_, authority.ttl, authority.rdata) if rr.type_ == Type.NS: self.CACHE.add_record(rr) ip = None for additional in additionals: if additional.name == rr.rdata.data and additional.type_ == Type.A: ip = additional.rdata.data new_slist.append((rr.rdata.data, ip)) elif rr.type_ == Type.SOA: pass else: raise ResolverException(RCode.FormErr) if new_slist: try: next_resolver = Resolver(self.caching, self.CACHE) self.SNAME, self.addresses, self.aliases = next_resolver.gethostbyname( self.SNAME, new_slist) except ResolverException: pass
def test_expired_cache_entry(self): cache = RecordCache(0) record = ResourceRecord( name=Name("bonobo.putin"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("1.0.0.1"), ) cache.add_record(record) self.assertEqual(cache.lookup(Name("bonobo.putin"), Type.A, Class.IN), None)
def test_invalid_domain_from_cache(self): cache = RecordCache(0) record = ResourceRecord( name=Name("bonobo.putin"), type_=Type.A, class_=Class.IN, ttl=60, rdata=ARecordData("1.0.0.1"), ) cache.add_record(record) self.assertEqual(cache.lookup(Name("bonobo.putin"), Type.A, Class.IN), record)
def test_expired_cache_entry(self): cache = RecordCache(0) resolver = Resolver(5, cache) cache.add_record( ResourceRecord( name=Name("hw.gumpe"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("1.0.0.2"), )) cache.add_record( ResourceRecord( name=Name("hw.gumpe"), type_=Type.CNAME, class_=Class.IN, ttl=0, rdata=CNAMERecordData(Name("gumpe.hw")), )) self.assertEqual(resolver.gethostbyname("hw.gumpe"), ("hw.gumpe", [], []))
def test_invalid_domain_from_cache(self): cache = RecordCache(0) resolver = Resolver(5, cache) cache.add_record( ResourceRecord( name=Name("bonobo.putin"), type_=Type.A, class_=Class.IN, ttl=60, rdata=ARecordData("1.0.0.1"), )) cache.add_record( ResourceRecord( name=Name("bonobo.putin"), type_=Type.CNAME, class_=Class.IN, ttl=60, rdata=CNAMERecordData(Name("putin.bonobo")), )) self.assertEqual(resolver.gethostbyname("bonobo.putin"), ("bonobo.putin", ["putin.bonobo."], ["1.0.0.1"]))
def read_master_file(self, filename): """Read the zone from a master file See section 5 of RFC 1035. Args: filename (str): the filename of the master file """ typeData = {"NS": "nsdname", "A": "address", "CNAME": "cname"} try: with open(filename, "r") as file_: for l in file_: rr = l.split(";")[0] if not rr: continue entries = re.split("[\\n]?[\ +]*[\\t+]*", rr)[:-1] name = entries[0] ttl = int(entries[1]) type_ = entries[2] address = entries[3] if type_ not in typeData: continue rr = { "type": type_, "name": name, "class": "IN", "ttl": ttl, "rdata": { typeData[type_]: address } } if name not in self.records: self.records[name] = [ResourceRecord.from_dict(rr)] else: self.records[name].append(ResourceRecord.from_dict(rr)) except: print("could not read zone file") return self
def test_TTL_expiration(self): """ cache a record, wait till ttl expires, see if record is removed from cache """ rr = ResourceRecord("wiki.nl", Type.A, Class.IN, self.ttl, RecordData.create(Type.A, "192.168.123.456")) cache = RecordCache() cache.add_record(rr) time.sleep(rr.ttl) lookup_vals = cache.lookup("wiki.nl", Type.A, Class.IN) self.assertFalse(lookup_vals)
def resource_from_json(dct): """ Convert JSON object to ResourceRecord Usage: records = json.loads(string, object_hook=resource_from_json) """ name = dct["name"] type_ = Type.from_string(dct["type"]) class_ = Class.from_string(dct["class"]) ttl = dct["ttl"] rdata = RecordData.create(type_, dct["rdata"]) t = dct["time"] return ResourceRecord(name, type_, class_, ttl, rdata, t)
def test_ttl_overwrite(self): cache = RecordCache(60) record = ResourceRecord( name=Name("bonobo.putin"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("1.0.0.1"), ) cache.add_record(record) cache_entry = cache.lookup(Name("bonobo.putin"), Type.A, Class.IN) self.assertEqual(cache_entry, record) self.assertEqual(cache_entry.ttl, 60)
def from_bytes(cls, packet): """ Create Message from bytes Args: packet (bytes): byte representation of the message """ parser = Parser() # Parse header header, offset = Header.from_bytes(packet), 12 # Parse questions questions = [] for _ in range(header.qd_count): question, offset = Question.from_bytes(packet, offset, parser) questions.append(question) # Parse answers answers = [] for _ in range(header.an_count): answer, offset = ResourceRecord.from_bytes(packet, offset, parser) answers.append(answer) # Parse authorities authorities = [] for _ in range(header.ns_count): authority, offset = ResourceRecord.from_bytes(packet, offset, parser) authorities.append(authority) # Parse additionals additionals = [] for _ in range(header.ar_count): additional, offset = ResourceRecord.from_bytes(packet, offset, parser) additionals.append(additional) return cls(header, questions, answers, authorities, additionals)
def load_and_parse(self, content): content = re.sub(re.compile(";.*?\n"), "\n", content) #Remove comments content = re.sub(re.compile("\n\n*\n"), "\n", content) #Remove whitespaces content = re.sub(re.compile(" * "), " ", content) #Remove multiple spaces between words content = re.sub(re.compile("\t\t*\t"), " ", content) #Remove tabs between words content = re.sub(re.compile("\n *"), "\n", content) #Remove spaces at start of line content = re.sub(re.compile("\n\t*"), "\n", content) #Remove tabs at start of line content = re.compile(r'\(.*?\)', re.DOTALL)\ .sub(lambda x: x.group().replace('\n', ''), content) #Remove newlines between () content = re.sub(re.compile("\t+"), " ", content) #Remove tabs between words default_ttl = None origin = None recordSet = [] for line in content.split('\n'): if line[:4] == "$TTL": prev_ttl = self.time_to_seconds(line[4:].strip()) elif line[:7] == "$ORIGIN": origin = line[7:].strip() elif "SOA" not in line: parts = line.split(' ') rr_name = parts[0] rr_ttl = self.time_to_seconds(parts[1]) offset = int(rr_ttl == 0) if offset: rr_ttl = prev_ttl rr_class = Class[parts[1 + offset]] rr_type = parts[2 + offset] if Type[rr_type] == Type.CNAME or Type[rr_type] == Type.NS: rr_data = RecordData.create( Type[rr_type], Name(parts[3 + offset].rstrip('.'))) else: rr_data = RecordData.create(Type[rr_type], parts[3 + offset].rstrip('.')) self.add_node( rr_name, ResourceRecord(Name(rr_name), Type[rr_type], rr_class, rr_ttl, rr_data))
def test_outside_zone(self): question = Question(Name("gaia.cs.umass.edu"), Type.A, Class.IN) header = Header(1337, 0, 1, 0, 0, 0) header.rd = 1 query = Message(header, questions=[question]) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(query.to_bytes(), (SERVER, PORT)) data = s.recv(512) s.close() message = Message.from_bytes(data) self.assertEqual(message.answers, [ ResourceRecord(name=Name("gaia.cs.umass.edu"), type_=Type.A, class_=Class.IN, ttl=0, rdata=ARecordData("128.119.245.12")), ])
def test_cache_disk_io(self): """ Add a record to the cache, write to disk, read from disk, do a lookup """ rr = ResourceRecord("wiki.nl", Type.A, Class.IN, self.ttl, RecordData.create(Type.A, "192.168.123.456")) cache = RecordCache() cache.write_cache_file() # overwrite the current cache file # add rr to cache and write to disk cache.add_record(rr) cache.write_cache_file() # read from disk again new_cache = RecordCache() new_cache.read_cache_file() lookup_vals = new_cache.lookup("wiki.nl", Type.A, Class.IN) self.assertEqual([rr], lookup_vals)
def lookup(self, dname, type_, class_): """Lookup resource records in cache Lookup for the resource records for a domain name with a specific type and class. Args: dname (str): domain name type_ (Type): type class_ (Class): class """ dname = Name(dname) rrs = [ ResourceRecord.from_dict(r) for r in self.records if (time.time() - r["timestamp"]) < r["ttl"] ] rs = [ r for r in rrs if r.name == dname and r.class_ == class_ and r.type_ == type_ ] return rs
def setUpClass(cls): cls.ttl = 3 rr = ResourceRecord("wiki.nl", Type.A, Class.IN, cls.ttl, RecordData.create(Type.A, "192.168.123.456"))
def getType(t): if(t == 'A'): return Type.A if(t == 'NS'): return Type.NS if(t == 'CNAME'): return Type.CNAME temp = [] """Read the cache file from disk""" with open("roothints.md") as file_: lines = file_.readlines() for l in lines: #remove comments b = re.sub(r';[^\n]*', "", l) te = l.split() if(te[0] != ';'): temp.append(te) if("." in te[0]): type_ = getType(te[2]) if(type_ == Type.A): rdata = ARecordData(te[3]) elif(type_ == Type.NS): rdata = NSRecordData(te[3]) elif(type_ == Type.CNAME): rdata = CNAMERecordData(te[3]) rr = ResourceRecord(te[0], type_, Class.IN, te[1], rdata) print(rr.to_dict())
def handle_request(self): """ Attempts to answer the received query """ #Check this next to the given algorithm #print("Catalog:",self.catalog.zones) #for zone in self.catalog.zones: # print("Records:",self.catalog.zones[zone].records) if self.message.header.opcode != 0: #Send a not implemented error, we don't need to support those kinds of queries print("[-] - Received a nonstandard query. This is unsupported.") header = Header(ident, 0, 1, 0, 0, 0) header.qr = 1 header.rd = self.message.header.rd header.ra = 1 header.rcode = 4 self.sendResponse(Message(header, self.message.questions, [])) return #print("[*] - Handling request.") if len(self.message.questions) != 1: #Send a format error response print("[-] - Invalid request.") header = Header(ident, 0, 1, 0, 0, 0) header.qr = 1 header.rd = self.message.header.rd header.ra = 1 header.rcode = 1 self.sendResponse(Message(header, self.message.questions, [])) return #print("MSG:",self.message) #print("RECEIVED QUESTION",self.message.questions[0]) hname = str(self.message.questions[0].qname) #print("Solving",hname,type(hname)) ident = self.message.header.ident #print("Checking zone") answer, authority, found = self.check_zone(hname) #print("Wat we in de zone hebben gevonden") #print("ANS:",answer,"AUTH:",authority,"FOUND:",found) #found = False if found: print("Found in zone") header = Header(ident, 0, 1, len(answer), len(authority), 0) header.qr = 1 header.aa = 1 header.rd = self.message.header.rd header.ra = 1 self.sendResponse( Message(header, self.message.questions, answer, authority)) elif self.message.header.rd == 1: h, al, ad = self.resolver.gethostbyname(hname) #Make and send th appropriate response header = Header(ident, 0, 1, len(al) + len(ad), 0, 0) header.qr = 1 header.rd = self.message.header.rd header.ra = 1 aliases = [ ResourceRecord(Name(h), Type.CNAME, Class.IN, self.ttl, RecordData.create(Type.CNAME, Name(alias))) for alias in al ] addresses = [ ResourceRecord(Name(h), Type.A, Class.IN, self.ttl, RecordData.create(Type.A, address)) for address in ad ] self.sendResponse( Message(header, self.message.questions, aliases + addresses)) else: #Send an empty response header = Header(ident, 0, 1, 0, 0, 0) header.qr = 1 header.rd = self.message.header.rd header.ra = 1 header.rcode = 0 self.sendResponse(Message(header, self.message.questions, []))