Project 1.2: IP and UDP
CS233/333 - Networks and Distributed Systems
Fall, 1999.
Due Date: Tuesday, October 26, 11:59 p.m.
Note: I'm still cleaning this up a bit, but this is the assignment (more or less).
1. Overview
In this part of the project, you are going to jump into the shark-infested
waters of implementing a simple version of IP and a variation of the UDP
protocol. You will also be implementing part of the socket interface.
A word of warning: Do not start this part of the project unless you absolutely
understand what is going on with the last handin.
2. The Big Picture
When you finish this part of the project, you will have a module
'lsock' that can be used to write simple networked programs using your
own implementation of the UDP protocol. For example:
# A simple UDP server
from lsock import *
s = socket(AF_CS333,SOCK_DGRAM)
bind(s,1234)
# Loop forever, waiting for data
while 1:
data = read(s,1000)
peer = getpeername(s)
print "Received a connection from ", peer
print data
# Send some data to the server
from lsock import *
s = socket
sendto(s,"Hello World", ("192.168.69.4",1234))
...
Of course, a few details are missing here...
3. IP
First, you need to provide support for the IP protocol. Implementing
the IP protocol is not much different than what you have done already
in implementing the low-level packet module. Only this time, instead
of receiving data off of the raw hardware, you code will be receiving
data from the packet module. The basic idea of how this module works
is as follows:
- Receive a packet from the network. This is done by registering an IP protocol
handler function with your low-level packet module (exactly the same idea as with ARP).
- Perform a few checks when data is received. Notably you should
check the destination IP address to see if it matches the local
machine and compute the IP header checksum to see if any errors have
occurred.
- Examine the IP protocol field and dispatch the packet to a
higher-level protocol handler. For example, TCP has a protocol type
of 6 and UDP has a protocol of 17. The protocol you will be
implementing this part of the project is CS333UDP which as a protocol
ID of 50 (defined as proto.IP_CS333UDP).
- As for sending a packet, your code needs to assemble all of the needed IP headers,
compute a checksum, do an ARP to find the ethernet address of the target machine,
create a properly formatted ethernet packet, and send it using the packet
send function you created earlier.
To do all of this, create a file 'ip.py' that defines the following functions:
- ip_handler(p). This function is used to receive an IP datagram.
As input it accepts a raw packet as read in project1.1. Upon reception of
a packet, it should strip off the ethernet headers and perform a checksum
check of the IP header information. It should then examine the IP protocol field
and pass the IP datagram on to an appropriate IP protocol handler function (if defined).
If no IP protocol has been defined or the checksum calculation fails, the received
packet is silently discarded.
- ip_send(p). This function is used to send an IP datagram on the network.
It should properly assemble the proper IP header structure, compute the IP header
checksum, assign an identification number, figure out the target ethernet address
of the destination (using arp()), and send the packet to the network device using
your packet send function defined in project 1.1.
- register_protocol(proto,handler). This function registers a handler
function for a specific IP protocol. You should maintain an internal data structure
that maps protocol types to handler functions. The ip_handler() function will look at this
when it figures out what to do with received IP datagrams.
- unregister_protocol(proto). Unregisters the handler for a specific IP
protocol.
Implementing this part of the project should be fairly straightforward. However,
there are a few tricky parts to worry about:
- Think about how you want to represent an IP datagram. Since these will be passed up
to higher level protocols, it may make sense to work this out first. Personally, I
created a class IP_packet that had various methods for assembling and disassembling
the packet, computing checksum values, and so forth. Of course, this is not the
only way to do this.
- The following function demonstrates one way to perform 1's complement addition
on 16-bit integers (needed to compute the IP header checksum).
# 16-bit 1's complement addition
def onesadd16(x,y):
z = (x & 0xffff) + (y & 0xffff)
return (z + ((z & 0x10000) >> 16)) & 0xffff
- You may want to implement a generic checksum() function to perform
the IP checksum since it is also used by UDP and other parts of the
protocol. When implementing this function, you will also want to make
note of the following: when computing the checksum of an odd number of
bytes, you need to add a pad byte of (0) to the end so that the data can be
divided into 16-bit words!
4. UDP
In a file, udp.py, you will be implementing a variation of the UDP protocol. This is
*NOT* the same as the regular UDP protocol--in fact, it has an entirely different
protocol type so that it won't interfere with normal IP traffic. Your UDP module
should do the following:
- Define a handler function that is used to process incoming UDP packets. Your
handler function should be registered using the ip.register_protocol() function as follows:
import proto
import ip
...
ip.register_protocol(proto.IP_CS333UDP, udp_handler)
Please note that the protocol number for your protocol should be proto.IP_CS333UDP,
not the protocol number of 17 used for normal UDP.
- Create a class UDP_socket that defines a socket structure for your
UDP protocol. This socket should have the following attributes:
- A port number (the port to which the socket is bound).
- An incoming datagram queue. This is where the handler function will place data as
it arrives. Your queue should have a maximum of 5 entries.
- Methods for binding, closing, reading, and writing of data.
- In addition, your module must maintain a table of port numbers to sockets.
When your handler function decodes the UDP header, it will look at the target port number
and perform a lookup in this table. If a match is found, the datagram is placed
on the socket's incoming queue. If no matching port is found, the datagram is discarded.
General implementation notes:
- The header for CS333UDP should be exactly the same as for regular UDP. However,
when computing the UDP checksum, only compute the checksum over the UDP header and
its data (i.e., do not use any information in the IP header).
5. Sockets
Finally, create a file lsock.py that defines the following functions:
- socket(family,proto). Create a new socket object. family
is the address family and proto is the protocol. Your implementation should
look at these values and create a socket of appropriate type. For this project,
proto.AF_CS333 is the only recognized address family. proto.SOCK_DGRAM
specifies a datagram protocol. Thus, when someone calls this function as follows:
from lsock import *
s = socket(AF_CS333, SOCK_DGRAM)
You should end up instantiating a one of your UDP_socket objects created in the
previous section.
- bind(s,port). Binds socket s to a port number. When called,
this function should examine any internal tables to see if the port number is
already in use. If so, an error is generated. Otherwise, the socket's address
should be set to the given port number and an entry to the socket placed
in the internal port table. After this call has been completed, any datagrams
addressed to this port should be placed in the socket's internal data queue.
- read(s,maxsize). Read at most maxsize bytes from
socket s. The read operation should examine the sockets incoming data
queue. If it is non-empty, the function should return immediately
with data (not exceeding the maximum length). If the queue is empty,
the function should block until data arrives (when the underlying
protocol handler receives a datagram and places it in the socket's
queue).
- sendto(s,data,addr). Sends a datagram data to the socket s.
addr is a tuple of the form (ipaddr, port) that specifies
the address of the remote machine. For example:
sendto(s,"Hello",("192.168.69.4",1234))
This function blocks until the packet is successfully sent. If the packet
can not be sent for some reason (unknown host), an error should be returned.
Otherwise, the actual number of bytes sent should be returned.
- getpeername(s). Returns the address of the remote machine
as a tuple (ipaddr,port). In the case of UDP, this returns the address
of the machine that sent the last datagram read using the read()
function.
close(s). Closes the socket. This should clear the socket's
address, incoming data queue, and remove the socket's port number from any
internal port tables. Once closed, sockets should not be reused.
6. Testing
Your code will be tested with a number of simple networked programs
that only make use of the 'lsock' module. That is, they will create
some sockets and try to communicate with each other in various ways.
Because the internals will not be tested explicitly, you have quite a
bit of implementation freedom in terms of how you actually implement
IP and UDP. Tests will be announced on the mailing list.
Note: Again, you shouldn't be writing thousands of lines of code for this part
of the project (my solution was around 400 lines of code). However, it took me a
few days to fully wrap my brain around the implementation details.
Please don't wait until the last minute to start.