fix(obs): warn on TAP link-local IPv4

IPv4 link-local on the TAP adapter means Windows self-assigned APIPA because
LAN DHCP did not complete through the tunnel. The previous user diagnostic
only said "TAP IP detected", which made a failed DHCP path look neutral
during MVP testing.

Return a full UserDiagnostic from TAP IP classification so IPv4 link-local
can be a warning while normal IPv4 still reports the received DHCP address.
Keep IPv6 link-local neutral because it is expected on many Windows
interfaces and is not evidence of LAN DHCP success or failure.

TESTING.md now tells the operator to troubleshoot 169.254.x.x like
`Waiting for TAP IP`.

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

Refs: PLAN.md diagnostics; TESTING.md MVP guide
This commit is contained in:
2026-05-22 05:54:29 +02:00
parent 2c490b2693
commit 92daa1d2ae
2 changed files with 33 additions and 12 deletions
+4
View File
@@ -242,6 +242,10 @@ If the client says `Waiting for TAP IP`, DHCP is not making the full round trip.
Check relay/gateway frame logs for broadcast traffic and check that the gateway Check relay/gateway frame logs for broadcast traffic and check that the gateway
is on wired Ethernet. is on wired Ethernet.
If the client reports a TAP link-local address such as `169.254.x.x`, treat it
the same way: Windows has self-assigned an address, but LAN DHCP did not
complete through the tunnel.
If startup fails with a TAP MAC mismatch, disable/enable the TAP adapter or If startup fails with a TAP MAC mismatch, disable/enable the TAP adapter or
reinstall TAP-Windows6 so Windows reloads the `NetworkAddress` value. Do not reinstall TAP-Windows6 so Windows reloads the `NetworkAddress` value. Do not
continue with a mismatched MAC. continue with a mismatched MAC.
+29 -12
View File
@@ -434,8 +434,8 @@ impl ClientDiagnostics {
)); ));
} }
if let Some(message) = tap_ip_user_message(self.tap.ip()) { if let Some(diagnostic) = tap_ip_user_diagnostic(self.tap.ip()) {
diagnostics.push(UserDiagnostic::new(UserDiagnosticLevel::Info, message)); diagnostics.push(diagnostic);
} }
if let Some(rtt_ms) = self.quic.relay_rtt_ms() { if let Some(rtt_ms) = self.quic.relay_rtt_ms() {
@@ -456,10 +456,20 @@ impl ClientDiagnostics {
} }
} }
fn tap_ip_user_message(ip: Option<IpAddr>) -> Option<String> { fn tap_ip_user_diagnostic(ip: Option<IpAddr>) -> Option<UserDiagnostic> {
match ip { match ip {
Some(IpAddr::V4(ip)) if !ip.is_link_local() => Some(format!("DHCP received: {ip}")), Some(IpAddr::V4(ip)) if ip.is_link_local() => Some(UserDiagnostic::new(
Some(ip) => Some(format!("TAP IP detected: {ip}")), 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, None => None,
} }
} }
@@ -712,14 +722,21 @@ mod tests {
} }
#[test] #[test]
fn avoids_calling_link_local_tap_ip_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();
assert_eq!(diagnostic.level(), UserDiagnosticLevel::Warning);
assert_eq!( assert_eq!(
tap_ip_user_message(Some("169.254.10.20".parse().unwrap())), diagnostic.message(),
Some("TAP IP detected: 169.254.10.20".to_string()) "TAP has link-local IP 169.254.10.20; waiting for LAN DHCP"
);
assert_eq!(
tap_ip_user_message(Some("fe80::1".parse().unwrap())),
Some("TAP IP detected: fe80::1".to_string())
); );
} }
#[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");
}
} }