feat(ctrl): send graceful disconnects
The relay already accepts post-handshake Disconnect control messages, but the client and gateway shutdown paths only sent a QUIC application close. That made normal shutdown indistinguishable from transport closure until the relay inferred a generic Normal leave. Client and gateway shutdown now send a best-effort Disconnect message with the human-readable shutdown reason before closing QUIC. The client-core drain uses Quinn's runtime timer instead of taking a Tokio runtime dependency. The gateway uses its existing Tokio runtime and applies the same short drain window on both explicit shutdown and Ctrl-C in the Linux bridge loop. The endpoint integration tests now assert that the server receives Disconnect after the stats stream, which also protects against closing too quickly and aborting the control stream. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-core \ connects_to_relay_control_stream_as_client -- --nocapture - cargo test -p lanparty-gateway \ connects_to_relay_control_stream_as_gateway -- --nocapture - cargo test -p lanparty-client-core - cargo test -p lanparty-gateway - cargo clippy -p lanparty-client-core --all-targets -- -D warnings - cargo clippy -p lanparty-gateway --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
future::poll_fn,
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::{Path, PathBuf},
|
||||
@@ -14,13 +15,15 @@ use std::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bytes::Bytes;
|
||||
use lanparty_ctrl::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlMessage, EndpointHello, MAX_CONTROL_MESSAGE_LEN, RELAY_ALPN,
|
||||
RoomCode, ServerWelcome, decode_control_frame, encode_control_message,
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlMessage, DisconnectReason, EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN, RELAY_ALPN, RoomCode, ServerWelcome, decode_control_frame,
|
||||
encode_control_message,
|
||||
};
|
||||
use lanparty_obs::{QuicDiagnostics, TunnelStats};
|
||||
use lanparty_proto::{EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram};
|
||||
@@ -28,6 +31,7 @@ use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||
use rustls::pki_types::CertificateDer;
|
||||
|
||||
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN;
|
||||
const DISCONNECT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ClientIdentity {
|
||||
@@ -288,6 +292,10 @@ impl ClientSession {
|
||||
}
|
||||
|
||||
pub async fn shutdown(self, reason: &str) {
|
||||
if send_disconnect(&self.connection, reason).await.is_ok() {
|
||||
drain_disconnect().await;
|
||||
}
|
||||
|
||||
self.connection.close(0_u32.into(), reason.as_bytes());
|
||||
self.endpoint.wait_idle().await;
|
||||
}
|
||||
@@ -537,6 +545,25 @@ async fn send_control_event(connection: &quinn::Connection, message: ControlMess
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_disconnect(connection: &quinn::Connection, message: &str) -> Result<()> {
|
||||
send_control_event(
|
||||
connection,
|
||||
ControlMessage::Disconnect {
|
||||
reason: DisconnectReason::Normal,
|
||||
message: message.to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drain_disconnect() {
|
||||
let Some(runtime) = quinn::default_runtime() else {
|
||||
return;
|
||||
};
|
||||
let mut timer = runtime.new_timer(runtime.now() + DISCONNECT_DRAIN_TIMEOUT);
|
||||
poll_fn(|cx| timer.as_mut().poll(cx)).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
@@ -711,6 +738,18 @@ mod tests {
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 1, 1));
|
||||
stats_received_tx.send(()).unwrap();
|
||||
|
||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||
let disconnect_frame = disconnect_recv
|
||||
.read_to_end(MAX_CONTROL_FRAME_LEN)
|
||||
.await
|
||||
.unwrap();
|
||||
let disconnect_message = decode_control_frame(&disconnect_frame).unwrap();
|
||||
let ControlMessage::Disconnect { reason, message } = disconnect_message else {
|
||||
panic!("expected client disconnect event");
|
||||
};
|
||||
assert_eq!(reason, DisconnectReason::Normal);
|
||||
assert_eq!(message, "test complete");
|
||||
|
||||
connection.closed().await;
|
||||
endpoint.close(0_u32.into(), b"test complete");
|
||||
endpoint.wait_idle().await;
|
||||
|
||||
Reference in New Issue
Block a user