refactor(proto): share Ethernet safety classification
Safety filtering now applies at several tunnel boundaries. The relay remains the trust boundary, while the client and gateway also drop unsafe frames before spending relay bandwidth. Duplicating EtherType and IPv4/IPv6 parsers across crates would make those rules drift as the MVP grows. Move the Ethernet safety classifiers into lanparty-proto, expose typed safety drop reasons, and map them back into the existing DropReason vocabulary. The relay now uses the shared client and gateway classifiers, the gateway keeps its local LAN-send drops through the shared classifier, and the client drops the same remote-to-LAN safety cases before QUIC DATAGRAM encoding. Document the client-side local drops and list the additional suspicious drop reasons in the manual MVP test guide. Test Plan: - cargo test -p lanparty-proto safety - cargo test -p lanparty-client-core connects_to_relay_control_stream_as_client - cargo test -p lanparty-gateway connects_to_relay_control_stream_as_gateway - cargo test -p lanparty-relay - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo check -p lanparty-client-tap --target x86_64-pc-windows-gnu --tests - cargo check -p lanparty-client-route --target x86_64-pc-windows-gnu --tests - cargo check -p lanparty-client-tap --target x86_64-pc-windows-msvc --tests - cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc --tests - git diff --check Refs: PLAN.md safety filters and client source-MAC isolation
This commit is contained in:
@@ -8,6 +8,7 @@ mod ethernet;
|
||||
mod mac;
|
||||
mod mtu;
|
||||
mod overlay;
|
||||
mod safety;
|
||||
|
||||
pub use ethernet::{
|
||||
ETHERNET_HEADER_LEN, EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN,
|
||||
@@ -22,3 +23,8 @@ pub use overlay::{
|
||||
FrameType, OVERLAY_HEADER_LEN, OVERLAY_MAGIC, OVERLAY_VERSION, OverlayHeader, OverlayPacket,
|
||||
ProtoError, decode_datagram, encode_datagram, validate_datagram_budget,
|
||||
};
|
||||
pub use safety::{
|
||||
ETHERTYPE_8021AD, ETHERTYPE_8021Q, ETHERTYPE_EAPOL, ETHERTYPE_IPV4, ETHERTYPE_IPV6,
|
||||
ETHERTYPE_LLDP, ETHERTYPE_QINQ, ETHERTYPE_SLOW_PROTOCOLS, EthernetSafetyDrop,
|
||||
gateway_lan_safety_drop_reason, remote_client_safety_drop_reason,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
use crate::{ETHERNET_HEADER_LEN, EthernetFrame, MacAddr};
|
||||
|
||||
pub const ETHERTYPE_IPV4: u16 = 0x0800;
|
||||
pub const ETHERTYPE_EAPOL: u16 = 0x888e;
|
||||
pub const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809;
|
||||
pub const ETHERTYPE_LLDP: u16 = 0x88cc;
|
||||
pub const ETHERTYPE_IPV6: u16 = 0x86dd;
|
||||
pub const ETHERTYPE_8021Q: u16 = 0x8100;
|
||||
pub const ETHERTYPE_8021AD: u16 = 0x88a8;
|
||||
pub const ETHERTYPE_QINQ: u16 = 0x9100;
|
||||
|
||||
const IP_PROTOCOL_UDP: u8 = 17;
|
||||
const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
|
||||
const IPV6_NEXT_HEADER_ROUTING: u8 = 43;
|
||||
const IPV6_NEXT_HEADER_FRAGMENT: u8 = 44;
|
||||
const IPV6_NEXT_HEADER_AH: u8 = 51;
|
||||
const IPV6_NEXT_HEADER_NO_NEXT: u8 = 59;
|
||||
const IPV6_NEXT_HEADER_DESTINATION_OPTIONS: u8 = 60;
|
||||
const IPV6_NEXT_HEADER_ICMPV6: u8 = 58;
|
||||
const DHCPV4_SERVER_PORT: u16 = 67;
|
||||
const DHCPV4_CLIENT_PORT: u16 = 68;
|
||||
const DHCPV6_CLIENT_PORT: u16 = 546;
|
||||
const DHCPV6_SERVER_PORT: u16 = 547;
|
||||
const ICMPV6_ROUTER_ADVERTISEMENT: u8 = 134;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EthernetSafetyDrop {
|
||||
InvalidSourceMac,
|
||||
JumboFrame,
|
||||
ControlPlaneEtherType,
|
||||
VlanTaggedFrame,
|
||||
DhcpServerReply,
|
||||
Ipv6RouterAdvertisement,
|
||||
Ipv6Fragment,
|
||||
}
|
||||
|
||||
pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
|
||||
if !frame.source().is_valid_unicast() {
|
||||
return Some(EthernetSafetyDrop::InvalidSourceMac);
|
||||
}
|
||||
|
||||
common_safety_drop_reason(frame)
|
||||
}
|
||||
|
||||
pub fn remote_client_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
|
||||
if let Some(drop_reason) = common_safety_drop_reason(frame) {
|
||||
return Some(drop_reason);
|
||||
}
|
||||
|
||||
if is_vlan_tagged_frame(frame) {
|
||||
return Some(EthernetSafetyDrop::VlanTaggedFrame);
|
||||
}
|
||||
|
||||
if is_dhcp_server_reply(frame) {
|
||||
return Some(EthernetSafetyDrop::DhcpServerReply);
|
||||
}
|
||||
|
||||
if is_ipv6_router_advertisement(frame) {
|
||||
return Some(EthernetSafetyDrop::Ipv6RouterAdvertisement);
|
||||
}
|
||||
|
||||
if is_ipv6_fragment(frame) {
|
||||
return Some(EthernetSafetyDrop::Ipv6Fragment);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn common_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
|
||||
if frame.is_jumbo() {
|
||||
return Some(EthernetSafetyDrop::JumboFrame);
|
||||
}
|
||||
|
||||
if is_control_plane_frame(frame) {
|
||||
return Some(EthernetSafetyDrop::ControlPlaneEtherType);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_control_plane_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
matches!(
|
||||
frame.ethertype_or_len(),
|
||||
ETHERTYPE_EAPOL | ETHERTYPE_SLOW_PROTOCOLS | ETHERTYPE_LLDP
|
||||
) || is_link_local_control_destination(frame.destination())
|
||||
}
|
||||
|
||||
fn is_vlan_tagged_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
matches!(
|
||||
frame.ethertype_or_len(),
|
||||
ETHERTYPE_8021Q | ETHERTYPE_8021AD | ETHERTYPE_QINQ
|
||||
)
|
||||
}
|
||||
|
||||
fn is_link_local_control_destination(mac: MacAddr) -> bool {
|
||||
let [a, b, c, d, e, f] = mac.octets();
|
||||
|
||||
[a, b, c, d, e] == [0x01, 0x80, 0xc2, 0x00, 0x00] && f <= 0x0f
|
||||
}
|
||||
|
||||
fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool {
|
||||
is_ipv4_dhcp_server_reply(frame) || is_ipv6_dhcp_server_reply(frame)
|
||||
}
|
||||
|
||||
fn is_ipv4_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool {
|
||||
let bytes = frame.bytes();
|
||||
if frame.ethertype_or_len() != ETHERTYPE_IPV4 || bytes.len() < ETHERNET_HEADER_LEN + 20 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ipv4 = &bytes[ETHERNET_HEADER_LEN..];
|
||||
let version = ipv4[0] >> 4;
|
||||
let header_len = usize::from(ipv4[0] & 0x0f) * 4;
|
||||
if version != 4 || header_len < 20 || ipv4.len() < header_len + 8 || ipv4[9] != IP_PROTOCOL_UDP
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let udp = &ipv4[header_len..];
|
||||
let source_port = u16::from_be_bytes([udp[0], udp[1]]);
|
||||
let destination_port = u16::from_be_bytes([udp[2], udp[3]]);
|
||||
|
||||
source_port == DHCPV4_SERVER_PORT && destination_port == DHCPV4_CLIENT_PORT
|
||||
}
|
||||
|
||||
fn is_ipv6_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool {
|
||||
let bytes = frame.bytes();
|
||||
if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ipv6 = &bytes[ETHERNET_HEADER_LEN..];
|
||||
if !is_ipv6_packet(ipv6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(udp_offset) = ipv6_upper_layer_payload_offset(ipv6, IP_PROTOCOL_UDP) else {
|
||||
return false;
|
||||
};
|
||||
let Some(udp) = ipv6.get(udp_offset..udp_offset.saturating_add(4)) else {
|
||||
return false;
|
||||
};
|
||||
let source_port = u16::from_be_bytes([udp[0], udp[1]]);
|
||||
let destination_port = u16::from_be_bytes([udp[2], udp[3]]);
|
||||
|
||||
source_port == DHCPV6_SERVER_PORT && destination_port == DHCPV6_CLIENT_PORT
|
||||
}
|
||||
|
||||
fn is_ipv6_router_advertisement(frame: EthernetFrame<'_>) -> bool {
|
||||
let bytes = frame.bytes();
|
||||
if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ipv6 = &bytes[ETHERNET_HEADER_LEN..];
|
||||
if !is_ipv6_packet(ipv6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(icmpv6_offset) = ipv6_upper_layer_payload_offset(ipv6, IPV6_NEXT_HEADER_ICMPV6) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ipv6.get(icmpv6_offset)
|
||||
.is_some_and(|message_type| *message_type == ICMPV6_ROUTER_ADVERTISEMENT)
|
||||
}
|
||||
|
||||
fn is_ipv6_fragment(frame: EthernetFrame<'_>) -> bool {
|
||||
let bytes = frame.bytes();
|
||||
if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ipv6 = &bytes[ETHERNET_HEADER_LEN..];
|
||||
is_ipv6_packet(ipv6) && ipv6_has_extension_header(ipv6, IPV6_NEXT_HEADER_FRAGMENT)
|
||||
}
|
||||
|
||||
fn is_ipv6_packet(ipv6: &[u8]) -> bool {
|
||||
ipv6.first().is_some_and(|first| first >> 4 == 6)
|
||||
}
|
||||
|
||||
fn ipv6_has_extension_header(ipv6: &[u8], expected_next_header: u8) -> bool {
|
||||
ipv6_upper_layer_payload_offset(ipv6, expected_next_header).is_some()
|
||||
}
|
||||
|
||||
fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Option<usize> {
|
||||
let mut next_header = *ipv6.get(6)?;
|
||||
let mut offset = 40_usize;
|
||||
|
||||
loop {
|
||||
match next_header {
|
||||
next_header if next_header == expected_next_header => return Some(offset),
|
||||
IPV6_NEXT_HEADER_NO_NEXT => return None,
|
||||
IPV6_NEXT_HEADER_HOP_BY_HOP
|
||||
| IPV6_NEXT_HEADER_ROUTING
|
||||
| IPV6_NEXT_HEADER_DESTINATION_OPTIONS => {
|
||||
let extension = ipv6.get(offset..offset.checked_add(2)?)?;
|
||||
next_header = extension[0];
|
||||
let extension_len = (usize::from(extension[1]) + 1) * 8;
|
||||
offset = offset.checked_add(extension_len)?;
|
||||
if offset > ipv6.len() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
IPV6_NEXT_HEADER_FRAGMENT => {
|
||||
let fragment = ipv6.get(offset..offset.checked_add(8)?)?;
|
||||
next_header = fragment[0];
|
||||
offset = offset.checked_add(8)?;
|
||||
}
|
||||
IPV6_NEXT_HEADER_AH => {
|
||||
let extension = ipv6.get(offset..offset.checked_add(2)?)?;
|
||||
next_header = extension[0];
|
||||
let extension_len = (usize::from(extension[1]) + 2) * 4;
|
||||
offset = offset.checked_add(extension_len)?;
|
||||
if offset > ipv6.len() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MAX_STANDARD_ETHERNET_PAYLOAD_LEN;
|
||||
|
||||
#[test]
|
||||
fn classifies_gateway_lan_safety_drops() {
|
||||
assert_eq!(gateway_lan_safety_drop_reason(ethernet()), None);
|
||||
let jumbo_payload = vec![0; MAX_STANDARD_ETHERNET_PAYLOAD_LEN + 1];
|
||||
assert_eq!(
|
||||
gateway_lan_safety_drop_reason(frame_with_source(MacAddr::BROADCAST)),
|
||||
Some(EthernetSafetyDrop::InvalidSourceMac)
|
||||
);
|
||||
assert_eq!(
|
||||
gateway_lan_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV4,
|
||||
&jumbo_payload,
|
||||
)),
|
||||
Some(EthernetSafetyDrop::JumboFrame)
|
||||
);
|
||||
assert_eq!(
|
||||
gateway_lan_safety_drop_reason(control_plane_frame()),
|
||||
Some(EthernetSafetyDrop::ControlPlaneEtherType)
|
||||
);
|
||||
assert_eq!(
|
||||
gateway_lan_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_8021Q,
|
||||
&[0; 4],
|
||||
)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_remote_client_safety_drops() {
|
||||
assert_eq!(remote_client_safety_drop_reason(ethernet()), None);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(control_plane_frame()),
|
||||
Some(EthernetSafetyDrop::ControlPlaneEtherType)
|
||||
);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_8021Q,
|
||||
&[0; 4],
|
||||
)),
|
||||
Some(EthernetSafetyDrop::VlanTaggedFrame)
|
||||
);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV4,
|
||||
&ipv4_udp_payload(DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT),
|
||||
)),
|
||||
Some(EthernetSafetyDrop::DhcpServerReply)
|
||||
);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV6,
|
||||
&ipv6_udp_after_destination_options_payload(DHCPV6_SERVER_PORT, DHCPV6_CLIENT_PORT,),
|
||||
)),
|
||||
Some(EthernetSafetyDrop::DhcpServerReply)
|
||||
);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV6,
|
||||
&ipv6_router_advertisement_after_destination_options_payload(),
|
||||
)),
|
||||
Some(EthernetSafetyDrop::Ipv6RouterAdvertisement)
|
||||
);
|
||||
assert_eq!(
|
||||
remote_client_safety_drop_reason(ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV6,
|
||||
&ipv6_payload(
|
||||
IPV6_NEXT_HEADER_FRAGMENT,
|
||||
&ipv6_fragment_payload(IP_PROTOCOL_UDP, b"fragment"),
|
||||
),
|
||||
)),
|
||||
Some(EthernetSafetyDrop::Ipv6Fragment)
|
||||
);
|
||||
}
|
||||
|
||||
fn ethernet() -> EthernetFrame<'static> {
|
||||
ethernet_with_payload(
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_IPV4,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn frame_with_source(source: MacAddr) -> EthernetFrame<'static> {
|
||||
ethernet_with_payload(
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
source,
|
||||
ETHERTYPE_IPV4,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn control_plane_frame() -> EthernetFrame<'static> {
|
||||
ethernet_with_payload(
|
||||
MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_LLDP,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn ethernet_with_payload(
|
||||
destination: MacAddr,
|
||||
source: MacAddr,
|
||||
ethertype_or_len: u16,
|
||||
payload: &[u8],
|
||||
) -> EthernetFrame<'static> {
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&destination.octets());
|
||||
frame.extend_from_slice(&source.octets());
|
||||
frame.extend_from_slice(ðertype_or_len.to_be_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
EthernetFrame::parse(frame.leak()).unwrap()
|
||||
}
|
||||
|
||||
fn ipv4_udp_payload(source_port: u16, destination_port: u16) -> Vec<u8> {
|
||||
let mut packet = vec![0; 28];
|
||||
packet[0] = 0x45;
|
||||
packet[9] = IP_PROTOCOL_UDP;
|
||||
packet[20..22].copy_from_slice(&source_port.to_be_bytes());
|
||||
packet[22..24].copy_from_slice(&destination_port.to_be_bytes());
|
||||
packet
|
||||
}
|
||||
|
||||
fn udp_payload(source_port: u16, destination_port: u16) -> Vec<u8> {
|
||||
let mut packet = vec![0; 8];
|
||||
packet[0..2].copy_from_slice(&source_port.to_be_bytes());
|
||||
packet[2..4].copy_from_slice(&destination_port.to_be_bytes());
|
||||
packet
|
||||
}
|
||||
|
||||
fn ipv6_udp_after_destination_options_payload(
|
||||
source_port: u16,
|
||||
destination_port: u16,
|
||||
) -> Vec<u8> {
|
||||
ipv6_payload(
|
||||
IPV6_NEXT_HEADER_DESTINATION_OPTIONS,
|
||||
&ipv6_extension_payload(IP_PROTOCOL_UDP, &udp_payload(source_port, destination_port)),
|
||||
)
|
||||
}
|
||||
|
||||
fn ipv6_router_advertisement_after_destination_options_payload() -> Vec<u8> {
|
||||
ipv6_payload(
|
||||
IPV6_NEXT_HEADER_DESTINATION_OPTIONS,
|
||||
&ipv6_extension_payload(IPV6_NEXT_HEADER_ICMPV6, &[ICMPV6_ROUTER_ADVERTISEMENT]),
|
||||
)
|
||||
}
|
||||
|
||||
fn ipv6_fragment_payload(next_header: u8, fragment_payload: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(8 + fragment_payload.len());
|
||||
packet.push(next_header);
|
||||
packet.push(0);
|
||||
packet.extend_from_slice(&0_u16.to_be_bytes());
|
||||
packet.extend_from_slice(&1_u32.to_be_bytes());
|
||||
packet.extend_from_slice(fragment_payload);
|
||||
packet
|
||||
}
|
||||
|
||||
fn ipv6_payload(next_header: u8, upper_payload: &[u8]) -> Vec<u8> {
|
||||
let mut packet = vec![0; 40];
|
||||
packet[0] = 0x60;
|
||||
packet[6] = next_header;
|
||||
packet.extend_from_slice(upper_payload);
|
||||
packet
|
||||
}
|
||||
|
||||
fn ipv6_extension_payload(next_header: u8, upper_payload: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(8 + upper_payload.len());
|
||||
packet.push(next_header);
|
||||
packet.push(0);
|
||||
packet.extend_from_slice(&[0; 6]);
|
||||
packet.extend_from_slice(upper_payload);
|
||||
packet
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user