fix(client): clear gateway status from welcome identity
The client initialized gateway connectivity from ServerWelcome, but welcome only exposed a boolean. If a gateway disconnected before the client saw the catch-up PeerJoined event, the later unknown PeerLeft could not be tied to the gateway and the status could stay connected. Carry an optional gateway peer id in ServerWelcome. The relay fills it from the joining gateway or the existing room gateway, and the Windows client stores it so a matching unknown PeerLeft clears gateway connectivity. The boolean remains for wire compatibility with older welcomes that do not carry the id. Test Plan: - cargo fmt --check - cargo test -p lanparty-ctrl server_welcome - cargo test -p lanparty-relay accepts_gateway_and_client_into_room - cargo test -p lanparty-relay reports_missing_gateway_to_client_joining_first - cargo test -p lanparty-client-win relay_lifecycle - cargo test -p lanparty-client-win \ clears_gateway_status_when_welcome_gateway_leaves_before_join_event - cargo test -p lanparty-relay bridges_real_client_and_gateway_sessions - cargo test -p lanparty-client-core connects_to_relay_control_stream_as_client - 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 lifecycle cleanup
This commit is contained in:
@@ -105,7 +105,8 @@ Public relay binary and relay-owned room state:
|
|||||||
- live Ethernet datagram forwarding with no ingress reflection
|
- live Ethernet datagram forwarding with no ingress reflection
|
||||||
- forwarding drops for Ethernet frames above the negotiated TAP MTU
|
- forwarding drops for Ethernet frames above the negotiated TAP MTU
|
||||||
- per-peer egress budget checks against the negotiated datagram size
|
- per-peer egress budget checks against the negotiated datagram size
|
||||||
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
|
- reliable `PeerJoined`/`PeerLeft` notifications plus gateway identity in
|
||||||
|
welcome messages
|
||||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN
|
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN
|
||||||
tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames,
|
tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames,
|
||||||
including frames behind ordinary IPv6 extension headers
|
including frames behind ordinary IPv6 extension headers
|
||||||
|
|||||||
@@ -921,6 +921,7 @@ mod tests {
|
|||||||
assert_eq!(client.welcome().room_id(), 7);
|
assert_eq!(client.welcome().room_id(), 7);
|
||||||
assert_eq!(client.welcome().peer_id(), 2);
|
assert_eq!(client.welcome().peer_id(), 2);
|
||||||
assert!(!client.welcome().gateway_connected());
|
assert!(!client.welcome().gateway_connected());
|
||||||
|
assert_eq!(client.welcome().gateway_peer_id(), None);
|
||||||
assert!(client.quic_max_datagram_size() <= 1400);
|
assert!(client.quic_max_datagram_size() <= 1400);
|
||||||
assert!(client.quic_diagnostics().datagram_supported());
|
assert!(client.quic_diagnostics().datagram_supported());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||||
};
|
};
|
||||||
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
|
use std::{collections::BTreeMap, fs, net::IpAddr, path::PathBuf};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -795,17 +795,23 @@ fn format_control_event(event: &ControlMessage) -> String {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ClientRelayStatus {
|
struct ClientRelayStatus {
|
||||||
gateway_connected: Arc<AtomicBool>,
|
gateway_connected: Arc<AtomicBool>,
|
||||||
|
gateway_peer_id: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientRelayStatus {
|
impl ClientRelayStatus {
|
||||||
|
const UNKNOWN_GATEWAY_PEER_ID: u32 = 0;
|
||||||
|
|
||||||
fn new(gateway_connected: bool) -> Self {
|
fn new(gateway_connected: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gateway_connected: Arc::new(AtomicBool::new(gateway_connected)),
|
gateway_connected: Arc::new(AtomicBool::new(gateway_connected)),
|
||||||
|
gateway_peer_id: Arc::new(AtomicU32::new(Self::UNKNOWN_GATEWAY_PEER_ID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_welcome(session: &ClientSession) -> Self {
|
fn from_welcome(session: &ClientSession) -> Self {
|
||||||
Self::new(session.welcome().gateway_connected())
|
let status = Self::new(session.welcome().gateway_connected());
|
||||||
|
status.set_gateway_peer_id(session.welcome().gateway_peer_id());
|
||||||
|
status
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gateway_connected(&self) -> bool {
|
fn gateway_connected(&self) -> bool {
|
||||||
@@ -815,6 +821,23 @@ impl ClientRelayStatus {
|
|||||||
fn set_gateway_connected(&self, gateway_connected: bool) {
|
fn set_gateway_connected(&self, gateway_connected: bool) {
|
||||||
self.gateway_connected
|
self.gateway_connected
|
||||||
.store(gateway_connected, Ordering::Relaxed);
|
.store(gateway_connected, Ordering::Relaxed);
|
||||||
|
if !gateway_connected {
|
||||||
|
self.set_gateway_peer_id(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gateway_peer_id(&self) -> Option<u32> {
|
||||||
|
match self.gateway_peer_id.load(Ordering::Relaxed) {
|
||||||
|
Self::UNKNOWN_GATEWAY_PEER_ID => None,
|
||||||
|
peer_id => Some(peer_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gateway_peer_id(&self, gateway_peer_id: Option<u32>) {
|
||||||
|
self.gateway_peer_id.store(
|
||||||
|
gateway_peer_id.unwrap_or(Self::UNKNOWN_GATEWAY_PEER_ID),
|
||||||
|
Ordering::Relaxed,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,12 +852,19 @@ impl ControlEventFormatter {
|
|||||||
ControlMessage::PeerJoined(peer) => {
|
ControlMessage::PeerJoined(peer) => {
|
||||||
self.peers.insert(peer.peer_id(), peer.clone());
|
self.peers.insert(peer.peer_id(), peer.clone());
|
||||||
if peer.role() == Role::Gateway {
|
if peer.role() == Role::Gateway {
|
||||||
|
relay_status.set_gateway_peer_id(Some(peer.peer_id()));
|
||||||
relay_status.set_gateway_connected(true);
|
relay_status.set_gateway_connected(true);
|
||||||
}
|
}
|
||||||
format_peer_joined(peer)
|
format_peer_joined(peer)
|
||||||
}
|
}
|
||||||
ControlMessage::PeerLeft { peer_id, reason } => {
|
ControlMessage::PeerLeft { peer_id, reason } => {
|
||||||
let Some(peer) = self.peers.remove(peer_id) else {
|
let Some(peer) = self.peers.remove(peer_id) else {
|
||||||
|
if relay_status.gateway_peer_id() == Some(*peer_id) {
|
||||||
|
relay_status.set_gateway_connected(false);
|
||||||
|
return format!(
|
||||||
|
"relay event: LAN gateway disconnected (peer {peer_id}, {reason:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
return format!("relay event: peer {peer_id} left ({reason:?})");
|
return format!("relay event: peer {peer_id} left ({reason:?})");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1502,6 +1532,7 @@ mod tests {
|
|||||||
"relay event: LAN gateway connected as peer 1"
|
"relay event: LAN gateway connected as peer 1"
|
||||||
);
|
);
|
||||||
assert!(relay_status.gateway_connected());
|
assert!(relay_status.gateway_connected());
|
||||||
|
assert_eq!(relay_status.gateway_peer_id(), Some(1));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.format(&client, &relay_status),
|
formatter.format(&client, &relay_status),
|
||||||
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
||||||
@@ -1519,6 +1550,25 @@ mod tests {
|
|||||||
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
||||||
);
|
);
|
||||||
assert!(!relay_status.gateway_connected());
|
assert!(!relay_status.gateway_connected());
|
||||||
|
assert_eq!(relay_status.gateway_peer_id(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clears_gateway_status_when_welcome_gateway_leaves_before_join_event() {
|
||||||
|
let gateway_left = ControlMessage::PeerLeft {
|
||||||
|
peer_id: 7,
|
||||||
|
reason: DisconnectReason::Normal,
|
||||||
|
};
|
||||||
|
let mut formatter = ControlEventFormatter::default();
|
||||||
|
let relay_status = ClientRelayStatus::new(true);
|
||||||
|
relay_status.set_gateway_peer_id(Some(7));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
formatter.format(&gateway_left, &relay_status),
|
||||||
|
"relay event: LAN gateway disconnected (peer 7, Normal)"
|
||||||
|
);
|
||||||
|
assert!(!relay_status.gateway_connected());
|
||||||
|
assert_eq!(relay_status.gateway_peer_id(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn mac(last_octet: u8) -> MacAddr {
|
const fn mac(last_octet: u8) -> MacAddr {
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ pub struct ServerWelcome {
|
|||||||
mode: ConnectionMode,
|
mode: ConnectionMode,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
gateway_connected: bool,
|
gateway_connected: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
gateway_peer_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerWelcome {
|
impl ServerWelcome {
|
||||||
@@ -238,6 +240,7 @@ impl ServerWelcome {
|
|||||||
effective_tap_mtu,
|
effective_tap_mtu,
|
||||||
mode: ConnectionMode::Relay,
|
mode: ConnectionMode::Relay,
|
||||||
gateway_connected: false,
|
gateway_connected: false,
|
||||||
|
gateway_peer_id: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +253,18 @@ impl ServerWelcome {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn with_gateway_connected(mut self, gateway_connected: bool) -> Self {
|
pub const fn with_gateway_connected(mut self, gateway_connected: bool) -> Self {
|
||||||
self.gateway_connected = gateway_connected;
|
self.gateway_connected = gateway_connected;
|
||||||
|
if !gateway_connected {
|
||||||
|
self.gateway_peer_id = None;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn with_gateway_peer_id(mut self, gateway_peer_id: Option<u32>) -> Self {
|
||||||
|
self.gateway_peer_id = gateway_peer_id;
|
||||||
|
if let Some(_peer_id) = gateway_peer_id {
|
||||||
|
self.gateway_connected = true;
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +287,10 @@ impl ServerWelcome {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.gateway_peer_id == Some(0) {
|
||||||
|
return Err(ControlError::InvalidPeerId);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +323,11 @@ impl ServerWelcome {
|
|||||||
pub const fn gateway_connected(&self) -> bool {
|
pub const fn gateway_connected(&self) -> bool {
|
||||||
self.gateway_connected
|
self.gateway_connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn gateway_peer_id(&self) -> Option<u32> {
|
||||||
|
self.gateway_peer_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
@@ -538,6 +562,14 @@ mod tests {
|
|||||||
ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(),
|
ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(),
|
||||||
ControlError::EffectiveMtuTooSmall { .. }
|
ControlError::EffectiveMtuTooSmall { .. }
|
||||||
));
|
));
|
||||||
|
assert_eq!(
|
||||||
|
ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16)
|
||||||
|
.unwrap()
|
||||||
|
.with_gateway_peer_id(Some(0))
|
||||||
|
.validate()
|
||||||
|
.unwrap_err(),
|
||||||
|
ControlError::InvalidPeerId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -546,7 +578,15 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(welcome.mode(), ConnectionMode::Relay);
|
assert_eq!(welcome.mode(), ConnectionMode::Relay);
|
||||||
assert!(!welcome.gateway_connected());
|
assert!(!welcome.gateway_connected());
|
||||||
assert!(welcome.with_gateway_connected(true).gateway_connected());
|
assert_eq!(welcome.gateway_peer_id(), None);
|
||||||
|
|
||||||
|
let with_gateway = welcome.with_gateway_peer_id(Some(7));
|
||||||
|
assert!(with_gateway.gateway_connected());
|
||||||
|
assert_eq!(with_gateway.gateway_peer_id(), Some(7));
|
||||||
|
|
||||||
|
let without_gateway = with_gateway.with_gateway_connected(false);
|
||||||
|
assert!(!without_gateway.gateway_connected());
|
||||||
|
assert_eq!(without_gateway.gateway_peer_id(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -581,6 +621,7 @@ mod tests {
|
|||||||
let welcome: ServerWelcome = serde_json::from_str(&json).unwrap();
|
let welcome: ServerWelcome = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
assert_eq!(welcome.mode(), ConnectionMode::Relay);
|
assert_eq!(welcome.mode(), ConnectionMode::Relay);
|
||||||
|
assert_eq!(welcome.gateway_peer_id(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -375,9 +375,12 @@ impl Room {
|
|||||||
format!("invalid peer: {error}"),
|
format!("invalid peer: {error}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let gateway_connected = matches!(peer.role(), Role::Gateway) || self.gateway.is_some();
|
let gateway_peer_id = match peer.role() {
|
||||||
|
Role::Gateway => Some(peer.peer_id()),
|
||||||
|
Role::Client => self.gateway.as_ref().map(|gateway| gateway.info.peer_id()),
|
||||||
|
};
|
||||||
let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu)
|
let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu)
|
||||||
.map(|welcome| welcome.with_gateway_connected(gateway_connected))
|
.map(|welcome| welcome.with_gateway_peer_id(gateway_peer_id))
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
Reject::new(
|
Reject::new(
|
||||||
RejectReason::InternalError,
|
RejectReason::InternalError,
|
||||||
@@ -912,9 +915,11 @@ mod tests {
|
|||||||
assert_eq!(gateway.peer().role(), Role::Gateway);
|
assert_eq!(gateway.peer().role(), Role::Gateway);
|
||||||
assert_eq!(gateway.welcome().peer_id(), 1);
|
assert_eq!(gateway.welcome().peer_id(), 1);
|
||||||
assert!(gateway.welcome().gateway_connected());
|
assert!(gateway.welcome().gateway_connected());
|
||||||
|
assert_eq!(gateway.welcome().gateway_peer_id(), Some(1));
|
||||||
assert_eq!(client.peer().role(), Role::Client);
|
assert_eq!(client.peer().role(), Role::Client);
|
||||||
assert_eq!(client.welcome().peer_id(), 2);
|
assert_eq!(client.welcome().peer_id(), 2);
|
||||||
assert!(client.welcome().gateway_connected());
|
assert!(client.welcome().gateway_connected());
|
||||||
|
assert_eq!(client.welcome().gateway_peer_id(), Some(1));
|
||||||
assert_eq!(snapshot.gateway().unwrap().peer_id(), 1);
|
assert_eq!(snapshot.gateway().unwrap().peer_id(), 1);
|
||||||
assert_eq!(snapshot.clients().len(), 1);
|
assert_eq!(snapshot.clients().len(), 1);
|
||||||
assert_eq!(snapshot.effective_tap_mtu(), 1200);
|
assert_eq!(snapshot.effective_tap_mtu(), 1200);
|
||||||
@@ -929,6 +934,7 @@ mod tests {
|
|||||||
let client = registry.join(client_hello(1)).unwrap();
|
let client = registry.join(client_hello(1)).unwrap();
|
||||||
|
|
||||||
assert!(!client.welcome().gateway_connected());
|
assert!(!client.welcome().gateway_connected());
|
||||||
|
assert_eq!(client.welcome().gateway_peer_id(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1412,7 +1412,9 @@ mod tests {
|
|||||||
assert_eq!(gateway.welcome().room_id(), client.welcome().room_id());
|
assert_eq!(gateway.welcome().room_id(), client.welcome().room_id());
|
||||||
assert_eq!(gateway.welcome().peer_id(), 1);
|
assert_eq!(gateway.welcome().peer_id(), 1);
|
||||||
assert_eq!(client.welcome().peer_id(), 2);
|
assert_eq!(client.welcome().peer_id(), 2);
|
||||||
|
assert_eq!(gateway.welcome().gateway_peer_id(), Some(1));
|
||||||
assert!(client.welcome().gateway_connected());
|
assert!(client.welcome().gateway_connected());
|
||||||
|
assert_eq!(client.welcome().gateway_peer_id(), Some(1));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
gateway.quic_max_datagram_size(),
|
gateway.quic_max_datagram_size(),
|
||||||
client.quic_max_datagram_size()
|
client.quic_max_datagram_size()
|
||||||
|
|||||||
Reference in New Issue
Block a user