From 1d6fc849c980940ad01f1845f575161826c0f14b Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Sat, 23 Apr 2016 17:59:44 +0200 Subject: Makefile --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index bfc5394..09cde87 100644 --- a/Makefile +++ b/Makefile @@ -4,3 +4,9 @@ all: $(TARGET) %.pdf: %.tex latexmk -pdf $< + +clean: + latexmk -C + +.PHONY: clean + -- cgit v1.2.3 From 9b448a8da6afdf94e405b781e08edeffd7946561 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Wed, 27 Apr 2016 12:41:36 +0200 Subject: Added dates --- assignment1.tex | 1 + assignment2.tex | 1 + assignment3.tex | 1 + assignment4.tex | 1 + 4 files changed, 4 insertions(+) diff --git a/assignment1.tex b/assignment1.tex index baee70f..256e012 100644 --- a/assignment1.tex +++ b/assignment1.tex @@ -2,6 +2,7 @@ \author{Camil Staps\\\small{s4498062}} \title{Networking\\\large{Assignment 1}} +\date{February 16, 2016} \usepackage{polyglossia} \setmainlanguage{english} diff --git a/assignment2.tex b/assignment2.tex index ecbea27..b82536b 100644 --- a/assignment2.tex +++ b/assignment2.tex @@ -2,6 +2,7 @@ \author{Camil Staps\\\small{s4498062}} \title{Networking\\\large{Assignment 2}} +\date{March 1, 2016} \usepackage{polyglossia} \setmainlanguage{english} diff --git a/assignment3.tex b/assignment3.tex index c50e5c2..624e817 100644 --- a/assignment3.tex +++ b/assignment3.tex @@ -2,6 +2,7 @@ \author{Camil Staps\\\small{s4498062}} \title{Networking\\\large{Assignment 3}} +\date{March 22, 2016} \usepackage{polyglossia} \setmainlanguage{english} diff --git a/assignment4.tex b/assignment4.tex index b30c6d4..e1cd8c1 100644 --- a/assignment4.tex +++ b/assignment4.tex @@ -2,6 +2,7 @@ \author{Camil Staps\\\small{s4498062}} \title{Networking\\\large{Assignment 4}} +\date{April 13, 2016} \usepackage{polyglossia} \setmainlanguage{english} -- cgit v1.2.3 From 26071f7d92fec61b8ca0c28b848a8c2552c4b0b8 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Wed, 27 Apr 2016 17:10:14 +0200 Subject: Assignment 5 --- assignment5.tex | 108 +++++++++++++++++++++++++++++++++++ assignment5/traceroute.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 assignment5.tex create mode 100755 assignment5/traceroute.py diff --git a/assignment5.tex b/assignment5.tex new file mode 100644 index 0000000..8289bea --- /dev/null +++ b/assignment5.tex @@ -0,0 +1,108 @@ +\documentclass[a4paper,9pt]{article} + +\author{Camil Staps\\\small{s4498062}} +\title{Networking\\\large{Assignment 5}} +\date{April 27, 2016} + +\usepackage{polyglossia} +\setmainlanguage{english} +\usepackage{geometry} +\usepackage{enumitem} +\setenumerate{label=\alph*)} +\usepackage{amsmath} +\usepackage{minted} + +\begin{document} + +\maketitle + +\section{Packet forwarding} +\begin{enumerate} + \item + \begin{tabular}{l | l} + Dest. addr. & Interface \\\hline + H1 & 1 \\ + H2 & 2 \\ + H3 & 3 \\ + \end{tabular} + + \item No: in a datagram network, the forwarding table entries are tuples of + destination address prefixes and link interfaces. There is no way to + distinguish packets coming from host H1 from packets coming from host H2. + + \item + \begin{tabular}{l | l | l | l} + \multicolumn{2}{c|}{Incoming} & \multicolumn{2}{c}{Outgoing} \\\hline + Interface & VC\# & Interface & VC\# \\\hline + 1 & 13 & 3 & 23 \\ + 2 & 37 & 4 & 47 \\ + \end{tabular} + + \item + \begin{enumerate}[label=\Alph*.] + \setcounter{enumii}{1} + \item + \begin{tabular}{l | l | l | l} + \multicolumn{2}{c|}{Incoming} & \multicolumn{2}{c}{Outgoing} \\\hline + Interface & VC\# & Interface & VC\# \\\hline + 1 & 23 & 2 & 33 \\ + \end{tabular} + + \item + \begin{tabular}{l | l | l | l} + \multicolumn{2}{c|}{Incoming} & \multicolumn{2}{c}{Outgoing} \\\hline + Interface & VC\# & Interface & VC\# \\\hline + 1 & 47 & 2 & 57 \\ + \end{tabular} + + \item + \begin{tabular}{l | l | l | l} + \multicolumn{2}{c|}{Incoming} & \multicolumn{2}{c}{Outgoing} \\\hline + Interface & VC\# & Interface & VC\# \\\hline + 1 & 33 & 3 & 43 \\ + 2 & 57 & 3 & 67 \\ + \end{tabular} + \end{enumerate} +\end{enumerate} + +\section{Switching} +\begin{enumerate}[label=\arabic*.] + \item Only one packet can be handled at the same time, so we may have to wait + for $n-1$ packets. That gives a delay of $(n-1)\cdot D$. + \item Every packet goes to a different output port, so we will never go over + the bus bandwidth (which is at least 1). Therefore, there is no queuing + delay in this case. + \item Every packet goes to a different output port and therefore uses a + different vertical bar in the interconnection network. Queuing can only + occur when two packets need the same vertical line, but as explained this + cannot happen. Therefore, there is no queuing delay in this case either. +\end{enumerate} + +\section{Subnets} +223.1.17.128/26 can support up to 64 addresses (so also 60).\\ +223.1.17.0/25 can support up to 128 addresses (so also 90).\\ +223.1.17.192/26 can support up to 64 addresses (so also 12). + +The subnets do not conflict; their binary prefixes in the last byte are +\texttt{10}, \texttt{0} and \texttt{11} respectively. + +\section{ICMP Traceroute} +\texttt{do\_one\_ping} and the underlying functions have been adapted to return +a tuple \texttt{(address, icmp\_type)} (or \texttt{(None, None)} in case of a +timeout). + +The function \texttt{ping} has been replaced by \texttt{traceroute}. This uses +a counter \texttt{i} to increase the TTL of the echo requests we send. For each +TTL we call \texttt{do\_one\_ping} which gives us either \texttt{(None, None)} +if no server responded, or \texttt{(address, icmp\_type)}, where +\texttt{icmp\_type} is either \texttt{ICMP\_ECHO\_REPLY} or +\texttt{ICMP\_TIME\_EXCEEDED}. We can use this to stop (in case of a reply) or +continue (in case of a TTL excess). + +The logic may be found in \texttt{traceroute}: + +\inputminted[linenos,xleftmargin=1cm,firstline=102,lastline=118]{python}% + {assignment5/traceroute.py} + +\end{document} + diff --git a/assignment5/traceroute.py b/assignment5/traceroute.py new file mode 100755 index 0000000..68f2d36 --- /dev/null +++ b/assignment5/traceroute.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +"""Python implementation of the Unix traceroute program""" +import argparse +import os +import socket +import struct +import sys +import time + +ICMP_ECHO_REQUEST = 8 +ICMP_ECHO_REPLY = 0 +ICMP_TIME_EXCEEDED = 11 + + +def checksum(str_): + """The ICMP checksum as defined in RFC 792""" + str_ = bytearray(str_) + csum = 0 + count_to = (len(str_) // 2) * 2 + + for count in range(0, count_to, 2): + this_val = str_[count+1] * 256 + str_[count] + csum = csum + this_val + csum = csum & 0xffffffff + + if count_to < len(str_): + csum = csum + str_[-1] + csum = csum & 0xffffffff + + csum = (csum >> 16) + (csum & 0xffff) + csum = csum + (csum >> 16) + answer = ~csum + answer = answer & 0xffff + answer = answer >> 8 | (answer << 8 & 0xff00) + return answer + + +def receive_one_ping(my_socket, rec_id, timeout): + """Attempt to receive a ping. + + Arguments: + my_socket -- a Python socket to receive from + rec_id -- only receive packets with this identifier (in case of echo reply) + timeout -- timeout in seconds + + Return value: + A tuple (responding server, icmp message type) + """ + start_time = time.time() + + while (start_time + timeout - time.time()) > 0: + try: + rec_packet, _ = my_socket.recvfrom(1024) + except socket.timeout: + break # timed out + + # Fetch the ICMPHeader fromt the IP + icmp_header = rec_packet[20:28] + source_ip = '%d.%d.%d.%d' % struct.unpack("BBBB", rec_packet[12:16]) + + icmp_type, _, _, packet_id, _ = struct.unpack("bbHHh", icmp_header) + + if icmp_type == ICMP_TIME_EXCEEDED or \ + icmp_type == ICMP_ECHO_REPLY and packet_id == rec_id: + return (source_ip, icmp_type) + + return None, None + +def send_one_ping(my_socket, dest_addr, snd_id, ttl=30): + """Send an ICMP echo message""" + # Make a dummy packet with a 0 checksum + header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, 0, snd_id, 1) + data = struct.pack("d", time.time()) + + # Calculate the checksum on the data and the dummy header. + my_checksum = socket.htons(checksum(header + data)) + if sys.platform == 'darwin': + my_checksum = my_checksum & 0xffff + + header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, snd_id, 1) + packet = header + data + + my_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl) + my_socket.sendto(packet, (dest_addr, 1)) + + +def do_one_ping(dest_addr, timeout, ttl=30): + """Send one echo and receive either an echo reply or a time exceeded msg""" + icmp = socket.getprotobyname("icmp") + + my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) + my_socket.settimeout(timeout) + + my_id = os.getpid() & 0xffff + send_one_ping(my_socket, dest_addr, my_id, ttl) + response = receive_one_ping(my_socket, my_id, timeout) + + my_socket.close() + return response + + +def traceroute(host, timeout=1.0, hops=10): + """Python version of the Unix traceroute program""" + dest = socket.gethostbyname(host) + print 'traceroute to %s (%s), %d hops max' % (host, dest, hops) + + i = 0 + while i < hops: + i += 1 + address, icmp_type = do_one_ping(dest, timeout, ttl=i) + if address == None: + print '%2d *' % i + else: + name, _, _ = socket.gethostbyaddr(address) + print '%2d %s (%s)' % (i, name, address) + + if icmp_type == ICMP_ECHO_REPLY: + break + + +def main(): + """Wrapper for traceroute to get host from the command line""" + parser = argparse.ArgumentParser(description='Traceroute') + parser.add_argument( + 'host', metavar='HOST', type=str, nargs=1, + help='The host to traceroute to') + parser.add_argument( + '--timeout', type=float, default=1.0, + help='Number of seconds to wait for response to a probe (default 1.0)') + parser.add_argument( + '--hops', type=int, default=10, + help='Maximum number of hops (default 10)') + + args = parser.parse_args() + traceroute(args.host[0], timeout=args.timeout, hops=args.hops) + + +if __name__ == '__main__': + main() + -- cgit v1.2.3 From 8572a90c30cc5d6a31e550307ed64e6ce1b3bf57 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Sun, 8 May 2016 22:46:20 +0200 Subject: Assignment 6 --- assignment6-arp-bootstrap.pcap | Bin 0 -> 4210 bytes assignment6.tex | 115 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 assignment6-arp-bootstrap.pcap create mode 100644 assignment6.tex diff --git a/assignment6-arp-bootstrap.pcap b/assignment6-arp-bootstrap.pcap new file mode 100644 index 0000000..1c83d60 Binary files /dev/null and b/assignment6-arp-bootstrap.pcap differ diff --git a/assignment6.tex b/assignment6.tex new file mode 100644 index 0000000..1e755d1 --- /dev/null +++ b/assignment6.tex @@ -0,0 +1,115 @@ +\documentclass[a4paper,9pt]{article} + +\author{Camil Staps\\\small{s4498062}} +\title{Networking\\\large{Assignment 6}} +\date{May 9, 2016} + +\usepackage{polyglossia} +\setmainlanguage{english} +\usepackage{geometry} +\usepackage{amsmath} +\usepackage{enumitem} +\usepackage{minted} + +\begin{document} + +\maketitle + +\section{IP Fragmentation} +\begin{enumerate} + \item The necessary IP headers add 20 bytes overhead, so we can send + $700-20=680$ bytes per fragment. Then we need + $\left\lceil\frac{2400}{680}\right\rceil=4$ fragments. + \item Length, fragmentation flag, fragmentation offset, identifier. + \item + \begin{tabular}{l | l | l | l} + Length & ID & Frag. flag & Frag. offset \\\hline + 700 & 422 & 1 & 0 \\ + 700 & 422 & 1 & 85 \\ + 700 & 422 & 1 & 170 \\ + 380 & 422 & 0 & 255 \\ + \end{tabular} +\end{enumerate} + +\section{Distance vector} +\begin{enumerate} + \item $\{\text{w}\mapsto2, \text{y}\mapsto5, \text{u}\mapsto7\}$. + \item $c(x,w) \leftarrow 1$. + \item $c(x,y) \leftarrow 4$. +\end{enumerate} + +\section{Inter-AS routing with BGP} +\begin{enumerate} + \item + To B: (i) for network W, AS-Path A, some next hop; (ii) for network V, + AS-Path A, some next hop. \\ + To C: for network V, AS-Path A, some next hop. + \item + From A: for network V, AS-Path A, some next hop. \\ + From B: (i) for network X, AS-Path B, some next hop; (ii) for network W, + AS-Path B-A, some next hop; (iii) for network V, AS-Path B-A, some next + hop. +\end{enumerate} + +\clearpage +\section{DHCP Wireshark} +\bgroup\footnotesize\begin{verbatim} +34 192.168.1.70 192.168.1.1 DHCP 342 DHCP Release - Transaction ID 0xbff3c458 +37 0.0.0.0 255.255.255.255 DHCP 342 DHCP Discover - Transaction ID 0x19a9e831 +38 192.168.1.1 192.168.1.70 DHCP 316 DHCP Offer - Transaction ID 0x19a9e831 +39 0.0.0.0 255.255.255.255 DHCP 342 DHCP Request - Transaction ID 0x19a9e831 +40 192.168.1.1 192.168.1.70 DHCP 316 DHCP ACK - Transaction ID 0x19a9e831 +75 192.168.1.70 192.168.1.1 DHCP 342 DHCP Release - Transaction ID 0x3cb8ec3a +79 0.0.0.0 255.255.255.255 DHCP 342 DHCP Discover - Transaction ID 0x26644c66 +84 192.168.1.1 192.168.1.70 DHCP 316 DHCP Offer - Transaction ID 0x26644c66 +85 0.0.0.0 255.255.255.255 DHCP 342 DHCP Request - Transaction ID 0x26644c66 +86 192.168.1.1 192.168.1.70 DHCP 316 DHCP ACK - Transaction ID 0x26644c66 +\end{verbatim}\egroup + +\begin{enumerate}[label=\alph*)] + \item \texttt{60:57:18:b1:e8:8d}; bytes 6-11 of the first packet. + \item Of course, the DHCP message type has been changed from discover to + request. Also, option 54 (Server Identifier) has been added. + \item This option has the value \texttt{192.168.1.1}. + \item 1 day (86400s); found in the end of packet 40. + \item The client uses port \texttt{68}; the server \texttt{67}. The client + requests and gets \texttt{192.168.1.70}; the server uses + \texttt{192.168.1.1}. Before the client receives its IP and knows the IP of + the DHCP server, it sends broadcasts from \texttt{0.0.0.0} to + \texttt{255.255.255.255}. + \item \texttt{0x19a9e831}; \texttt{0x26644c66}. The purpose of this field is + `to associate messages and responses between a client and a server' (RFC + 2131). This is necessary because we need to maintain a state. The client + should know what request is being answered to when it receives a response. + \item The server does not acknowledge the release. It is strictly speaking + not necessary (to explicitly release), however, can be useful to prevent + denial of service: since the subnet here is \texttt{255.255.255.0} we have + only 255 addresses to give out. If addresses were never released, it could + easily happen that all addresses are used (if some client has a problem and + requests new addresses all the time). In any case, releasing addresses + should be seen as a service clients may/should provide the server with. + \item + \bgroup\footnotesize\begin{verbatim} +46 IntelCor_b1:e8:8d Broadcast ARP 42 Who has 192.168.1.1? Tell 192.168.1.70 +47 ZyxelCom_c5:63:e1 IntelCor_b1:e8:8d ARP 42 192.168.1.1 is at 4c:9e:ff:c5:63:e1 + \end{verbatim}\egroup + + (There are some other ARP packets in the capture, but they are from an + unrelated host.) + + This is after the DHCP ACK. It seems superfluous, because the DHCP ACK is + coming from \texttt{192.168.1.1}, so the client could get the information + it requests in packet 46 from packet 40. Therefore, this packet is probably + sent from a different layer or program. I'm guessing it has to do with + \texttt{192.168.1.1} being advertised as router and/or DNS server. The + client receives this information on another level than where it makes the + DHCP requests, and therefore doesn't recognise that it already received the + information implicitly. It then uses ARP to find the link layer address of + the DNS server and/or the router that is advertised in the ACK. +\end{enumerate} + +Note: for privacy reasons, I only provide captures of the bootstrap and ARP +protocols. + +\end{document} + -- cgit v1.2.3 From d444f1f955381741de43be6ecc5054e5306f5893 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Sun, 22 May 2016 17:48:05 +0200 Subject: Start assignment 7 --- assignment7.tex | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 assignment7.tex diff --git a/assignment7.tex b/assignment7.tex new file mode 100644 index 0000000..bf9ea1e --- /dev/null +++ b/assignment7.tex @@ -0,0 +1,121 @@ +\documentclass[a4paper,9pt]{article} + +\author{Camil Staps\\\small{s4498062}} +\title{Networking\\\large{Assignment 7}} +\date{May 22, 2016} + +\usepackage{polyglossia} +\setmainlanguage{english} +\usepackage{geometry} +\usepackage[hidelinks]{hyperref} +\usepackage{amsmath} +\usepackage{enumitem} +\usepackage{subcaption} +\usepackage{verbatimbox} + +\begin{document} + +\maketitle + +\section{Cyclic Redundancy Checks} +\begin{enumerate}[label=\alph*)] + \item It computes the remainder of $(D\cdot2^r\oplus R) \div G$, and checks + that it equals $0$. + \item See \autoref{fig:longdiv:b}. The answer is \texttt{111}. + \item See \autoref{fig:longdiv:c}. The answer is \texttt{010}. + \item See \autoref{fig:longdiv:d}. The answer is \texttt{111}. +\end{enumerate} + +\begin{figure}[h] + \small + \begin{subfigure}[b]{.32\linewidth} + \centering + \begin{verbbox} + 1001010101 000 + 1001 + 0000010101 000 + 1001 + 0000000111 000 + 100 1 + 0000000011 100 + 10 01 + 0000000001 110 + 1 001 + 0000000000 111 + \end{verbbox} + \theverbbox% + \caption{\label{fig:longdiv:b}} + \end{subfigure} + \begin{subfigure}[b]{.32\linewidth} + \centering + \begin{verbbox} + 0101101010 000 + 1001 + 0001001010 000 + 1001 + 0000000010 000 + 10 01 + 0000000000 010 + \end{verbbox} + \theverbbox% + \caption{\label{fig:longdiv:c}} + \end{subfigure} + \begin{subfigure}[b]{.32\linewidth} + \centering + \begin{verbbox} + 1010100000 000 + 1001 + 0011100000 000 + 1001 + 0001110000 000 + 1001 + 0000111000 000 + 1001 + 0000011100 000 + 1001 + 0000001110 000 + 1001 + 0000000111 000 + 100 1 + 0000000011 100 + 10 01 + 0000000001 110 + 1 001 + 0000000000 111 + \end{verbbox} + \theverbbox% + \caption{\label{fig:longdiv:d}} + \end{subfigure} + \caption{Computing CRC using long division\label{fig:longdiv}} +\end{figure} + +\section{Address Resolution Protocol} +\begin{enumerate} + \item + A --- 192.168.1.100; B --- 192.168.1.200;\\ + C --- 192.168.2.100; D --- 192.168.2.200;\\ + E --- 192.168.3.100; F --- 192.168.3.200. + \item We call the left router R and the right router S. Let $R_i$ be the + interface of router $R$ for subnet $i$. Then: + + A --- 00.00.00.00.00.A0; B --- 00.00.00.00.00.B0;\\ + C --- 00.00.00.00.00.C0; D --- 00.00.00.00.00.D0;\\ + E --- 00.00.00.00.00.E0; F --- 00.00.00.00.00.F0;\\ + R$_1$ --- 88.88.88.00.00.00; R$_2$ --- 88.88.88.00.88.00;\\ + S$_2$ --- 88.88.88.88.00.00; S$_3$ --- 88.88.88.88.88.00. + \item %todo + \item %todo +\end{enumerate} + +\section{Self-learning} +\begin{enumerate}[label=(\roman*)] + \item %todo + \item %todo + \item %todo + \item %todo +\end{enumerate} + +\section{All things learned put together} +%todo + +\end{document} -- cgit v1.2.3 From 0db11906cecfed86b8b1fb4168543624e6418f6e Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 23 May 2016 21:51:53 +0200 Subject: Assignment 7, deel 2 --- assignment7.tex | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/assignment7.tex b/assignment7.tex index bf9ea1e..5be9ff5 100644 --- a/assignment7.tex +++ b/assignment7.tex @@ -91,20 +91,55 @@ \section{Address Resolution Protocol} \begin{enumerate} - \item - A --- 192.168.1.100; B --- 192.168.1.200;\\ - C --- 192.168.2.100; D --- 192.168.2.200;\\ - E --- 192.168.3.100; F --- 192.168.3.200. \item We call the left router R and the right router S. Let $R_i$ be the interface of router $R$ for subnet $i$. Then: + A --- 192.168.1.100; B --- 192.168.1.200;\\ + C --- 192.168.2.100; D --- 192.168.2.200;\\ + E --- 192.168.3.100; F --- 192.168.3.200;\\ + R$_1$ --- 192.168.1.1; R$_2$ --- 192.168.2.1;\\ + S$_2$ --- 192.168.2.2; S$_3$ --- 192.168.3.2;\\ + + \item A --- 00.00.00.00.00.A0; B --- 00.00.00.00.00.B0;\\ C --- 00.00.00.00.00.C0; D --- 00.00.00.00.00.D0;\\ E --- 00.00.00.00.00.E0; F --- 00.00.00.00.00.F0;\\ R$_1$ --- 88.88.88.00.00.00; R$_2$ --- 88.88.88.00.88.00;\\ S$_2$ --- 88.88.88.88.00.00; S$_3$ --- 88.88.88.88.88.00. - \item %todo - \item %todo + + \item Host E looks up the MAC address of the first hop router (S$_3$), and + sends the datagram there. Router S receives the datagram, figures out that + the next hop is router R, looks up the MAC address of R$_2$ and sends the + datagram there. Router R then forwards the datagram in a frame to host B on + interface R$_1$. Concretely, the following frames are sent: + + \begin{table}[h] + \centering + \begin{tabular}{l | l | l | l} + Src. IP & Src. MAC & Dest. IP & Dest. MAC \\\hline + 192.168.3.100 & 00.00.00.00.00.E0 & 192.168.3.2 & 88.88.88.88.88.00\\ + 192.168.2.2 & 88.88.88.88.88.00 & 192.168.2.1 & 88.88.88.00.88.00\\ + 192.168.1.1 & 88.88.88.00.88.00 & 192.168.1.200 & 00.00.00.00.00.B0\\ + \end{tabular} + \end{table} + + \item The same happens, but before E can send the frame to S$_3$, it has to + get to know its address. It sends an ARP request to the broadcast address + with the request for 192.168.3.2's (that is, S$_3$'s) MAC address. The + router replies with an ARP packet. After this, E has all the addresses it + needs (and all other nodes as well), so the same as above happens. + + \begin{table}[h] + \centering + \begin{tabular}{l | l | l | l | l} + Src. IP & Src. MAC & Dest. IP & Dest. MAC & Protocol\\\hline + 192.168.3.100 & 00.00.00.00.00.E0 & 192.168.3.2 & FF.FF.FF.FF.FF.FF & ARP\\ + 192.168.3.2 & 88.88.88.88.88.00 & 192.168.3.100 & 00.00.00.00.00.E0 & ARP\\ + 192.168.3.100 & 00.00.00.00.00.E0 & 192.168.3.2 & 88.88.88.88.88.00 & IP\\ + 192.168.2.2 & 88.88.88.88.88.00 & 192.168.2.1 & 88.88.88.00.88.00 & IP\\ + 192.168.1.1 & 88.88.88.00.88.00 & 192.168.1.200 & 00.00.00.00.00.B0 & IP\\ + \end{tabular} + \end{table} \end{enumerate} \section{Self-learning} -- cgit v1.2.3 From 7500f59ce0dbd8d376c008001dd638ad2844ab2b Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 23 May 2016 22:01:31 +0200 Subject: Framework 2 --- project2/proj2_s4498062/.gitignore | 1 + project2/proj2_s4498062/README.md | 45 +++++ project2/proj2_s4498062/dns/__init__.py | 1 + project2/proj2_s4498062/dns/cache.py | 89 ++++++++++ project2/proj2_s4498062/dns/classes.py | 55 ++++++ project2/proj2_s4498062/dns/domainname.py | 127 ++++++++++++++ project2/proj2_s4498062/dns/message.py | 278 ++++++++++++++++++++++++++++++ project2/proj2_s4498062/dns/rcodes.py | 81 +++++++++ project2/proj2_s4498062/dns/resolver.py | 72 ++++++++ project2/proj2_s4498062/dns/resource.py | 241 ++++++++++++++++++++++++++ project2/proj2_s4498062/dns/server.py | 53 ++++++ project2/proj2_s4498062/dns/types.py | 67 +++++++ project2/proj2_s4498062/dns/zone.py | 54 ++++++ project2/proj2_s4498062/dns_client.py | 28 +++ project2/proj2_s4498062/dns_server.py | 28 +++ project2/proj2_s4498062/dns_tests.py | 34 ++++ 16 files changed, 1254 insertions(+) create mode 100644 project2/proj2_s4498062/.gitignore create mode 100644 project2/proj2_s4498062/README.md create mode 100644 project2/proj2_s4498062/dns/__init__.py create mode 100644 project2/proj2_s4498062/dns/cache.py create mode 100644 project2/proj2_s4498062/dns/classes.py create mode 100644 project2/proj2_s4498062/dns/domainname.py create mode 100644 project2/proj2_s4498062/dns/message.py create mode 100644 project2/proj2_s4498062/dns/rcodes.py create mode 100644 project2/proj2_s4498062/dns/resolver.py create mode 100644 project2/proj2_s4498062/dns/resource.py create mode 100644 project2/proj2_s4498062/dns/server.py create mode 100644 project2/proj2_s4498062/dns/types.py create mode 100644 project2/proj2_s4498062/dns/zone.py create mode 100644 project2/proj2_s4498062/dns_client.py create mode 100644 project2/proj2_s4498062/dns_server.py create mode 100644 project2/proj2_s4498062/dns_tests.py diff --git a/project2/proj2_s4498062/.gitignore b/project2/proj2_s4498062/.gitignore new file mode 100644 index 0000000..94487b9 --- /dev/null +++ b/project2/proj2_s4498062/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/project2/proj2_s4498062/README.md b/project2/proj2_s4498062/README.md new file mode 100644 index 0000000..ce89073 --- /dev/null +++ b/project2/proj2_s4498062/README.md @@ -0,0 +1,45 @@ +# Project 2 Framework + +## Description + +This directory contains a framework for a DNS resolver and a recursive DNS server. +The framework provides classes for manipulating DNS messages (and converting them to bytes). +The framework also contains a few stubs which you need to implement. +Most files contain pointers to the relevant sections of RFC 1034 and RFC 1035. +These are not the only relevant sections though, and you might need to read more of the RFCs. + +It is probably a good idea to read RFC 1034 before proceeding. +This RFC explains an overview of DNS and introduces some of the naming which is also used in the framework. + +## File structure + +* proj1_sn1_sn2 + * dns + * cache.py: Contains a cache for the resolver. You have to implement this. + * classes.py: Enum of CLASSes and QCLASSes. + * domainname.py: Classes for reading and writing domain names as bytes. + * message.py: Classes for DNS messages. + * rcodes.py: Enum of RCODEs. + * resolver.py: Class for a DNS resolver. You have to implement this. + * resource.py: Classes for DNS resource records. + * server.py: Contains a DNS server. You have to implement this. + * types.py: Enum of TYPEs and QTYPEs. + * zone.py: name space zones. You have to implement this. + * dns_client.py: A simple DNS client, which serves as an example user of the resolver. + * dns_server.py: Code for starting the DNS server and parsing args. + * dns_tests.py: Tests for your resolver, cache and server. You have to implement this. + +## Implementation Hints and Tips + +You should start with implementing the resolver, which you need for the server. +You will need message.py, resource.py, types.py, classes.py and rcodes.py. +You can ignore the code for converting from and to bytes from these files if +you want, but it might be useful (especially for debugging). + +After finishing the resolver you need to implement caching and the DNS server. +You can implement these in any order that you like. +I suggest implementing the recursive part (the resolving) of your DNS server, before implementing the management of the servers zone. + +Wireshark and dns_client.py are useful tools for debugging your resolver. +Wireshark and nslookup are useful tools for debugging your server. + diff --git a/project2/proj2_s4498062/dns/__init__.py b/project2/proj2_s4498062/dns/__init__.py new file mode 100644 index 0000000..18ff536 --- /dev/null +++ b/project2/proj2_s4498062/dns/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python2 diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py new file mode 100644 index 0000000..e148d3a --- /dev/null +++ b/project2/proj2_s4498062/dns/cache.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python2 + +"""A cache for resource records + +This module contains a class which implements a cache for DNS resource records, +you still have to do most of the implementation. The module also provides a +class and a function for converting ResourceRecords from and to JSON strings. +It is highly recommended to use these. +""" + +import json + +from dns.resource import ResourceRecord, RecordData +from dns.types import Type +from dns.classes import Class + + +class ResourceEncoder(json.JSONEncoder): + """ Conver ResourceRecord to JSON + + Usage: + string = json.dumps(records, cls=ResourceEncoder, indent=4) + """ + def default(self, obj): + if isinstance(obj, ResourceRecord): + return { + "name": obj.name, + "type": Type.to_string(obj.type_), + "class": Class.to_string(obj.class_), + "ttl": obj.ttl, + "rdata": obj.rdata.data + } + return json.JSONEncoder.default(self, obj) + + +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"]) + return ResourceRecord(name, type_, class_, ttl, rdata) + + +class RecordCache(object): + """ Cache for ResourceRecords """ + + def __init__(self, ttl): + """ Initialize the RecordCache + + Args: + ttl (int): TTL of cached entries (if > 0) + """ + self.records = [] + self.ttl = ttl + + 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 + """ + pass + + def add_record(self, record): + """ Add a new Record to the cache + + Args: + record (ResourceRecord): the record added to the cache + """ + pass + + def read_cache_file(self): + """ Read the cache file from disk """ + pass + + def write_cache_file(self): + """ Write the cache file to disk """ + pass diff --git a/project2/proj2_s4498062/dns/classes.py b/project2/proj2_s4498062/dns/classes.py new file mode 100644 index 0000000..2a6a67d --- /dev/null +++ b/project2/proj2_s4498062/dns/classes.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python2 + +""" DNS CLASS and QCLASS values + +This module contains an Enum of CLASS and QCLASS values. The Enum also contains +a method for converting values to strings. See sections 3.2.4 and 3.2.5 of RFC +1035 for more information. +""" + + +class Class(object): + """ Enum of CLASS and QCLASS values + + Usage: + >>> Class.IN + 1 + >>> Class.ANY + 255 + """ + + IN = 1 + CS = 2 + CH = 3 + HS = 4 + ANY = 255 + + by_string = { + "IN": IN, + "CS": CS, + "CH": CH, + "HS": HS, + "*": ANY + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(class_): + """ Convert a Class to a string + + Usage: + >>> Class.to_string(Class.IN) + 'IN' + """ + return Class.by_value[class_] + + @staticmethod + def from_string(string): + """ Convert a string to a Class + + Usage: + >>> Class.from_string('IN') + 1 + """ + return Class.by_string[string] diff --git a/project2/proj2_s4498062/dns/domainname.py b/project2/proj2_s4498062/dns/domainname.py new file mode 100644 index 0000000..8876b48 --- /dev/null +++ b/project2/proj2_s4498062/dns/domainname.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python2 + +""" Parsing and composing domain names + +This module contains two classes for converting domain names to and from bytes. +You won't have to use these classes. They're used internally in Message, +Question, ResourceRecord and RecordData. You can read section 4.1.4 of RFC 1035 +if you want more info. +""" + +import struct + + +class Composer(object): + """ Converts a string representation of a domain name to bytes """ + + def __init__(self): + self.offsets = dict() + + def to_bytes(self, offset, dnames): + # Convert each domain name in to bytes + result = b"" + for dname in dnames: + # Split domain name into labels + labels = dname.split(".") + + # Determine keys of subdomains in offset dict + keys = [] + for label in reversed(labels): + name = label + if keys: + name += "." + keys[-1] + keys.append(name) + keys.reverse() + + # Convert label to bytes + add_null = True + for j, label in enumerate(labels): + if keys[j] in self.offsets: + offset = self.offsets[keys[j]] + pointer = (3 << 14) + offset + result += struct.pack("!H", pointer) + add_null = False + offset += 2 + break + else: + self.offsets[keys[j]] = offset + result += struct.pack("!B{}s".format(len(label)), + len(label), + label) + offset += 1 + len(label) + + # Add null character at end + if add_null: + result += b"\x00" + offset += 1 + + return result + + +class Parser(object): + """ Convert byte representations of domain names to strings """ + + def __init__(self): + self.labels = dict() + + def from_bytes(self, packet, offset, num): + """ Convert domain name from bytes to string + + Args: + packet (bytes): packet containing the domain name + offset (int): offset of domain name in packet + num (int): number of domain names to decode + + Returns: + str, int + """ + + dnames = [] + + # Read the domain names + for _ in range(num): + # Read a new domain name + dname = "" + prev_offsets = [] + done = False + while done is False: + # Read length of next label + llength = struct.unpack_from("!B", packet, offset)[0] + + # Done reading domain when length is zero + if llength == 0: + offset += 1 + break + + # Compression label + elif (llength >> 6) == 3: + new_offset = offset + 2 + target = struct.unpack_from("!H", packet, offset)[0] + target -= 3 << 14 + label = self.labels[target] + done = True + + # Normal label + else: + new_offset = offset + llength + 1 + label = struct.unpack_from("{}s".format(llength), + packet, offset+1)[0] + + # Add label to dictionary + self.labels[offset] = label + for prev_offset in prev_offsets: + self.labels[prev_offset] += "." + label + prev_offsets.append(offset) + + # Update offset + offset = new_offset + + # Append label to domain name + if len(dname) > 0: + dname += "." + dname += label + + # Append domain name to list + dnames.append(dname) + + return dnames, offset diff --git a/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py new file mode 100644 index 0000000..54933b7 --- /dev/null +++ b/project2/proj2_s4498062/dns/message.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python2 + +""" DNS messages + +This module contains classes for DNS messages, their header section and +question fields. See section 4 of RFC 1035 for more info. +""" + +import struct + +from dns.domainname import Parser, Composer +from dns.resource import ResourceRecord + + +class Message(object): + """ DNS message """ + + def __init__(self, header, questions=None, answers=None, authorities=None, additionals=None): + """ Create a new DNS message + + Args: + header (Header): the header section + questions ([Question]): the question section + answers ([ResourceRecord]): the answer section + authorities ([ResourceRecord]): the authority section + additionals ([ResourceRecord]): the additional section + """ + if questions is None: + questions = [] + if answers is None: + answers = [] + if authorities is None: + authorities = [] + if additionals is None: + additionals = [] + + self.header = header + self.questions = questions + self.answers = answers + self.authorities = authorities + self.additionals = additionals + + @property + def resources(self): + """ Getter for all resource records """ + return self.answers + self.authorities + self.additionals + + def to_bytes(self): + """ Convert Message to bytes """ + composer = Composer() + + # Add header + result = self.header.to_bytes() + + # Add questions + for question in self.questions: + offset = len(result) + result += question.to_bytes(offset, composer) + + # Add answers + for answer in self.answers: + offset = len(result) + result += answer.to_bytes(offset, composer) + + # Add authorities + for authority in self.authorities: + offset = len(result) + result += authority.to_bytes(offset, composer) + + # Add additionals + for additional in self.additionals: + offset = len(result) + result += additional.to_bytes(offset, composer) + + return result + + @classmethod + 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) + + +class Header(object): + """ The header section of a DNS message + + Contains a number of properties which are accessible as normal member + variables. + + See section 4.1.1 of RFC 1035 for their meaning. + """ + + def __init__(self, ident, flags, qd_count, an_count, ns_count, ar_count): + """ Create a new Header object + + Args: + ident (int): identifier + qd_count (int): number of entries in question section + an_count (int): number of entries in answer section + ns_count (int): number of entries in authority section + ar_count (int): number of entries in additional section + """ + self.ident = ident + self._flags = flags + self.qd_count = qd_count + self.an_count = an_count + self.ns_count = ns_count + self.ar_count = ar_count + + def to_bytes(self): + """ Convert header to bytes """ + return struct.pack("!6H", + self.ident, + self._flags, + self.qd_count, + self.an_count, + self.ns_count, + self.ar_count) + + @classmethod + def from_bytes(cls, packet): + """ Convert Header from bytes """ + if len(packet) < 12: + raise ValueError("header is too short") + return cls(*struct.unpack_from("!6H", packet)) + + @property + def flags(self): + return self._flags + @flags.setter + def flags(self, value): + if value >= (1 << 16): + raise ValueError("value too big for flags") + self._flags = value + + @property + def qr(self): + return self._flags & (1 << 15) + @qr.setter + def qr(self, value): + if value: + self._flags |= (1 << 15) + else: + self._flags &= ~(1 << 15) + + @property + def opcode(self): + return (self._flags & (((1 << 4) - 1) << 11)) >> 11 + @opcode.setter + def opcode(self, value): + if value > 0b1111: + raise ValueError("invalid opcode") + self._flags &= ~(((1 << 4) - 1) << 11) + self._flags |= value << 11 + + @property + def aa(self): + return self._flags & (1 << 10) + @aa.setter + def aa(self, value): + if value: + self._flags |= (1 << 10) + else: + self._flags &= ~(1 << 10) + + @property + def tc(self): + return self._flags & (1 << 9) + @tc.setter + def tc(self, value): + if value: + self._flags |= (1 << 9) + else: + self._flags &= ~(1 << 9) + + @property + def rd(self): + return self._flags & (1 << 8) + @rd.setter + def rd(self, value): + if value: + self._flags |= (1 << 8) + else: + self._flags &= ~(1 << 8) + + @property + def ra(self): + return self._flags & (1 << 7) + @ra.setter + def ra(self, value): + if value: + self._flags |= (1 << 7) + else: + self._flags &= ~(1 << 7) + + @property + def z(self): + return self._flags & (((1 << 3) - 1) << 4) >> 4 + @z.setter + def z(self, value): + if value: + raise ValueError("non-zero zero flag") + + @property + def rcode(self): + return self._flags & ((1 << 4) - 1) + @rcode.setter + def rcode(self, value): + if value > 0b1111: + raise ValueError("invalid return code") + self._flags &= ~((1 << 4) - 1) + self._flags |= value + + +class Question(object): + """ An entry in the question section. + + See section 4.1.2 of RFC 1035 for more info. + """ + + def __init__(self, qname, qtype, qclass): + """ Create a new entry in the question section + + Args: + qname (str): QNAME + qtype (Type): QTYPE + qclass (Class): QCLASS + """ + self.qname = qname + self.qtype = qtype + self.qclass = qclass + + def to_bytes(self, offset, composer): + """ Convert Question to bytes """ + bqname = composer.to_bytes(offset, [self.qname]) + bqtype = struct.pack("!H", self.qtype) + bqclass = struct.pack("!H", self.qclass) + return bqname + bqtype + bqclass + + @classmethod + def from_bytes(cls, packet, offset, parser): + """ Convert Question from bytes """ + qnames, offset = parser.from_bytes(packet, offset, 1) + qname = qnames[0] + qtype, qclass = struct.unpack_from("!2H", packet, offset) + return cls(qname, qtype, qclass), offset + 4 diff --git a/project2/proj2_s4498062/dns/rcodes.py b/project2/proj2_s4498062/dns/rcodes.py new file mode 100644 index 0000000..4b1d33d --- /dev/null +++ b/project2/proj2_s4498062/dns/rcodes.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python2 + +""" DNS RCODE values + +This module contains an Enum of RCODE values. See section 4.1.4 of RFC 1035 for +more info. +""" + +class RCode(object): + """ Enum of RCODE values + + Usage: + >>> NoError + 0 + >>> NXDomain + 3 + """ + + NoError = 0 + FormErr = 1 + ServFail = 2 + NXDomain = 3 + NotImp = 4 + Refused = 5 + YXDomain = 6 + YXRRSet = 7 + NXRRSet = 8 + NotAuth = 9 + NotZone = 10 + BADVERS = 16 + BADSIG = 16 + BADKEY = 17 + BADTIME = 18 + BADMODE = 19 + BADNAME = 20 + BADALG = 21 + BADTRUNC = 22 + + by_string = { + "NoError": NoError, + "FormErr": FormErr, + "ServFail": ServFail, + "NXDomain": NXDomain, + "NotImp": NotImp, + "Refused": Refused, + "YXDomain": YXDomain, + "YXRRSet": YXRRSet, + "NXRRSet": NXRRSet, + "NotAuth": NotAuth, + "NotZone": NotZone, + "BADVERS": BADVERS, + "BADSIG": BADSIG, + "BADKEY": BADKEY, + "BADTIME": BADTIME, + "BADMODE": BADMODE, + "BADNAME": BADNAME, + "BADALG": BADALG, + "BADTRUNC": BADTRUNC + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(rcode): + """ Convert an RCode to a string + + Usage: + >>> RCode.to_string(RCode.NoError) + 'NoError' + """ + return RCode.by_value[rcode] + + @staticmethod + def from_string(string): + """ Convert a string to an RCode + + Usage: + >>> RCode.from_string('NoError') + 0 + """ + return RCode.by_string[string] diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py new file mode 100644 index 0000000..ba27f74 --- /dev/null +++ b/project2/proj2_s4498062/dns/resolver.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python2 + +""" DNS Resolver + +This module contains a class for resolving hostnames. You will have to implement +things in this module. This resolver will be both used by the DNS client and the +DNS server, but with a different list of servers. +""" + +import socket + +from dns.classes import Class +from dns.types import Type + +import dns.cache +import dns.message +import dns.rcodes + +class Resolver(object): + """ DNS resolver """ + + def __init__(self, caching, ttl): + """ Initialize the resolver + + Args: + caching (bool): caching is enabled if True + ttl (int): ttl of cache entries (if > 0) + """ + self.caching = caching + self.ttl = ttl + + def gethostbyname(self, hostname): + """ Translate a host name to IPv4 address. + + Currently this method contains an example. You will have to replace + this example with example with the algorithm described in section + 5.3.3 in RFC 1034. + + Args: + hostname (str): the hostname to resolve + + Returns: + (str, [str], [str]): (hostname, aliaslist, ipaddrlist) + """ + timeout = 2 + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + + # Create and send query + question = dns.message.Question(hostname, Type.A, Class.IN) + header = dns.message.Header(9001, 0, 1, 0, 0, 0) + header.qr = 0 + header.opcode = 0 + header.rd = 1 + query = dns.message.Message(header, [question]) + sock.sendto(query.to_bytes(), ("8.8.8.8", 53)) + + # Receive response + data = sock.recv(512) + response = dns.message.Message.from_bytes(data) + + # Get data + aliases = [] + for additional in response.additionals: + if additional.type_ == Type.CNAME: + aliases.append(additional.rdata.data) + addresses = [] + for answer in response.answers: + if answer.type_ == Type.A: + addresses.append(answer.rdata.data) + + return hostname, aliases, addresses diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py new file mode 100644 index 0000000..fdf51de --- /dev/null +++ b/project2/proj2_s4498062/dns/resource.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python2 + +""" A DNS resource record + +This class contains classes for DNS resource records and record data. This +module is fully implemented. You will have this module in the implementation +of your resolver and server. +""" + +import socket +import struct + +from dns.types import Type + + +class ResourceRecord(object): + """ DNS resource record """ + def __init__(self, name, type_, class_, ttl, rdata): + """ Create a new resource record + + Args: + name (str): domain name + type_ (Type): the type + class_ (Class): the class + rdata (RecordData): the record data + """ + self.name = name + self.type_ = type_ + self.class_ = class_ + self.ttl = ttl + self.rdata = rdata + + def to_bytes(self, offset, composer): + """ Convert ResourceRecord to bytes """ + record = composer.to_bytes(offset, [self.name]) + record += struct.pack("!HHI", self.type_, self.class_, self.ttl) + offset += len(record) + 2 + rdata = self.rdata.to_bytes(offset, composer) + record += struct.pack("!H", len(rdata)) + rdata + return record + + @classmethod + def from_bytes(cls, packet, offset, parser): + """ Convert ResourceRecord from bytes """ + names, offset = parser.from_bytes(packet, offset, 1) + name = names[0] + type_, class_, ttl, rdlength = struct.unpack_from("!HHIH", packet, offset) + offset += 10 + rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) + offset += rdlength + return cls(name, type_, class_, ttl, rdata), offset + + +class RecordData(object): + """ Record Data """ + + def __init__(self, data): + """ Initialize the record data + + Args: + data (str): data + """ + self.data = data + + @staticmethod + def create(type_, data): + """ Create a RecordData object from bytes + + Args: + type_ (Type): type + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + classdict = { + Type.A: ARecordData, + Type.CNAME: CNAMERecordData, + Type.NS: NSRecordData, + Type.AAAA: AAAARecordData + } + if type_ in classdict: + return classdict[type_](data) + else: + return GenericRecordData(data) + + @staticmethod + def from_bytes(type_, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + type_ (Type): type + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + classdict = { + Type.A: ARecordData, + Type.CNAME: CNAMERecordData, + Type.NS: NSRecordData, + Type.AAAA: AAAARecordData + } + if type_ in classdict: + return classdict[type_].from_bytes( + packet, offset, rdlength, parser) + else: + return GenericRecordData.from_bytes( + packet, offset, rdlength, parser) + + +class ARecordData(RecordData): + """ Record data for A type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return socket.inet_aton(self.data) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = socket.inet_ntoa(packet[offset:offset+4]) + return cls(data) + + +class CNAMERecordData(RecordData): + """ Record data for CNAME type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return composer.to_bytes(offset, [self.data]) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + names, offset = parser.from_bytes(packet, offset, 1) + data = names[0] + return cls(data) + + +class NSRecordData(RecordData): + """ Record data for NS type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return composer.to_bytes(offset, [self.data]) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + names, offset = parser.from_bytes(packet, offset, 1) + data = names[0] + return cls(data) + + +class AAAARecordData(RecordData): + """ Record data for AAAA type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return socket.inet_pton(socket.AF_INET6, self.data) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = socket.inet_ntop(socket.AF_INET6, packet[offset:offset+16]) + return cls(data) + + +class GenericRecordData(RecordData): + """ Generic Record Data (for other types) """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return self.data + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = packet[offset:offset+rdlength] + return cls(data) diff --git a/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py new file mode 100644 index 0000000..d4e3109 --- /dev/null +++ b/project2/proj2_s4498062/dns/server.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python2 + +""" A recursive DNS server + +This module provides a recursive DNS server. You will have to implement this +server using the algorithm described in section 4.3.2 of RFC 1034. +""" + +from threading import Thread + + +class RequestHandler(Thread): + """ A handler for requests to the DNS server """ + + def __init__(self): + """ Initialize the handler thread """ + super(RequestHandler, self).__init__() + self.daemon = True + + def run(self): + """ Run the handler thread """ + # TODO: Handle DNS request + pass + + +class Server(object): + """ A recursive DNS server """ + + def __init__(self, port, caching, ttl): + """ Initialize the server + + Args: + port (int): port that server is listening on + caching (bool): server uses resolver with caching if true + ttl (int): ttl for records (if > 0) of cache + """ + self.caching = caching + self.ttl = ttl + self.port = port + self.done = False + # TODO: create socket + + def serve(self): + """ Start serving request """ + # TODO: start listening + while not self.done: + # TODO: receive request and open handler + pass + + def shutdown(self): + """ Shutdown the server """ + self.done = True + # TODO: shutdown socket diff --git a/project2/proj2_s4498062/dns/types.py b/project2/proj2_s4498062/dns/types.py new file mode 100644 index 0000000..f918050 --- /dev/null +++ b/project2/proj2_s4498062/dns/types.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python2 + +""" DNS TYPE and QTYPE values + +This module contains an Enum for TYPE and QTYPE values. This Enum also contains +a method for converting Enum values to strings. See sections 3.2.2 and 3.2.3 of +RFC 1035 for more information. +""" + +class Type(object): + """ DNS TYPE and QTYPE + + Usage: + >>> Type.A + 1 + >>> Type.CNAME + 5 + """ + A = 1 + NS = 2 + CNAME = 5 + SOA = 6 + WKS = 11 + PTR = 12 + HINFO = 13 + MINFO = 14 + MX = 15 + TXT = 16 + AAAA = 28 + ANY = 255 + + by_string = { + "A": A, + "NS": NS, + "CNAME": CNAME, + "SOA": SOA, + "WKS": WKS, + "PTR": PTR, + "HINFO": HINFO, + "MINFO": MINFO, + "MX": MX, + "TXT": TXT, + "AAAA": AAAA, + "*": ANY + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(type_): + """ Convert a Type to a string + + Usage: + >>> Type.to_string(Type.A) + 'A' + """ + return Type.by_value[type_] + + @staticmethod + def from_string(string): + """ Convert a string to a Type + + Usage: + >>> Type.from_string('CNAME') + 5 + """ + return Type.by_string[string] diff --git a/project2/proj2_s4498062/dns/zone.py b/project2/proj2_s4498062/dns/zone.py new file mode 100644 index 0000000..8ada3db --- /dev/null +++ b/project2/proj2_s4498062/dns/zone.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python2 + +""" Zones of domain name space + +See section 6.1.2 of RFC 1035 and section 4.2 of RFC 1034. +Instead of tree structures we simply use dictionaries from domain names to +zones or record sets. + +These classes are merely a suggestion, feel free to use something else. +""" + + +class Catalog(object): + """ A catalog of zones """ + + def __init__(self): + """ Initialize the catalog """ + self.zones = {} + + def add_zone(self, name, zone): + """ Add a new zone to the catalog + + Args: + name (str): root domain name + zone (Zone): zone + """ + self.zones[name] = zone + + +class Zone(object): + """ A zone in the domain name space """ + + def __init__(self): + """ Initialize the Zone """ + self.records = {} + + def add_node(self, name, record_set): + """ Add a record set to the zone + + Args: + name (str): domain name + record_set ([ResourceRecord]): resource records + """ + self.records[name] = record_set + + 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 + """ + pass diff --git a/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py new file mode 100644 index 0000000..b15d566 --- /dev/null +++ b/project2/proj2_s4498062/dns_client.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python2 + +""" Simple DNS client + +A simple example of a client using the DNS resolver. +""" + +import dns.resolver + +if __name__ == "__main__": + # Parse arguments + import argparse + parser = argparse.ArgumentParser(description="DNS Client") + parser.add_argument("hostname", help="hostname to resolve") + parser.add_argument("-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries") + args = parser.parse_args() + + # Resolve hostname + resolver = dns.resolver.Resolver(args.caching, args.ttl) + hostname, aliases, addresses = resolver.gethostbyname(args.hostname) + + # Print output + print(hostname) + print(aliases) + print(addresses) diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py new file mode 100644 index 0000000..07e0d9a --- /dev/null +++ b/project2/proj2_s4498062/dns_server.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python2 + +""" DNS server + +This script contains the code for starting a DNS server. +""" + +import dns.server + +if __name__ == "__main__": + # Parse arguments + import argparse + parser = argparse.ArgumentParser(description="DNS Server") + parser.add_argument("-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries (if > 0)") + parser.add_argument("-p", "--port", type=int, default=5353, + help="Port which server listens on") + args = parser.parse_args() + + # Start server + server = dns.server.Server(args.port, args.caching, args.ttl) + try: + server.serve() + except KeyboardInterrupt: + server.shutdown() + print() diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py new file mode 100644 index 0000000..26bc00b --- /dev/null +++ b/project2/proj2_s4498062/dns_tests.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python2 + +""" Tests for your DNS resolver and server """ + +portnr = 5353 +server = "localhost" + +class TestResolver(unittest.TestCase): + pass + + +class TestResolverCache(unittest.TestCase): + pass + + +class TestServer(unittest.TestCase): + pass + + +if __name__ == "__main__": + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser(description="HTTP Tests") + parser.add_argument("-s", "--server", type=str, default="localhost") + parser.add_argument("-p", "--port", type=int, default=5001) + args, extra = parser.parse_known_args() + portnr = args.port + server = args.server + + # Pass the extra arguments to unittest + sys.argv[1:] = extra + + # Start test suite + unittest.main() -- cgit v1.2.3 From 73ceb0d501b63dadc33e21e9ec367bf0e2f26a6a Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 23 May 2016 22:09:33 +0200 Subject: Framework 2: most basic pylint stuff --- project2/proj2_s4498062/dns/__init__.py | 2 +- project2/proj2_s4498062/dns/cache.py | 2 -- project2/proj2_s4498062/dns/classes.py | 3 +-- project2/proj2_s4498062/dns/domainname.py | 4 +--- project2/proj2_s4498062/dns/message.py | 16 +++++++++------- project2/proj2_s4498062/dns/rcodes.py | 1 + project2/proj2_s4498062/dns/resolver.py | 1 + project2/proj2_s4498062/dns/resource.py | 3 ++- project2/proj2_s4498062/dns/types.py | 2 ++ project2/proj2_s4498062/dns_client.py | 27 +++++++++++++++++---------- project2/proj2_s4498062/dns_server.py | 22 +++++++++++++++------- 11 files changed, 50 insertions(+), 33 deletions(-) diff --git a/project2/proj2_s4498062/dns/__init__.py b/project2/proj2_s4498062/dns/__init__.py index 18ff536..992b090 100644 --- a/project2/proj2_s4498062/dns/__init__.py +++ b/project2/proj2_s4498062/dns/__init__.py @@ -1 +1 @@ -#!/usr/bin/env python2 +"""DNS tools""" diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py index e148d3a..3ef14b3 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """A cache for resource records This module contains a class which implements a cache for DNS resource records, diff --git a/project2/proj2_s4498062/dns/classes.py b/project2/proj2_s4498062/dns/classes.py index 2a6a67d..b6123cd 100644 --- a/project2/proj2_s4498062/dns/classes.py +++ b/project2/proj2_s4498062/dns/classes.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ DNS CLASS and QCLASS values This module contains an Enum of CLASS and QCLASS values. The Enum also contains @@ -18,6 +16,7 @@ class Class(object): 255 """ + # pylint: disable=invalid-name IN = 1 CS = 2 CH = 3 diff --git a/project2/proj2_s4498062/dns/domainname.py b/project2/proj2_s4498062/dns/domainname.py index 8876b48..81b5f4c 100644 --- a/project2/proj2_s4498062/dns/domainname.py +++ b/project2/proj2_s4498062/dns/domainname.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ Parsing and composing domain names This module contains two classes for converting domain names to and from bytes. @@ -18,7 +16,7 @@ class Composer(object): self.offsets = dict() def to_bytes(self, offset, dnames): - # Convert each domain name in to bytes + """Convert each domain name in to bytes""" result = b"" for dname in dnames: # Split domain name into labels diff --git a/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py index 54933b7..baaba17 100644 --- a/project2/proj2_s4498062/dns/message.py +++ b/project2/proj2_s4498062/dns/message.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ DNS messages This module contains classes for DNS messages, their header section and @@ -15,7 +13,9 @@ from dns.resource import ResourceRecord class Message(object): """ DNS message """ - def __init__(self, header, questions=None, answers=None, authorities=None, additionals=None): + def __init__( + self, header, questions=None, answers=None, authorities=None, + additionals=None): """ Create a new DNS message Args: @@ -101,14 +101,14 @@ class Message(object): # Parse authorities authorities = [] for _ in range(header.ns_count): - authority, offset = ResourceRecord.from_bytes(packet, offset, parser) - authorities.append(authority) + auth, offset = ResourceRecord.from_bytes(packet, offset, parser) + authorities.append(auth) # Parse additionals additionals = [] for _ in range(header.ar_count): - additional, offset = ResourceRecord.from_bytes(packet, offset, parser) - additionals.append(additional) + addit, offset = ResourceRecord.from_bytes(packet, offset, parser) + additionals.append(addit) return cls(header, questions, answers, authorities, additionals) @@ -122,6 +122,8 @@ class Header(object): See section 4.1.1 of RFC 1035 for their meaning. """ + # pylint: disable=missing-docstring, invalid-name + def __init__(self, ident, flags, qd_count, an_count, ns_count, ar_count): """ Create a new Header object diff --git a/project2/proj2_s4498062/dns/rcodes.py b/project2/proj2_s4498062/dns/rcodes.py index 4b1d33d..4f40621 100644 --- a/project2/proj2_s4498062/dns/rcodes.py +++ b/project2/proj2_s4498062/dns/rcodes.py @@ -6,6 +6,7 @@ This module contains an Enum of RCODE values. See section 4.1.4 of RFC 1035 for more info. """ + class RCode(object): """ Enum of RCODE values diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index ba27f74..29451bf 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -16,6 +16,7 @@ import dns.cache import dns.message import dns.rcodes + class Resolver(object): """ DNS resolver """ diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index fdf51de..19a09bb 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -44,7 +44,8 @@ class ResourceRecord(object): """ Convert ResourceRecord from bytes """ names, offset = parser.from_bytes(packet, offset, 1) name = names[0] - type_, class_, ttl, rdlength = struct.unpack_from("!HHIH", packet, offset) + type_, class_, ttl, rdlength = struct.unpack_from( + "!HHIH", packet, offset) offset += 10 rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) offset += rdlength diff --git a/project2/proj2_s4498062/dns/types.py b/project2/proj2_s4498062/dns/types.py index f918050..494232c 100644 --- a/project2/proj2_s4498062/dns/types.py +++ b/project2/proj2_s4498062/dns/types.py @@ -7,6 +7,7 @@ a method for converting Enum values to strings. See sections 3.2.2 and 3.2.3 of RFC 1035 for more information. """ + class Type(object): """ DNS TYPE and QTYPE @@ -16,6 +17,7 @@ class Type(object): >>> Type.CNAME 5 """ + # pylint: disable=invalid-name A = 1 NS = 2 CNAME = 5 diff --git a/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py index b15d566..0f5e50f 100644 --- a/project2/proj2_s4498062/dns_client.py +++ b/project2/proj2_s4498062/dns_client.py @@ -7,22 +7,29 @@ A simple example of a client using the DNS resolver. import dns.resolver -if __name__ == "__main__": + +def main(): + """DNS client""" # Parse arguments import argparse parser = argparse.ArgumentParser(description="DNS Client") parser.add_argument("hostname", help="hostname to resolve") - parser.add_argument("-c", "--caching", action="store_true", - help="Enable caching") - parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, - help="TTL value of cached entries") + parser.add_argument( + "-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument( + "-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries") args = parser.parse_args() - + # Resolve hostname resolver = dns.resolver.Resolver(args.caching, args.ttl) hostname, aliases, addresses = resolver.gethostbyname(args.hostname) - + # Print output - print(hostname) - print(aliases) - print(addresses) + print hostname + print aliases + print addresses + +if __name__ == "__main__": + main() diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py index 07e0d9a..4ac2ec4 100644 --- a/project2/proj2_s4498062/dns_server.py +++ b/project2/proj2_s4498062/dns_server.py @@ -7,16 +7,21 @@ This script contains the code for starting a DNS server. import dns.server -if __name__ == "__main__": + +def main(): + """DNS server""" # Parse arguments import argparse parser = argparse.ArgumentParser(description="DNS Server") - parser.add_argument("-c", "--caching", action="store_true", - help="Enable caching") - parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, - help="TTL value of cached entries (if > 0)") - parser.add_argument("-p", "--port", type=int, default=5353, - help="Port which server listens on") + parser.add_argument( + "-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument( + "-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries (if > 0)") + parser.add_argument( + "-p", "--port", type=int, default=5353, + help="Port which server listens on") args = parser.parse_args() # Start server @@ -26,3 +31,6 @@ if __name__ == "__main__": except KeyboardInterrupt: server.shutdown() print() + +if __name__ == "__main__": + main() -- cgit v1.2.3 From d946765f5a96dd4cfae2d5139dc6f4bd3cf85e60 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 24 May 2016 17:57:35 +0200 Subject: Working resolver --- project2/proj2_s4498062/dns/resolver.py | 140 ++++++++++++++++++++++++++------ project2/proj2_s4498062/dns/resource.py | 10 +++ 2 files changed, 123 insertions(+), 27 deletions(-) diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index 29451bf..ffb7c16 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -7,20 +7,37 @@ things in this module. This resolver will be both used by the DNS client and the DNS server, but with a different list of servers. """ +from random import randint import socket from dns.classes import Class from dns.types import Type import dns.cache -import dns.message +from dns.message import Message, Question, Header import dns.rcodes class Resolver(object): """ DNS resolver """ - def __init__(self, caching, ttl): + ROOT_SERVERS = [ + '198.41.0.4', + '192.228.79.201', + '192.33.4.12', + '199.7.91.13', + '192.203.230.10', + '192.5.5.241', + '192.112.36.4', + '198.97.190.53', + '192.36.148.17', + '192.58.128.30', + '193.0.14.129', + '199.7.83.42', + '202.12.27.33' + ] + + def __init__(self, caching, ttl, timeout=3): """ Initialize the resolver Args: @@ -29,6 +46,38 @@ class Resolver(object): """ self.caching = caching self.ttl = ttl + self.timeout = timeout + + def do_query(self, hint, hostname, type_, class_=Class.IN): + """Do a query to a hint""" + ident = randint(0, 65535) + header = Header(ident, 0, 1, 0, 0, 0) + header.qr = 0 + header.opcode = 0 + header.rd = 1 + req = Message(header, [Question(hostname, type_, class_)]) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(self.timeout) + sock.sendto(req.to_bytes(), (hint, 53)) + + try: + data = sock.recv(512) + resp = Message.from_bytes(data) + if resp.header.ident == ident: + return resp + except socket.timeout: + pass + return None + + def do_query_to_multiple(self, hints, hostname, type_, class_=Class.IN): + """Do a query to multiple hints, return the remaining hints""" + while hints != []: + hint = hints.pop() + response = self.do_query(hint, hostname, type_, class_) + if response is not None: + return hints, response + return [], None def gethostbyname(self, hostname): """ Translate a host name to IPv4 address. @@ -43,31 +92,68 @@ class Resolver(object): Returns: (str, [str], [str]): (hostname, aliaslist, ipaddrlist) """ - timeout = 2 - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(timeout) - - # Create and send query - question = dns.message.Question(hostname, Type.A, Class.IN) - header = dns.message.Header(9001, 0, 1, 0, 0, 0) - header.qr = 0 - header.opcode = 0 - header.rd = 1 - query = dns.message.Message(header, [question]) - sock.sendto(query.to_bytes(), ("8.8.8.8", 53)) + domains = hostname.split('.') + hints = self.ROOT_SERVERS - # Receive response - data = sock.recv(512) - response = dns.message.Message.from_bytes(data) + if domains == []: + return hostname, [], [] - # Get data + domain = domains.pop(-1) aliases = [] - for additional in response.additionals: - if additional.type_ == Type.CNAME: - aliases.append(additional.rdata.data) - addresses = [] - for answer in response.answers: - if answer.type_ == Type.A: - addresses.append(answer.rdata.data) - - return hostname, aliases, addresses + while hints != []: + hints, resp = self.do_query_to_multiple(hints, domain, Type.A) + if resp == None: + continue + + info = resp.answers + resp.authorities + resp.additionals + + aliases += [ + r.rdata.data for r in info + if r.match(type_=Type.CNAME, class_=Class.IN, name=domain)] + + # Case 1: answer + ips = [ + r.rdata.data for r in info + if r.match(type_=Type.A, class_=Class.IN, name=domain)] + if ips != []: + return hostname, aliases, ips + # Case 2: name servers + auths = [ + r.rdata.data for r in info + if r.match(type_=Type.NS, class_=Class.IN, name=domain)] + ips = [ + add.rdata.data for ns in auths for add in info + if add.match(name=ns, type_=Type.A)] + if ips != []: + hints += ips + if domain != hostname: + domain = domains.pop(-1) + '.' + domain + continue + if auths != []: + auths = [h for a in auths for h in self.gethostbyname(a)[2]] + hints += auths + if domain != hostname: + domain = domains.pop(-1) + '.' + domain + continue + # Case 3: delegation to other name servers + parent = '.'.join(domain.split('.')[1:]) + refs = [ + r.rdata.data for r in info + if r.match(type_=Type.NS, class_=Class.IN, name=parent)] + ips = [ + add.rdata.data for ns in refs for add in info + if add.match(name=ns, type_=Type.A)] + if ips != []: + hints += ips + continue + if refs != []: + refs = [h for r in refs for h in self.gethostbyname(r)[2]] + hints += refs + continue + # Case 4: aliases + for alias in aliases: + _, extra_aliases, alias_addresses = self.gethostbyname(alias) + if alias_addresses != []: + return hostname, aliases + extra_aliases, alias_addresses + + return hostname, aliases, [] diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index 19a09bb..89201ec 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -30,6 +30,13 @@ class ResourceRecord(object): self.ttl = ttl self.rdata = rdata + def match(self, name=None, type_=None, class_=None, ttl=None): + """Check if the record matches properties""" + return (name is None or self.name == name) and \ + (type_ is None or self.type_ == type_) and \ + (class_ is None or self.class_ == class_) and \ + (ttl is None or self.ttl == ttl) + def to_bytes(self, offset, composer): """ Convert ResourceRecord to bytes """ record = composer.to_bytes(offset, [self.name]) @@ -51,6 +58,9 @@ class ResourceRecord(object): offset += rdlength return cls(name, type_, class_, ttl, rdata), offset + def __repr__(self): + return ' '.join(map(str, [self.name, self.type_, self.rdata.data])) + class RecordData(object): """ Record Data """ -- cgit v1.2.3 From 72411105c1945509f762d4d8cf077fb51f8f49b8 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 24 May 2016 17:57:53 +0200 Subject: Made cli apps executable --- project2/proj2_s4498062/dns_client.py | 0 project2/proj2_s4498062/dns_server.py | 0 project2/proj2_s4498062/dns_tests.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 project2/proj2_s4498062/dns_client.py mode change 100644 => 100755 project2/proj2_s4498062/dns_server.py mode change 100644 => 100755 project2/proj2_s4498062/dns_tests.py diff --git a/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py old mode 100644 new mode 100755 diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py old mode 100644 new mode 100755 diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py old mode 100644 new mode 100755 -- cgit v1.2.3 From bc1d79113ad3fdfcf3319b3cc36f1a0253e64f9d Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 24 May 2016 18:16:17 +0200 Subject: Caching (mostly from da97de6) --- project2/proj2_s4498062/.gitignore | 3 +- project2/proj2_s4498062/dns/cache.py | 55 ++++++++++++++++++++++++++------- project2/proj2_s4498062/dns/resolver.py | 48 ++++++++++++++++------------ project2/proj2_s4498062/dns/resource.py | 26 +++++++--------- 4 files changed, 85 insertions(+), 47 deletions(-) diff --git a/project2/proj2_s4498062/.gitignore b/project2/proj2_s4498062/.gitignore index 94487b9..0191c0c 100644 --- a/project2/proj2_s4498062/.gitignore +++ b/project2/proj2_s4498062/.gitignore @@ -1 +1,2 @@ -*.pyc +*.pyc +.dns.cache diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py index 3ef14b3..9cde66f 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -7,6 +7,7 @@ It is highly recommended to use these. """ import json +import time from dns.resource import ResourceRecord, RecordData from dns.types import Type @@ -22,11 +23,12 @@ class ResourceEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, ResourceRecord): return { - "name": obj.name, - "type": Type.to_string(obj.type_), - "class": Class.to_string(obj.class_), - "ttl": obj.ttl, - "rdata": obj.rdata.data + "name": obj.name, + "type": Type.to_string(obj.type_), + "class": Class.to_string(obj.class_), + "ttl": obj.ttl, + "rdata": obj.rdata.data, + "timestamp": obj.timestamp } return json.JSONEncoder.default(self, obj) @@ -42,20 +44,33 @@ def resource_from_json(dct): class_ = Class.from_string(dct["class"]) ttl = dct["ttl"] rdata = RecordData.create(type_, dct["rdata"]) - return ResourceRecord(name, type_, class_, ttl, rdata) + timestamp = dct["timestamp"] + return ResourceRecord(name, type_, class_, ttl, rdata, timestamp) class RecordCache(object): """ Cache for ResourceRecords """ - def __init__(self, ttl): + FILE = '.dns.cache' + + def __init__(self): """ Initialize the RecordCache Args: ttl (int): TTL of cached entries (if > 0) """ self.records = [] - self.ttl = ttl + self.read_cache_file() + + def __del__(self): + self.write_cache_file() + + def remove_old(self): + """Remove entries for which the TTL has expired""" + now = int(time.clock()) + for record in reversed(self.records): + if record.ttl + record.timestamp < now: + self.records.remove(record) def lookup(self, dname, type_, class_): """ Lookup resource records in cache @@ -68,7 +83,16 @@ class RecordCache(object): type_ (Type): type class_ (Class): class """ - pass + self.remove_old() + return [ + r for r in self.records + if r.match(name=dname, type_=type_, class_=class_)] + + def add_records_from(self, msg): + for record in msg.answers + msg.authorities + msg.additionals: + if record.type_ in [Type.A, Type.AAAA, Type.CNAME, Type.NS] and \ + record.class_ == Class.IN: + self.add_record(record) def add_record(self, record): """ Add a new Record to the cache @@ -76,12 +100,19 @@ class RecordCache(object): Args: record (ResourceRecord): the record added to the cache """ - pass + self.records.append(record) def read_cache_file(self): """ Read the cache file from disk """ - pass + try: + with open(self.FILE, 'r') as jsonfile: + self.records = json.load( + jsonfile, object_hook=resource_from_json) + except IOError: + pass def write_cache_file(self): """ Write the cache file to disk """ - pass + self.remove_old() + with open(self.FILE, 'w') as jsonfile: + json.dump(self.records, jsonfile, cls=ResourceEncoder, indent=4) diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index ffb7c16..fe46492 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -13,7 +13,7 @@ import socket from dns.classes import Class from dns.types import Type -import dns.cache +from dns.cache import RecordCache from dns.message import Message, Question, Header import dns.rcodes @@ -48,8 +48,16 @@ class Resolver(object): self.ttl = ttl self.timeout = timeout - def do_query(self, hint, hostname, type_, class_=Class.IN): + if self.caching: + self.cache = RecordCache() + + def do_query(self, hint, hostname, type_, class_=Class.IN, caching=True): """Do a query to a hint""" + if self.caching and caching: + records = self.cache.lookup(hostname, type_, class_) + if records != []: + return records + ident = randint(0, 65535) header = Header(ident, 0, 1, 0, 0, 0) header.qr = 0 @@ -65,19 +73,22 @@ class Resolver(object): data = sock.recv(512) resp = Message.from_bytes(data) if resp.header.ident == ident: - return resp + if self.caching and caching: + self.cache.add_records_from(resp) + return resp.answers + resp.authorities + resp.additionals except socket.timeout: pass - return None + return [] - def do_query_to_multiple(self, hints, hostname, type_, class_=Class.IN): + def do_query_to_multiple( + self, hints, hostname, type_, class_=Class.IN, caching=True): """Do a query to multiple hints, return the remaining hints""" while hints != []: hint = hints.pop() - response = self.do_query(hint, hostname, type_, class_) + response = self.do_query(hint, hostname, type_, class_, caching) if response is not None: return hints, response - return [], None + return [], [] def gethostbyname(self, hostname): """ Translate a host name to IPv4 address. @@ -101,11 +112,7 @@ class Resolver(object): domain = domains.pop(-1) aliases = [] while hints != []: - hints, resp = self.do_query_to_multiple(hints, domain, Type.A) - if resp == None: - continue - - info = resp.answers + resp.authorities + resp.additionals + hints, info = self.do_query_to_multiple(hints, domain, Type.A) aliases += [ r.rdata.data for r in info @@ -117,7 +124,8 @@ class Resolver(object): if r.match(type_=Type.A, class_=Class.IN, name=domain)] if ips != []: return hostname, aliases, ips - # Case 2: name servers + + # Case 2: name servers for this domain auths = [ r.rdata.data for r in info if r.match(type_=Type.NS, class_=Class.IN, name=domain)] @@ -135,21 +143,23 @@ class Resolver(object): if domain != hostname: domain = domains.pop(-1) + '.' + domain continue - # Case 3: delegation to other name servers + + # Case 3: name servers for the same domain parent = '.'.join(domain.split('.')[1:]) - refs = [ + auths = [ r.rdata.data for r in info if r.match(type_=Type.NS, class_=Class.IN, name=parent)] ips = [ - add.rdata.data for ns in refs for add in info + add.rdata.data for ns in auths for add in info if add.match(name=ns, type_=Type.A)] if ips != []: hints += ips continue - if refs != []: - refs = [h for r in refs for h in self.gethostbyname(r)[2]] - hints += refs + if auths != []: + auths = [h for r in auths for h in self.gethostbyname(r)[2]] + hints += auths continue + # Case 4: aliases for alias in aliases: _, extra_aliases, alias_addresses = self.gethostbyname(alias) diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index 89201ec..b1c8ae4 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ A DNS resource record This class contains classes for DNS resource records and record data. This @@ -9,13 +7,14 @@ of your resolver and server. import socket import struct +import time from dns.types import Type class ResourceRecord(object): """ DNS resource record """ - def __init__(self, name, type_, class_, ttl, rdata): + def __init__(self, name, type_, class_, ttl, rdata, timestamp=time.time()): """ Create a new resource record Args: @@ -29,6 +28,7 @@ class ResourceRecord(object): self.class_ = class_ self.ttl = ttl self.rdata = rdata + self.timestamp = timestamp def match(self, name=None, type_=None, class_=None, ttl=None): """Check if the record matches properties""" @@ -51,16 +51,12 @@ class ResourceRecord(object): """ Convert ResourceRecord from bytes """ names, offset = parser.from_bytes(packet, offset, 1) name = names[0] - type_, class_, ttl, rdlength = struct.unpack_from( - "!HHIH", packet, offset) + type_, class_, ttl, rdlen = struct.unpack_from("!HHIH", packet, offset) offset += 10 - rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) - offset += rdlength + rdata = RecordData.from_bytes(type_, packet, offset, rdlen, parser) + offset += rdlen return cls(name, type_, class_, ttl, rdata), offset - def __repr__(self): - return ' '.join(map(str, [self.name, self.type_, self.rdata.data])) - class RecordData(object): """ Record Data """ @@ -121,7 +117,7 @@ class RecordData(object): class ARecordData(RecordData): - """ Record data for A type """ + """Data of an A record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -147,7 +143,7 @@ class ARecordData(RecordData): class CNAMERecordData(RecordData): - """ Record data for CNAME type """ + """Data of a CNAME record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -174,7 +170,7 @@ class CNAMERecordData(RecordData): class NSRecordData(RecordData): - """ Record data for NS type """ + """Data of an NS record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -201,7 +197,7 @@ class NSRecordData(RecordData): class AAAARecordData(RecordData): - """ Record data for AAAA type """ + """Data of an AAAA record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -227,7 +223,7 @@ class AAAARecordData(RecordData): class GenericRecordData(RecordData): - """ Generic Record Data (for other types) """ + """Data of a generic record""" def to_bytes(self, offset, composer): """ Convert to bytes -- cgit v1.2.3 From b0f35bd7c31c0ed1188cbf325de1aead0e8f8ee9 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Wed, 25 May 2016 16:11:27 +0200 Subject: Start zone reading --- project2/proj2_s4498062/dns/regexes.py | 46 +++++++++++++++++ project2/proj2_s4498062/dns/server.py | 21 +++++++- project2/proj2_s4498062/dns_server.py | 11 +++-- project2/proj2_s4498062/named.root | 90 ++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 project2/proj2_s4498062/dns/regexes.py create mode 100644 project2/proj2_s4498062/named.root diff --git a/project2/proj2_s4498062/dns/regexes.py b/project2/proj2_s4498062/dns/regexes.py new file mode 100644 index 0000000..ffa1770 --- /dev/null +++ b/project2/proj2_s4498062/dns/regexes.py @@ -0,0 +1,46 @@ +"""Regexes used in the DNS protocol""" + + +def grpm(regex): + """Make a matching group""" + return grp(regex, matching=True) + + +def grp(regex, matching=False): + """Make a group""" + return r'(' + (r'' if matching else r'?:') + regex + r')' + + +def opt(regex): + """Make an optional group""" + return grp(grp(regex) + r'?') + + +def regex_opt_r(*regexes): + """Make a group that matches one of the given regexes""" + return grp(r'|'.join(regexes)) + + +DIGIT = r'\d' +LETTER = r'[a-zA-Z]' +LETDIG = grp(regex_opt_r(DIGIT, LETTER)) +LETDIGHYP = grp(regex_opt_r(LETDIG, r'-')) +LDHSTR = grp(LETDIGHYP + r'+') +LABEL = grp(LETTER + opt(opt(LDHSTR) + LETDIG)) +SUBDOMAIN = grp(grpm(grp(LABEL + r'\.') + r'*') + grpm(LABEL)) +DOMAIN = regex_opt_r(SUBDOMAIN, r' ') + +# Fast, non-matching domain +_DOMAIN = r'(?:(?:[a-zA-Z](?:[a-zA-Z\d\-]*[a-zA-Z\d])?\.)*)?' + +IP = r'(?:(?:\d{1,3}\.){3}\d{1,3})' + +CLASS = regex_opt_r(r'IN', r'CH') +TYPE = regex_opt_r(r'A', r'CNAME', r'HINFO', r'MX', r'NS', r'PTR', r'SOA') +TTL = r'\d+' +RDATA = r'.*?' +RR = regex_opt_r( + grp(grpm(TTL) + r'\s+' + opt(grpm(CLASS))), + grp(grpm(CLASS) + r'\s+' + opt(grpm(TTL))) + ) + r'\s+' + grpm(TYPE) + r'\s+' + grpm(RDATA) + r'\s*(?:(?" +; configuration file of BIND domain name servers). +; +; This file is made available by InterNIC +; under anonymous FTP as +; file /domain/named.cache +; on server FTP.INTERNIC.NET +; -OR- RS.INTERNIC.NET +; +; last update: March 23, 2016 +; related version of root zone: 2016032301 +; +; formerly NS.INTERNIC.NET +; +. 3600000 NS A.ROOT-SERVERS.NET. +A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4 +A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30 +; +; FORMERLY NS1.ISI.EDU +; +. 3600000 NS B.ROOT-SERVERS.NET. +B.ROOT-SERVERS.NET. 3600000 A 192.228.79.201 +B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:84::b +; +; FORMERLY C.PSI.NET +; +. 3600000 NS C.ROOT-SERVERS.NET. +C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12 +C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c +; +; FORMERLY TERP.UMD.EDU +; +. 3600000 NS D.ROOT-SERVERS.NET. +D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13 +D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d +; +; FORMERLY NS.NASA.GOV +; +. 3600000 NS E.ROOT-SERVERS.NET. +E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10 +; +; FORMERLY NS.ISC.ORG +; +. 3600000 NS F.ROOT-SERVERS.NET. +F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241 +F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f +; +; FORMERLY NS.NIC.DDN.MIL +; +. 3600000 NS G.ROOT-SERVERS.NET. +G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4 +; +; FORMERLY AOS.ARL.ARMY.MIL +; +. 3600000 NS H.ROOT-SERVERS.NET. +H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53 +H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53 +; +; FORMERLY NIC.NORDU.NET +; +. 3600000 NS I.ROOT-SERVERS.NET. +I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17 +I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53 +; +; OPERATED BY VERISIGN, INC. +; +. 3600000 NS J.ROOT-SERVERS.NET. +J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30 +J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30 +; +; OPERATED BY RIPE NCC +; +. 3600000 NS K.ROOT-SERVERS.NET. +K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129 +K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1 +; +; OPERATED BY ICANN +; +. 3600000 NS L.ROOT-SERVERS.NET. +L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42 +L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42 +; +; OPERATED BY WIDE +; +. 3600000 NS M.ROOT-SERVERS.NET. +M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33 +M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35 +; End of file -- cgit v1.2.3 From 6e8b544d472c710d0aaac026a47f7477c13a0b93 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 30 May 2016 11:31:31 +0200 Subject: Simple DNS server --- project2/proj2_s4498062/dns/message.py | 8 ++++ project2/proj2_s4498062/dns/resolver.py | 18 +++++--- project2/proj2_s4498062/dns/server.py | 75 ++++++++++++++++++++++++++++----- project2/proj2_s4498062/dns_server.py | 11 ++--- 4 files changed, 91 insertions(+), 21 deletions(-) diff --git a/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py index baaba17..fd858b2 100644 --- a/project2/proj2_s4498062/dns/message.py +++ b/project2/proj2_s4498062/dns/message.py @@ -8,6 +8,8 @@ import struct from dns.domainname import Parser, Composer from dns.resource import ResourceRecord +from dns.types import Type +from dns.classes import Class class Message(object): @@ -278,3 +280,9 @@ class Question(object): qname = qnames[0] qtype, qclass = struct.unpack_from("!2H", packet, offset) return cls(qname, qtype, qclass), offset + 4 + + def __repr__(self): + return '{} {} {}'.format( + self.qname, + Type.to_string(self.qtype), + Class.to_string(self.qclass)) diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index fe46492..2c22adb 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -15,7 +15,6 @@ from dns.types import Type from dns.cache import RecordCache from dns.message import Message, Question, Header -import dns.rcodes class Resolver(object): @@ -93,10 +92,6 @@ class Resolver(object): def gethostbyname(self, hostname): """ Translate a host name to IPv4 address. - Currently this method contains an example. You will have to replace - this example with example with the algorithm described in section - 5.3.3 in RFC 1034. - Args: hostname (str): the hostname to resolve @@ -106,6 +101,19 @@ class Resolver(object): domains = hostname.split('.') hints = self.ROOT_SERVERS + if self.caching: + addrs = self.cache.lookup(hostname, Type.A, Class.IN) + cnames = self.cache.lookup(hostname, Type.CNAME, Class.IN) + if addrs != []: + return hostname, \ + [r.rdata.data for r in cnames], \ + [r.rdata.data for r in addrs] + for cname in cnames: + print 'trying', cname.rdata.data + cname, aliases, addrs = self.gethostbyname(cname.rdata.data) + if addrs != []: + return str(cname), aliases, addrs + if domains == []: return hostname, [], [] diff --git a/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py index f01043d..f830651 100644 --- a/project2/proj2_s4498062/dns/server.py +++ b/project2/proj2_s4498062/dns/server.py @@ -3,27 +3,70 @@ This module provides a recursive DNS server. You will have to implement this server using the algorithm described in section 4.3.2 of RFC 1034. """ - import re +import socket from threading import Thread import dns.regexes as rgx from dns.classes import Class from dns.types import Type +from dns.message import Header, Message +from dns.resolver import Resolver +from dns.resource import ResourceRecord, ARecordData, CNAMERecordData class RequestHandler(Thread): """ A handler for requests to the DNS server """ - def __init__(self): + def __init__(self, skt, ttl, data, addr, zone): + # pylint: disable=too-many-arguments """ Initialize the handler thread """ super(RequestHandler, self).__init__() self.daemon = True + self.skt = skt + self.ttl = ttl + self.data = data + self.addr = addr + self.zone = zone def run(self): """ Run the handler thread """ - # TODO: Handle DNS request - pass + resolver = Resolver(True, self.ttl) + + request = Message.from_bytes(self.data) + answs, adds, auths = [], [], [] + + for req in request.questions: + rrs = [ + r for r in self.zone + if r.match(type_=req.qtype, class_=req.qclass, name=req.qname)] + if rrs != []: + auths += rrs + elif req.qtype in [Type.A, Type.CNAME] and req.qclass == Class.IN: + name, cnames, addrs = resolver.gethostbyname(req.qname) + if name != req.qname: + answs.append(ResourceRecord( + str(req.qname), Type.CNAME, Class.IN, self.ttl, + CNAMERecordData(str(name)))) + # pylint: disable=bad-continuation + addrs = [ResourceRecord( + name, Type.A, Class.IN, self.ttl, + ARecordData(data)) + for data in addrs] + cnames = [ResourceRecord( + name, Type.CNAME, Class.IN, self.ttl, + CNAMERecordData(data)) + for data in cnames] + if req.qtype == Type.A: + answs += addrs + cnames + if req.qtype == Type.CNAME: + answs += cnames + + header = Header( + request.header.ident, 0, 0, len(answs), len(auths), len(adds)) + response = Message(header, None, answs, auths, adds) + + self.skt.sendto(response.to_bytes(), self.addr) class Server(object): @@ -41,23 +84,32 @@ class Server(object): self.ttl = ttl self.port = port self.done = False - # TODO: create socket + self.zone = [] + + self.skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def serve(self): """ Start serving request """ - # TODO: start listening + self.skt.bind(('localhost', self.port)) while not self.done: - # TODO: receive request and open handler - pass + data, addr = self.skt.recvfrom(512) + reqh = RequestHandler( + self.skt, self.ttl, data, addr, zone=self.zone) + reqh.start() def shutdown(self): """ Shutdown the server """ + self.skt.close() self.done = True - # TODO: shutdown socket def parse_zone_file(self, fname): + """Parse a zone file + + Will crash if the zone file has incorrect syntax. + """ with open(fname) as zonef: zone = zonef.read() + ttl, class_ = 3600000, Class.IN for match in re.finditer(rgx.ZONE_LINE_DOMAIN, zone, re.MULTILINE): match = match.groups() name = match[0] @@ -66,5 +118,6 @@ class Server(object): match[2] or match[3] or Class.to_string(class_)) type_ = Type.from_string(match[5]) data = match[6] - print match - print name, ttl, Class.to_string(class_), Type.to_string(type_), data + + record = ResourceRecord(name, type_, class_, ttl, data) + self.zone.append(record) diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py index 6fe9a3a..41d837a 100755 --- a/project2/proj2_s4498062/dns_server.py +++ b/project2/proj2_s4498062/dns_server.py @@ -27,11 +27,12 @@ def main(): # Start server server = dns.server.Server(args.port, args.caching, args.ttl) server.parse_zone_file('named.root') - #try: - # server.serve() - #except KeyboardInterrupt: - # server.shutdown() - # print() + + try: + server.serve() + except KeyboardInterrupt: + server.shutdown() + print if __name__ == "__main__": main() -- cgit v1.2.3 From 61c1214dce59fd19e86bd9362207fb5e658d9a88 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 30 May 2016 15:21:17 +0200 Subject: Fixes for resolver & server; tests for resolver; cloogle zonefile --- project2/proj2_s4498062/cloogle.zone | 13 +++++ project2/proj2_s4498062/dns/cache.py | 7 +-- project2/proj2_s4498062/dns/resolver.py | 42 ++++----------- project2/proj2_s4498062/dns/resource.py | 4 +- project2/proj2_s4498062/dns/server.py | 18 +++++-- project2/proj2_s4498062/dns_server.py | 4 +- project2/proj2_s4498062/dns_tests.py | 68 ++++++++++++++++++++++--- project2/proj2_s4498062/named.root | 90 --------------------------------- 8 files changed, 105 insertions(+), 141 deletions(-) create mode 100644 project2/proj2_s4498062/cloogle.zone delete mode 100644 project2/proj2_s4498062/named.root diff --git a/project2/proj2_s4498062/cloogle.zone b/project2/proj2_s4498062/cloogle.zone new file mode 100644 index 0000000..807b3f8 --- /dev/null +++ b/project2/proj2_s4498062/cloogle.zone @@ -0,0 +1,13 @@ +; vim: ft=bindzone: +; The Cloogle zone file + +cloogle.org. 3600000 NS ns1.p01.antagonist.nl +cloogle.org. 3600000 NS ns2.p01.antagonist.net +cloogle.org. 3600000 NS ns3.p01.antagonist.de + +cloogle.org. 3600000 A 84.22.111.158 +cloogle.org. 3600000 AAAA 2a02:2770:17:0:21a:4aff:fe1d:9a23 + +www.cloogle.org. 3600000 CNAME cloogle.org + +; End of file diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py index 9cde66f..a8b62be 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -53,7 +53,7 @@ class RecordCache(object): FILE = '.dns.cache' - def __init__(self): + def __init__(self, ttl): """ Initialize the RecordCache Args: @@ -61,15 +61,16 @@ class RecordCache(object): """ self.records = [] self.read_cache_file() + self.ttl = ttl def __del__(self): self.write_cache_file() def remove_old(self): """Remove entries for which the TTL has expired""" - now = int(time.clock()) + now = int(time.time()) for record in reversed(self.records): - if record.ttl + record.timestamp < now: + if min(self.ttl, record.ttl) + record.timestamp < now: self.records.remove(record) def lookup(self, dname, type_, class_): diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index 2c22adb..4c09681 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -48,7 +48,7 @@ class Resolver(object): self.timeout = timeout if self.caching: - self.cache = RecordCache() + self.cache = RecordCache(ttl) def do_query(self, hint, hostname, type_, class_=Class.IN, caching=True): """Do a query to a hint""" @@ -98,9 +98,6 @@ class Resolver(object): Returns: (str, [str], [str]): (hostname, aliaslist, ipaddrlist) """ - domains = hostname.split('.') - hints = self.ROOT_SERVERS - if self.caching: addrs = self.cache.lookup(hostname, Type.A, Class.IN) cnames = self.cache.lookup(hostname, Type.CNAME, Class.IN) @@ -109,66 +106,45 @@ class Resolver(object): [r.rdata.data for r in cnames], \ [r.rdata.data for r in addrs] for cname in cnames: - print 'trying', cname.rdata.data cname, aliases, addrs = self.gethostbyname(cname.rdata.data) if addrs != []: return str(cname), aliases, addrs - if domains == []: + if hostname == '': return hostname, [], [] - domain = domains.pop(-1) + hints = self.ROOT_SERVERS[:] aliases = [] while hints != []: - hints, info = self.do_query_to_multiple(hints, domain, Type.A) + hints, info = self.do_query_to_multiple(hints, hostname, Type.A) aliases += [ r.rdata.data for r in info - if r.match(type_=Type.CNAME, class_=Class.IN, name=domain)] + if r.match(type_=Type.CNAME, class_=Class.IN, name=hostname)] # Case 1: answer ips = [ r.rdata.data for r in info - if r.match(type_=Type.A, class_=Class.IN, name=domain)] + if r.match(type_=Type.A, class_=Class.IN, name=hostname)] if ips != []: return hostname, aliases, ips - # Case 2: name servers for this domain + # Case 2: name servers auths = [ r.rdata.data for r in info - if r.match(type_=Type.NS, class_=Class.IN, name=domain)] + if r.match(type_=Type.NS, class_=Class.IN)] ips = [ add.rdata.data for ns in auths for add in info if add.match(name=ns, type_=Type.A)] if ips != []: hints += ips - if domain != hostname: - domain = domains.pop(-1) + '.' + domain continue if auths != []: auths = [h for a in auths for h in self.gethostbyname(a)[2]] hints += auths - if domain != hostname: - domain = domains.pop(-1) + '.' + domain - continue - - # Case 3: name servers for the same domain - parent = '.'.join(domain.split('.')[1:]) - auths = [ - r.rdata.data for r in info - if r.match(type_=Type.NS, class_=Class.IN, name=parent)] - ips = [ - add.rdata.data for ns in auths for add in info - if add.match(name=ns, type_=Type.A)] - if ips != []: - hints += ips - continue - if auths != []: - auths = [h for r in auths for h in self.gethostbyname(r)[2]] - hints += auths continue - # Case 4: aliases + # Case 3: aliases for alias in aliases: _, extra_aliases, alias_addresses = self.gethostbyname(alias) if alias_addresses != []: diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index b1c8ae4..e552c22 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -14,7 +14,7 @@ from dns.types import Type class ResourceRecord(object): """ DNS resource record """ - def __init__(self, name, type_, class_, ttl, rdata, timestamp=time.time()): + def __init__(self, name, type_, class_, ttl, rdata, timestamp=None): """ Create a new resource record Args: @@ -28,7 +28,7 @@ class ResourceRecord(object): self.class_ = class_ self.ttl = ttl self.rdata = rdata - self.timestamp = timestamp + self.timestamp = int(timestamp or time.time()) def match(self, name=None, type_=None, class_=None, ttl=None): """Check if the record matches properties""" diff --git a/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py index f830651..10cad8b 100644 --- a/project2/proj2_s4498062/dns/server.py +++ b/project2/proj2_s4498062/dns/server.py @@ -12,7 +12,8 @@ from dns.classes import Class from dns.types import Type from dns.message import Header, Message from dns.resolver import Resolver -from dns.resource import ResourceRecord, ARecordData, CNAMERecordData +from dns.resource import \ + ResourceRecord, ARecordData, NSRecordData, CNAMERecordData class RequestHandler(Thread): @@ -112,12 +113,21 @@ class Server(object): ttl, class_ = 3600000, Class.IN for match in re.finditer(rgx.ZONE_LINE_DOMAIN, zone, re.MULTILINE): match = match.groups() - name = match[0] - ttl = int(match[1] or match[4] or ttl) + name = match[0][:-1] + ttl = int(match[1] or match[4] or ttl * 1000) / 1000 class_ = Class.from_string( match[2] or match[3] or Class.to_string(class_)) type_ = Type.from_string(match[5]) data = match[6] - record = ResourceRecord(name, type_, class_, ttl, data) + if type_ == Type.A: + cls = ARecordData + elif type_ == Type.NS: + cls = NSRecordData + elif type_ == Type.CNAME: + cls = CNAMERecordData + else: + continue + + record = ResourceRecord(name, type_, class_, ttl, cls(data)) self.zone.append(record) diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py index 41d837a..3bdd04d 100755 --- a/project2/proj2_s4498062/dns_server.py +++ b/project2/proj2_s4498062/dns_server.py @@ -20,13 +20,13 @@ def main(): "-t", "--ttl", metavar="time", type=int, default=0, help="TTL value of cached entries (if > 0)") parser.add_argument( - "-p", "--port", type=int, default=5353, + "-p", "--port", type=int, default=5300, help="Port which server listens on") args = parser.parse_args() # Start server server = dns.server.Server(args.port, args.caching, args.ttl) - server.parse_zone_file('named.root') + server.parse_zone_file('cloogle.zone') try: server.serve() diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py index 26bc00b..4a054b6 100755 --- a/project2/proj2_s4498062/dns_tests.py +++ b/project2/proj2_s4498062/dns_tests.py @@ -1,16 +1,70 @@ #!/usr/bin/env python2 +""" Tests for the DNS resolver and server """ -""" Tests for your DNS resolver and server """ +import sys +import time +import unittest -portnr = 5353 -server = "localhost" +from dns.cache import RecordCache +from dns.classes import Class +from dns.resolver import Resolver +from dns.resource import ResourceRecord, CNAMERecordData +from dns.types import Type + +portnr = 5300 +server = '127.0.0.1' class TestResolver(unittest.TestCase): - pass + """Test cases for the resolver with caching disabled""" + + def setUp(self): + self.resolv = Resolver(False, 0) + + def test_solve(self): + """Test solving some FQDN""" + host, aliases, addrs = self.resolv.gethostbyname('camilstaps.nl') + self.assertEqual(host, 'camilstaps.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, ['84.22.111.158']) + + def test_nonexistant(self): + """Test solving a nonexistant FQDN""" + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'nothing.ru.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, []) class TestResolverCache(unittest.TestCase): - pass + """Test cases for the resolver with caching enabled""" + + TTL = 3 + + def setup_resolver(self): + """Setup a resolver with an invalid cache""" + self.resolv = Resolver(True, self.TTL) + self.cache = RecordCache(self.TTL) + self.cache.add_record(ResourceRecord( + 'nothing.ru.nl', Type.CNAME, Class.IN, self.TTL, + CNAMERecordData('camilstaps.nl'))) + self.resolv.cache = self.cache + + def test_solve_invalid(self): + """Test solving an invalid cached FQDN""" + self.setup_resolver() + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'camilstaps.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, ['84.22.111.158']) + + def test_solve_invalid_after_expiration(self): + """Test solving an invalid cached FQDN after TTL expiration""" + self.setup_resolver() + time.sleep(self.TTL + 1) + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'nothing.ru.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, []) class TestServer(unittest.TestCase): @@ -22,11 +76,11 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="HTTP Tests") parser.add_argument("-s", "--server", type=str, default="localhost") - parser.add_argument("-p", "--port", type=int, default=5001) + parser.add_argument("-p", "--port", type=int, default=5300) args, extra = parser.parse_known_args() portnr = args.port server = args.server - + # Pass the extra arguments to unittest sys.argv[1:] = extra diff --git a/project2/proj2_s4498062/named.root b/project2/proj2_s4498062/named.root deleted file mode 100644 index 3c82146..0000000 --- a/project2/proj2_s4498062/named.root +++ /dev/null @@ -1,90 +0,0 @@ -; This file holds the information on root name servers needed to -; initialize cache of Internet domain name servers -; (e.g. reference this file in the "cache . " -; configuration file of BIND domain name servers). -; -; This file is made available by InterNIC -; under anonymous FTP as -; file /domain/named.cache -; on server FTP.INTERNIC.NET -; -OR- RS.INTERNIC.NET -; -; last update: March 23, 2016 -; related version of root zone: 2016032301 -; -; formerly NS.INTERNIC.NET -; -. 3600000 NS A.ROOT-SERVERS.NET. -A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4 -A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30 -; -; FORMERLY NS1.ISI.EDU -; -. 3600000 NS B.ROOT-SERVERS.NET. -B.ROOT-SERVERS.NET. 3600000 A 192.228.79.201 -B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:84::b -; -; FORMERLY C.PSI.NET -; -. 3600000 NS C.ROOT-SERVERS.NET. -C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12 -C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c -; -; FORMERLY TERP.UMD.EDU -; -. 3600000 NS D.ROOT-SERVERS.NET. -D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13 -D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d -; -; FORMERLY NS.NASA.GOV -; -. 3600000 NS E.ROOT-SERVERS.NET. -E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10 -; -; FORMERLY NS.ISC.ORG -; -. 3600000 NS F.ROOT-SERVERS.NET. -F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241 -F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f -; -; FORMERLY NS.NIC.DDN.MIL -; -. 3600000 NS G.ROOT-SERVERS.NET. -G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4 -; -; FORMERLY AOS.ARL.ARMY.MIL -; -. 3600000 NS H.ROOT-SERVERS.NET. -H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53 -H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53 -; -; FORMERLY NIC.NORDU.NET -; -. 3600000 NS I.ROOT-SERVERS.NET. -I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17 -I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53 -; -; OPERATED BY VERISIGN, INC. -; -. 3600000 NS J.ROOT-SERVERS.NET. -J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30 -J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30 -; -; OPERATED BY RIPE NCC -; -. 3600000 NS K.ROOT-SERVERS.NET. -K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129 -K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1 -; -; OPERATED BY ICANN -; -. 3600000 NS L.ROOT-SERVERS.NET. -L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42 -L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42 -; -; OPERATED BY WIDE -; -. 3600000 NS M.ROOT-SERVERS.NET. -M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33 -M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35 -; End of file -- cgit v1.2.3