blob: 615578b3d3d9d33cc1e11b62429d640a3af4b811 [file] [log] [blame] [view]
# 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](https://fuchsia-docs.firebaseapp.com/rust/packet/index.html).*
### 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. `Serializer`s 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 `Serializer`s 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 `Serializer`s.
### 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*