feat(obs): distinguish one-way broadcast flow

The client previously reported "Broadcast traffic flowing" as soon as either
broadcast TX or RX was nonzero. During the MVP DHCP/ARP proof, one-way
broadcast is useful but weaker evidence than bidirectional broadcast.

Keep the existing healthy message for two-way broadcast, but report
outbound-only broadcast as a warning that the client is still waiting for a LAN
broadcast reply, and report inbound-only broadcast separately. This makes the
Windows client status lines more precise when DHCP is stuck.

Test Plan:
- cargo test -p lanparty-obs
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
- git diff --cached --check

Refs: MVP Windows DHCP diagnostics
This commit is contained in:
2026-05-22 06:58:20 +02:00
parent ca57b90228
commit 608e1a6f55
3 changed files with 63 additions and 4 deletions
+2 -1
View File
@@ -271,7 +271,8 @@ after bridging starts become visible in later status lines, preferring a
non-link-local IPv4 address when Windows reports several TAP addresses. Each non-link-local IPv4 address when Windows reports several TAP addresses. Each
snapshot also emits short user-facing lines such as relay/gateway connection status, snapshot also emits short user-facing lines such as relay/gateway connection status,
relay-route and TAP readiness warnings, DHCP address presence, relay RTT, and relay-route and TAP readiness warnings, DHCP address presence, relay RTT, and
broadcast-flow confirmation when those signals are observed. Malformed frames broadcast-flow confirmation. One-way broadcast diagnostics distinguish frames
sent toward the LAN from broadcast frames received back from the LAN. Malformed frames
read from TAP, invalid or unauthorized source-MAC frames, L2 control-plane read from TAP, invalid or unauthorized source-MAC frames, L2 control-plane
traffic, remote VLAN tags, DHCP server replies, IPv6 Router Advertisements, IPv6 traffic, remote VLAN tags, DHCP server replies, IPv6 Router Advertisements, IPv6
fragments, jumbo frames, and TAP frames whose encoded datagrams exceed the fragments, jumbo frames, and TAP frames whose encoded datagrams exceed the
+2
View File
@@ -246,6 +246,8 @@ Client health:
```text ```text
Relay RTT: 23 ms Relay RTT: 23 ms
Broadcast traffic flowing Broadcast traffic flowing
Broadcast sent toward LAN; waiting for LAN broadcast reply
LAN broadcast received
client frame direction=TapToRelay ... action=Forwarded drop_reason=- client frame direction=TapToRelay ... action=Forwarded drop_reason=-
client frame direction=RelayToTap ... action=Forwarded drop_reason=- client frame direction=RelayToTap ... action=Forwarded drop_reason=-
``` ```
+59 -3
View File
@@ -447,11 +447,23 @@ impl ClientDiagnostics {
)); ));
} }
if self.stats.broadcast_frames_tx() > 0 || self.stats.broadcast_frames_rx() > 0 { match (
diagnostics.push(UserDiagnostic::new( self.stats.broadcast_frames_tx() > 0,
self.stats.broadcast_frames_rx() > 0,
) {
(true, true) => diagnostics.push(UserDiagnostic::new(
UserDiagnosticLevel::Info, UserDiagnosticLevel::Info,
"Broadcast traffic flowing", "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 diagnostics
@@ -723,6 +735,50 @@ mod tests {
assert_eq!(user_diagnostics[2].level(), UserDiagnosticLevel::Warning); 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] #[test]
fn reports_link_local_tap_ipv4_as_waiting_for_lan_dhcp() { 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(); let diagnostic = tap_ip_user_diagnostic(Some("169.254.10.20".parse().unwrap())).unwrap();