feat(gateway): add relay Ethernet datagram helpers

GatewayConnection can now send and receive Ethernet frames over the admitted
relay QUIC connection. Outgoing frames are wrapped in the shared overlay format
with the gateway's assigned room id and peer id; incoming datagrams are ignored
unless they are Ethernet frames for the assigned room from another peer.

The receive helper also parses the payload as an Ethernet frame before exposing
it, which keeps the future AF_PACKET bridge from injecting malformed runt
payloads if the relay path ever misbehaves.

The loopback connector test now verifies the full post-handshake datagram path:
the gateway sends a frame to the test relay, the relay validates the overlay
metadata, and the gateway receives a relay-sent Ethernet frame back.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings

Refs: PLAN.md gateway relay datagram send/receive
This commit is contained in:
2026-05-21 18:12:07 +02:00
parent 1b00deb419
commit 128903c312
4 changed files with 103 additions and 1 deletions
+2
View File
@@ -5,8 +5,10 @@ edition.workspace = true
[dependencies]
anyhow.workspace = true
bytes.workspace = true
clap.workspace = true
lanparty-ctrl = { path = "../lanparty-ctrl" }
lanparty-proto = { path = "../lanparty-proto" }
libc.workspace = true
quinn.workspace = true
rustls.workspace = true
+97
View File
@@ -15,11 +15,13 @@ use std::{
};
use anyhow::{Context, Result, bail};
use bytes::Bytes;
use clap::Parser;
use lanparty_ctrl::{
CONTROL_LENGTH_PREFIX_LEN, ControlMessage, EndpointHello, MAX_CONTROL_MESSAGE_LEN, RELAY_ALPN,
RoomCode, ServerWelcome, decode_control_frame, encode_control_message,
};
use lanparty_proto::{EthernetFrame, FrameType, decode_datagram, encode_datagram};
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
use rustls::pki_types::CertificateDer;
@@ -164,6 +166,24 @@ pub struct GatewayConnection {
welcome: ServerWelcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedEthernetFrame {
source_peer_id: u32,
payload: Bytes,
}
impl ReceivedEthernetFrame {
#[must_use]
pub const fn source_peer_id(&self) -> u32 {
self.source_peer_id
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
}
impl GatewayConnection {
#[must_use]
pub const fn config(&self) -> &GatewayConfig {
@@ -175,6 +195,47 @@ impl GatewayConnection {
&self.welcome
}
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
let datagram = encode_datagram(
FrameType::Ethernet,
self.welcome.room_id(),
self.welcome.peer_id(),
0,
frame,
)
.context("failed to encode gateway Ethernet datagram")?;
self.connection
.send_datagram(Bytes::from(datagram))
.context("failed to send gateway Ethernet datagram")?;
Ok(())
}
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
loop {
let datagram = self.connection.read_datagram().await?;
let Ok(packet) = decode_datagram(&datagram) else {
continue;
};
let header = packet.header();
if header.frame_type() != FrameType::Ethernet
|| header.room_id() != self.welcome.room_id()
|| header.peer_id() == self.welcome.peer_id()
{
continue;
}
if EthernetFrame::parse(packet.payload()).is_err() {
continue;
}
return Ok(ReceivedEthernetFrame {
source_peer_id: header.peer_id(),
payload: Bytes::copy_from_slice(packet.payload()),
});
}
}
pub async fn shutdown(self, reason: &str) {
self.connection.close(0_u32.into(), reason.as_bytes());
self.endpoint.wait_idle().await;
@@ -258,6 +319,7 @@ async fn request_control_message(
mod tests {
use std::time::Duration;
use bytes::Bytes;
use lanparty_ctrl::Role;
use quinn::{ServerConfig, TransportConfig, crypto::rustls::QuicServerConfig};
use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
@@ -340,6 +402,24 @@ mod tests {
send.write_all(&response).await.unwrap();
send.finish().unwrap();
let datagram = connection.read_datagram().await.unwrap();
let packet = decode_datagram(&datagram).unwrap();
let header = packet.header();
assert_eq!(header.frame_type(), FrameType::Ethernet);
assert_eq!(header.room_id(), 7);
assert_eq!(header.peer_id(), 1);
assert_eq!(packet.payload(), ethernet_frame(b"to relay").as_slice());
let response = encode_datagram(
FrameType::Ethernet,
7,
99,
0,
&ethernet_frame(b"from relay"),
)
.unwrap();
connection.send_datagram(Bytes::from(response)).unwrap();
connection.closed().await;
endpoint.close(0_u32.into(), b"test complete");
endpoint.wait_idle().await;
@@ -360,6 +440,14 @@ mod tests {
assert_eq!(gateway.welcome().room_id(), 7);
assert_eq!(gateway.welcome().peer_id(), 1);
gateway.send_ethernet(&ethernet_frame(b"to relay")).unwrap();
let received = tokio::time::timeout(Duration::from_secs(5), gateway.recv_ethernet())
.await
.unwrap()
.unwrap();
assert_eq!(received.source_peer_id(), 99);
assert_eq!(received.payload(), ethernet_frame(b"from relay").as_slice());
gateway.shutdown("test complete").await;
tokio::time::timeout(Duration::from_secs(5), server_task)
.await
@@ -390,4 +478,13 @@ mod tests {
(server_config, certificate)
}
fn ethernet_frame(payload: &[u8]) -> Vec<u8> {
let mut frame = Vec::new();
frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 2]);
frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 1]);
frame.extend_from_slice(&0x0800_u16.to_be_bytes());
frame.extend_from_slice(payload);
frame
}
}