Files
Easytier/easytier-core/src/gateway/udp_broadcast.rs
T
KKRainbow afbba5d928 refactor(core): migrate packet processing from pnet to smoltcp (#2456)
Replace pnet_packet parsing and mutation across gateway packet paths with the existing smoltcp wire APIs. Preserve length validation, fragmentation classification, TCP flags, and checksum behavior while removing the core pnet_packet feature dependency.
Reject stale non-initiator OSPF sync sessions: only initiator requests may create missing sessions, and a rejection clears the old initiator role only when the remote session generation is unchanged. This fixes an unbounded RPC storm caused by a delayed route sync recreating a session after both peers relinquished the initiator role, with regression tests for session creation and response reordering.
2026-08-01 10:57:23 +08:00

719 lines
23 KiB
Rust

use std::net::Ipv4Addr;
use cidr::Ipv4Inet;
use smoltcp::wire::{
IPV4_HEADER_LEN, IpAddress, IpProtocol, Ipv4Packet, UDP_HEADER_LEN, UdpPacket,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PhysicalInterface {
addr: Ipv4Addr,
directed_broadcast: Ipv4Addr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NonContiguousIpv4Netmask(Ipv4Addr);
impl NonContiguousIpv4Netmask {
pub fn netmask(self) -> Ipv4Addr {
self.0
}
}
impl PhysicalInterface {
pub fn from_observation(
addr: Ipv4Addr,
netmask: Option<Ipv4Addr>,
is_internal: bool,
virtual_addr: Ipv4Addr,
) -> Result<Option<Self>, NonContiguousIpv4Netmask> {
if is_internal || addr == virtual_addr {
return Ok(None);
}
let Some(netmask) = netmask else {
return Ok(None);
};
let prefix = prefix_len_from_netmask(netmask).ok_or(NonContiguousIpv4Netmask(netmask))?;
Ok(Self::from_ip_and_prefix(addr, prefix))
}
pub fn from_ip_and_prefix(addr: Ipv4Addr, prefix: u8) -> Option<Self> {
if should_ignore_interface_addr(addr) || prefix > 30 {
return None;
}
Some(Self {
addr,
directed_broadcast: directed_broadcast(addr, prefix)?,
})
}
pub fn address(&self) -> Ipv4Addr {
self.addr
}
pub fn directed_broadcast(&self) -> Ipv4Addr {
self.directed_broadcast
}
}
#[derive(Debug, Clone)]
pub struct BroadcastRelayConfig {
virtual_ipv4: Ipv4Inet,
physical_interfaces: Vec<PhysicalInterface>,
}
impl BroadcastRelayConfig {
pub fn new(virtual_ipv4: Ipv4Inet, physical_interfaces: Vec<PhysicalInterface>) -> Self {
let mut eligible_interfaces = Vec::with_capacity(physical_interfaces.len());
for interface in physical_interfaces {
if interface.addr == virtual_ipv4.address() || eligible_interfaces.contains(&interface)
{
continue;
}
eligible_interfaces.push(interface);
}
Self {
virtual_ipv4,
physical_interfaces: eligible_interfaces,
}
}
pub fn virtual_ipv4(&self) -> &Ipv4Inet {
&self.virtual_ipv4
}
pub fn physical_interfaces(&self) -> &[PhysicalInterface] {
&self.physical_interfaces
}
fn is_physical_source(&self, addr: Ipv4Addr) -> bool {
self.physical_interfaces
.iter()
.any(|iface| iface.addr == addr)
}
fn normalize_destination(&self, dst: Ipv4Addr) -> Option<Ipv4Addr> {
if dst.is_broadcast() || dst.is_multicast() {
return Some(dst);
}
self.physical_interfaces
.iter()
.any(|iface| iface.directed_broadcast == dst)
.then_some(self.virtual_ipv4.last_address())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedPacket {
pub packet: Vec<u8>,
pub destination: Ipv4Addr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UdpPacketSummary {
pub src: Ipv4Addr,
pub dst: Ipv4Addr,
pub src_port: u16,
pub dst_port: u16,
pub ip_len: usize,
pub udp_len: usize,
pub payload_len: usize,
}
impl UdpPacketSummary {
pub fn parse(packet: &[u8]) -> Option<Self> {
if packet.len() < IPV4_HEADER_LEN {
return None;
}
let ipv4_packet = Ipv4Packet::new_unchecked(packet);
if ipv4_packet.version() != 4 || ipv4_packet.next_header() != IpProtocol::Udp {
return None;
}
let header_len = usize::from(ipv4_packet.header_len());
let total_len = usize::from(ipv4_packet.total_len());
if header_len < IPV4_HEADER_LEN
|| total_len < header_len + UDP_HEADER_LEN
|| total_len > packet.len()
{
return None;
}
let udp_packet = UdpPacket::new_unchecked(&packet[header_len..total_len]);
let udp_len = usize::from(udp_packet.len());
if udp_len < UDP_HEADER_LEN || header_len + udp_len != total_len {
return None;
}
Some(Self {
src: ipv4_packet.src_addr(),
dst: ipv4_packet.dst_addr(),
src_port: udp_packet.src_port(),
dst_port: udp_packet.dst_port(),
ip_len: total_len,
udp_len,
payload_len: udp_len - UDP_HEADER_LEN,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UdpBroadcastPacketRejection {
MalformedIpv4,
NotUdpIpv4,
Fragmented,
BadIpv4Length,
IgnoredSource,
VirtualSourceDuplicate,
NonPhysicalSource,
UnsupportedDestination,
LoopbackDestination,
MalformedUdp,
BadUdpLength,
}
impl UdpBroadcastPacketRejection {
pub fn reason(self) -> &'static str {
match self {
Self::MalformedIpv4 => "malformed_ipv4",
Self::NotUdpIpv4 => "not_udp_ipv4",
Self::Fragmented => "fragmented",
Self::BadIpv4Length => "bad_ipv4_length",
Self::IgnoredSource => "ignored_source",
Self::VirtualSourceDuplicate => "virtual_source_duplicate",
Self::NonPhysicalSource => "non_physical_source",
Self::UnsupportedDestination => "unsupported_destination",
Self::LoopbackDestination => "loopback_destination",
Self::MalformedUdp => "malformed_udp",
Self::BadUdpLength => "bad_udp_length",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ParsedUdpBroadcastPacket {
header_len: usize,
udp_len: usize,
normalized_destination: Ipv4Addr,
}
fn should_ignore_interface_addr(addr: Ipv4Addr) -> bool {
addr.is_unspecified() || addr.is_loopback() || addr.is_multicast() || addr.is_broadcast()
}
fn prefix_len_from_netmask(mask: Ipv4Addr) -> Option<u8> {
let raw = u32::from(mask);
let prefix = raw.count_ones() as u8;
let expected = if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
};
(raw == expected).then_some(prefix)
}
fn directed_broadcast(addr: Ipv4Addr, prefix: u8) -> Option<Ipv4Addr> {
if prefix > 32 {
return None;
}
let mask = if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
};
Some(Ipv4Addr::from(u32::from(addr) | !mask))
}
fn parse_udp_broadcast(
packet: &[u8],
config: &BroadcastRelayConfig,
) -> Result<ParsedUdpBroadcastPacket, UdpBroadcastPacketRejection> {
if packet.len() < IPV4_HEADER_LEN {
return Err(UdpBroadcastPacketRejection::MalformedIpv4);
}
let ipv4_packet = Ipv4Packet::new_unchecked(packet);
if ipv4_packet.version() != 4 || ipv4_packet.next_header() != IpProtocol::Udp {
return Err(UdpBroadcastPacketRejection::NotUdpIpv4);
}
if ipv4_packet.frag_offset() != 0 || ipv4_packet.more_frags() {
return Err(UdpBroadcastPacketRejection::Fragmented);
}
let header_len = usize::from(ipv4_packet.header_len());
let total_len = usize::from(ipv4_packet.total_len());
if header_len < IPV4_HEADER_LEN
|| total_len < header_len + UDP_HEADER_LEN
|| total_len > packet.len()
{
return Err(UdpBroadcastPacketRejection::BadIpv4Length);
}
let src = ipv4_packet.src_addr();
let dst = ipv4_packet.dst_addr();
if should_ignore_interface_addr(src) {
return Err(UdpBroadcastPacketRejection::IgnoredSource);
}
if src == config.virtual_ipv4.address() {
return Err(UdpBroadcastPacketRejection::VirtualSourceDuplicate);
}
if !config.is_physical_source(src) {
return Err(UdpBroadcastPacketRejection::NonPhysicalSource);
}
let normalized_destination = config
.normalize_destination(dst)
.ok_or(UdpBroadcastPacketRejection::UnsupportedDestination)?;
if normalized_destination.is_loopback() {
return Err(UdpBroadcastPacketRejection::LoopbackDestination);
}
let udp_packet = UdpPacket::new_unchecked(&packet[header_len..total_len]);
let udp_len = usize::from(udp_packet.len());
if udp_len < UDP_HEADER_LEN || header_len + udp_len != total_len {
return Err(UdpBroadcastPacketRejection::BadUdpLength);
}
Ok(ParsedUdpBroadcastPacket {
header_len,
udp_len,
normalized_destination,
})
}
pub fn normalize_udp_broadcast_packet(
packet: &[u8],
config: &BroadcastRelayConfig,
) -> Result<NormalizedPacket, UdpBroadcastPacketRejection> {
let parsed = parse_udp_broadcast(packet, config)?;
let header_len = parsed.header_len;
let packet_len = header_len + parsed.udp_len;
let destination = parsed.normalized_destination;
let virtual_ipv4 = config.virtual_ipv4.address();
let mut normalized = packet[..packet_len].to_vec();
{
let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut normalized);
ipv4_packet.set_src_addr(virtual_ipv4);
ipv4_packet.set_dst_addr(destination);
ipv4_packet.set_total_len(packet_len as u16);
ipv4_packet.set_checksum(0);
}
{
let mut udp_packet = UdpPacket::new_unchecked(&mut normalized[header_len..packet_len]);
udp_packet.fill_checksum(
&IpAddress::Ipv4(virtual_ipv4),
&IpAddress::Ipv4(destination),
);
}
{
Ipv4Packet::new_unchecked(&mut normalized).fill_checksum();
}
Ok(NormalizedPacket {
packet: normalized,
destination,
})
}
#[derive(Clone)]
pub struct UdpBroadcastRelayStats {
packets_captured: crate::foundation::stats::CounterHandle,
packets_ignored: crate::foundation::stats::CounterHandle,
packets_forwarded: crate::foundation::stats::CounterHandle,
packets_forward_failed: crate::foundation::stats::CounterHandle,
}
impl UdpBroadcastRelayStats {
pub(crate) fn new(
packets_captured: crate::foundation::stats::CounterHandle,
packets_ignored: crate::foundation::stats::CounterHandle,
packets_forwarded: crate::foundation::stats::CounterHandle,
packets_forward_failed: crate::foundation::stats::CounterHandle,
) -> Self {
Self {
packets_captured,
packets_ignored,
packets_forwarded,
packets_forward_failed,
}
}
pub fn record_captured(&self) {
self.packets_captured.inc();
}
pub fn record_ignored(&self) {
self.packets_ignored.inc();
}
pub fn record_forwarded(&self) {
self.packets_forwarded.inc();
}
pub fn record_forward_failed(&self) {
self.packets_forward_failed.inc();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> BroadcastRelayConfig {
BroadcastRelayConfig::new(
"10.144.144.1/24".parse().unwrap(),
vec![PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(192, 168, 1, 7), 24).unwrap()],
)
}
fn build_udp_packet(src: Ipv4Addr, dst: Ipv4Addr, payload: &[u8]) -> Vec<u8> {
let mut packet = vec![0; 20 + 8 + payload.len()];
{
let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut packet);
ipv4_packet.set_version(4);
ipv4_packet.set_header_len(20);
ipv4_packet.set_total_len((20 + 8 + payload.len()) as u16);
ipv4_packet.set_hop_limit(64);
ipv4_packet.set_next_header(IpProtocol::Udp);
ipv4_packet.set_src_addr(src);
ipv4_packet.set_dst_addr(dst);
}
{
let mut udp_packet = UdpPacket::new_unchecked(&mut packet[20..]);
udp_packet.set_src_port(12345);
udp_packet.set_dst_port(37020);
udp_packet.set_len((8 + payload.len()) as u16);
udp_packet.payload_mut().copy_from_slice(payload);
udp_packet.fill_checksum(&IpAddress::Ipv4(src), &IpAddress::Ipv4(dst));
}
Ipv4Packet::new_unchecked(&mut packet).fill_checksum();
packet
}
fn assert_valid_checksums(packet: &[u8]) {
let ipv4_packet = Ipv4Packet::new_checked(packet).unwrap();
assert!(ipv4_packet.verify_checksum());
let udp_packet = UdpPacket::new_checked(ipv4_packet.payload()).unwrap();
assert!(udp_packet.verify_checksum(
&IpAddress::Ipv4(ipv4_packet.src_addr()),
&IpAddress::Ipv4(ipv4_packet.dst_addr()),
));
}
#[test]
fn rewrites_limited_broadcast() {
let packet = build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), Ipv4Addr::BROADCAST, b"hello");
let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap();
let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap();
assert_eq!(normalized.destination, Ipv4Addr::BROADCAST);
assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1));
assert_eq!(ipv4_packet.dst_addr(), Ipv4Addr::BROADCAST);
assert_eq!(&ipv4_packet.payload()[8..], b"hello");
assert_valid_checksums(&normalized.packet);
}
#[test]
fn rewrites_directed_broadcast() {
let packet = build_udp_packet(
Ipv4Addr::new(192, 168, 1, 7),
Ipv4Addr::new(192, 168, 1, 255),
b"directed",
);
let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap();
let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap();
assert_eq!(normalized.destination, Ipv4Addr::new(10, 144, 144, 255));
assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1));
assert_eq!(ipv4_packet.dst_addr(), Ipv4Addr::new(10, 144, 144, 255));
assert_eq!(&ipv4_packet.payload()[8..], b"directed");
assert_valid_checksums(&normalized.packet);
}
#[test]
fn preserves_multicast_destination() {
let multicast = Ipv4Addr::new(239, 255, 255, 250);
let packet = build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), multicast, b"multicast");
let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap();
let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap();
assert_eq!(normalized.destination, multicast);
assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1));
assert_eq!(ipv4_packet.dst_addr(), multicast);
assert_eq!(&ipv4_packet.payload()[8..], b"multicast");
assert_valid_checksums(&normalized.packet);
}
#[test]
fn rejects_malformed_packets() {
assert_eq!(
normalize_udp_broadcast_packet(&[], &config()),
Err(UdpBroadcastPacketRejection::MalformedIpv4)
);
let mut packet =
build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), Ipv4Addr::BROADCAST, b"bad");
packet[2..4].copy_from_slice(&10u16.to_be_bytes());
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::BadIpv4Length)
);
}
#[test]
fn rejects_fragments() {
let mut packet = build_udp_packet(
Ipv4Addr::new(192, 168, 1, 7),
Ipv4Addr::BROADCAST,
b"fragment",
);
{
let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut packet);
ipv4_packet.set_more_frags(true);
}
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::Fragmented)
);
}
#[test]
fn rejects_non_broadcast_destinations() {
let packet = build_udp_packet(
Ipv4Addr::new(192, 168, 1, 7),
Ipv4Addr::new(192, 168, 1, 10),
b"unicast",
);
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::UnsupportedDestination)
);
}
#[test]
fn rejects_virtual_source_duplicates() {
let packet = build_udp_packet(Ipv4Addr::new(10, 144, 144, 1), Ipv4Addr::BROADCAST, b"loop");
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::VirtualSourceDuplicate)
);
}
#[test]
fn rejects_non_udp_ipv4_packets() {
let mut packet =
build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), Ipv4Addr::BROADCAST, b"tcp");
Ipv4Packet::new_unchecked(&mut packet).set_next_header(IpProtocol::Tcp);
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::NotUdpIpv4)
);
}
#[test]
fn rejects_ignored_and_non_physical_sources() {
let ignored = build_udp_packet(Ipv4Addr::LOCALHOST, Ipv4Addr::BROADCAST, b"ignored");
assert_eq!(
normalize_udp_broadcast_packet(&ignored, &config()),
Err(UdpBroadcastPacketRejection::IgnoredSource)
);
let non_physical =
build_udp_packet(Ipv4Addr::new(192, 168, 1, 8), Ipv4Addr::BROADCAST, b"other");
assert_eq!(
normalize_udp_broadcast_packet(&non_physical, &config()),
Err(UdpBroadcastPacketRejection::NonPhysicalSource)
);
}
#[test]
fn rejects_loopback_destination_after_mapping() {
let config = BroadcastRelayConfig::new(
"127.0.0.1/24".parse().unwrap(),
vec![PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(192, 168, 1, 7), 24).unwrap()],
);
let packet = build_udp_packet(
Ipv4Addr::new(192, 168, 1, 7),
Ipv4Addr::new(192, 168, 1, 255),
b"loopback",
);
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config),
Err(UdpBroadcastPacketRejection::LoopbackDestination)
);
}
#[test]
fn summarizes_packets_and_rejects_bad_udp_length() {
let mut packet = build_udp_packet(
Ipv4Addr::new(192, 168, 1, 7),
Ipv4Addr::BROADCAST,
b"summary",
);
assert_eq!(
UdpPacketSummary::parse(&packet),
Some(UdpPacketSummary {
src: Ipv4Addr::new(192, 168, 1, 7),
dst: Ipv4Addr::BROADCAST,
src_port: 12345,
dst_port: 37020,
ip_len: 35,
udp_len: 15,
payload_len: 7,
})
);
packet[24..26].copy_from_slice(&8u16.to_be_bytes());
assert_eq!(
normalize_udp_broadcast_packet(&packet, &config()),
Err(UdpBroadcastPacketRejection::BadUdpLength)
);
assert_eq!(UdpPacketSummary::parse(&packet), None);
}
#[test]
fn rejection_reasons_preserve_log_values() {
let reasons = [
(UdpBroadcastPacketRejection::MalformedIpv4, "malformed_ipv4"),
(UdpBroadcastPacketRejection::NotUdpIpv4, "not_udp_ipv4"),
(UdpBroadcastPacketRejection::Fragmented, "fragmented"),
(
UdpBroadcastPacketRejection::BadIpv4Length,
"bad_ipv4_length",
),
(UdpBroadcastPacketRejection::IgnoredSource, "ignored_source"),
(
UdpBroadcastPacketRejection::VirtualSourceDuplicate,
"virtual_source_duplicate",
),
(
UdpBroadcastPacketRejection::NonPhysicalSource,
"non_physical_source",
),
(
UdpBroadcastPacketRejection::UnsupportedDestination,
"unsupported_destination",
),
(
UdpBroadcastPacketRejection::LoopbackDestination,
"loopback_destination",
),
(UdpBroadcastPacketRejection::MalformedUdp, "malformed_udp"),
(UdpBroadcastPacketRejection::BadUdpLength, "bad_udp_length"),
];
for (rejection, reason) in reasons {
assert_eq!(rejection.reason(), reason);
}
}
#[test]
fn detects_directed_broadcast_from_prefix() {
let physical =
PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(172, 16, 5, 10), 20).unwrap();
assert_eq!(
physical.directed_broadcast(),
Ipv4Addr::new(172, 16, 15, 255)
);
assert_eq!(
prefix_len_from_netmask(Ipv4Addr::new(255, 255, 240, 0)),
Some(20)
);
assert_eq!(prefix_len_from_netmask(Ipv4Addr::new(255, 0, 255, 0)), None);
}
#[test]
fn classifies_physical_interface_observations() {
let addr = Ipv4Addr::new(192, 168, 1, 7);
let virtual_addr = Ipv4Addr::new(10, 144, 144, 1);
let netmask = Ipv4Addr::new(255, 255, 255, 0);
let non_contiguous = Ipv4Addr::new(255, 0, 255, 0);
assert_eq!(
PhysicalInterface::from_observation(addr, Some(netmask), false, virtual_addr),
Ok(PhysicalInterface::from_ip_and_prefix(addr, 24))
);
assert_eq!(
PhysicalInterface::from_observation(addr, None, false, virtual_addr),
Ok(None)
);
assert_eq!(
PhysicalInterface::from_observation(addr, Some(non_contiguous), true, virtual_addr),
Ok(None)
);
assert_eq!(
PhysicalInterface::from_observation(
virtual_addr,
Some(non_contiguous),
false,
virtual_addr,
),
Ok(None)
);
assert_eq!(
PhysicalInterface::from_observation(addr, Some(non_contiguous), false, virtual_addr,),
Err(NonContiguousIpv4Netmask(non_contiguous))
);
}
#[test]
fn keeps_link_local_interfaces() {
let physical =
PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(169, 254, 13, 10), 16).unwrap();
assert_eq!(
physical.directed_broadcast(),
Ipv4Addr::new(169, 254, 255, 255)
);
}
#[test]
fn rejects_ineligible_interface_addresses_and_prefixes() {
for addr in [
Ipv4Addr::UNSPECIFIED,
Ipv4Addr::LOCALHOST,
Ipv4Addr::new(239, 1, 2, 3),
Ipv4Addr::BROADCAST,
] {
assert_eq!(PhysicalInterface::from_ip_and_prefix(addr, 24), None);
}
assert_eq!(
PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(192, 168, 1, 7), 31),
None
);
}
#[test]
fn config_excludes_virtual_and_duplicate_interfaces() {
let virtual_interface =
PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(10, 144, 144, 1), 24).unwrap();
let physical_interface =
PhysicalInterface::from_ip_and_prefix(Ipv4Addr::new(192, 168, 1, 7), 24).unwrap();
let config = BroadcastRelayConfig::new(
"10.144.144.1/24".parse().unwrap(),
vec![virtual_interface, physical_interface, physical_interface],
);
assert_eq!(config.physical_interfaces(), &[physical_interface]);
}
}