Project 3: Multithreaded servers
In this project, you will build on Project 2 by implementing a DHCP server rather than a client.
Set up a repository on stu.cs.jmu.edu
based on the instructions in
the CS 361 Submission Procedures,
using the submit directory p3-dhcp.git
and the file name
p3-dhcp.tar.gz
.
Implementation requirements
This project builds on Project 2 but from the perspective of the server. In this project, you will not need to focus as much on the interpretation and manipulation of the binary data, as we have provided some helper functions that provide that functionality for you. Instead, your code will focus on receiving UDP messages from a client and constructing the appropriate response.
As with the previous project, this project will be built incrementally. Your first task involves responding to a single client message, then shutting down. Next, you will implement the server responses for the full four-message sequence, assigning IP addresses for up to four clients. You'll then add support for releasing an IP address. Finally, you'll add a minimal multithreaded structure to emulate the behavior of a more realistic server.
Phase 1: DHCP echo server (partial credit)
For this phase, you will implement the server partner of the client from
Phase 3 of Project 2. In that step, the command-line
option -x 0
served as a special case where the client sent a single
DHCPDISCOVER
message and the server replied with a
DHCPOFFER
. Your code will need to set up a server socket, receive
the message from a client, construct the response, and send it back.
Similarly, your echo server will also respond to DHCPREQUEST
messages based on the request's server ID option. Throughout this project, you
will assume that your server is operating on 192.168.1.0 (note, though, that
your actual socket communication will use 127.0.0.1 for all communication). If
the DHCPREQUEST
includes a server ID that matches, then you will
respond with the corresponding DHCPACK
. Otherwise, you will send
a DHCPNAK
to indicate the request failed. You do not need to do
any other error checking.
As in the previous project, you will need to consult
RFC 2131 and
RFC 2132 to determine the
fields that are required for each message type. Note that many of the fields
in the BOOTP struct (such as chaddr
) are identical between the
client and server messages. Consequently, you can simplify your implementation
by using the received data as the basis of your response.
Testing your server
Throughout this project, you are building a server. Servers typically execute
in the background without a user directly viewing its STDOUT output. To capture
this nature, all output from your code will be discarded by
make test
. To assist with debugging, you can print out
anything that you want, but the testing framework will be based solely on output
from the provided client
program.
To test your code, you should use two windows. In one window, you should
execute your server by running ./dhcps -s 10
to keep the server
running for 10 seconds before shutting down. In a second window, you will
run the provided ./tests/client
program with one of the text
files in tests/data
. You do not need to understand or modify the
structure of these files, but an explanation is provided below for those
interested.
Phase 2: A DHCP server (C requirements)
In this phase, you'll extend your echo server by implementing the full
four-message sequence for DHCP. That is, you will first receive a
DHCPDISCOVER
and reply with a DHCPOFFER
that
includes a unique IP address. The client will then send a DHCPREQUEST
.
If the IP address and server ID match, you will respond with a
DHCPACK
. However, if there is a mismatch for either, you will
send a DHCPNACK
.
You'll need to support up to 4 clients, assigning the IP addresses
192.168.1.1, 192.168.1.2, etc. If you receive a 5th DHCPDISCOVER
,
you should reply with a DHCPNAK
.
Phase 3: Tracking and releasing assignments (B requirements)
For this phase, you'll need to build a mechanism for tracking the IP addresses
that have been assigned to a given chaddr
. You'll need to do this
for a couple of reasons. First, messages might be interleaved. For example,
assume you originally encounter the following sequence:
Client 1 (xid=15, chaddr=1234) sends DHCPDISCOVER
Server offers 192.168.1.1 to Client 1
Client 1 sends DHCPREQUEST for 192.168.1.1
Server sends ACK to Client 1
Client 2 (xid=99, chaddr=5566) sends DHCPDISCOVER
Server offers 192.168.1.2 to Client 2
...
You now have to support the following sequence:
Client 1 (xid=15, chaddr=1234) sends DHCPDISCOVER
Server offers 192.168.1.1 to Client 1
Client 2 (xid=99, chaddr=5566) sends DHCPDISCOVER
Server offers 192.168.1.2 to Client 2
Client 1 sends DHCPREQUEST for 192.168.1.1
Server sends ACK to Client 1
...
The second reason for tracking offers is to add support for
DHCPRELEASE
messages. These are sent by clients voluntarily to
allow an IP address to be re-assigned. When you receive a DHCPRELEASE
,
you do not send a reply to the client.
It is important to note one key feature that goes
along with DHCPRELEASE
actions: servers try to assign the same
IP address to subsequent DHCPDISCOVER
messages from the same
chaddr
. That is, if a client previously used 192.168.1.3, sent a
DHCPRELEASE
to release its DHCP lease, then sends a subsequent
DHCPDISCOVER
, the server should again offer 192.168.1.3 unless
it has been assigned to a different client.
There are many ways you could implement this
tracking and removal of records. A simple way would be to create an array
of four records, each of which contains the chaddr
and a
"tombstone" record, which are commonly used in data structures like
hash tables.
When you use a tombstone, you just mark the record as no longer in use but
leave the data otherwise intact.
Phase 4: Once more with threads (A requirements)
In this final phase, you'll emulate part of the behavior of a
multithreaded DHCP server. Instead of processing DHCPDISCOVER
requests right away, your main thread (the one calling recvfrom()
)
will launch a new thread, then go back to waiting on another incoming message.
Your main thread will get a total of 8 requests, then join all 8 threads
before shutting down.
In the helper threads, you only need to work on constructing a
DHCPOFFER
message. For simplicity, you can either use the same
IP address for all (192.168.1.1) or return them in incrementing order
(192.168.1.1, 192.168.1.2, 192.168.1.3, etc.). For additional simplicity, you
do not need to implement the full protocol here. Each thread should exit after
sending the DHCPOFFER
.
However, to show that you are handling these requests in a separate thread,
you will not return them in the order received. Instead, in
the helper thread, you will call usleep()
on the XID
value to sleep for a certain number of microseconds before responding. The
effect of this is that your responses should be received by the client in a
sorted ascending order based on the XID
values.
Testing notes
Given the values used for the XID
values, the output of this
test case (there is only one for this phase) should be deterministic.
However, it is possible that a request/respone gets flipped because of fluke
timing caused by a lot of processes on stu
or other random
factors. If this happens, you will need to manually inspect the output to
convince yourself that your code is working correctly. When grading this phase,
we will also run your code manually and inspect the implementation to determine
if it is correct.
Additional notes
The files used by the provided client program contain information about how to generate messages in a predictable manner. These files contain lines such as the following:
1 1 42 010102020303:...
3 1 42 010102020303 192.168.1.0 192.168.1.1:[...]
Each line starts by indicating the message type (1=DHCPDISCOVER on the first
line 3=DHCPREQUEST on the second), then the hardware type (1=ETH for both), and
the hardware address (010102020303 is chaddr
). The DHCPREQUEST
message must also include a server ID (192.168.1.0) and requested IP address
(192.168.1.1). Note that the information after the ":" is just to provide a
human-readable interpretation and is not actually used.