Skip to content

Commit 683618b

Browse files
committed
feat(net): Check whether ICMP embedded packet is full
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]>
1 parent 4d3dd89 commit 683618b

File tree

3 files changed

+175
-2
lines changed

3 files changed

+175
-2
lines changed

net/src/headers/embedded.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,135 @@ pub struct EmbeddedHeaders {
3434
net: Option<Net>,
3535
net_ext: ArrayVec<NetExt, MAX_NET_EXTENSIONS>,
3636
transport: Option<EmbeddedTransport>,
37+
full_payload: bool,
38+
}
39+
40+
impl EmbeddedHeaders {
41+
pub fn is_full_payload(&self) -> bool {
42+
self.full_payload
43+
}
44+
45+
pub fn check_full_payload(
46+
&mut self,
47+
buf: &[u8],
48+
remaining: usize,
49+
headers_size: usize,
50+
icmp_length: usize,
51+
) {
52+
self.full_payload = false;
53+
54+
match &mut self.transport {
55+
None
56+
| Some(EmbeddedTransport::Tcp(TruncatedTcp::PartialHeader(_)))
57+
| Some(EmbeddedTransport::Udp(TruncatedUdp::PartialHeader(_))) => {
58+
// We couldn't parse the full transport header, of course we don't have the full,
59+
// valid payload
60+
return;
61+
}
62+
Some(EmbeddedTransport::Tcp(TruncatedTcp::FullHeader(_)))
63+
| Some(EmbeddedTransport::Udp(TruncatedUdp::FullHeader(_))) => {
64+
// There's a chance payload is full, keep going
65+
}
66+
}
67+
68+
// We want to compare the total size of the original IP packet with the length of the ICMP
69+
// payload, knowing that :
70+
//
71+
// Is size_ip_packet == size_icmp_payload?
72+
//
73+
// But for IPv6 we don't have the size of the full packet in the header, we need to sum up
74+
// the sizes of all headers and it's painful. Instead, let's use the length of data we've
75+
// consumed while parsing the ICMP payload. It covers the L3 + L4 headers. The check
76+
// becomes:
77+
//
78+
// Is size_ip_headers + size_ip_payload == size_icmp_payload?
79+
//
80+
// Where size_ip_headers is the length consumed, minus the length of the transport header.
81+
// So in the end, our final check is:
82+
//
83+
// Is size_headers_parsed - size_transport_header + size_ip_payload == size_icmp_payload?
84+
85+
// Find the IP payload length
86+
let ip_payload_length = match &self.net {
87+
None => {
88+
return;
89+
}
90+
Some(Net::Ipv4(ip)) => {
91+
let Ok(ipv4_payload_length) = ip.0.payload_len().map(usize::from) else {
92+
return;
93+
};
94+
ipv4_payload_length
95+
}
96+
Some(Net::Ipv6(ip)) => {
97+
let ipv6_payload_length = ip.0.payload_length;
98+
if ipv6_payload_length == 0 {
99+
// IPv6 Jumbogram (RFC 2675) - we can't know the payload length and it's
100+
// unlikely it's all in the ICMP message payload anyway.
101+
return;
102+
}
103+
ipv6_payload_length as usize
104+
}
105+
};
106+
107+
// Find the transport header length
108+
let transport_header_length = match &mut self.transport {
109+
Some(EmbeddedTransport::Tcp(TruncatedTcp::FullHeader(tcp))) => tcp.header_len().get(),
110+
Some(EmbeddedTransport::Udp(TruncatedUdp::FullHeader(_))) => 8,
111+
_ => unreachable!(), // Checked at the beginning of the function
112+
};
113+
114+
// Compute the size of the IP headers
115+
let Some(size_ip_headers) = headers_size.checked_sub(transport_header_length) else {
116+
return;
117+
};
118+
119+
let full_packet_size = size_ip_headers + ip_payload_length;
120+
121+
if icmp_length > 0 {
122+
// ICMP message may optionally contain the length of the embedded piece of the original
123+
// IP packet. If this is the case, we just need to check the announced IP packet length
124+
// against this value.
125+
//
126+
// From RFC 4884: The length attribute represents the length of the padded "original
127+
// datagram" field.
128+
match self.net {
129+
Some(Net::Ipv4(_)) => {
130+
if icmp_length < full_packet_size {
131+
// The embedded message is shorter than the original packet
132+
return;
133+
}
134+
if icmp_length > buf.len() {
135+
// Embedded payload is larger than our buffer? Something's wrong
136+
return;
137+
}
138+
let padding_length = icmp_length - full_packet_size;
139+
// ICMPv4: Padding is on 32-bit boundaries
140+
self.full_payload = padding_length < 32
141+
&& buf[full_packet_size..icmp_length].iter().all(|b| *b == 0);
142+
}
143+
Some(Net::Ipv6(_)) => {
144+
if icmp_length < full_packet_size {
145+
// The embedded message is shorter than the original packet
146+
return;
147+
}
148+
if icmp_length > buf.len() {
149+
// Embedded payload is larger than our buffer? Something's wrong
150+
return;
151+
}
152+
let padding_length = icmp_length - full_packet_size;
153+
// ICMPv6: Padding is on 64-bit boundaries
154+
self.full_payload = padding_length < 64
155+
&& buf[full_packet_size..icmp_length].iter().all(|b| *b == 0);
156+
}
157+
None => {
158+
unreachable!() // Checked earlier in the function
159+
}
160+
}
161+
}
162+
163+
// Check that the full headers + payload are present
164+
self.full_payload = full_packet_size == remaining;
165+
}
37166
}
38167

39168
impl ParseWith for EmbeddedHeaders {

net/src/icmp4/mod.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,35 @@ impl Icmp4 {
130130
})
131131
}
132132

133+
fn payload_length(&self, buf: &[u8]) -> usize {
134+
// See RFC 4884. Icmpv4Type::Redirect does not get an optional length field.
135+
match self.icmp_type() {
136+
Icmpv4Type::DestinationUnreachable(_)
137+
| Icmpv4Type::TimeExceeded(_)
138+
| Icmpv4Type::ParameterProblem(_) => {
139+
let payload_length = buf[4];
140+
payload_length as usize * 4
141+
}
142+
_ => 0,
143+
}
144+
}
145+
133146
pub(crate) fn parse_payload(&self, cursor: &mut Reader) -> Option<EmbeddedHeaders> {
134147
if !self.is_error_message() {
135148
return None;
136149
}
137-
let (headers, consumed) =
150+
let (mut headers, consumed) =
138151
EmbeddedHeaders::parse_with(EmbeddedIpVersion::Ipv4, cursor.inner).ok()?;
139152
cursor.consume(consumed).ok()?;
153+
154+
// Mark whether the payload of the embedded IP packet is full
155+
headers.check_full_payload(
156+
&cursor.inner[cursor.inner.len() - cursor.remaining as usize..],
157+
cursor.remaining as usize,
158+
consumed.get() as usize,
159+
self.payload_length(cursor.inner),
160+
);
161+
140162
Some(headers)
141163
}
142164
}

net/src/icmp6/mod.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,35 @@ impl Icmp6 {
6565
})
6666
}
6767

68+
fn payload_length(&self, buf: &[u8]) -> usize {
69+
// See RFC 4884.
70+
match self.icmp_type() {
71+
Icmpv6Type::DestinationUnreachable(_)
72+
| Icmpv6Type::TimeExceeded(_)
73+
| Icmpv6Type::ParameterProblem(_) => {
74+
let payload_length = buf[3];
75+
payload_length as usize * 8
76+
}
77+
_ => 0,
78+
}
79+
}
80+
6881
pub(crate) fn parse_payload(&self, cursor: &mut Reader) -> Option<EmbeddedHeaders> {
6982
if !self.is_error_message() {
7083
return None;
7184
}
72-
let (headers, consumed) =
85+
let (mut headers, consumed) =
7386
EmbeddedHeaders::parse_with(EmbeddedIpVersion::Ipv6, cursor.inner).ok()?;
7487
cursor.consume(consumed).ok()?;
88+
89+
// Mark whether the payload of the embedded IP packet is full
90+
headers.check_full_payload(
91+
&cursor.inner[cursor.inner.len() - cursor.remaining as usize..],
92+
cursor.remaining as usize,
93+
consumed.get() as usize,
94+
self.payload_length(cursor.inner),
95+
);
96+
7597
Some(headers)
7698
}
7799
}

0 commit comments

Comments
 (0)