feat(ctrl): define tunnel control messages
Add the reliable control-plane schema that will run over QUIC streams. This covers the phase-1 handshake shape without mixing in relay sockets, TAP access, or gateway packet IO. The schema includes endpoint hello messages with role, room, MAC, and datagram budget, plus server welcome, rejection, peer lifecycle, stats, and disconnect messages. Constructors and validation enforce room-code syntax, client MAC identity rules, reserved peer IDs, and effective TAP MTU limits. Decoded control messages can be validated explicitly so serde input cannot silently bypass the same invariants. The actual stream codec remains future work; this commit only fixes the typed contract the codec will carry. Test Plan: - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: PLAN.md reliable QUIC control stream requirements
This commit is contained in:
Generated
+5
@@ -13,6 +13,11 @@ version = "0.1.0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lanparty-ctrl"
|
name = "lanparty-ctrl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"lanparty-proto",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lanparty-gateway"
|
name = "lanparty-gateway"
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ Transport-agnostic tunnel contract shared by all binaries:
|
|||||||
- MAC address parsing and identity validation
|
- MAC address parsing and identity validation
|
||||||
- QUIC datagram to TAP MTU budget helpers
|
- QUIC datagram to TAP MTU budget helpers
|
||||||
|
|
||||||
|
### `lanparty-ctrl`
|
||||||
|
|
||||||
|
Reliable control-plane schema shared by the QUIC stream handlers:
|
||||||
|
|
||||||
|
- endpoint hello messages with role, room, MAC, and datagram budget
|
||||||
|
- server welcome, reject, peer lifecycle, stats, and disconnect messages
|
||||||
|
- room-code, role/MAC, peer-id, and effective-MTU validation
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
lanparty-proto = { path = "../lanparty-proto" }
|
||||||
|
serde.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|||||||
@@ -1,14 +1,572 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
//! Reliable control-plane messages for the LAN party tunnel.
|
||||||
left + right
|
//!
|
||||||
|
//! QUIC streams will carry these messages. The crate does not choose a stream
|
||||||
|
//! codec yet; it defines the typed handshake and status model that client,
|
||||||
|
//! relay, and gateway must validate consistently.
|
||||||
|
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
use lanparty_proto::{MIN_USEFUL_TAP_MTU, MacAddr, MtuError, recommended_tap_mtu};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub const CONTROL_PROTOCOL_VERSION: u16 = 1;
|
||||||
|
pub const MIN_ROOM_CODE_LEN: usize = 1;
|
||||||
|
pub const MAX_ROOM_CODE_LEN: usize = 64;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||||
|
pub enum ControlError {
|
||||||
|
#[error("room code cannot be empty")]
|
||||||
|
EmptyRoomCode,
|
||||||
|
#[error("room code is too long: got {len}, maximum is {max}")]
|
||||||
|
RoomCodeTooLong { len: usize, max: usize },
|
||||||
|
#[error("invalid room code character at byte {index}: {byte:#04x}")]
|
||||||
|
InvalidRoomCodeByte { index: usize, byte: u8 },
|
||||||
|
#[error("unsupported control protocol version {actual}; supported version is {supported}")]
|
||||||
|
UnsupportedVersion { actual: u16, supported: u16 },
|
||||||
|
#[error("client hello must announce a MAC address")]
|
||||||
|
MissingClientMac,
|
||||||
|
#[error("gateway hello must not announce a client MAC address")]
|
||||||
|
UnexpectedGatewayMac,
|
||||||
|
#[error("client MAC {mac} is not a locally administered unicast address")]
|
||||||
|
InvalidClientMac { mac: MacAddr },
|
||||||
|
#[error("peer id 0 is reserved")]
|
||||||
|
InvalidPeerId,
|
||||||
|
#[error("effective TAP MTU {mtu} is below the minimum useful MTU {minimum}")]
|
||||||
|
EffectiveMtuTooSmall { mtu: u16, minimum: usize },
|
||||||
|
#[error(transparent)]
|
||||||
|
Mtu(#[from] MtuError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(try_from = "String", into = "String")]
|
||||||
|
pub struct RoomCode(String);
|
||||||
|
|
||||||
|
impl RoomCode {
|
||||||
|
pub fn new(value: impl Into<String>) -> Result<Self, ControlError> {
|
||||||
|
let value = value.into();
|
||||||
|
validate_room_code(&value)?;
|
||||||
|
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RoomCode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RoomCode {
|
||||||
|
type Err = ControlError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for RoomCode {
|
||||||
|
type Error = ControlError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoomCode> for String {
|
||||||
|
fn from(value: RoomCode) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Role {
|
||||||
|
Client,
|
||||||
|
Gateway,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct EndpointHello {
|
||||||
|
protocol_version: u16,
|
||||||
|
role: Role,
|
||||||
|
room: RoomCode,
|
||||||
|
announced_mac: Option<MacAddr>,
|
||||||
|
max_datagram_size: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndpointHello {
|
||||||
|
pub fn client(
|
||||||
|
room: RoomCode,
|
||||||
|
announced_mac: MacAddr,
|
||||||
|
max_datagram_size: u16,
|
||||||
|
) -> Result<Self, ControlError> {
|
||||||
|
let hello = Self {
|
||||||
|
protocol_version: CONTROL_PROTOCOL_VERSION,
|
||||||
|
role: Role::Client,
|
||||||
|
room,
|
||||||
|
announced_mac: Some(announced_mac),
|
||||||
|
max_datagram_size,
|
||||||
|
};
|
||||||
|
hello.validate()?;
|
||||||
|
|
||||||
|
Ok(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gateway(room: RoomCode, max_datagram_size: u16) -> Result<Self, ControlError> {
|
||||||
|
let hello = Self {
|
||||||
|
protocol_version: CONTROL_PROTOCOL_VERSION,
|
||||||
|
role: Role::Gateway,
|
||||||
|
room,
|
||||||
|
announced_mac: None,
|
||||||
|
max_datagram_size,
|
||||||
|
};
|
||||||
|
hello.validate()?;
|
||||||
|
|
||||||
|
Ok(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), ControlError> {
|
||||||
|
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
|
||||||
|
return Err(ControlError::UnsupportedVersion {
|
||||||
|
actual: self.protocol_version,
|
||||||
|
supported: CONTROL_PROTOCOL_VERSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recommended_tap_mtu(usize::from(self.max_datagram_size))?;
|
||||||
|
|
||||||
|
match (self.role, self.announced_mac) {
|
||||||
|
(Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(()),
|
||||||
|
(Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }),
|
||||||
|
(Role::Client, None) => Err(ControlError::MissingClientMac),
|
||||||
|
(Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac),
|
||||||
|
(Role::Gateway, None) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn protocol_version(&self) -> u16 {
|
||||||
|
self.protocol_version
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn role(&self) -> Role {
|
||||||
|
self.role
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn room(&self) -> &RoomCode {
|
||||||
|
&self.room
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn announced_mac(&self) -> Option<MacAddr> {
|
||||||
|
self.announced_mac
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_datagram_size(&self) -> u16 {
|
||||||
|
self.max_datagram_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ServerWelcome {
|
||||||
|
protocol_version: u16,
|
||||||
|
room_id: u64,
|
||||||
|
peer_id: u32,
|
||||||
|
effective_tap_mtu: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerWelcome {
|
||||||
|
pub fn new(room_id: u64, peer_id: u32, effective_tap_mtu: u16) -> Result<Self, ControlError> {
|
||||||
|
if peer_id == 0 {
|
||||||
|
return Err(ControlError::InvalidPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if usize::from(effective_tap_mtu) < MIN_USEFUL_TAP_MTU {
|
||||||
|
return Err(ControlError::EffectiveMtuTooSmall {
|
||||||
|
mtu: effective_tap_mtu,
|
||||||
|
minimum: MIN_USEFUL_TAP_MTU,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
protocol_version: CONTROL_PROTOCOL_VERSION,
|
||||||
|
room_id,
|
||||||
|
peer_id,
|
||||||
|
effective_tap_mtu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), ControlError> {
|
||||||
|
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
|
||||||
|
return Err(ControlError::UnsupportedVersion {
|
||||||
|
actual: self.protocol_version,
|
||||||
|
supported: CONTROL_PROTOCOL_VERSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peer_id == 0 {
|
||||||
|
return Err(ControlError::InvalidPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if usize::from(self.effective_tap_mtu) < MIN_USEFUL_TAP_MTU {
|
||||||
|
return Err(ControlError::EffectiveMtuTooSmall {
|
||||||
|
mtu: self.effective_tap_mtu,
|
||||||
|
minimum: MIN_USEFUL_TAP_MTU,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn protocol_version(&self) -> u16 {
|
||||||
|
self.protocol_version
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn room_id(&self) -> u64 {
|
||||||
|
self.room_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn peer_id(&self) -> u32 {
|
||||||
|
self.peer_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn effective_tap_mtu(&self) -> u16 {
|
||||||
|
self.effective_tap_mtu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct PeerInfo {
|
||||||
|
peer_id: u32,
|
||||||
|
role: Role,
|
||||||
|
mac: Option<MacAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PeerInfo {
|
||||||
|
pub fn new(peer_id: u32, role: Role, mac: Option<MacAddr>) -> Result<Self, ControlError> {
|
||||||
|
if peer_id == 0 {
|
||||||
|
return Err(ControlError::InvalidPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
match (role, mac) {
|
||||||
|
(Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(Self {
|
||||||
|
peer_id,
|
||||||
|
role,
|
||||||
|
mac: Some(mac),
|
||||||
|
}),
|
||||||
|
(Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }),
|
||||||
|
(Role::Client, None) => Err(ControlError::MissingClientMac),
|
||||||
|
(Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac),
|
||||||
|
(Role::Gateway, None) => Ok(Self {
|
||||||
|
peer_id,
|
||||||
|
role,
|
||||||
|
mac: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), ControlError> {
|
||||||
|
if self.peer_id == 0 {
|
||||||
|
return Err(ControlError::InvalidPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
match (self.role, self.mac) {
|
||||||
|
(Role::Client, Some(mac)) if mac.is_valid_client_identity() => Ok(()),
|
||||||
|
(Role::Client, Some(mac)) => Err(ControlError::InvalidClientMac { mac }),
|
||||||
|
(Role::Client, None) => Err(ControlError::MissingClientMac),
|
||||||
|
(Role::Gateway, Some(_)) => Err(ControlError::UnexpectedGatewayMac),
|
||||||
|
(Role::Gateway, None) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn peer_id(&self) -> u32 {
|
||||||
|
self.peer_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn role(&self) -> Role {
|
||||||
|
self.role
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn mac(&self) -> Option<MacAddr> {
|
||||||
|
self.mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RejectReason {
|
||||||
|
UnsupportedVersion,
|
||||||
|
RoomFull,
|
||||||
|
GatewayAlreadyConnected,
|
||||||
|
DuplicateMac,
|
||||||
|
InvalidMac,
|
||||||
|
MtuTooSmall,
|
||||||
|
MalformedHello,
|
||||||
|
Unauthorized,
|
||||||
|
InternalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DisconnectReason {
|
||||||
|
Normal,
|
||||||
|
RelayShutdown,
|
||||||
|
ProtocolError,
|
||||||
|
TimedOut,
|
||||||
|
Replaced,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Reject {
|
||||||
|
reason: RejectReason,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reject {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(reason: RejectReason, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
reason,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn reason(&self) -> &RejectReason {
|
||||||
|
&self.reason
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn message(&self) -> &str {
|
||||||
|
&self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct TunnelStats {
|
||||||
|
ethernet_frames_tx: u64,
|
||||||
|
ethernet_frames_rx: u64,
|
||||||
|
datagrams_tx: u64,
|
||||||
|
datagrams_rx: u64,
|
||||||
|
dropped_frames: u64,
|
||||||
|
malformed_frames: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TunnelStats {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
ethernet_frames_tx: u64,
|
||||||
|
ethernet_frames_rx: u64,
|
||||||
|
datagrams_tx: u64,
|
||||||
|
datagrams_rx: u64,
|
||||||
|
dropped_frames: u64,
|
||||||
|
malformed_frames: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
ethernet_frames_tx,
|
||||||
|
ethernet_frames_rx,
|
||||||
|
datagrams_tx,
|
||||||
|
datagrams_rx,
|
||||||
|
dropped_frames,
|
||||||
|
malformed_frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn ethernet_frames_tx(&self) -> u64 {
|
||||||
|
self.ethernet_frames_tx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn ethernet_frames_rx(&self) -> u64 {
|
||||||
|
self.ethernet_frames_rx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn datagrams_tx(&self) -> u64 {
|
||||||
|
self.datagrams_tx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn datagrams_rx(&self) -> u64 {
|
||||||
|
self.datagrams_rx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn dropped_frames(&self) -> u64 {
|
||||||
|
self.dropped_frames
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn malformed_frames(&self) -> u64 {
|
||||||
|
self.malformed_frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
|
||||||
|
pub enum ControlMessage {
|
||||||
|
Hello(EndpointHello),
|
||||||
|
Welcome(ServerWelcome),
|
||||||
|
Reject(Reject),
|
||||||
|
PeerJoined(PeerInfo),
|
||||||
|
PeerLeft {
|
||||||
|
peer_id: u32,
|
||||||
|
reason: DisconnectReason,
|
||||||
|
},
|
||||||
|
Stats(TunnelStats),
|
||||||
|
Disconnect {
|
||||||
|
reason: DisconnectReason,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlMessage {
|
||||||
|
pub fn validate(&self) -> Result<(), ControlError> {
|
||||||
|
match self {
|
||||||
|
Self::Hello(hello) => hello.validate(),
|
||||||
|
Self::Welcome(welcome) => welcome.validate(),
|
||||||
|
Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } => Ok(()),
|
||||||
|
Self::PeerJoined(peer) => peer.validate(),
|
||||||
|
Self::PeerLeft { peer_id, .. } if *peer_id == 0 => Err(ControlError::InvalidPeerId),
|
||||||
|
Self::PeerLeft { .. } => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_room_code(value: &str) -> Result<(), ControlError> {
|
||||||
|
if value.len() < MIN_ROOM_CODE_LEN {
|
||||||
|
return Err(ControlError::EmptyRoomCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.len() > MAX_ROOM_CODE_LEN {
|
||||||
|
return Err(ControlError::RoomCodeTooLong {
|
||||||
|
len: value.len(),
|
||||||
|
max: MAX_ROOM_CODE_LEN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, byte) in value.bytes().enumerate() {
|
||||||
|
if !byte.is_ascii_alphanumeric() && byte != b'-' && byte != b'_' {
|
||||||
|
return Err(ControlError::InvalidRoomCodeByte { index, byte });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn room() -> RoomCode {
|
||||||
|
RoomCode::new("ABCD-1234").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_mac() -> MacAddr {
|
||||||
|
MacAddr::new([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee])
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn validates_room_codes() {
|
||||||
let result = add(2, 2);
|
assert_eq!(RoomCode::new("").unwrap_err(), ControlError::EmptyRoomCode);
|
||||||
assert_eq!(result, 4);
|
assert!(RoomCode::new("ABCD_1234-z").is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
RoomCode::new("bad room").unwrap_err(),
|
||||||
|
ControlError::InvalidRoomCodeByte {
|
||||||
|
index: 3,
|
||||||
|
byte: b' '
|
||||||
|
}
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
RoomCode::new("a".repeat(MAX_ROOM_CODE_LEN + 1)).unwrap_err(),
|
||||||
|
ControlError::RoomCodeTooLong { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_valid_client_hello() {
|
||||||
|
let hello = EndpointHello::client(room(), client_mac(), 1400).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(hello.protocol_version(), CONTROL_PROTOCOL_VERSION);
|
||||||
|
assert_eq!(hello.role(), Role::Client);
|
||||||
|
assert_eq!(hello.announced_mac(), Some(client_mac()));
|
||||||
|
assert_eq!(hello.max_datagram_size(), 1400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_hello_rejects_invalid_mac_and_tiny_datagram_budget() {
|
||||||
|
assert!(matches!(
|
||||||
|
EndpointHello::client(room(), MacAddr::BROADCAST, 1400).unwrap_err(),
|
||||||
|
ControlError::InvalidClientMac { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
EndpointHello::client(room(), client_mac(), 128).unwrap_err(),
|
||||||
|
ControlError::Mtu(MtuError::DatagramTooSmall { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_valid_gateway_hello() {
|
||||||
|
let hello = EndpointHello::gateway(room(), 1400).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(hello.role(), Role::Gateway);
|
||||||
|
assert_eq!(hello.announced_mac(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_welcome_rejects_reserved_peer_id_and_tiny_mtu() {
|
||||||
|
assert_eq!(
|
||||||
|
ServerWelcome::new(1, 0, MIN_USEFUL_TAP_MTU as u16).unwrap_err(),
|
||||||
|
ControlError::InvalidPeerId
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(),
|
||||||
|
ControlError::EffectiveMtuTooSmall { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_info_enforces_role_mac_rules() {
|
||||||
|
let client = PeerInfo::new(7, Role::Client, Some(client_mac())).unwrap();
|
||||||
|
assert_eq!(client.peer_id(), 7);
|
||||||
|
assert_eq!(client.mac(), Some(client_mac()));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
PeerInfo::new(8, Role::Client, None).unwrap_err(),
|
||||||
|
ControlError::MissingClientMac
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
PeerInfo::new(9, Role::Gateway, Some(client_mac())).unwrap_err(),
|
||||||
|
ControlError::UnexpectedGatewayMac
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exposes_stats_counters() {
|
||||||
|
let stats = TunnelStats::new(1, 2, 3, 4, 5, 6);
|
||||||
|
|
||||||
|
assert_eq!(stats.ethernet_frames_tx(), 1);
|
||||||
|
assert_eq!(stats.ethernet_frames_rx(), 2);
|
||||||
|
assert_eq!(stats.datagrams_tx(), 3);
|
||||||
|
assert_eq!(stats.datagrams_rx(), 4);
|
||||||
|
assert_eq!(stats.dropped_frames(), 5);
|
||||||
|
assert_eq!(stats.malformed_frames(), 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user