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
15 changes: 15 additions & 0 deletions src/iface/ingress_packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ pub enum LocalInputVerdict {
}

pub trait IpIngressFilter {
/// Override ARP reply authorization with the integration's address policy.
/// `None` preserves the interface's standalone `any_ip` behavior. This
/// decision controls replies only, not neighbor-cache learning. A local
/// address may belong to another interface in the same network namespace.
/// Integrations must validate both the sender and target according to
/// their routing policy, including any martian-source restrictions.
#[cfg(feature = "proto-ipv4")]
fn arp_reply_allowed(
&self,
_source: crate::wire::Ipv4Address,
_target: crate::wire::Ipv4Address,
) -> Option<bool> {
None
}

/// Stop before acquiring another RX token if the integration's prepared
/// policy view has been replaced. The caller can then release its locks
/// and rebuild the policy and routing views without consuming the packet.
Expand Down
7 changes: 6 additions & 1 deletion src/iface/interface/ethernet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ impl InterfaceInner {

match eth_frame.ethertype() {
#[cfg(feature = "proto-ipv4")]
EthernetProtocol::Arp => self.process_arp(self.now, &eth_frame),
EthernetProtocol::Arp => self.process_arp(
self.now,
&eth_frame,
#[cfg(feature = "alloc")]
filter.map(|(_, filter)| &*filter),
),
#[cfg(feature = "proto-ipv4")]
EthernetProtocol::Ipv4 => {
let ipv4_packet = check!(Ipv4Packet::new_checked(eth_frame.payload()));
Expand Down
47 changes: 31 additions & 16 deletions src/iface/interface/ipv4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ impl InterfaceInner {
&mut self,
timestamp: Instant,
eth_frame: &EthernetFrame<&'frame [u8]>,
#[cfg(feature = "alloc")] filter: Option<&dyn IpIngressFilter>,
) -> Option<EthernetPacket<'frame>> {
if !self.neighbor_discovery_enabled {
return None;
Expand All @@ -592,39 +593,53 @@ impl InterfaceInner {
target_protocol_addr,
..
} => {
// Only process ARP packets for us.
if !self.has_ip_addr(target_protocol_addr) && !self.any_ip {
return None;
}
// Reply ownership and learning are separate: an integration
// may authorize weak-host replies without broadening the
// interface's existing neighbor-learning policy.
let learning_target = self.has_ip_addr(target_protocol_addr) || self.any_ip;
#[cfg(feature = "alloc")]
let ownership = filter.and_then(|filter| {
filter.arp_reply_allowed(source_protocol_addr, target_protocol_addr)
});
#[cfg(not(feature = "alloc"))]
let ownership: Option<bool> = None;

// Only process REQUEST and RESPONSE.
if let ArpOperation::Unknown(_) = operation {
net_debug!("arp: unknown operation code");
return None;
}

// Discard packets with non-unicast source addresses.
if !source_protocol_addr.x_is_unicast() || !source_hardware_addr.is_unicast() {
// Only an explicitly authorized local request may use the
// zero source address for duplicate-address detection. Never
// learn that address, and preserve legacy standalone behavior.
let dad_request = source_protocol_addr.is_unspecified()
&& operation == ArpOperation::Request
&& ownership == Some(true);
if (!source_protocol_addr.x_is_unicast() && !dad_request)
|| !source_hardware_addr.is_unicast()
{
net_debug!("arp: non-unicast source address");
return None;
}

if !self.in_same_network(&IpAddress::Ipv4(source_protocol_addr)) {
net_debug!("arp: source IP address not in same network as us");
return None;
}
let same_network = self.in_same_network(&IpAddress::Ipv4(source_protocol_addr));

// Fill the ARP cache from any ARP packet aimed at us (both request or response).
// We fill from requests too because if someone is requesting our address they
// are probably going to talk to us, so we avoid having to request their address
// when we later reply to them.
self.neighbor_cache.fill(
source_protocol_addr.into(),
source_hardware_addr.into(),
timestamp,
);
if learning_target && same_network && !dad_request {
self.neighbor_cache.fill(
source_protocol_addr.into(),
source_hardware_addr.into(),
timestamp,
);
}

if operation == ArpOperation::Request {
if operation == ArpOperation::Request
&& ownership.unwrap_or(learning_target && same_network)
{
let src_hardware_addr = self.hardware_addr.ethernet_or_panic();

Some(EthernetPacket::Arp(ArpRepr::EthernetIpv4 {
Expand Down
177 changes: 177 additions & 0 deletions src/iface/interface/tests/ipv4.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,182 @@
use super::*;

#[cfg(all(feature = "alloc", feature = "medium-ethernet"))]
#[rstest]
#[case(Some(false), (false, true, false), ArpOperation::Request, true, (false, false))]
#[case(Some(false), (true, true, false), ArpOperation::Request, true, (false, true))]
#[case(Some(false), (false, false, false), ArpOperation::Request, true, (false, true))]
#[case(Some(false), (true, false, false), ArpOperation::Request, true, (false, true))]
#[case(Some(true), (false, false, false), ArpOperation::Request, true, (true, true))]
#[case(Some(true), (false, true, false), ArpOperation::Request, true, (true, false))]
#[case(Some(true), (false, true, true), ArpOperation::Request, true, (true, false))]
#[case(Some(true), (false, false, false), ArpOperation::Reply, true, (false, true))]
#[case(Some(false), (true, false, false), ArpOperation::Reply, true, (false, true))]
#[case(Some(true), (true, false, false), ArpOperation::Unknown(3), true, (false, false))]
#[case(Some(true), (true, false, false), ArpOperation::Request, false, (false, false))]
#[case(None, (false, false, false), ArpOperation::Request, true, (true, true))]
#[case(None, (false, true, false), ArpOperation::Request, true, (false, false))]
#[case(None, (true, true, false), ArpOperation::Request, true, (true, true))]
#[case(None, (true, true, true), ArpOperation::Request, true, (false, false))]
fn arp_reply_ownership_preserves_learning(
#[case] ownership: Option<bool>,
#[case] interface: (bool, bool, bool),
#[case] operation: ArpOperation,
#[case] valid_mac: bool,
#[case] expected: (bool, bool),
) {
let (any_ip, foreign_target, no_address) = interface;
let (reply, learn) = expected;
struct Ownership(Option<bool>, Ipv4Address, Ipv4Address);
impl IpIngressFilter for Ownership {
fn arp_reply_allowed(&self, source: Ipv4Address, target: Ipv4Address) -> Option<bool> {
assert_eq!(source, self.1);
assert_eq!(target, self.2);
self.0
}

fn pre_routing(
&mut self,
_: &mut IngressPacket<'_>,
_: PacketMeta,
_: HardwareAddress,
) -> PreRoutingVerdict {
panic!("ARP must not enter IP hooks")
}
}

let (mut iface, mut sockets, _) = setup(Medium::Ethernet);
iface.set_any_ip(any_ip);
if no_address {
iface.update_ip_addrs(|addresses| addresses.clear());
}
let target = if foreign_target {
Ipv4Address::new(192, 168, 1, 3)
} else {
Ipv4Address::new(127, 0, 0, 1)
};
let source = if foreign_target {
Ipv4Address::new(192, 168, 1, 2)
} else {
Ipv4Address::new(127, 0, 0, 2)
};
if any_ip && foreign_target && !no_address {
iface.update_ip_addrs(|addresses| {
addresses.clear();
addresses.push(IpCidr::new(source.into(), 24)).unwrap();
});
}
let source_mac = if valid_mac {
EthernetAddress([0x52, 0x54, 0, 0, 0, 1])
} else {
EthernetAddress::BROADCAST
};
let arp = ArpRepr::EthernetIpv4 {
operation,
source_hardware_addr: source_mac,
source_protocol_addr: source,
target_hardware_addr: EthernetAddress::default(),
target_protocol_addr: target,
};
let mut bytes = [0u8; 42];
let mut frame = EthernetFrame::new_unchecked(&mut bytes[..]);
frame.set_dst_addr(EthernetAddress::BROADCAST);
frame.set_src_addr(source_mac);
frame.set_ethertype(EthernetProtocol::Arp);
arp.emit(&mut ArpPacket::new_unchecked(frame.payload_mut()));
let mut scratch = Vec::new();
let mut filter = Ownership(ownership, source, target);
let result = iface.inner.process_ethernet_filtered(
&mut sockets,
PacketMeta::default(),
&bytes,
&mut iface.fragments,
&mut scratch,
&mut filter,
);
assert_eq!(result.is_some(), reply);
if reply {
assert_eq!(
result,
Some(EthernetPacket::Arp(ArpRepr::EthernetIpv4 {
operation: ArpOperation::Reply,
source_hardware_addr: iface.inner.hardware_addr.ethernet_or_panic(),
source_protocol_addr: target,
target_hardware_addr: source_mac,
target_protocol_addr: source,
}))
);
}
assert_eq!(
iface
.inner
.neighbor_cache
.lookup(&source.into(), Instant::ZERO)
.found(),
learn
);
}

#[cfg(all(feature = "alloc", feature = "medium-ethernet"))]
#[rstest]
#[case(Some(true), ArpOperation::Request, true)]
#[case(Some(false), ArpOperation::Request, false)]
#[case(None, ArpOperation::Request, false)]
#[case(Some(true), ArpOperation::Reply, false)]
fn arp_local_dad_does_not_learn_zero(
#[case] ownership: Option<bool>,
#[case] operation: ArpOperation,
#[case] reply: bool,
) {
struct Ownership(Option<bool>);
impl IpIngressFilter for Ownership {
fn arp_reply_allowed(&self, source: Ipv4Address, target: Ipv4Address) -> Option<bool> {
assert_eq!(source, Ipv4Address::UNSPECIFIED);
assert_eq!(target, Ipv4Address::new(127, 0, 0, 1));
self.0
}
fn pre_routing(
&mut self,
_: &mut IngressPacket<'_>,
_: PacketMeta,
_: HardwareAddress,
) -> PreRoutingVerdict {
panic!("ARP must not enter IP hooks")
}
}
let (mut iface, mut sockets, _) = setup(Medium::Ethernet);
iface.set_any_ip(true);
let arp = ArpRepr::EthernetIpv4 {
operation,
source_hardware_addr: EthernetAddress([0x52, 0x54, 0, 0, 0, 1]),
source_protocol_addr: Ipv4Address::UNSPECIFIED,
target_hardware_addr: EthernetAddress::default(),
target_protocol_addr: Ipv4Address::new(127, 0, 0, 1),
};
let mut bytes = [0u8; 42];
let mut frame = EthernetFrame::new_unchecked(&mut bytes[..]);
frame.set_dst_addr(EthernetAddress::BROADCAST);
frame.set_src_addr(EthernetAddress([0x52, 0x54, 0, 0, 0, 1]));
frame.set_ethertype(EthernetProtocol::Arp);
arp.emit(&mut ArpPacket::new_unchecked(frame.payload_mut()));
let mut scratch = Vec::new();
let mut filter = Ownership(ownership);
assert_eq!(
iface
.inner
.process_ethernet_filtered(
&mut sockets,
PacketMeta::default(),
&bytes,
&mut iface.fragments,
&mut scratch,
&mut filter,
)
.is_some(),
reply
);
assert_eq!(iface.inner.neighbor_cache.iter().count(), 0);
}

#[cfg(all(feature = "medium-ip", feature = "socket-udp"))]
#[test]
fn changed_output_policy_keeps_later_socket_packet_queued() {
Expand Down
Loading