bd22a68a6f
The MVP tunnel negotiates an effective TAP MTU and configures the Windows TAP IP interface to that value, but the forwarding path only rejected frames that were standard-Ethernet jumbo frames or exceeded the QUIC datagram budget. A frame could therefore be larger than the negotiated TAP MTU while still fitting inside the QUIC datagram budget. Make the TAP-MTU frame limit an explicit shared protocol helper and enforce it at every data-path boundary: Windows client send/receive, Linux gateway send/receive, and relay forwarding. Such frames now produce TapMtuExceeded in logs and counters instead of being forwarded until a later layer drops or accepts them implicitly. This keeps the no-fragmentation contract honest: one Ethernet frame still maps to one QUIC datagram, but only if that frame also fits the room's negotiated TAP MTU. Test Plan: - cargo fmt --check - cargo test -p lanparty-proto tap_mtu - cargo test -p lanparty-client-core connects_to_relay_control_stream_as_client - cargo test -p lanparty-gateway connects_to_relay_control_stream_as_gateway - cargo test -p lanparty-relay drops_frames_above_effective_tap_mtu - cargo test -p lanparty-relay rate_limits_client_total_bandwidth_after_burst - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo build --release -p lanparty-relay -p lanparty-gateway - git diff --check - git diff --cached --check Refs: MVP no-fragmentation tunnel MTU contract
802 lines
22 KiB
Rust
802 lines
22 KiB
Rust
//! 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, EthernetSafetyDrop, MacAddr};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum FrameDirection {
|
|
TapToRelay,
|
|
RelayToTap,
|
|
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,
|
|
InvalidSourceMac,
|
|
UnauthorizedSourceMac,
|
|
DuplicateMac,
|
|
ControlPlaneEtherType,
|
|
DhcpServerReply,
|
|
Ipv6RouterAdvertisement,
|
|
Ipv6Fragment,
|
|
VlanTaggedFrame,
|
|
TapMtuExceeded,
|
|
DatagramBudget,
|
|
UnknownDestination,
|
|
RateLimit,
|
|
}
|
|
|
|
impl From<EthernetSafetyDrop> for DropReason {
|
|
fn from(value: EthernetSafetyDrop) -> Self {
|
|
match value {
|
|
EthernetSafetyDrop::InvalidSourceMac => Self::InvalidSourceMac,
|
|
EthernetSafetyDrop::JumboFrame => Self::JumboFrame,
|
|
EthernetSafetyDrop::ControlPlaneEtherType => Self::ControlPlaneEtherType,
|
|
EthernetSafetyDrop::VlanTaggedFrame => Self::VlanTaggedFrame,
|
|
EthernetSafetyDrop::DhcpServerReply => Self::DhcpServerReply,
|
|
EthernetSafetyDrop::Ipv6RouterAdvertisement => Self::Ipv6RouterAdvertisement,
|
|
EthernetSafetyDrop::Ipv6Fragment => Self::Ipv6Fragment,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
pub struct FrameLog {
|
|
direction: FrameDirection,
|
|
source_mac: Option<MacAddr>,
|
|
destination_mac: Option<MacAddr>,
|
|
ethertype_or_len: Option<u16>,
|
|
frame_len: usize,
|
|
peer_id: Option<u32>,
|
|
action: FrameAction,
|
|
drop_reason: Option<DropReason>,
|
|
}
|
|
|
|
impl FrameLog {
|
|
#[must_use]
|
|
pub fn from_ethernet(
|
|
direction: FrameDirection,
|
|
peer_id: Option<u32>,
|
|
action: FrameAction,
|
|
drop_reason: Option<DropReason>,
|
|
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<u32>,
|
|
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<MacAddr> {
|
|
self.source_mac
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn destination_mac(&self) -> Option<MacAddr> {
|
|
self.destination_mac
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn ethertype_or_len(&self) -> Option<u16> {
|
|
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<u32> {
|
|
self.peer_id
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn action(&self) -> FrameAction {
|
|
self.action
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn drop_reason(&self) -> Option<DropReason> {
|
|
self.drop_reason
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
|
|
pub struct TunnelStats {
|
|
ethernet_frames_tx: u64,
|
|
ethernet_frames_rx: u64,
|
|
#[serde(default)]
|
|
broadcast_frames_tx: u64,
|
|
#[serde(default)]
|
|
broadcast_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,
|
|
broadcast_frames_tx: 0,
|
|
broadcast_frames_rx: 0,
|
|
datagrams_tx,
|
|
datagrams_rx,
|
|
dropped_frames,
|
|
malformed_frames,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn with_broadcast_frames(
|
|
mut self,
|
|
broadcast_frames_tx: u64,
|
|
broadcast_frames_rx: u64,
|
|
) -> Self {
|
|
self.broadcast_frames_tx = broadcast_frames_tx;
|
|
self.broadcast_frames_rx = broadcast_frames_rx;
|
|
self
|
|
}
|
|
|
|
#[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 broadcast_frames_tx(&self) -> u64 {
|
|
self.broadcast_frames_tx
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn broadcast_frames_rx(&self) -> u64 {
|
|
self.broadcast_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,
|
|
gateway_connected: bool,
|
|
}
|
|
|
|
impl RelayDiagnostics {
|
|
#[must_use]
|
|
pub const fn new(reachable: bool, route_pinned: bool, gateway_connected: bool) -> Self {
|
|
Self {
|
|
reachable,
|
|
route_pinned,
|
|
gateway_connected,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn reachable(&self) -> bool {
|
|
self.reachable
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn route_pinned(&self) -> bool {
|
|
self.route_pinned
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn gateway_connected(&self) -> bool {
|
|
self.gateway_connected
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
pub struct QuicDiagnostics {
|
|
datagram_supported: bool,
|
|
max_datagram_size: Option<u16>,
|
|
#[serde(default)]
|
|
relay_rtt_ms: Option<u64>,
|
|
}
|
|
|
|
impl QuicDiagnostics {
|
|
#[must_use]
|
|
pub const fn new(datagram_supported: bool, max_datagram_size: Option<u16>) -> Self {
|
|
Self {
|
|
datagram_supported,
|
|
max_datagram_size,
|
|
relay_rtt_ms: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn with_relay_rtt_ms(mut self, relay_rtt_ms: Option<u64>) -> Self {
|
|
self.relay_rtt_ms = relay_rtt_ms;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn datagram_supported(&self) -> bool {
|
|
self.datagram_supported
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn max_datagram_size(&self) -> Option<u16> {
|
|
self.max_datagram_size
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn relay_rtt_ms(&self) -> Option<u64> {
|
|
self.relay_rtt_ms
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
pub struct TapDiagnostics {
|
|
adapter_found: bool,
|
|
mac: Option<MacAddr>,
|
|
mtu: Option<u16>,
|
|
ip: Option<IpAddr>,
|
|
}
|
|
|
|
impl TapDiagnostics {
|
|
#[must_use]
|
|
pub const fn new(
|
|
adapter_found: bool,
|
|
mac: Option<MacAddr>,
|
|
mtu: Option<u16>,
|
|
ip: Option<IpAddr>,
|
|
) -> 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<MacAddr> {
|
|
self.mac
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn mtu(&self) -> Option<u16> {
|
|
self.mtu
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn ip(&self) -> Option<IpAddr> {
|
|
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
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn user_diagnostics(&self) -> Vec<UserDiagnostic> {
|
|
let mut diagnostics = Vec::new();
|
|
|
|
diagnostics.push(if self.relay.reachable() {
|
|
UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to relay")
|
|
} else {
|
|
UserDiagnostic::new(UserDiagnosticLevel::Error, "Relay not reachable")
|
|
});
|
|
|
|
diagnostics.push(if self.relay.gateway_connected() {
|
|
UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to LAN gateway")
|
|
} else {
|
|
UserDiagnostic::new(UserDiagnosticLevel::Warning, "Waiting for LAN gateway")
|
|
});
|
|
|
|
if !self.relay.route_pinned() {
|
|
diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Warning,
|
|
"Relay route not pinned",
|
|
));
|
|
}
|
|
|
|
if !self.tap.adapter_found() {
|
|
diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Error,
|
|
"TAP adapter not found",
|
|
));
|
|
} else if self.tap.ip().is_none() {
|
|
diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Warning,
|
|
"Waiting for TAP IP",
|
|
));
|
|
}
|
|
|
|
if let Some(diagnostic) = tap_ip_user_diagnostic(self.tap.ip()) {
|
|
diagnostics.push(diagnostic);
|
|
}
|
|
|
|
if let Some(rtt_ms) = self.quic.relay_rtt_ms() {
|
|
diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Info,
|
|
format!("Relay RTT: {rtt_ms} ms"),
|
|
));
|
|
}
|
|
|
|
match (
|
|
self.stats.broadcast_frames_tx() > 0,
|
|
self.stats.broadcast_frames_rx() > 0,
|
|
) {
|
|
(true, true) => diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Info,
|
|
"Broadcast traffic flowing",
|
|
)),
|
|
(true, false) => diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Warning,
|
|
"Broadcast sent toward LAN; waiting for LAN broadcast reply",
|
|
)),
|
|
(false, true) => diagnostics.push(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Info,
|
|
"LAN broadcast received",
|
|
)),
|
|
(false, false) => {}
|
|
}
|
|
|
|
diagnostics
|
|
}
|
|
}
|
|
|
|
fn tap_ip_user_diagnostic(ip: Option<IpAddr>) -> Option<UserDiagnostic> {
|
|
match ip {
|
|
Some(IpAddr::V4(ip)) if ip.is_link_local() => Some(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Warning,
|
|
format!("TAP has link-local IP {ip}; waiting for LAN DHCP"),
|
|
)),
|
|
Some(IpAddr::V4(ip)) => Some(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Info,
|
|
format!("DHCP received: {ip}"),
|
|
)),
|
|
Some(ip) => Some(UserDiagnostic::new(
|
|
UserDiagnosticLevel::Info,
|
|
format!("TAP IP detected: {ip}"),
|
|
)),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
#[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<String>) -> 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)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
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).with_broadcast_frames(7, 8);
|
|
let diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(true, 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.relay().gateway_connected());
|
|
assert!(diagnostics.quic().datagram_supported());
|
|
assert_eq!(diagnostics.quic().max_datagram_size(), Some(1400));
|
|
assert_eq!(diagnostics.quic().relay_rtt_ms(), None);
|
|
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().broadcast_frames_tx(), 7);
|
|
assert_eq!(diagnostics.stats().broadcast_frames_rx(), 8);
|
|
assert_eq!(diagnostics.stats().dropped_frames(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn defaults_missing_broadcast_stats_to_zero() {
|
|
let stats: TunnelStats = serde_json::from_str(
|
|
r#"{
|
|
"ethernet_frames_tx": 1,
|
|
"ethernet_frames_rx": 2,
|
|
"datagrams_tx": 3,
|
|
"datagrams_rx": 4,
|
|
"dropped_frames": 5,
|
|
"malformed_frames": 6
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(stats.broadcast_frames_tx(), 0);
|
|
assert_eq!(stats.broadcast_frames_rx(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn defaults_missing_relay_rtt_to_unknown() {
|
|
let diagnostics: QuicDiagnostics = serde_json::from_str(
|
|
r#"{
|
|
"datagram_supported": true,
|
|
"max_datagram_size": 1400
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(diagnostics.relay_rtt_ms(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn derives_user_diagnostics_from_client_snapshot() {
|
|
let diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(true, true, true),
|
|
QuicDiagnostics::new(true, Some(1400)).with_relay_rtt_ms(Some(23)),
|
|
TapDiagnostics::new(
|
|
true,
|
|
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
|
Some(1200),
|
|
Some("10.73.42.51".parse().unwrap()),
|
|
),
|
|
TunnelStats::new(1, 2, 3, 4, 5, 6).with_broadcast_frames(7, 8),
|
|
);
|
|
|
|
let user_diagnostics = diagnostics.user_diagnostics();
|
|
let messages: Vec<_> = user_diagnostics
|
|
.iter()
|
|
.map(UserDiagnostic::message)
|
|
.collect();
|
|
|
|
assert_eq!(
|
|
messages,
|
|
[
|
|
"Connected to relay",
|
|
"Connected to LAN gateway",
|
|
"DHCP received: 10.73.42.51",
|
|
"Relay RTT: 23 ms",
|
|
"Broadcast traffic flowing",
|
|
]
|
|
);
|
|
assert!(
|
|
user_diagnostics
|
|
.iter()
|
|
.all(|diagnostic| diagnostic.level() == UserDiagnosticLevel::Info)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_user_diagnostic_warnings_for_missing_connections() {
|
|
let diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(false, false, false),
|
|
QuicDiagnostics::new(false, None),
|
|
TapDiagnostics::new(false, None, None, None),
|
|
TunnelStats::default(),
|
|
);
|
|
|
|
let user_diagnostics = diagnostics.user_diagnostics();
|
|
|
|
assert_eq!(
|
|
user_diagnostics
|
|
.iter()
|
|
.map(UserDiagnostic::message)
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
"Relay not reachable",
|
|
"Waiting for LAN gateway",
|
|
"Relay route not pinned",
|
|
"TAP adapter not found",
|
|
]
|
|
);
|
|
assert_eq!(user_diagnostics[0].level(), UserDiagnosticLevel::Error);
|
|
assert_eq!(user_diagnostics[1].level(), UserDiagnosticLevel::Warning);
|
|
assert_eq!(user_diagnostics[2].level(), UserDiagnosticLevel::Warning);
|
|
assert_eq!(user_diagnostics[3].level(), UserDiagnosticLevel::Error);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_waiting_for_tap_ip_when_adapter_has_no_address() {
|
|
let diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(true, true, true),
|
|
QuicDiagnostics::new(true, Some(1400)),
|
|
TapDiagnostics::new(
|
|
true,
|
|
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
|
Some(1200),
|
|
None,
|
|
),
|
|
TunnelStats::default(),
|
|
);
|
|
|
|
let user_diagnostics = diagnostics.user_diagnostics();
|
|
|
|
assert_eq!(
|
|
user_diagnostics
|
|
.iter()
|
|
.map(UserDiagnostic::message)
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
"Connected to relay",
|
|
"Connected to LAN gateway",
|
|
"Waiting for TAP IP",
|
|
]
|
|
);
|
|
assert_eq!(user_diagnostics[2].level(), UserDiagnosticLevel::Warning);
|
|
}
|
|
|
|
#[test]
|
|
fn distinguishes_one_way_broadcast_diagnostics() {
|
|
let mut diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(true, true, true),
|
|
QuicDiagnostics::new(true, Some(1400)),
|
|
TapDiagnostics::new(
|
|
true,
|
|
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
|
Some(1200),
|
|
None,
|
|
),
|
|
TunnelStats::default().with_broadcast_frames(1, 0),
|
|
);
|
|
let broadcast = diagnostics
|
|
.user_diagnostics()
|
|
.into_iter()
|
|
.find(|diagnostic| diagnostic.message().contains("Broadcast"))
|
|
.expect("outbound broadcast should produce a diagnostic");
|
|
assert_eq!(broadcast.level(), UserDiagnosticLevel::Warning);
|
|
assert_eq!(
|
|
broadcast.message(),
|
|
"Broadcast sent toward LAN; waiting for LAN broadcast reply"
|
|
);
|
|
|
|
diagnostics = ClientDiagnostics::new(
|
|
RelayDiagnostics::new(true, true, true),
|
|
QuicDiagnostics::new(true, Some(1400)),
|
|
TapDiagnostics::new(
|
|
true,
|
|
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
|
Some(1200),
|
|
None,
|
|
),
|
|
TunnelStats::default().with_broadcast_frames(0, 1),
|
|
);
|
|
let broadcast = diagnostics
|
|
.user_diagnostics()
|
|
.into_iter()
|
|
.find(|diagnostic| diagnostic.message().contains("broadcast"))
|
|
.expect("inbound broadcast should produce a diagnostic");
|
|
assert_eq!(broadcast.level(), UserDiagnosticLevel::Info);
|
|
assert_eq!(broadcast.message(), "LAN broadcast received");
|
|
}
|
|
|
|
#[test]
|
|
fn reports_link_local_tap_ipv4_as_waiting_for_lan_dhcp() {
|
|
let diagnostic = tap_ip_user_diagnostic(Some("169.254.10.20".parse().unwrap())).unwrap();
|
|
|
|
assert_eq!(diagnostic.level(), UserDiagnosticLevel::Warning);
|
|
assert_eq!(
|
|
diagnostic.message(),
|
|
"TAP has link-local IP 169.254.10.20; waiting for LAN DHCP"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_ipv6_link_local_tap_ip_without_calling_it_dhcp() {
|
|
let diagnostic = tap_ip_user_diagnostic(Some("fe80::1".parse().unwrap())).unwrap();
|
|
|
|
assert_eq!(diagnostic.level(), UserDiagnosticLevel::Info);
|
|
assert_eq!(diagnostic.message(), "TAP IP detected: fe80::1");
|
|
}
|
|
}
|