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):
- 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.
- 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.
- 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.
- 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.