-
Notifications
You must be signed in to change notification settings - Fork 5
net: Parse inner IP packet for ICMP Error messages #887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
55c7db2
to
be1dcd0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds infrastructure to parse potentially-truncated IP datagrams embedded inside ICMP Error messages, which is necessary for NAT implementation according to RFC 5508. The main purpose is to handle the special case where ICMP Error messages contain fragments of original IP packets that caused the error, which may be truncated but still need to have their addresses and ports translated.
- Introduces truncated TCP/UDP header types to handle partial headers with only port information
- Adds embedded header parsing infrastructure for ICMP Error message payloads
- Refactors existing
ParsePayload
trait implementations into regular methods
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
File | Description |
---|---|
net/src/tcp/truncated.rs | New truncated TCP header types for partial parsing |
net/src/udp/truncated.rs | New truncated UDP header types for partial parsing |
net/src/headers/embedded.rs | New embedded headers infrastructure for ICMP Error parsing |
net/src/tcp/mod.rs | Refactors ParsePayload trait and renames error enum |
net/src/udp/mod.rs | Refactors ParsePayload trait and adds truncated exports |
net/src/icmp4/mod.rs | Adds ICMP Error payload parsing functionality |
net/src/icmp6/mod.rs | Adds ICMP Error payload parsing functionality |
net/src/ipv4/mod.rs | Adds embedded payload parsing methods |
net/src/ipv6/mod.rs | Adds embedded payload parsing methods and macro usage |
net/src/headers/mod.rs | Integrates embedded headers and uses new macro |
net/src/parse.rs | Adds macro for enum From implementations |
net/src/vlan/mod.rs | Refactors ParsePayload trait implementation |
net/src/vxlan/mod.rs | Removes ParsePayload trait implementation |
net/src/ip_auth/mod.rs | Refactors ParsePayload trait and adds embedded parsing |
net/src/eth/mod.rs | Refactors ParsePayload trait implementation |
dataplane/src/packet_processor/ipforward.rs | Updates Headers construction |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
df4a712
to
683618b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
net/src/tcp/mod.rs:1
- The error variant name
ZeroDestPort
is inconsistent with the newZeroDestinationPort
variant. Both should use the same naming convention for consistency.
// SPDX-License-Identifier: Apache-2.0
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
683618b
to
23954d8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are all the tests? A fuzzer for the truncated stuff is probably useful. We can build one by Deparsing a full TCP packet we construct from our library and then truncating them in various ways.
} | ||
} | ||
|
||
// Trait ParseHeader above requires its second generic parameter to implement From<T>, leading in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this in parse.rs? Shouldn't it be in mod.rs or maybe its own file? No need to change it now, but in a future PR we can move it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where I needed it at the moment. Happy to move it later, or to look into other locations in the codebase where we could reuse it, too (or maybe I'll just ask Claude to look that up for me)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left as is for now.
Apparently, I cannot read, so tests can be later. |
All header variants implement the ParsePayload trait... Or do they? Looking at the code, the benefits from the trait are unclear: - All specific header types implement it. That is, all appart Ipv6Ext, which can't work with it, and implements an alternative ParsePayloadWith trait. - The trait is implemented, but *never used*! If we were to use it, we'd struggle because of Ipv6Ext. - Several header types have an empty implementation: it just returns an error. What's the point, in that case? - The structure of the trait makes it difficult to extend, to pass additional parameters to the parsing functions. ParsePayloadWith has been brought up as a workaround for passing a parameters, but it wouldn't allow making the method generic (with regards to the return type), for example. So let's remove the ParsePayload (and ParsePayloadWith) implementations: - For each header type that implements it, move the implementation to the header type itself (outside of a trait implementation). - For Ipv6Ext, do something similar, and also rename the method as parse_payload(), even if it does take an additional argument. - For header types that have an "empty" implementation, just drop the method entirely. - Keep the trait definition and its implementation for struct Header for now. Signed-off-by: Quentin Monnet <[email protected]>
... For consistency with UDP, which uses UdpParseError. Also rename error variant ZeroDestPort as ZeroDestinationPort, again to make it more similar to what UDP does. Signed-off-by: Quentin Monnet <[email protected]>
In the net parser, we have a number of enum with members taking arguments of a specific type, and for which we implement the From trait from that type into the enum type. This is convenient for converting parsed headers into the enum, but this leads to verbose implementations of the trait for all enum variants. This is easy enough to make it less verbose with a dedicated macro. We can also use this macro for struct Header in headers.rs. This is in prevision for implementing the From trait on even more enum objects. Signed-off-by: Quentin Monnet <[email protected]>
In preparation for adding a submodule to the "headers" module, move it to its own dedicated directory. Signed-off-by: Quentin Monnet <[email protected]>
TruncatedTcp is an enum that can be either a regular TCP header, or a "truncated TCP header", meaning that it contains at least 8 bytes - the source and destination header of the original TCP header - plus an undetermined number of additional bytes of header, possiby 0. This truncated header can be used to represent the beginning of a TCP header in an IP packet embedded as payload for ICMP Error messages. We need to be able to access to the TCP ports for this embedded packet for NAT, because we need to update them for NAT. TruncatedUdp, as one may have guessed, implements the same structure for possibly-truncated UDP headers. Signed-off-by: Quentin Monnet <[email protected]>
Introduce new enum EmbeddedHeader and struct EmbeddedHeaders, counterparts to enum Header and struct Headers, but for parsing IP packets embedded inside of ICMP Error messages (such as "Destination Unreachable"). It is not possible to reuse the existing enum and structs for IP packets embedded in ICMP messages, because the packets may not be complete, but we still need to access the transport-layer ports if available, for example, even if the transport header is truncated; and the implementation of the Parse trait for the existing structs does not allow that (nor should it). Make an (optional) EmbeddedHeaders object part of struct Headers, so we can hold the parsed data and reuse it when "deparsing" the packet, although the DeParse trait implementation for EmbeddedHeaders will be completed in a follow-up commit. Signed-off-by: Quentin Monnet <[email protected]>
Implement trait DeParse to write headers for an embedded IP packet. Signed-off-by: Quentin Monnet <[email protected]>
Now that we have our new struct EmbeddedHeaders, implement parsing for the payloads of the various IP and related headers towards enum EmbeddedHeader objects. The new parse_embedded_payload() come with a bit of duplcation from the existing parse_payload(), but it's not that bad. They're not entirely the same: they use TruncatedTcp and TruncatedUdp instead of Tcp/Udp, and they only process header types that can be present in the embedded IP packet. Once we have these methods in place, we can complete the implementation of the ParsePayload trait for enum EmbeddedHeader. Signed-off-by: Quentin Monnet <[email protected]>
ICMP Error messages (such as "Destination Unreachable") may contain a payload, made of a full or truncated version of the original IP packet that triggered the error. Parse this packet using the existing EmbeddedHeaders structure added in previous commits. Signed-off-by: Quentin Monnet <[email protected]>
Introduce a method to check whether the IP packet embedded inside of an ICMP Error message is full. This can be useful for NAT. As part of NAT, we should validate and update the checksum for the inner IP and transport layers, but it's not worth updating the checksum if the payload is not complete. It's delicate to perform all checks required for this verification, and we can't call the function just from any location, because we need to access: - The buffer containing the ICMP header, to extract the optional length field to check for the presence of ICMP extensions (this field is not exposed as part of the Icmpv4Header), - The length comsummed when parsing headers for the embedded packet - this is the best alternative we have to re-parsing the full IPv6 header to compute its length, - The parsed data, in particular the total length (IPv4) or payload length (IPv6) and header length (TCP), from the collected headers. For this reason, we store the result as part of the EmbeddedHeaders struct, so we can reuse it when necessary. Signed-off-by: Quentin Monnet <[email protected]>
Add unit tests for struct EmbeddedHeaders, to check parsing/deparsing and full payload availability. Most unit tests were drafted by Claude. Most have been reworked or extended by the author, in particular for checks with ICMP extensions. Generated-by: Claude (Sonnet 4.5) Signed-off-by: Quentin Monnet <[email protected]>
23954d8
to
99cc19b
Compare
@mvachhar I added some tests, see the last two commits, although they're not exhaustive yet. They validate |
Add Bolero generators for TruncatedTcp, TruncatedUdp, and EmbeddedHeaders structs. Also add some basic fuzzing tests leveraging these generators to deparse and reparse generated headers. Note that the tests to not cover IP extensions (which are not covered for regular Headers, either). Signed-off-by: Quentin Monnet <[email protected]>
99cc19b
to
aa43a3d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is lovely work. Very nicely done.
My only reservation was with some length calculations I need to think about a bit more, but we 100% do not need to block merge on that. Worst case we clean up the length calculations later.
I'm especially happy to see the fuzzers at the end
Ipv6ExtNext::Ipv6Ext(value) | ||
} | ||
} | ||
impl_from_for_enum!( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
I tend to write this kind of macro call with []
or possibly {}
instead of ()
. It doesn't matter to the compiler, but it does make it look more like an array to the reader.
Comment
nit:
I tend to write this kind of macro call with []
or possibly {}
instead of ()
. It doesn't matter to the compiler, but it does make it look more like an array to the reader.
impl_from_for_enum![
Ipv6ExtNext,
Tcp(Tcp),
Udp(Udp),
Icmp6(Icmp6),
IpAuth(IpAuth),
Ipv6Ext(Ipv6Ext)
];
<$target>::$variant(value) | ||
} | ||
} | ||
)* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you might add a line break above this line so that cargo-expand
unrolls the macro more legibly when debugging
source_port: TcpPort, | ||
destination_port: TcpPort, | ||
// The rest of the header, as a byte vector, for de-parsing | ||
everything_else: Vec<u8>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
future: this is certainly reasonable for the moment, but we will very likely want to change the type of everything_else
from Vec<u8, Global>
to Vec<u8, BlinkAlloc>
in the near future. We should wait for performance data to make that call tho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, we can do better than that assuming it isn't too onerous to add a lifetime param to the TruncatedTcpHeader
type. If we make this slice a &'a [u8]
or (more likely) an &'a mut [u8]
then we can avoid the whole deep copy operation
let parsed_source_port = u16::from_be_bytes([buf[0], buf[1]]); | ||
let parsed_destination_port = u16::from_be_bytes([buf[2], buf[3]]); | ||
|
||
// buf.len() is always non-zero, and assumed lower than u16::MAX |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it isn't just assumed, it is enforced. Mbuf
can't / won't go that high.
|
||
// Structure representing the set of headers for an IP packet embedded as the payload for an ICMP | ||
// Error message. We need a dedicated struct and processing, because this packet may be truncated. | ||
// RFC 792 stipulates that an ICMP Error message should embed an IP header and only a minimum of 64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is out of scope here to address this comment, but this is the exactly the type of thing for which duvet is handy (also making ticket systems and todo messages make sense).
self.full_payload | ||
} | ||
|
||
pub fn check_full_payload( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to think about this a little bit. Especially as how it relates to packets which change size due to encap/decap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at minimum this will need aggressive fuzzing.
In fact, this is a strong candidate for a kani poof
headers_size: usize, | ||
icmp_length: usize, | ||
) { | ||
self.full_payload = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love the need to keep this type of stat synchronized either. I will need to think of an alternative tho before I actually complain 🤷
} | ||
|
||
fn payload_length(&self, buf: &[u8]) -> usize { | ||
// See RFC 4884. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will check the RFC but right away this feels dangerous. Are we trusting the length field in the packet?
} | ||
|
||
#[cfg(test)] | ||
mod tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests may be fine but this is 100% bolero territory
use crate::udp::TruncatedUdp; | ||
use bolero::{Driver, ValueGenerator}; | ||
|
||
#[allow(dead_code)] // rustc not able to infer we construct this through .with_generator() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the issue is actually that CommonEmbeddedHeaders
is only used in tests and those get compiled out so rustc complains about this being unused.
This PR adds the infrastructure necessary to parse the potentially-truncated IP datagrams embedded inside of ICMP Error messages, such as "Destination Unreachable" messages required for MTU to work. We need to parse these, because we need to NAT the addresses and ports in these embedded datagrams, as per RFC 5508.
It's a large Pull Request, with big commits, although I've tried to keep some logical steps - it's probably best reviewed per-commit.
Note: I'm unsure I'll keep the commit to check whether the payload for the embedded packet is full - it may be less expensive to just update the TCP/UDP checksum incrementally, inconditionally.
TO DO: tests!
Related:
Follow-up tasks for ICMP support in NAT: