Each option has a tag byte specifying its type, a length byte, and a number of data bytes specified by the length:
|tag (1)||len (1)||data (len)|
There are two special fixed length options with no length or data bytes, “pad” (tag 0, used for alignment) and “end” (255, marks the end of the options field).
The description is simple enough but there are many hidden difficulties here. First, we have to support variable length data for each option but we don’t want to allocate additional memory. Second, the number of options is also variable, adding another layer of complexity. Third, DHCP supports the notion of option overloading which permits options to be placed in the
file. Finally, there is a notion of container options such as option 82 (“Relay Agent Information”) which is essentially options-within-an-option. We really have our work cut out for us.
Let’s talk about zero-allocation variable data first. To support reading arbitrary options, we cannot use standard loops and enumerators as these would require implementing interfaces and allocating heap memory. There may be a way with some complexity to support a nonstandard enumerator with a struct type but let’s stick with a simpler model using an enumeration callback (similar to Rx
public void ReadOptions<T>(T obj, Action<DhcpOption, T> read);
T obj is to avoid requiring lambda capturing — remember our high performance promise!
On the other hand, for writing options without using a list or array we clearly need some intermediate state. Luckily, our DhcpMessageBuffer class is already stateful. Let’s take advantage of this by adding a
nextOption “cursor.” This
int value will start at 0 and keep track of where in our options buffer we left off. A set of write methods will manipulate this value as necessary:
public DhcpOption WriteOptionHeader(DhcpOptionTag tag, byte length); public DhcpOption WriteOption(DhcpOptionTag tag, ReadOnlySpan<char> chars, Encoding encoding); public void WritePadding(byte length); public void WriteEndOption();
This forward-only surface area is reminiscent of classes like XmlWriter. Here is a sample use case:
DhcpMessageBuffer output = /* . . . */; DhcpOption msgType = output.WriteOptionHeader(DhcpOptionTag.DhcpMsgType, 1); msgType.Data = (byte)DhcpMessageType.Offer; DhcpOption serverId = output.WriteOptionHeader(DhcpOptionTag.DhcpServerId, 4); IP(10, 20, 30, 40).WriteTo(serverId.Data); output.WriteOption(DhcpOptionTag.HostName, "MYNAME", Encoding.UTF8); output.WritePadding(5); output.WriteEndOption();
Note the difference between
WriteOptionHeader where the caller is expected to do the data writing and
WriteHeader which assumes a string valued option and takes care of efficiently doing the required low-level character encoding operations with
This supports what we need but is not that pretty. Wouldn’t you rather have more strongly-typed methods for common options? No problem; we’ll add some extension methods (DhcpMessageBufferExtensions.cs):
// . . . output.WriteDhcpMsgTypeOption(DhcpMessageType.Offer); // . . . output.WriteHostNameOption("MYNAME"); // . . .
Note that options like option 3 (“Router”) can have a variable number of elements. Since we can’t use params (too expensive to allocate an array!), we are forced to define all the overrides we think we’ll need. In this sample I stopped at four parameters.
Now, on to option overloading. This is a pain, and for simplicity I chose to only support reading overloaded options but not writing them. The way option overloading works is that a special option 52 (“Overload”) is given which specifies a flag telling you which fields are overloaded. Based on this flag, you would need to read one or two additional buffers to process options. Essentially it boils down to doing a first pass over the main buffer, returning the overload flag (if present), and then optionally reading the other buffers. Luckily, the other buffers have fixed locations within the header and can be easily sliced out of the underlying buffer.
Finally, we need to handle container options and sub-options. This is really just a special case of options in general. We will need one more state variable
containerStart to track the current container option starting point and a few additional methods for writing:
public void WriteContainerOptionHeader(DhcpOptionTag tag); public DhcpSubOption WriteSubOptionHeader(byte code, byte length); public DhcpSubOption WriteSubOption(byte code, ReadOnlySpan<char> chars, Encoding encoding); public void EndContainerOption();
The container option header must come first. It then must be followed by one or more sub-options, and must be terminated by the container end. No intervening options can be written. Blazing fast performance would dictate that we not do any checks of these conditions, so please just be careful about ordering here! To make things more convenient, container option types can be given first-class treatment, such as this example for relay agent information:
DhcpMessageBuffer output = /* . . . */ // strongly-typed container option wrapper DhcpRelayAgentSubOptionsBuffer buffer = output.WriteRelayAgentInformationOptionHeader(); // specific sub-option methods buffer.WriteAgentCircuitId("circ1"); buffer.WriteAgentRemoteId("re1"); buffer.WriteLinkSelection(new IPAddressV4(1, 2, 3, 4)); buffer.WriteSubscriberId("s1"); // done writing the sub-options buffer.End(); // now we can continue to write other options output.WriteSubnetMaskOption(new IPAddressV4(255, 255, 255, 0)); output.WriteEndOption();
To keep us honest, we need a few more benchmarks dealing with options (DhcpMessageBufferBenchmarks.cs):
| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------- |----------:|---------:|---------:|----------:|------:|------:|------:|----------:| | Load | 85.36 ns | 0.465 ns | 0.388 ns | 85.25 ns | - | - | - | - | | LoadWithOptions | 181.87 ns | 3.620 ns | 5.528 ns | 178.91 ns | - | - | - | - | | SaveWithOptions | 257.51 ns | 1.247 ns | 1.041 ns | 257.47 ns | - | - | - | - | | Save | 70.50 ns | 0.669 ns | 0.593 ns | 70.36 ns | - | - | - | - |
After hours of work, we now have a fast, zero-allocation, fully functional DhcpMessageBuffer, a supporting DhcpOptionsBuffer struct, a DhcpOption struct, a DhcpSubOption struct, and various other supporting types. But what use is a server without a network? To be continued….