Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/iface/interface/ipv4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ impl Interface {
}
}

/// Whether an ICMPv4 "destination unreachable" code is a hard error for TCP.
///
/// RFC 1122 §4.2.3.9 classifies codes 0, 1 and 5 as soft errors that must not
/// abort a connection; RFC 5461 §4 also mentions this in a more general term. Note that
/// `FragRequired` must stay soft error: it drives Path MTU Discovery, not teardown the socket.
#[cfg(feature = "socket-tcp")]
fn icmpv4_dst_unreachable_is_hard(reason: Icmpv4DstUnreachable) -> bool {
match reason {
// Explicit refusal; retrying will not help.
Icmpv4DstUnreachable::ProtoUnreachable
| Icmpv4DstUnreachable::PortUnreachable
| Icmpv4DstUnreachable::NetProhibited
| Icmpv4DstUnreachable::HostProhibited
| Icmpv4DstUnreachable::CommProhibited => true,
// Everything else, including FragRequired and the transient
// net/host unreachable codes, is a soft error.
_ => false,
}
}

impl InterfaceInner {
/// Get the next IPv4 fragment identifier.
#[cfg(feature = "proto-ipv4-fragmentation")]
Expand Down Expand Up @@ -359,13 +379,35 @@ impl InterfaceInner {
// Ignore any echo replies.
Icmpv4Repr::EchoReply { .. } => None,

// A hard error referring to one of our TCP connections aborts an
// outstanding connection attempt. RFC 1122 §4.2.3.9 & RFC 5461 §4
#[cfg(feature = "socket-tcp")]
Icmpv4Repr::DstUnreachable {
reason,
header,
data,
} if header.next_header == IpProtocol::Tcp
&& icmpv4_dst_unreachable_is_hard(reason) =>
{
super::tcp_deliver_icmp_hard_error(
_sockets,
header.src_addr.into(),
header.dst_addr.into(),
data,
);
None
}

// Don't report an error if a packet with unknown type
// has been handled by an ICMP socket
#[cfg(feature = "socket-icmp")]
_ if handled_by_icmp_socket => None,

// FIXME: do something correct here?
// By doing nothing, this arm handles the case when auto echo replies are disabled.
// FIXME:
// - FragRequired would drive Path MTU Discovery (RFC 1191), which
// needs a per-destination MTU cache that does not exist yet.
// - TimeExceeded and ParamProblem are informational but could be handled.
// This arm also handles the case when auto echo replies are disabled.
_ => None,
}
}
Expand Down
48 changes: 46 additions & 2 deletions src/iface/interface/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ impl Default for HopByHopResponse<'_> {
}
}

/// Whether an ICMPv6 "destination unreachable" code is a hard error for TCP.
///
/// RFC 5461 §4 asks TCP to be conservative here: codes that indicate a
/// transient routing condition are soft errors and must not abort a connection
/// attempt, while an explicit administrative or policy rejection will not
/// resolve itself and may as well fail fast.
#[cfg(feature = "socket-tcp")]
fn icmpv6_dst_unreachable_is_hard(reason: Icmpv6DstUnreachable) -> bool {
match reason {
// Transient: the route may come back.
Icmpv6DstUnreachable::NoRoute | Icmpv6DstUnreachable::AddrUnreachable => false,
// Explicit refusal; retrying will not help.
Icmpv6DstUnreachable::AdminProhibit
| Icmpv6DstUnreachable::BeyondScope
| Icmpv6DstUnreachable::PortUnreachable
| Icmpv6DstUnreachable::FailedPolicy
| Icmpv6DstUnreachable::RejectRoute => true,
// Unknown codes are treated as soft.
_ => false,
}
}

impl InterfaceInner {
/// Return the IPv6 address that is a candidate source address for the given destination
/// address, based on RFC 6724.
Expand Down Expand Up @@ -436,13 +458,35 @@ impl InterfaceInner {
_ => None,
},

// A hard error referring to one of our TCP connections aborts an
// outstanding connection attempt. RFC 1122 §4.2.3.9 & RFC 5461 §4
#[cfg(feature = "socket-tcp")]
Icmpv6Repr::DstUnreachable {
reason,
header,
data,
} if header.next_header == IpProtocol::Tcp
&& icmpv6_dst_unreachable_is_hard(reason) =>
{
super::tcp_deliver_icmp_hard_error(
_sockets,
header.src_addr.into(),
header.dst_addr.into(),
data,
);
None
}

// Don't report an error if a packet with unknown type
// has been handled by an ICMP socket
#[cfg(feature = "socket-icmp")]
_ if handled_by_icmp_socket => None,

// FIXME: do something correct here?
// By doing nothing, this arm handles the case when auto echo replies are disabled.
// FIXME:
// - PktTooBig would drive Path MTU Discovery (RFC 8201), which
// needs a per-destination MTU cache that does not exist yet.
// - TimeExceeded and ParamProblem are informational.
// This arm also handles the case when auto echo replies are disabled.
_ => None,
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/iface/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,35 @@ impl Interface {
}
}

/// Deliver an ICMP hard error to the TCP socket owning the connection that the
/// error refers to, if any.
#[cfg(feature = "socket-tcp")]
fn tcp_deliver_icmp_hard_error(
sockets: &mut SocketSet,
local_addr: IpAddress,
remote_addr: IpAddress,
payload: &[u8],
) {
use crate::socket::AnySocket;
use crate::socket::tcp::Socket as TcpSocket;

let packet = match TcpPacket::new_checked(payload) {
Ok(packet) => packet,
Err(_) => return,
};
let local = IpEndpoint::new(local_addr, packet.src_port());
let remote = IpEndpoint::new(remote_addr, packet.dst_port());

for socket in sockets
.items_mut()
.filter_map(|i| TcpSocket::downcast_mut(&mut i.socket))
{
if socket.local_endpoint() == Some(local) && socket.remote_endpoint() == Some(remote) {
socket.on_icmp_hard_error();
}
}
}

impl InterfaceInner {
#[allow(unused)] // unused depending on which sockets are enabled
pub(crate) fn now(&self) -> Instant {
Expand Down
124 changes: 124 additions & 0 deletions src/iface/interface/tests/ipv4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1548,3 +1548,127 @@ fn test_ipv4_fragment_size() {
);
}
}

/// Build an ICMPv4 Destination Unreachable quoting a TCP segment we sent, feed
/// it to the interface, and return the resulting state of the socket.
#[cfg(all(feature = "medium-ip", feature = "socket-tcp"))]
fn icmpv4_dst_unreachable_effect_on_connect(
reason: Icmpv4DstUnreachable,
) -> crate::socket::tcp::State {
use crate::socket::tcp;

let (mut iface, mut sockets, _device) = setup(Medium::Ip);

let local = Ipv4Address::new(192, 168, 1, 1);
let remote = Ipv4Address::new(192, 168, 1, 2);
let (local_port, remote_port) = (49152u16, 80u16);

let handle = sockets.add(tcp::Socket::new(
tcp::SocketBuffer::new(vec![0; 64]),
tcp::SocketBuffer::new(vec![0; 64]),
));
sockets
.get_mut::<tcp::Socket>(handle)
.connect(
&mut iface.inner,
IpEndpoint::new(remote.into(), remote_port),
(IpAddress::from(local), local_port),
)
.unwrap();
assert_eq!(
sockets.get_mut::<tcp::Socket>(handle).state(),
tcp::State::SynSent
);

let quoted = TcpRepr {
src_port: local_port,
dst_port: remote_port,
control: TcpControl::Syn,
seq_number: TcpSeqNumber(0),
ack_number: None,
window_len: 1024,
window_scale: None,
max_seg_size: None,
sack_permitted: false,
sack_ranges: [None, None, None],
timestamp: None,
payload: &[],
};
let mut quoted_bytes = vec![0u8; quoted.buffer_len()];
quoted.emit(
&mut TcpPacket::new_unchecked(&mut quoted_bytes),
&local.into(),
&remote.into(),
&ChecksumCapabilities::default(),
);

let icmp_repr = Icmpv4Repr::DstUnreachable {
reason,
header: Ipv4Repr {
src_addr: local,
dst_addr: remote,
next_header: IpProtocol::Tcp,
payload_len: quoted_bytes.len(),
hop_limit: 64,
},
data: &quoted_bytes,
};
let mut icmp_bytes = vec![0u8; icmp_repr.buffer_len()];
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut icmp_bytes),
&ChecksumCapabilities::default(),
);

iface.inner.process_icmpv4(
&mut sockets,
Ipv4Repr {
src_addr: remote,
dst_addr: local,
next_header: IpProtocol::Icmp,
payload_len: icmp_bytes.len(),
hop_limit: 64,
},
&icmp_bytes,
);

sockets.get_mut::<tcp::Socket>(handle).state()
}

/// A hard error aborts an outstanding connection attempt. RFC 1122 §4.2.3.9
#[test]
#[cfg(all(feature = "medium-ip", feature = "socket-tcp"))]
fn test_icmpv4_hard_error_aborts_tcp_connect() {
for reason in [
Icmpv4DstUnreachable::ProtoUnreachable,
Icmpv4DstUnreachable::PortUnreachable,
Icmpv4DstUnreachable::NetProhibited,
Icmpv4DstUnreachable::HostProhibited,
Icmpv4DstUnreachable::CommProhibited,
] {
assert_eq!(
icmpv4_dst_unreachable_effect_on_connect(reason),
crate::socket::tcp::State::Closed,
"{reason} should abort the connection attempt"
);
}
}

/// Soft errors, and FragRequired in particular, must not abort the attempt:
/// codes 0, 1 and 5 are transient (RFC 1122 §4.2.3.9) and FragRequired drives
/// Path MTU Discovery rather than teardown.
#[test]
#[cfg(all(feature = "medium-ip", feature = "socket-tcp"))]
fn test_icmpv4_soft_error_does_not_abort_tcp_connect() {
for reason in [
Icmpv4DstUnreachable::NetUnreachable,
Icmpv4DstUnreachable::HostUnreachable,
Icmpv4DstUnreachable::SrcRouteFailed,
Icmpv4DstUnreachable::FragRequired,
] {
assert_eq!(
icmpv4_dst_unreachable_effect_on_connect(reason),
crate::socket::tcp::State::SynSent,
"{reason} is a soft error and must be ignored"
);
}
}
Loading
Loading