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: BOOTP packet interpretation (partial credit)

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

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. For this phase, you will not support the options field.

You are generally free to structure your code as you see fit, but we recommend using the provided files as described below. Doing so will allow you to make more module use of the code for later phases of the project:

src/dhcp.c, src/dhcp.h
Use these files to define the msg_t struct to hold the fixed-size BOOTP fields (i.e., not the options) and to work with this struct.
src/format.c
Use this file to control the printing of the fields for both BOOTP and DHCP.
src/interp.c
Use this file to define the control flow for interpreting binary data as either a BOOTP or DHCP message.

You can compile and run the first test input as follows with the formatted output shown:

$ make
$ ./dhcp 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 chaddr field tends to be a source of headaches for many students, as they try to find a way to print the field all at once with a single printf(). You cannot do this because these hardware addresses can have a variety of lengths, including odd sizes like 3 bytes. Instead, use a for-loop, relying on the fact that the length of the chaddr value is based on the hardware address length field (hlen).

Testing your interpreter

To test your implementation of this phase, run make test 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 implementation.
  • Run tests/integration.sh to test your dhcp 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: DHCP packet interpretation (C requirements)

Once you can interpret the BOOTP portion of the binary data, you will add support for the DHCP options. 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, which we are not implementing. You only need to support the ones designated as "MUST" when in or just after the "SELECTING" state.

Unlike the BOOTP fields, you cannot define a struct to capture the DHCP options. First, these options are variable-length and can change for different DHCP messages; structs need constant sizes. Second, structs with odd-sized fields (such as a 3-byte field) would be padded by the compiler; this padding will not exist in the actual DHCP messages. Finally, structs assume the options are in a predictable order, but DHCP options do not require this. One message can have option 53 (DHCP request) before the requested IP address (option 50), while another message can reverse this order.

Instead, you will need to traverse through the options in a byte-by-byte order. It is up to you decide how to do this. You should consult RFC 2132 for the option formats.

As a hint, this stage is a lot easier if you take advantage of pointer casting. For example, assume you have read the DHCP message into a uint8_t *buffer. The first portion of this is the BOOTP data, but the buffer also includes the DHCP options. However, you can cast ((msg_t *)buffer) to get a pointer to the beginning of buffer, but treating it as the BOOTP msg_t struct. Doing so allows you to pass this to all of the BOOTP-related functions you created for Phase 1. Depending on how many bytes you need at a time, you can also cast different offsets to uint8_t*, uint16_t*, or uint32_t* pointers.


Phase 3: UDP request/response (B 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 start this phase by extending the code in src/main.c to distinguish between interpreting data src/interp.c and creating a client (src/client.c). You can process the command-line here or in src/client.c as you see fit. You will then implement the rest of 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 later. 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).

As in Phase 1, the chaddr field tends to trip people up the most. When processing the command line, you will need to take two characters at a time, converting them into their numeric equivalent. For instance, the argument "a01c" denotes the ASCII-encoded string 0x61 ('a'), 0x30 ('0'), 0x31 ('1'), 0x63 ('c'), 0x00 ('\0'). Your will need to convert the first two characters into the single byte 0xa0, then convert the third and fourth characters into the single byte 0x1c.

Testing your client

You will continue to test your client using make test as before. For the last two test cases, this will involve starting a server, so there will be a delay. The testing script includes output for how you can start the server manually in a separate window to debug your client.


Phase 4: DHCP client (A 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)

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. This will allow you to use GDB to step through your client and isolate any potential bugs. Once you are reasonably confident your code works, you can run make test to invoke the testing script.



James Madison University logo


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