fix(gateway): filter remote frames before LAN injection
The relay already filters unsafe remote-client traffic, but the gateway is the last process before the physical LAN. Treating a relayed Ethernet frame as safe just because it came from the relay leaves the LAN boundary dependent on one upstream check. Add a gateway-local remote-to-LAN safety decision before AF_PACKET writes. The gateway now skips and logs relayed frames with invalid source MACs, L2 control traffic, remote VLAN tags, DHCP-server replies, IPv6 Router Advertisements, IPv6 fragments, or jumbo payloads. The public receive helper also loops past filtered frames so callers only receive frames that can be injected. Document the final gateway boundary check in the README and extend the gateway relay integration test so an unsafe relayed frame is filtered before the valid frame is delivered. Test Plan: - cargo test -p lanparty-gateway - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo fmt --check - git diff --check Refs: PLAN.md remote-to-LAN safety filters
This commit is contained in:
@@ -33,7 +33,7 @@ use lanparty_obs::{DropReason, TunnelStats};
|
||||
use lanparty_obs::{FrameAction, FrameDirection, FrameLog};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram,
|
||||
gateway_lan_safety_drop_reason, validate_datagram_budget,
|
||||
gateway_lan_safety_drop_reason, remote_client_safety_drop_reason, validate_datagram_budget,
|
||||
};
|
||||
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||
use rustls::pki_types::CertificateDer;
|
||||
@@ -213,6 +213,19 @@ pub struct ReceivedEthernetFrame {
|
||||
payload: Bytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct FilteredRelayEthernetFrame {
|
||||
source_peer_id: u32,
|
||||
payload: Bytes,
|
||||
drop_reason: DropReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum GatewayReceiveOutcome {
|
||||
Accepted(ReceivedEthernetFrame),
|
||||
Filtered(FilteredRelayEthernetFrame),
|
||||
}
|
||||
|
||||
impl ReceivedEthernetFrame {
|
||||
#[must_use]
|
||||
pub const fn source_peer_id(&self) -> u32 {
|
||||
@@ -359,22 +372,38 @@ impl GatewayConnection {
|
||||
)
|
||||
);
|
||||
}
|
||||
relay_frame = recv_gateway_ethernet(&connection, &welcome, &stats) => {
|
||||
let relay_frame = relay_frame?;
|
||||
cam_refresh
|
||||
.observe_remote_frame(relay_frame.source_peer_id(), relay_frame.payload())?;
|
||||
write_lan_ethernet(&packet_socket, relay_frame.payload()).await?;
|
||||
println!(
|
||||
"{}",
|
||||
gateway_frame_log_line(
|
||||
packet_socket.get_ref().interface(),
|
||||
FrameDirection::RemoteToLan,
|
||||
Some(relay_frame.source_peer_id()),
|
||||
relay_frame.payload(),
|
||||
FrameAction::Forwarded,
|
||||
None,
|
||||
)
|
||||
);
|
||||
relay_frame = recv_gateway_ethernet_outcome(&connection, &welcome, &stats) => {
|
||||
match relay_frame? {
|
||||
GatewayReceiveOutcome::Accepted(relay_frame) => {
|
||||
cam_refresh
|
||||
.observe_remote_frame(relay_frame.source_peer_id(), relay_frame.payload())?;
|
||||
write_lan_ethernet(&packet_socket, relay_frame.payload()).await?;
|
||||
println!(
|
||||
"{}",
|
||||
gateway_frame_log_line(
|
||||
packet_socket.get_ref().interface(),
|
||||
FrameDirection::RemoteToLan,
|
||||
Some(relay_frame.source_peer_id()),
|
||||
relay_frame.payload(),
|
||||
FrameAction::Forwarded,
|
||||
None,
|
||||
)
|
||||
);
|
||||
}
|
||||
GatewayReceiveOutcome::Filtered(relay_frame) => {
|
||||
println!(
|
||||
"{}",
|
||||
gateway_frame_log_line(
|
||||
packet_socket.get_ref().interface(),
|
||||
FrameDirection::RemoteToLan,
|
||||
Some(relay_frame.source_peer_id),
|
||||
&relay_frame.payload,
|
||||
FrameAction::Filtered,
|
||||
Some(relay_frame.drop_reason),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cam_refresh_tick.tick() => {
|
||||
for frame in cam_refresh.refresh_frames() {
|
||||
@@ -465,6 +494,19 @@ async fn recv_gateway_ethernet(
|
||||
welcome: &ServerWelcome,
|
||||
stats: &GatewayTunnelStats,
|
||||
) -> Result<ReceivedEthernetFrame> {
|
||||
loop {
|
||||
match recv_gateway_ethernet_outcome(connection, welcome, stats).await? {
|
||||
GatewayReceiveOutcome::Accepted(frame) => return Ok(frame),
|
||||
GatewayReceiveOutcome::Filtered(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_gateway_ethernet_outcome(
|
||||
connection: &quinn::Connection,
|
||||
welcome: &ServerWelcome,
|
||||
stats: &GatewayTunnelStats,
|
||||
) -> Result<GatewayReceiveOutcome> {
|
||||
loop {
|
||||
let datagram = connection.read_datagram().await?;
|
||||
stats.record_datagram_rx();
|
||||
@@ -489,13 +531,32 @@ async fn recv_gateway_ethernet(
|
||||
};
|
||||
|
||||
stats.record_ethernet_rx(ethernet_frame);
|
||||
return Ok(ReceivedEthernetFrame {
|
||||
if let Some(drop_reason) = remote_to_lan_safety_drop_reason(ethernet_frame) {
|
||||
stats.record_dropped_frame();
|
||||
return Ok(GatewayReceiveOutcome::Filtered(
|
||||
FilteredRelayEthernetFrame {
|
||||
source_peer_id: header.peer_id(),
|
||||
payload: Bytes::copy_from_slice(packet.payload()),
|
||||
drop_reason,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(GatewayReceiveOutcome::Accepted(ReceivedEthernetFrame {
|
||||
source_peer_id: header.peer_id(),
|
||||
payload: Bytes::copy_from_slice(packet.payload()),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_to_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<DropReason> {
|
||||
if !frame.source().is_valid_unicast() {
|
||||
return Some(DropReason::InvalidSourceMac);
|
||||
}
|
||||
|
||||
remote_client_safety_drop_reason(frame).map(DropReason::from)
|
||||
}
|
||||
|
||||
async fn send_gateway_stats(connection: &quinn::Connection, stats: TunnelStats) -> Result<()> {
|
||||
send_gateway_control_event(connection, ControlMessage::Stats(stats), "gateway stats").await
|
||||
}
|
||||
@@ -987,6 +1048,18 @@ mod tests {
|
||||
assert_eq!(header.peer_id(), 1);
|
||||
assert_eq!(packet.payload(), ethernet_frame(b"to relay").as_slice());
|
||||
|
||||
let filtered_response = encode_datagram(
|
||||
FrameType::Ethernet,
|
||||
7,
|
||||
99,
|
||||
0,
|
||||
&control_plane_ethernet_frame(),
|
||||
)
|
||||
.unwrap();
|
||||
connection
|
||||
.send_datagram(Bytes::from(filtered_response))
|
||||
.unwrap();
|
||||
|
||||
let response = encode_datagram(
|
||||
FrameType::Ethernet,
|
||||
7,
|
||||
@@ -1003,7 +1076,7 @@ mod tests {
|
||||
let ControlMessage::Stats(stats) = stats_message else {
|
||||
panic!("expected gateway stats event");
|
||||
};
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 5, 1));
|
||||
stats_received_tx.send(()).unwrap();
|
||||
|
||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||
@@ -1076,7 +1149,7 @@ mod tests {
|
||||
.is_err()
|
||||
);
|
||||
let stats = gateway.stats_snapshot();
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 5, 1));
|
||||
|
||||
gateway.send_stats_snapshot().await.unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(5), stats_received_rx)
|
||||
|
||||
Reference in New Issue
Block a user