Skip to content
Merged
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
44 changes: 44 additions & 0 deletions src/network/udp_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,33 @@ impl UDPSender {
self.socket_ref(id).map(AsRawFd::as_raw_fd)
}

/// Whether socket `id` can address `addr` at all. The unicast socket is
/// IPv4-only, and a multicast socket only speaks the family of the interface
/// address it was bound to. Sending across families fails with
/// `EAFNOSUPPORT` on every datagram, so we filter such destinations out
/// instead of letting them turn into a per-datagram warning. Peers legally
/// announce locators we cannot reach (a remote IPv6 locator, or our own
/// IPv6 interface addresses seen via loopback discovery), so this is normal.
fn socket_can_reach(&self, id: SocketId, addr: SocketAddr) -> bool {
match id {
SocketId::Unicast => addr.is_ipv4(),
SocketId::Multicast(i) => match self.multicast_sockets.get(i) {
Some((InterfaceSelector::Ip(iface_ip), _)) => iface_ip.is_ipv4() == addr.is_ipv4(),
None => false,
},
}
}

/// One non-blocking datagram send. Never blocks; classifies the result.
fn raw_send(&self, id: SocketId, addr: SocketAddr, buffer: &[u8]) -> SendOutcome {
let Some(socket) = self.socket_ref(id) else {
error!("raw_send: no socket for {id:?}");
return SendOutcome::Dropped;
};
if !self.socket_can_reach(id, addr) {
trace!("raw_send: {id:?} cannot reach {addr} (address family mismatch), dropping");
return SendOutcome::Dropped;
}
match socket.send_to(buffer, addr) {
Ok(bytes_sent) => {
if bytes_sent != buffer.len() {
Expand Down Expand Up @@ -538,4 +559,27 @@ mod tests {
assert_eq!(rec_data_2.len(), 6);
assert_eq!(rec_data_2, data);
}

// The unicast socket is IPv4-only, so an IPv6 destination is unreachable and
// must be dropped without attempting (and warning about) a send that can only
// ever fail with EAFNOSUPPORT.
#[test]
fn unicast_socket_rejects_ipv6_destination() {
let sender = UDPSender::new(11401).expect("failed to create UDPSender");

let v4_dest = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 10401);
let v6_dest = SocketAddr::new(
IpAddr::V6(std::net::Ipv6Addr::new(
0xfe80, 0, 0, 0, 0xd494, 0x8fff, 0xfe08, 0x3ce3,
)),
10401,
);

assert!(sender.socket_can_reach(SocketId::Unicast, v4_dest));
assert!(!sender.socket_can_reach(SocketId::Unicast, v6_dest));
assert_eq!(
sender.raw_send(SocketId::Unicast, v6_dest, &[0u8; 4]),
SendOutcome::Dropped
);
}
}
38 changes: 38 additions & 0 deletions src/network/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ fn get_local_unicast_locators_inner(
) -> Vec<Locator> {
ifaces
.iter()
// Our unicast transport (both `UDPListener` and the `UDPSender` unicast
// socket) is IPv4-only, so an announced IPv6 locator is unreachable: peers
// -- including our own participant, which discovers itself over multicast
// loopback -- would try to send there and get EAFNOSUPPORT per datagram.
// Link-local IPv6 is doubly useless, as a `Locator` carries no scope id.
.filter(|ifa| ifa.ip.is_ipv4())
.filter(|ifa| only_networks.is_none_or(|nets| nets.contains(&ifa.ip)))
.map(|ifa| Locator::from(SocketAddr::new(ifa.ip, port)))
.collect()
Expand Down Expand Up @@ -405,6 +411,38 @@ mod tests {
);
}

// Our unicast transport is IPv4-only, so IPv6 interface addresses must never
// be announced as unicast locators: peers (and we ourselves, via loopback
// discovery) would send there and get EAFNOSUPPORT on every datagram.
#[test]
fn unicast_locators_exclude_ipv6() {
let ifaces = vec![
iface(v4(192, 168, 0, 10), 1, false, true),
// link-local IPv6, as found on e.g. docker0 / veth interfaces
iface(
IpAddr::V6(Ipv6Addr::new(
0xfe80, 0, 0, 0, 0xd494, 0x8fff, 0xfe08, 0x3ce3,
)),
1,
false,
true,
),
iface(
IpAddr::V6(Ipv6Addr::new(0xfd73, 0x40a2, 0x1c3e, 0, 0, 0, 0, 1)),
1,
false,
true,
),
];

let filtered = get_local_unicast_locators_inner(&ifaces, 7412, None);

assert_eq!(
filtered,
vec![Locator::from(SocketAddr::new(v4(192, 168, 0, 10), 7412))]
);
}

#[test]
fn ifindex_map_prefers_ipv4_and_skips_index_zero() {
let ifaces = vec![
Expand Down
Loading