diff --git a/Cargo.lock b/Cargo.lock index 5f37d1f..399edc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ version = "0.1.0" name = "lanparty-ctrl" version = "0.1.0" dependencies = [ + "lanparty-obs", "lanparty-proto", "serde", "thiserror", @@ -26,6 +27,10 @@ version = "0.1.0" [[package]] name = "lanparty-obs" version = "0.1.0" +dependencies = [ + "lanparty-proto", + "serde", +] [[package]] name = "lanparty-proto" diff --git a/README.md b/README.md index 7a1ecec..04e404e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,14 @@ Reliable control-plane schema shared by the QUIC stream handlers: - server welcome, reject, peer lifecycle, stats, and disconnect messages - room-code, role/MAC, peer-id, and effective-MTU validation +### `lanparty-obs` + +Shared diagnostics and structured logging vocabulary: + +- gateway/relay frame logs with MACs, ethertype, length, peer, and action +- tunnel counters shared by control messages and runtime diagnostics +- client connectivity/TAP diagnostics and user-facing status messages + ## Build ```bash diff --git a/crates/lanparty-ctrl/Cargo.toml b/crates/lanparty-ctrl/Cargo.toml index 9ddbae6..0ef8b01 100644 --- a/crates/lanparty-ctrl/Cargo.toml +++ b/crates/lanparty-ctrl/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dependencies] +lanparty-obs = { path = "../lanparty-obs" } lanparty-proto = { path = "../lanparty-proto" } serde.workspace = true thiserror.workspace = true diff --git a/crates/lanparty-ctrl/src/lib.rs b/crates/lanparty-ctrl/src/lib.rs index 14684a0..32b0a02 100644 --- a/crates/lanparty-ctrl/src/lib.rs +++ b/crates/lanparty-ctrl/src/lib.rs @@ -6,6 +6,7 @@ use std::{fmt, str::FromStr}; +pub use lanparty_obs::TunnelStats; use lanparty_proto::{MIN_USEFUL_TAP_MTU, MacAddr, MtuError, recommended_tap_mtu}; use thiserror::Error; @@ -357,67 +358,6 @@ impl Reject { } } -#[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 { diff --git a/crates/lanparty-obs/Cargo.toml b/crates/lanparty-obs/Cargo.toml index d47bd59..c7ca86d 100644 --- a/crates/lanparty-obs/Cargo.toml +++ b/crates/lanparty-obs/Cargo.toml @@ -4,3 +4,5 @@ version.workspace = true edition.workspace = true [dependencies] +lanparty-proto = { path = "../lanparty-proto" } +serde.workspace = true diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index b93cf3f..b3740a0 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -1,5 +1,380 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +//! Shared diagnostics and structured logging models. +//! +//! Runtime crates can convert these values into `tracing` fields, JSON logs, or +//! user-facing status lines without each component inventing its own vocabulary. + +use std::net::IpAddr; + +use lanparty_proto::{EthernetFrame, MacAddr}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FrameDirection { + RemoteToLan, + LanToRemote, + RelayIngress, + RelayEgress, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FrameAction { + Forwarded, + Dropped, + Filtered, + RateLimited, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DropReason { + Malformed, + JumboFrame, + UnauthorizedSourceMac, + DuplicateMac, + ControlPlaneEtherType, + DhcpServerReply, + Ipv6RouterAdvertisement, + UnknownDestination, + RateLimit, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct FrameLog { + direction: FrameDirection, + source_mac: Option, + destination_mac: Option, + ethertype_or_len: Option, + frame_len: usize, + peer_id: Option, + action: FrameAction, + drop_reason: Option, +} + +impl FrameLog { + #[must_use] + pub fn from_ethernet( + direction: FrameDirection, + peer_id: Option, + action: FrameAction, + drop_reason: Option, + frame: EthernetFrame<'_>, + ) -> Self { + Self { + direction, + source_mac: Some(frame.source()), + destination_mac: Some(frame.destination()), + ethertype_or_len: Some(frame.ethertype_or_len()), + frame_len: frame.len(), + peer_id, + action, + drop_reason, + } + } + + #[must_use] + pub const fn malformed( + direction: FrameDirection, + peer_id: Option, + frame_len: usize, + ) -> Self { + Self { + direction, + source_mac: None, + destination_mac: None, + ethertype_or_len: None, + frame_len, + peer_id, + action: FrameAction::Dropped, + drop_reason: Some(DropReason::Malformed), + } + } + + #[must_use] + pub const fn direction(&self) -> FrameDirection { + self.direction + } + + #[must_use] + pub const fn source_mac(&self) -> Option { + self.source_mac + } + + #[must_use] + pub const fn destination_mac(&self) -> Option { + self.destination_mac + } + + #[must_use] + pub const fn ethertype_or_len(&self) -> Option { + self.ethertype_or_len + } + + #[must_use] + pub const fn frame_len(&self) -> usize { + self.frame_len + } + + #[must_use] + pub const fn peer_id(&self) -> Option { + self.peer_id + } + + #[must_use] + pub const fn action(&self) -> FrameAction { + self.action + } + + #[must_use] + pub const fn drop_reason(&self) -> Option { + self.drop_reason + } +} + +#[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)] +pub struct RelayDiagnostics { + reachable: bool, + route_pinned: bool, +} + +impl RelayDiagnostics { + #[must_use] + pub const fn new(reachable: bool, route_pinned: bool) -> Self { + Self { + reachable, + route_pinned, + } + } + + #[must_use] + pub const fn reachable(&self) -> bool { + self.reachable + } + + #[must_use] + pub const fn route_pinned(&self) -> bool { + self.route_pinned + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct QuicDiagnostics { + datagram_supported: bool, + max_datagram_size: Option, +} + +impl QuicDiagnostics { + #[must_use] + pub const fn new(datagram_supported: bool, max_datagram_size: Option) -> Self { + Self { + datagram_supported, + max_datagram_size, + } + } + + #[must_use] + pub const fn datagram_supported(&self) -> bool { + self.datagram_supported + } + + #[must_use] + pub const fn max_datagram_size(&self) -> Option { + self.max_datagram_size + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct TapDiagnostics { + adapter_found: bool, + mac: Option, + mtu: Option, + ip: Option, +} + +impl TapDiagnostics { + #[must_use] + pub const fn new( + adapter_found: bool, + mac: Option, + mtu: Option, + ip: Option, + ) -> Self { + Self { + adapter_found, + mac, + mtu, + ip, + } + } + + #[must_use] + pub const fn adapter_found(&self) -> bool { + self.adapter_found + } + + #[must_use] + pub const fn mac(&self) -> Option { + self.mac + } + + #[must_use] + pub const fn mtu(&self) -> Option { + self.mtu + } + + #[must_use] + pub const fn ip(&self) -> Option { + self.ip + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct ClientDiagnostics { + relay: RelayDiagnostics, + quic: QuicDiagnostics, + tap: TapDiagnostics, + stats: TunnelStats, +} + +impl ClientDiagnostics { + #[must_use] + pub const fn new( + relay: RelayDiagnostics, + quic: QuicDiagnostics, + tap: TapDiagnostics, + stats: TunnelStats, + ) -> Self { + Self { + relay, + quic, + tap, + stats, + } + } + + #[must_use] + pub const fn relay(&self) -> &RelayDiagnostics { + &self.relay + } + + #[must_use] + pub const fn quic(&self) -> &QuicDiagnostics { + &self.quic + } + + #[must_use] + pub const fn tap(&self) -> &TapDiagnostics { + &self.tap + } + + #[must_use] + pub const fn stats(&self) -> &TunnelStats { + &self.stats + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum UserDiagnosticLevel { + Info, + Warning, + Error, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct UserDiagnostic { + level: UserDiagnosticLevel, + message: String, +} + +impl UserDiagnostic { + #[must_use] + pub fn new(level: UserDiagnosticLevel, message: impl Into) -> Self { + Self { + level, + message: message.into(), + } + } + + #[must_use] + pub const fn level(&self) -> UserDiagnosticLevel { + self.level + } + + #[must_use] + pub fn message(&self) -> &str { + &self.message + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type", content = "payload", rename_all = "snake_case")] +pub enum DiagnosticEvent { + Frame(FrameLog), + Client(ClientDiagnostics), + Stats(TunnelStats), + User(UserDiagnostic), } #[cfg(test)] @@ -7,8 +382,71 @@ mod tests { use super::*; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn builds_frame_log_from_ethernet_header() { + let bytes = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x08, 0x06, + ]; + let frame = EthernetFrame::parse(&bytes).unwrap(); + + let log = FrameLog::from_ethernet( + FrameDirection::RemoteToLan, + Some(7), + FrameAction::Forwarded, + None, + frame, + ); + + assert_eq!(log.direction(), FrameDirection::RemoteToLan); + assert_eq!( + log.source_mac(), + Some(MacAddr::new([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee])) + ); + assert_eq!(log.destination_mac(), Some(MacAddr::BROADCAST)); + assert_eq!(log.ethertype_or_len(), Some(0x0806)); + assert_eq!(log.frame_len(), bytes.len()); + assert_eq!(log.peer_id(), Some(7)); + assert_eq!(log.action(), FrameAction::Forwarded); + assert_eq!(log.drop_reason(), None); + } + + #[test] + fn builds_malformed_frame_log_without_header_fields() { + let log = FrameLog::malformed(FrameDirection::RelayIngress, Some(3), 8); + + assert_eq!(log.direction(), FrameDirection::RelayIngress); + assert_eq!(log.source_mac(), None); + assert_eq!(log.destination_mac(), None); + assert_eq!(log.ethertype_or_len(), None); + assert_eq!(log.frame_len(), 8); + assert_eq!(log.peer_id(), Some(3)); + assert_eq!(log.action(), FrameAction::Dropped); + assert_eq!(log.drop_reason(), Some(DropReason::Malformed)); + } + + #[test] + fn exposes_client_diagnostics() { + let mac = MacAddr::new([0x02, 1, 2, 3, 4, 5]); + let stats = TunnelStats::new(1, 2, 3, 4, 5, 6); + let diagnostics = ClientDiagnostics::new( + RelayDiagnostics::new(true, true), + QuicDiagnostics::new(true, Some(1400)), + TapDiagnostics::new( + true, + Some(mac), + Some(1200), + Some("10.73.42.51".parse().unwrap()), + ), + stats, + ); + + assert!(diagnostics.relay().reachable()); + assert!(diagnostics.relay().route_pinned()); + assert!(diagnostics.quic().datagram_supported()); + assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400)); + assert!(diagnostics.tap().adapter_found()); + assert_eq!(diagnostics.tap().mac(), Some(mac)); + assert_eq!(diagnostics.tap().mtu(), Some(1200)); + assert_eq!(diagnostics.tap().ip().unwrap().to_string(), "10.73.42.51"); + assert_eq!(diagnostics.stats().dropped_frames(), 5); } }