blob: 76c3ca1b32363fa01c210a1df993f8643d612669 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! The Internet Control Message Protocol (ICMP).
use std::mem;
use log::trace;
use packet::{BufferMut, BufferSerializer, Serializer};
use specialize_ip_macro::specialize_ip_address;
use crate::device::DeviceId;
use crate::ip::{
send_icmp_response, send_ip_packet, IpAddress, IpProto, Ipv4, Ipv4Addr, Ipv6, Ipv6Addr,
IPV6_MIN_MTU,
};
use crate::wire::icmp::{
IcmpDestUnreachable, IcmpPacketBuilder, IcmpParseArgs, IcmpTimeExceeded, IcmpUnusedCode,
Icmpv4DestUnreachableCode, Icmpv4Packet, Icmpv4TimeExceededCode, Icmpv6DestUnreachableCode,
Icmpv6Packet, Icmpv6PacketTooBig, Icmpv6ParameterProblem, Icmpv6ParameterProblemCode,
Icmpv6TimeExceededCode,
};
use crate::{Context, EventDispatcher};
/// Receive an ICMP message in an IP packet.
#[specialize_ip_address]
pub(crate) fn receive_icmp_packet<D: EventDispatcher, A: IpAddress, B: BufferMut>(
ctx: &mut Context<D>,
src_ip: A,
dst_ip: A,
mut buffer: B,
) {
trace!("receive_icmp_packet({}, {})", src_ip, dst_ip);
#[ipv4addr]
{
let mut buffer = buffer;
let packet =
match buffer.parse_with::<_, Icmpv4Packet<_>>(IcmpParseArgs::new(src_ip, dst_ip)) {
Ok(packet) => packet,
Err(err) => return, // TODO(joshlf): Do something else here?
};
match packet {
Icmpv4Packet::EchoRequest(echo_request) => {
let req = *echo_request.message();
let code = echo_request.code();
// drop packet so we can re-use the underlying buffer
mem::drop(echo_request);
increment_counter!(ctx, "receive_icmp_packet::echo_request");
// we're responding to the sender, so these are flipped
let (src_ip, dst_ip) = (dst_ip, src_ip);
// TODO(joshlf): Do something if send_ip_packet returns an
// error?
send_ip_packet(ctx, dst_ip, IpProto::Icmp, |src_ip| {
BufferSerializer::new_vec(buffer).encapsulate(
IcmpPacketBuilder::<Ipv4, &[u8], _>::new(src_ip, dst_ip, code, req.reply()),
)
});
}
Icmpv4Packet::EchoReply(echo_reply) => {
increment_counter!(ctx, "receive_icmp_packet::echo_reply");
trace!("receive_icmp_packet: Received an EchoReply message");
}
_ => log_unimplemented!(
(),
"ip::icmp::receive_icmp_packet: Not implemented for this packet type"
),
}
}
#[ipv6addr]
{
let packet =
match buffer.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, dst_ip)) {
Ok(packet) => packet,
Err(err) => return, // TODO(joshlf): Do something else here?
};
match packet {
Icmpv6Packet::EchoRequest(echo_request) => {
let req = *echo_request.message();
let code = echo_request.code();
// drop packet so we can re-use the underlying buffer
mem::drop(echo_request);
increment_counter!(ctx, "receive_icmp_packet::echo_request");
// we're responding to the sender, so these are flipped
let (src_ip, dst_ip) = (dst_ip, src_ip);
// TODO(joshlf): Do something if send_ip_packet returns an
// error?
send_ip_packet(ctx, dst_ip, IpProto::Icmp, |src_ip| {
BufferSerializer::new_vec(buffer).encapsulate(
IcmpPacketBuilder::<Ipv6, &[u8], _>::new(src_ip, dst_ip, code, req.reply()),
)
});
}
Icmpv6Packet::EchoReply(echo_reply) => {
increment_counter!(ctx, "receive_icmp_packet::echo_reply");
trace!("receive_icmp_packet: Received an EchoReply message");
}
_ => log_unimplemented!(
(),
"ip::icmp::receive_icmp_packet: Not implemented for this packet type"
),
}
}
}
/// Send an ICMP message in response to receiving a packet destined for an
/// unsupported IPv4 protocol or IPv6 next header.
///
/// `send_icmp_protocol_unreachable` sends the appropriate ICMP or ICMPv6
/// message in response to receiving an IP packet from `src_ip` to `dst_ip`
/// identifying an unsupported protocol. For IPv4, this is an ICMP "destination
/// unreachable" message with a "protocol unreachable" code. For IPv6, this is
/// an ICMPv6 "parameter problem" message with an "unrecognized next header
/// type" code.
///
/// `original_packet` contains the contents of the entire original packet -
/// including all IP headers. `header_len` is the length of either the IPv4
/// header or all IPv6 headers (including extension headers) *before* the
/// payload with the problematic Next Header type. In other words, in an IPv6
/// packet with a single header with a Next Header type of TCP, it `header_len`
/// would be the length of the single header (40 bytes).
#[specialize_ip_address]
pub(crate) fn send_icmp_protocol_unreachable<D: EventDispatcher, A: IpAddress, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: A,
dst_ip: A,
original_packet: B,
header_len: usize,
) {
increment_counter!(ctx, "send_icmp_protocol_unreachable");
#[ipv4addr]
send_icmpv4_dest_unreachable(
ctx,
device,
src_ip,
dst_ip,
Icmpv4DestUnreachableCode::DestProtocolUnreachable,
original_packet,
header_len,
);
#[ipv6addr]
{
// Per RFC 4443, body contains as much of the original body as
// possible without exceeding IPv6 minimum MTU.
let mut original_packet = original_packet;
original_packet.shrink_back_to(IPV6_MIN_MTU as usize);
// TODO(joshlf): Do something if send_icmp_response returns an error?
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmpv6, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(IcmpPacketBuilder::<
Ipv6,
&[u8],
_,
>::new(
local_ip,
src_ip,
Icmpv6ParameterProblemCode::UnrecognizedNextHeaderType,
// Per RFC 4443, the pointer refers to the first byte of the
// packet whose Next Header field was unrecognized. It is
// measured as an offset from the beginning of the first IPv6
// header. E.g., a pointer of 40 (the length of a single IPv6
// header) would indicate that the Next Header field from that
// header - and hence of the first encapsulated packet - was
// unrecognized.
//
// NOTE: Since header_len is a usize, this could theoretically
// be a lossy conversion. However, all that means in practice is
// that, if a remote host somehow managed to get us to process a
// frame with a 4GB IP header and send an ICMP response, the
// pointer value would be wrong. It's not worth wasting special
// logic to avoid generating a malformed packet in a case that
// will almost certainly never happen.
Icmpv6ParameterProblem::new(header_len as u32),
))
});
}
}
/// Send an ICMP message in response to receiving a packet destined for an
/// unreachable local transport-layer port.
///
/// `send_icmp_port_unreachable` sends the appropriate ICMP or ICMPv6 message in
/// response to receiving an IP packet from `src_ip` to `dst_ip` with an
/// unreachable local transport-layer port. For both IPv4 and IPv6, this is an
/// ICMP(v6) "destination unreachable" message with a "port unreachable" code.
///
/// `original_packet` contains the contents of the entire original packet -
/// including all IP headers. `ipv4_header_len` is the length of the IPv4
/// header. It is ignored for IPv6.
#[specialize_ip_address]
pub(crate) fn send_icmp_port_unreachable<D: EventDispatcher, A: IpAddress, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: A,
dst_ip: A,
original_packet: B,
ipv4_header_len: usize,
) {
increment_counter!(ctx, "send_icmp_port_unreachable");
#[ipv4addr]
send_icmpv4_dest_unreachable(
ctx,
device,
src_ip,
dst_ip,
Icmpv4DestUnreachableCode::DestPortUnreachable,
original_packet,
ipv4_header_len,
);
#[ipv6addr]
send_icmpv6_dest_unreachable(
ctx,
device,
src_ip,
dst_ip,
Icmpv6DestUnreachableCode::PortUnreachable,
original_packet,
);
}
/// Send an ICMP message in response to receiving a packet destined for an
/// unreachable network.
///
/// `send_icmp_net_unreachable` sends the appropriate ICMP or ICMPv6 message in
/// response to receiving an IP packet from `src_ip` to an unreachable `dst_ip`.
/// For IPv4, this is an ICMP "destination unreachable" message with a "net
/// unreachable" code. For IPv6, this is an ICMPv6 "destination unreachable"
/// message with a "no route to destination" code.
///
/// `original_packet` contains the contents of the entire original packet -
/// including all IP headers. `ipv4_header_len` is the length of the IPv4
/// header. It is ignored for IPv6.
#[specialize_ip_address]
pub(crate) fn send_icmp_net_unreachable<D: EventDispatcher, A: IpAddress, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: A,
dst_ip: A,
original_packet: B,
ipv4_header_len: usize,
) {
increment_counter!(ctx, "send_icmp_net_unreachable");
#[ipv4addr]
send_icmpv4_dest_unreachable(
ctx,
device,
src_ip,
dst_ip,
Icmpv4DestUnreachableCode::DestNetworkUnreachable,
original_packet,
ipv4_header_len,
);
#[ipv6addr]
send_icmpv6_dest_unreachable(
ctx,
device,
src_ip,
dst_ip,
Icmpv6DestUnreachableCode::NoRoute,
original_packet,
);
}
/// Send an ICMP message in response to receiving a packet whose TTL has
/// expired.
///
/// `send_icmp_ttl_expired` sends the appropriate ICMP or ICMPv6 message in
/// response to receiving an IP packet from `src_ip` to `dst_ip` whose TTL
/// (IPv4)/Hop Limit (IPv6) has expired. For IPv4, this is an ICMP "time
/// exceeded" message with a "time to live exceeded in transit" code. For IPv6,
/// this is an ICMPv6 "time exceeded" message with a "hop limit exceeded in
/// transit" code.
///
/// `original_packet` contains the contents of the entire original packet -
/// including all IP headers. `ipv4_header_len` is the length of the IPv4
/// header. It is ignored for IPv6.
#[specialize_ip_address]
pub(crate) fn send_icmp_ttl_expired<D: EventDispatcher, A: IpAddress, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: A,
dst_ip: A,
original_packet: B,
ipv4_header_len: usize,
) {
increment_counter!(ctx, "send_icmp_ttl_expired");
#[ipv4addr]
{
// Per RFC 792, body contains entire IPv4 header + 64 bytes of
// original body.
let mut original_packet = original_packet;
original_packet.shrink_back_to(ipv4_header_len + 64);
// TODO(joshlf): Do something if send_icmp_response returns an error?
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmp, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(IcmpPacketBuilder::<
Ipv4,
&[u8],
_,
>::new(
local_ip,
src_ip,
Icmpv4TimeExceededCode::TtlExpired,
IcmpTimeExceeded::default(),
))
});
}
#[ipv6addr]
{
// Per RFC 4443, body contains as much of the original body as
// possible without exceeding IPv6 minimum MTU.
let mut original_packet = original_packet;
original_packet.shrink_back_to(IPV6_MIN_MTU as usize);
// TODO(joshlf): Do something if send_icmp_response returns an error?
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmpv6, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(IcmpPacketBuilder::<
Ipv6,
&[u8],
_,
>::new(
local_ip,
src_ip,
Icmpv6TimeExceededCode::HopLimitExceeded,
IcmpTimeExceeded::default(),
))
});
}
}
// TODO(joshlf): Test send_icmpv6_packet_too_big once we support dummy IPv6 test
// setups.
/// Send an ICMPv6 message in response to receiving a packet whose size exceeds
/// the MTU of the next hop interface.
///
/// `send_icmpv6_packet_too_big` sends an ICMPv6 "packet too big" message in
/// response to receiving an IP packet from `src_ip` to `dst_ip` whose size
/// exceeds the `mtu` of the next hop interface.
pub(crate) fn send_icmpv6_packet_too_big<D: EventDispatcher, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
mtu: u32,
mut original_packet: B,
) {
increment_counter!(ctx, "send_icmpv6_packet_too_big");
// Per RFC 4443, body contains as much of the original body as possible
// without exceeding IPv6 minimum MTU.
original_packet.shrink_back_to(IPV6_MIN_MTU as usize);
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmpv6, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(
IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
local_ip,
src_ip,
IcmpUnusedCode,
Icmpv6PacketTooBig::new(mtu),
),
)
});
}
fn send_icmpv4_dest_unreachable<D: EventDispatcher, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
code: Icmpv4DestUnreachableCode,
original_packet: B,
header_len: usize,
) {
// Per RFC 792, body contains entire IPv4 header + 64 bytes of original
// body.
let mut original_packet = original_packet;
original_packet.shrink_back_to(header_len + 64);
// TODO(joshlf): Do something if send_icmp_response returns an error?
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmp, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(
IcmpPacketBuilder::<Ipv4, &[u8], _>::new(
local_ip,
src_ip,
code,
IcmpDestUnreachable::default(),
),
)
});
}
fn send_icmpv6_dest_unreachable<D: EventDispatcher, B: BufferMut>(
ctx: &mut Context<D>,
device: DeviceId,
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
code: Icmpv6DestUnreachableCode,
original_packet: B,
) {
// Per RFC 4443, body contains as much of the original body as possible
// without exceeding IPv6 minimum MTU.
let mut original_packet = original_packet;
original_packet.shrink_back_to(IPV6_MIN_MTU as usize);
// TODO(joshlf): Do something if send_icmp_response returns an error?
send_icmp_response(ctx, device, src_ip, dst_ip, IpProto::Icmpv6, |local_ip| {
BufferSerializer::new_vec(original_packet).encapsulate(
IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
local_ip,
src_ip,
code,
IcmpDestUnreachable::default(),
),
)
});
}
#[cfg(test)]
mod tests {
use packet::{Buf, BufferSerializer, Serializer};
use std::fmt::Debug;
#[cfg(feature = "udp-icmp-port-unreachable")]
use std::num::NonZeroU16;
use super::*;
use crate::device::{DeviceId, FrameDestination};
use crate::ip::{receive_ip_packet, IpExt, Ipv4, Ipv4Addr};
use crate::testutil::{DummyEventDispatcherBuilder, DUMMY_CONFIG};
use crate::wire::icmp::{
IcmpEchoRequest, IcmpMessage, IcmpPacket, IcmpUnusedCode, MessageBody,
};
#[cfg(feature = "udp-icmp-port-unreachable")]
use crate::wire::udp::UdpPacketBuilder;
/// Test that receiving a particular IP packet results in a particular ICMP
/// response.
///
/// Test that receiving an IP packet from remote host
/// `DUMMY_CONFIG.remote_ip` to host `dst_ip` with `ttl` and `proto` results
/// in all of the counters in `assert_counters` being triggered at least
/// once, and exactly one ICMP packet being sent in response with
/// `expect_message` and `expect_code`.
///
/// The state is initialized to `testutil::DUMMY_CONFIG` when testing.
#[allow(clippy::too_many_arguments)]
fn test_receive_ip_packet<
C: PartialEq + Debug,
M: for<'a> IcmpMessage<Ipv4, &'a [u8], Code = C> + PartialEq + Debug,
F: for<'a> Fn(&IcmpPacket<Ipv4, &'a [u8], M>),
>(
body: &mut [u8],
dst_ip: Ipv4Addr,
ttl: u8,
proto: IpProto,
assert_counters: &[&str],
expect_message: M,
expect_code: C,
f: F,
) {
crate::testutil::set_logger_for_test();
let buffer = BufferSerializer::new_vec(Buf::new(body, ..))
.encapsulate(<Ipv4 as IpExt<&[u8]>>::PacketBuilder::new(
DUMMY_CONFIG.remote_ip,
dst_ip,
ttl,
proto,
))
.serialize_outer()
.unwrap();
let mut ctx = DummyEventDispatcherBuilder::from_config(DUMMY_CONFIG).build();
// currently only used by test_ttl_exceeded
ctx.state_mut().ip.v4.forward = true;
receive_ip_packet::<_, _, Ipv4>(
&mut ctx,
DeviceId::new_ethernet(1),
FrameDestination::Unicast,
buffer,
);
for counter in assert_counters {
assert!(*ctx.state().test_counters.get(counter) > 0, "counter at zero: {}", counter);
}
assert_eq!(ctx.dispatcher().frames_sent().len(), 1);
let (src_mac, dst_mac, src_ip, dst_ip, message, code) =
crate::testutil::parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv4, _, M, _>(
&ctx.dispatcher().frames_sent()[0].1,
f,
)
.unwrap();
assert_eq!(src_mac, DUMMY_CONFIG.local_mac);
assert_eq!(dst_mac, DUMMY_CONFIG.remote_mac);
assert_eq!(src_ip, DUMMY_CONFIG.local_ip);
assert_eq!(dst_ip, DUMMY_CONFIG.remote_ip);
assert_eq!(message, expect_message);
assert_eq!(code, expect_code);
}
#[test]
fn test_receive_echo() {
crate::testutil::set_logger_for_test();
let req = IcmpEchoRequest::new(0, 0);
let req_body = &[1, 2, 3, 4];
let mut buffer = BufferSerializer::new_vec(Buf::new(req_body.to_vec(), ..))
.encapsulate(IcmpPacketBuilder::<Ipv4, &[u8], _>::new(
DUMMY_CONFIG.remote_ip,
DUMMY_CONFIG.local_ip,
IcmpUnusedCode,
req,
))
.serialize_outer()
.unwrap();
test_receive_ip_packet(
buffer.as_mut(),
DUMMY_CONFIG.local_ip,
64,
IpProto::Icmp,
&["receive_icmp_packet::echo_request", "send_ip_packet"],
req.reply(),
IcmpUnusedCode,
|packet| assert_eq!(packet.original_packet().bytes(), req_body),
);
}
#[test]
fn test_protocol_unreachable() {
// Receive an IP packet for an unreachable protocol (255). Check to make
// sure that we respond with the appropriate ICMP message.
//
// TODO(joshlf): Also perform the check for IPv6 once we support dummy
// IPv6 networks.
test_receive_ip_packet(
&mut [0u8; 128],
DUMMY_CONFIG.local_ip,
64,
IpProto::Other(255),
&["send_icmp_protocol_unreachable", "send_icmp_response"],
IcmpDestUnreachable::default(),
Icmpv4DestUnreachableCode::DestProtocolUnreachable,
// ensure packet is truncated to the right length
|packet| assert_eq!(packet.original_packet().bytes().len(), 84),
);
}
#[test]
#[cfg(feature = "udp-icmp-port-unreachable")]
fn test_port_unreachable() {
// TODO(joshlf): Use TCP in addition to UDP since UDP only works with
// the udp-icmp-port-unreachable feature enabled.
// Receive an IP packet for an unreachable UDP port (1234). Check to
// make sure that we respond with the appropriate ICMP message.
//
// TODO(joshlf):
// - Also perform the check for IPv6 once we support dummy IPv6
// networks.
// - Perform the same check for TCP once the logic is implemented
let mut buf = [0u8; 128];
let mut buffer = BufferSerializer::new_vec(Buf::new(&mut buf[..], ..))
.encapsulate(UdpPacketBuilder::new(
DUMMY_CONFIG.remote_ip,
DUMMY_CONFIG.local_ip,
None,
NonZeroU16::new(1234).unwrap(),
))
.serialize_outer();
test_receive_ip_packet(
buffer.as_mut(),
DUMMY_CONFIG.local_ip,
64,
IpProto::Udp,
&["send_icmp_port_unreachable", "send_icmp_response"],
IcmpDestUnreachable::default(),
Icmpv4DestUnreachableCode::DestPortUnreachable,
// ensure packet is truncated to the right length
|packet| assert_eq!(packet.original_packet().bytes().len(), 84),
);
}
#[test]
fn test_net_unreachable() {
// Receive an IP packet for an unreachable destination address. Check to
// make sure that we respond with the appropriate ICMP message.
//
// TODO(joshlf): Also perform the check for IPv6 once we support dummy
// IPv6 networks.
test_receive_ip_packet(
&mut [0u8; 128],
Ipv4Addr::new([1, 2, 3, 4]),
64,
IpProto::Udp,
&["send_icmp_net_unreachable", "send_icmp_response"],
IcmpDestUnreachable::default(),
Icmpv4DestUnreachableCode::DestNetworkUnreachable,
// ensure packet is truncated to the right length
|packet| assert_eq!(packet.original_packet().bytes().len(), 84),
);
}
#[test]
fn test_ttl_expired() {
// Receive an IP packet with an expired TTL. Check to make sure that we
// respond with the appropriate ICMP message.
//
// TODO(joshlf): Also perform the check for IPv6 once we support dummy
// IPv6 networks.
test_receive_ip_packet(
&mut [0u8; 128],
DUMMY_CONFIG.remote_ip,
1,
IpProto::Udp,
&["send_icmp_ttl_expired", "send_icmp_response"],
IcmpTimeExceeded::default(),
Icmpv4TimeExceededCode::TtlExpired,
// ensure packet is truncated to the right length
|packet| assert_eq!(packet.original_packet().bytes().len(), 84),
);
}
}