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:
Generated
+2
@@ -453,8 +453,10 @@ name = "lanparty-gateway"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"clap",
|
||||
"lanparty-ctrl",
|
||||
"lanparty-proto",
|
||||
"libc",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
|
||||
@@ -82,4 +82,5 @@ cargo run -p lanparty-gateway -- \
|
||||
|
||||
The gateway currently connects to the relay as `role = gateway`, completes the
|
||||
control-stream hello/welcome handshake, opens an AF_PACKET socket on the LAN
|
||||
interface, and then waits for shutdown. The frame bridge loop is not wired yet.
|
||||
interface, and has relay Ethernet datagram send/receive helpers. The frame
|
||||
bridge loop is not wired yet.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
ðernet_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(ðernet_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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user