Project 1.1: Packets and ARP

CS233/333 - Networks and Distributed Systems
Fall, 1999.

Due Date: Friday, October 15, 11:59 p.m.

1. Introduction

In this part of the project, you are going to implement the code needed to read and write raw packets of network data. This functionality is normally performed by the operating system and your implementation will closely mimic this functionality (although at a much higher level). In addition, you will be implementing ARP (Address Resolution Protocol) which is used to associate IP addresses with ethernet hardware addresses (and which is needed in later stages of the project).

2. The Big Picture

In order to support the network, the operating system needs to be able to send and receive packets. Sending packets is reasonably straightforward--just send them to the network device. On the other hand, receiving packets is a little more complicated. First, packets may be received at any time--usually resulting in an I/O interrupt whenever a packet is received. Second, packets may arrive at a high rate of speed. This is especially true if the machine is using a high speed network device (gigabit ethernet for instance). Third, once a packet is received, the system needs to decode it and figure out what kind of network protocol it belongs to. For instance, IPX and TCP/IP traffic may appear on the same network interface. In this case, it would up to the system to dispatch an appropriate handler for each of the supported network protocols and to discard packets belonging to any unsupported protocol. Finally, in some cases, packets may be arriving faster than the system can decode and dispatch them. Needless to say, these factors lead to a number of design considerations: These tasks are sometimes implemented by breaking up the network handling code into two pieces: a top-half and a bottom-half. The top-half handler is responsible for reading raw packets off of the interface and placing them on an incoming packet queue. In order to collect packets arriving at a high-rate of speed, the top-half handler is optimized for speed--in other words, it does very little work other than reading received packets off of the network interface and saving them for later processing (it should also be noted that in OS terms, the top-half handler is an device interrupt handler that runs with system interrupts disabled--thus there are other reasons for wanting it to run as fast as possible). The bottom-half handler on the other hand, does all of the work. It removes incoming packets from the receive queue, decodes their protocol, and dispatches an appropriate handling function (or discards the packet if the protocol is unknown). Unlike the top-half handler, the bottom half has a lot more work to do and is generally slower. However, it is also interruptible--meaning that the top-half handler can continue to read packets off of the network even if the bottom half handler is busy.

Now, assuming that you aren't completely confused by now, what we've really got here is a "consumer-producer" problem from operating systems. The top-half handler is producing packets and placing them on an input queue. The bottom-half handler is consuming packets and sending them on to additional code for processing.

3. Packet Handling

Your first task is to implement a module for low-level packet handling. Your module should be placed in a file "packet.py" and contain the network top-half and bottom-half handlers as well as a function for sending packets.

3.1 The Network Top-Half

To implement the top-half handler, you need to create a thread that simply reads packets off of the network interface and stores them in a receive queue.

To get you started on this, please refer to the handouts on Python thread programming and consider the following example (which contains about 90% of the code needed for the top-half handler):

# Thread for reading raw network packets
import threading
import eth
import proto

class Network_top(threading.Thread):
    def __init__(self):
         threading.Thread.__init__(self)
         self.setDaemon(1)

    # Read packets
    def run(self):
         while 1:          
              data = eth.read_packet()             # Read a raw packet
              print "Got packet of %d bytes" % (len(data),)

# Now create and start the top-handler thread
top = Network_top()
top.start()
Now, modify the above code as follows: Make sure you fully understand this first part before proceeding any further. Your code shouldn't be very big (<50 lines of code). However, there may be a bit of a conceptual barrier to overcome if you have never worked with threads before.

3.2 The Network Bottom Half

The network bottom half handler is going to decode network packets and send them on to handler functions (if defined).

Here are a few suggestions:

3.3 Sending packets

Implement a function send_packet(data) that can be used to send a raw packet of information out of the network device. Your function should work as follows: Congratulations, if you made it this far, you have just implemented the lowest level of the network subsystem.

4. ARP

Once you have your low-level network handlers working, your first protocol to implement is ARP. ARP is used to associate IP addresses with ethernet addresses on a local area network. Please refer to the handouts for details of the protocol.

You need to implement two features. First, you need to create a function arp(ipaddr) that takes an IP address of the form '192.168.69.5' as an argument and returns the 6-byte ethernet address of the device to which packets should be sent. Higher-level functions in later stages of the project will use this function to figure out how to encode outgoing packets in a manner that makes sure they are delivered to the right destination. Second, you need to implement a handler function that knows how to correctly respond to ARP request packets. These two features are interrelated. In particular, the arp() function works by sending out ARP requests (which are then answered by the ARP handler on the other machines).

4.1 ARP Packet Handler

Using your low-level packet module, define a handler function for receiving ARP packets. ARP packets have a protocol ID of 0x0806 (available as the constant proto.ARP_PROTO). You should register your handler using the register_handler() function you created earlier.

Your handler function must operate roughly as follows:

Note: Build the handler function in pieces. In particular, it will be hard to implement the reply handling unless you have already implemented a substantial part of the arp() function below.

4.2 The arp() function

The arp(ipaddr) function takes an IP address as input and returns the 6-byte ethernet address of the machine assigned to that IP address. In addition, it maintains a cache of recent IP address to ethernet mappings.

Your arp() function must operate as follows:

4.3 Implementation Hints

Implementing this part of the project involves some particularly tricky (and potentially mind-bending) control flow. Here are a few things to consider:

5. What to hand in

You will be creating two modules:

Testing and grading

Testing this part of the assignment is easy. You should be able to load the arp module and determine the ethernet addresses of various machines on the network. For example:
>>> import eth
>>> import arp
>>> e = arp.arp("192.168.69.4")
>>> print eth.eth_address_string(e)
00:c0:80:7f:1a:20
>>> f = arp.arp("192.168.69.70")
Traceback (innermost last):
  File "", line 1, in ?
RuntimeError: No response. Timed out.
>>> g = arp.arp("128.135.11.100")
>>> print eth.eth_address_string(g)
72:c8:a2:1f:37:42         # (should be eth address of the gateway)
>>>
Finally, this is potentially the most difficult part of the project since it involves a bunch of new stuff (locks, threads, condition variables, etc...). Please come see me or post a message to cs333@cs.uchicago.edu for help.

Note: your solution to this part of the project should involve very little code (my solution was less than 300 lines of code not counting comments--you can probably do better than this).