Project 2: Protocol implementation

In this multi-part project, you will implement a simulation of the DHCP protocol, which is used to establish network settings for 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 p2-dhcp.git and the file name p2-dhcp.tar.gz.

Background information

DHCP is based on an older protocol called BOOTP. Both protocols use a data structure that was defined for exchanging network information. The data structure is described in RFC 2131, which you will need to reference throughout this project (see Figure 1). The options portion of the data structure is variable-sized, and the possible values are described in RFC 2132. Note that, with the exception of the End option, all options follow a specific structure:

  • Code - one byte to indicate which option is used
  • Len - one byte to specify the length of the option value
  • Value - variable-length field containing the option value

As an example, consider the following two options and their interpretation (all numbers are in hexadecimal):

35 01 03
Option 53 (0x35) = message type; value 3 = DHCP request
32 04 0a 02 01 01
Option 50 (0x32) = requested IP address; value 0x0a020101 (10.2.1.1)

The options that must be set vary based on which kind of message is being sent. These specifications are in Table 3 and Table 5 of RFC 2131. For this project, you are required to support the options that are identified as "MUST" or "MUST NOT" in those tables. (Note that some depend on which state the client is in. This will be discussed in the client implementation and you should ignore these until then.)

The basic sequence of messages in DHCP consist of the following (in order):

  1. Client sends DHCP Discover: This is a BOOTREQUEST message with information about the client's hardware and a randomly chosen xid to identify the request.
  2. Server sends DHCP Offer: This is a BOOTREPLY message with the client's hardware and xid values, along with a suggested IP address (yiaddr) and other information.
  3. Client sends DHCP Request: This is a BOOTREQUEST message with the client's hardware and xid values, along with the requested IP address and server identifier.
  4. Server sends DHCP ACK: This is a BOOTREPLY message with the client's hardware and xid values, along with the assigned IP address (yiaddr) and other information.

You will also need to support three other types of messages that can occur:

  • Client sends DHCP Decline if the address offered in the DHCP Offer is already in use.
  • Server sends DHCP NAK if the address requested in the DHCP Request is already in use.
  • Client sends DHCP Release to voluntarily unassign the IP address.

Testing infrastructure modification

This project consists of building three separate executables: interp, client, and server. For this reason, the standard make test procedure does not work adequately. Until you have a complete working implementation, it would freeze at various times. Instead, there will be separate testing structures for each phase, as explained below.


Implementation requirements

This project is designed to be completed incrementally in multiple phases. You should plan on an average of 10-14 work days for each phase. If you commit to this schedule, you will be able to complete all phases by the final deadline.


Phase 1: Packet interpretation (partial credit)

For this phase, you will create a command-line program (interp) that opens a binary file that adheres to the DHCP data structure requirements. Your task is to interpret the data in thtis file and print a human-readable version of the contents. To satisfy this set of requirements, you will need to interpret both the BOOTP and DHCP portions of the data.

To get started, you need to complete the msg_t definition in src/dhcp.h. The fields should be based on Section 2 of RFC 2131 and should use appropriate types, such as uint32_t for 32-bit values (not int) and struct in_addr for IPv4 addresses.

Most of your work for this task should be in src/dhcp.c. Doing so will allow you to make use of the functions you define here for later phases of the project. In src/interp.c, you will get the name of a file as a command-line argument (no other arguments are allowed). Then, open the requested file and call the formatting code that you defined in src/dhcp.c. You can compile and run the first test input as follows with the formatted output shown:

$ make interp
$ ./interp tests/data/bootp-1-eth
------------------------------------------------------
BOOTP Options
------------------------------------------------------
Op Code (op) = 1 [BOOTREQUEST]
Hardware Type (htype) = 1 [Ethernet (10Mb)]
Hardware Address Length (hlen) = 6
Hops (hops) = 0
Transaction ID (xid) = 42 (0x2a)
Seconds (secs) = 0 Days, 0:00:00
Flags (flags) = 0
Client IP Address (ciaddr) = 127.0.0.1
Your IP Address (yiaddr) = 0.0.0.0
Server IP Address (siaddr) = 0.0.0.0
Relay IP Address (giaddr) = 0.0.0.0
Client Ethernet Address (chaddr) = 08002b2ed85e

The dhcp.h file contains important constants that you should reference, including the supported hardware types and lengths. You only need to support the values listed in that file. When printing the hardware type, your output should match the names given in the ARP Parameters. All IP addresses are IPv4 and should be formatted in dotted decimal notation. The length of the chaddr value is based on the hardware address length field (hlen).

For the DHCP options (in contrast to the BOOTP options described above), your code needs to support the options specified as "MUST" in Sections 4.3.1 and 4.4.1 of RFC 2131. Note that some depend on the client state machine, and you only need to support the ones designated as "MUST" when in or just after the "SELECTING" state. These options are variable-length, and you should consult RFC 2132 for the option formats.

Testing your interpreter

To test your implementation of this phase, run make int in the p2-dhcp directory. In addition to compiling your code, this command will do the following:

  • Compile and run tests/testsuite based on the tests/public.c unit tests. Initially, there are no unit tests. You should add tests here to test your src/dhcp.c implementation.
  • Run tests/integration.sh to test your interp output based on the arguments in tests/itests.include. This will also run valgrind to test for memory leaks.

The input files are located in tests/data. These are binary files, so you should not open them in a text editor. If you want to examine their contents (in hexadecimal), you can run hexdump on them. (Note that the line that consists of just * indicates repeated lines of just 0s.) You can also call dump_packet() from your code.


Phase 2: UDP request/response (C requirements)

Once you have a means to interpet binary DHCP data, your next task is to demonstrate basic network communication by constructing a valid DHCP request and sending it to a pre-built server. You do not need to implement the full protocol at this point and your client will not need to wait for the response. Instead, it will just build the packet, use the code from the previous phase to print it out, send it to the server, then exit.

You will implement this portion of the project in src/client.c, though you may also wish to extend the implementation of src/dhcp.c, as some of this functionality would also be helpful to the server. Your client will need to support the following command-line arguments:

  • -x N : use N as the XID field (32-bit unsigned integer) [default 42]
  • -t N : use N as the hardware type (must be one named in src/dhcp.h) [default ETH]
  • -c N : use N as the hardware address (chaddr) [default 0102...]
  • -m N : create DHCP message type M [default DHCPDISCOVER]
  • -s N.N.N.N : specify the server IP DHCP option [default 127.0.0.1]
  • -r N.N.N.N : specify the requested IP DHCP option [default [127.0.0.2]
  • -p : initiate the protocol (send UDP packet)

Initially, you should ignore the -p option to focus on building the packet correctly. For the IP addresses, you will need to convert dotted decimal notation into a 32-bit unsigned integer. (HINT: you can do this with multiple calls to strtol().)

Once you are able to build all requests successfully, the last step of this phase is to implement the -p to send the request to a pre-built server. For this phase, the XID must be set to 0. This tells the server that it is not running a full protocol. Instead, the server will just print out what it received, then shut down.

IMPORTANT: A real DHCP implementation uses ports 67 and 68, along with two reserved IP addresses (255.255.255.255 and 0.0.0.0). Your code will not use these. Instead, you will create a socket to the hard-coded IP address 127.0.0.1 (localhost) and you will call get_port() to get a custom port number. The get_port() function selects a number based on your eID and returns the port number as a char * (which you will need to convert to a 16-bit unsigned integer).

Testing your client

For this phase, you can compile your client with make client and run it from the p2-dhcp directory. To run the test cases, you can run make cli. For the last two test cases, this will involve starting a server, so there will be a delay. The testing script for this phase is tests/integ-client.sh, and it uses the command-line arguments specified in tests/itests.cl.include.


Phase 3: DHCP client (B requirements)

For this phase, you will now implement a rudimentary DHCP client. You will extend your src/client.c implementation of -p so that a non-zero XID will initiate the protocol. Building on the code that you have already implemented to create a packet, you will start by sending a DHCP Discover message just as you did before. Next, you will continue the protocol by receiving a message from the server (should be a DHCP Offer), then sending a DHCP Request, and receiving one more message (DHCP ACK).

Note that you will need to extract information from the received messages in order to construct the expected response. For instance, the server indicates the offered address in the yiaddr field; your request must then specify this address in the DHCP requested address field. Again, consult Sections 4.3.1 and 4.4.1 of RFC 2131 for details.

At this point, it is recommended to modify your UDP socket setup code so that you also specify a SO_RCVTIMEO socket option. Otherwise, if your implementation is not completely correct, calls to recv() or recvfrom() will freeze.

Testing your client (again)

As above, you can compile your client with make client and run it from the p2-dhcp directory. You are strongly encouraged to test your code manually by running the server in one window (./server in the p2-dhcp/tests directory) and running the client in a separate window. Once you are reasonably confident your code works, you can run make pro to invoke the tests/integ-protocol.sh script. This script uses command-line arguments in tests/itests.proto.include.


Phase 4: DHCP server (A requirements)

Your final task is to implement the DHCP server using a pre-built client. The details for this phase are similar to those for Phrase 3, except you are implementing the other side of the protocol. A pre-build client will begin by sending a DHCP Discover message and your server will respond with a DHCP Offer. The client will then reply with a DHCP Request for which you will send a DHCP ACK. Again, you will need to consult the RFCs for the details.

There are a couple of extra requirements that you must pay attention to when implementing the server:

  • Your server must keep track of up to 10 assigned addresses and make sure that it does not assign the same IP address multiple times.
  • If an 11th address comes in as a DHCP Discover, your server should reply with a DHCP NAK to indicate failure.
  • Your server must use 10.0.2.0 as its own IP address (Server Identifier).
  • Client addresses must start at 10.0.2.1 and increment from there.
  • For all IP Address Lease Times, use exactly 30 days.

Note that most of the code examples in the book use TCP (SOCK_STREAM), which uses accept() for incoming connections. DHCP uses UDP (SOCK_DGRAM), which is connectionless and does not use accept(). Instead, your main server loop just uses recvfrom() to get messages from the client.

Testing your server (again)

Similar to the previous instructions, you compile your server with make server in the p2-dhcp directory. You can run the test cases with make srv, which invokes tests/integ-serv.sh. This script uses the command-line arguments in tests/itests.serv.include, but passes the arguments to the pre-built client. That is, to debug your server, you would run ./server with no arguments in p2-dhcp and run the client provided in p2-dhcp/tests (though you could use your own!).

Note that most of the argument lines in tests/itests.serv.include have multiple sets of quoted arguments, such as:

"-p -x 4096 -t 6 -c 08002B2ED85E" "-p -x 65000 -t 6"

This indicates that the first client will run with -p -x 4096 -t 6 -c 08002B2ED85E, then a second client will run with -p -x 65000 -t 6. That means your server should not shut down between client requests. Instead, your server will be in an infinite loop. You will break out of the loop if no request arrives within a certain number of seconds of calling recvfrom(). To do this, you must set a SO_RCVTIMEO socket option of to a specified number of seconds. If a timeout occurs, recvfrom() returns a negative value.

For debugging purposes, 10 seconds is probably sufficient for a timeout. That will give you enough time to type a command to start the client. For testing purposes (using make src), you will need to make the timeout less than 3 seconds. If the timeout is longer than that, the test case will fail because it will try to compare your program's output before your program has actually completed and written the output.



James Madison University logo


© 2011-2024 Michael S. Kirkpatrick.
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.