Parsing and Serialization

This document describes the design of parsing and serialization in the netstack.

Packet Buffers

The design of parsing and serialization is organized around the principle of zero copy. Whenever a packet is parsed, the resulting packet object (such as wire::ipv4::Ipv4Packet) is simply a reference to the buffer it was parsed from, and so parsing involves no copying of memory. This allows for a number of desirable design patterns.

Most of the design patterns relating to buffers are enabled by utilities provided by the packet crate. See its documentation here.

Packet Buffer Reuse

Since packet objects are merely views into an existing buffer, when they are dropped, the original buffer can be modified directly again. This allows buffers to be reused once the data inside of them is no longer needed. To accomplish this, we use the packet crate's ParseBuffer trait, which is a buffer that can keep track of which bytes have already been consumed by parsing, and which are yet to be parsed. The bytes which have not yet been parsed are called the “body”, and the bytes preceding and following the body are called the “prefix” and the “suffix.”

In order to demonstrate how we use buffers in the netstack, consider this hypothetical control flow in response to receiving an Ethernet frame:

  1. The Ethernet layer receives a buffer containing an Ethernet frame. It parses the buffer as an Ethernet frame. The EtherType indicates that the encapsulated payload is an IPv4 packet, so the payload is delivered to the IP layer for further processing. Note that the entire original buffer is delivered to the IP layer. However, since the Ethernet header has already been parsed from the buffer, the buffer's body contains only the bytes of the IP packet itself.
  2. The IP layer parses the payload as an IPv4 packet. The IP protocol number indicates that the encapsulated payload is a TCP segment, so the payload is delivered to the TCP layer for further processing. Again, the entire original buffer is delivered to the TCP layer, with the body now equal to the bytes of the TCP segment.
  3. The TCP layer parses the payload as a TCP segment. It contains data which the TCP layer would like to acknowledge. Once the TCP layer has extracted all of the data that it needs out of the segment, it drops the segment (the TcpSegment object, which itself borrowed the buffer), regaining direct mutable access to the original buffer. Since the buffer has access to the now-parsed prefix and suffix in addition to the body, the TCP stack can now use the entire buffer for serializing. It figures out how much space must be left at the beginning of the buffer for all lower-layer headers (in this case, IPv4 and Ethernet), and serializes a TCP Ack segment at the appropriate offset into the buffer. Now that the buffer is being used for serialization (rather than parsing), the body indicates the range of bytes which have been serialized so far, and the prefix and suffix represent empty space which can be used by lower layers to serialize their headers and footers.
  4. The TCP layer passes the buffer to the IP layer, with the body equal to the bytes of the TCP segment that has just been serialized. The IP layer treats this as the body for its IP packet. It serializes the appropriate IP header into the buffer's prefix, just preceding the body. It expands the body to include the now-serialized header, leaving the body equal to the bytes of the entire IP packet.
  5. The IP layer passes the buffer to the Ethernet layer, with the body now corresponding to the IP packet that it has just serialized. The Ethernet layer treats this as the body for its Ethernet frame. It serializes the appropriate Ethernet header, expands the body to include the bytes of the entire Ethernet frame, and passes the buffer to the Ethernet driver to be written to the appropriate network device.

Note that, in this entire control flow, only a single buffer is ever used, although it is at times used for different purposes. If, in step 3, the TCP layer finds that the buffer is too small for the TCP segment that it wishes to serialize, it can still allocate a larger buffer. However, so long as the existing buffer is sufficient, it may be reused.

Prefix, Suffix, and Padding

When using a single buffer to serialize a packet - including any encapsulating headers of lower layers of the stack - it is important to satisfy some constraints:

  • There must be enough space preceding and following an upper-layer body for lower-layer headers and footers.
  • If any lower-layer protocols have minimum body length requirements, there must be enough space following an upper-layer body for any padding bytes needed to satisfy those requirements.

Consider, for example, Ethernet. When serializing an IPv4 packet inside of an Ethernet frame, the IPv4 packet must leave enough room for the Ethernet header. Ethernet has a minimum body requirement, and so there must also be enough bytes following the IPv4 packet to be used as padding in order to meet this minimum in case the IPv4 packet itself is not sufficiently large.

To accomplish this, we use the packet crate's Serializer trait. A Serializer represents the metadata needed to serialize a request in the future. Serializers may be nested, which results in a Serializer describing a sequence of encapsulated packets to be serialized, each being used as the body of an encapsulating packet. When a sequence of nested Serializers is processed, the header, footer, and minimum body size requirements are computed starting with the outermost packet and working in. Once the innermost Serializer is reached, it is that Serializer's responsibility to provide a buffer:

  • The buffer must contain the body to be encapsulated in the next layer. The buffer implements the BufferMut trait (a superset of the functionality required by the ParseBuffer trait mentioned above), and the buffer's body indicates the bytes to be encapsulated by the next layer.
  • The buffer must satisfy the header, footer, and minimum body size requirements by providing enough prefix and suffix bytes before and after the body.

Once the innermost Serializer has produced its buffer, each subsequent packet serializes its headers and footers, expands the buffer's body to contain the whole packet as the body to be encapsulated in the next layer, and returns the buffer to be handled by the next layer.

When control makes its way to a layer of the stack that has a minimum body length requirement, that layer is responsible for consuming any bytes following the range for use as padding (and zeroing those bytes for security). This logic is handled by EncapsulatingSerializer's implementation of serialize.

Serializer is implemented by a number of different types, allowing for a range of serialization scenarios including:

  • Serializing a new packet in a buffer which previously stored an incoming packet.
  • Forwarding a packet by shrinking the incoming buffer's range to the body of the packet to be serialized, and then passing that buffer back down the stack of Serializers.

In-Place Packet Modification

In certain scenarios, it is necessary to modify an existing packet before re-serializing it. The canonical example of this is IP forwarding. When all packets are simply references into a pre-existing buffer, modifying and then re-serializing packets is cheap, as it can all be done in-place in the common case. For example, if an IP packet is received which needs to be forwarded, so long as the link-layer headers of the device over which it is to be forwarded are not larger than the link-layer headers of the device over which it was received, there will be enough space in the existing buffer to serialize new link-layer headers and deliver the resulting link-layer frame to the appropriate device without ever having to allocate extra buffers or copy any data between buffers.

Zeroing Buffers

If buffers that previously held other packets are re-used for serializing new packets, then there is a risk that data from the old packets will leak into the new packet if every byte of the new packet is not explicitly initialized. In order to prevent this:

  • Any code constructing the body of a packet is responsible for ensuring that all of the bytes of the body have been initialized.
  • Any code constructing the headers of a packet (usually serialization code in a submodule of the wire module) is responsible for ensuring that all of the bytes of the header have been initialized.

A special case of these requirements is post-body padding. For packet formats with minimum body size requirements, upper layers will provide extra buffer bytes beyond the end of the body. In the BufferMut used to store packets during serialization, these padding bytes are in the “suffix” just following the buffer's body.

The logic for adding padding is provided by EncapsulatingSerializer, described in the Prefix, Suffix, and Padding section above. EncapsulatingSerializer ensures that these padding bytes are zeroed.

See also: the _zeroed constructors of the zerocopy::LayoutVerified type