diff --git a/include/tquic.h b/include/tquic.h index d09c2231..68029152 100644 --- a/include/tquic.h +++ b/include/tquic.h @@ -966,6 +966,21 @@ void quic_conn_session(struct quic_conn_t *conn, const uint8_t **out, size_t *ou */ int quic_conn_early_data_reason(struct quic_conn_t *conn, const uint8_t **out, size_t *out_len); +/** + * Send a Ping frame on the active path(s) for keep-alive. + */ +int quic_conn_ping(struct quic_conn_t *conn); + +/** + * Send a Ping frame on the specified path for keep-alive. + * The API is only applicable to multipath quic connections. + */ +int quic_conn_ping_path(struct quic_conn_t *conn, + const struct sockaddr *local, + socklen_t local_len, + const struct sockaddr *remote, + socklen_t remote_len); + /** * Add a new path on the client connection. */ diff --git a/src/connection/connection.rs b/src/connection/connection.rs index 5679aafd..d3c2000d 100644 --- a/src/connection/connection.rs +++ b/src/connection/connection.rs @@ -1843,11 +1843,15 @@ impl Connection { self.try_write_new_token_frame(out, st, pkt_type, path_id)?; // Write a PING frame - if st.ack_elicit_required && !st.ack_eliciting && !self.is_closing() { + if ((st.ack_elicit_required && !st.ack_eliciting) + || self.paths.get_mut(path_id)?.need_send_ping) + && !self.is_closing() + { let frame = Frame::Ping { pmtu_probe: None }; Connection::write_frame_to_packet(frame, out, st)?; st.ack_eliciting = true; st.in_flight = true; + self.paths.get_mut(path_id)?.need_send_ping = false; } // No frames to be sent @@ -2832,6 +2836,9 @@ impl Connection { if space.need_send_buffered_frames() && path.recovery.can_send() { return Ok(pid); } + if path.need_send_ping { + return Ok(pid); + } continue; } None => continue, @@ -2924,6 +2931,7 @@ impl Connection { || self.local_error.as_ref().map_or(false, |e| e.is_app) || path.need_send_validation_frames(self.is_server) || path.dplpmtud.should_probe() + || path.need_send_ping || self.cids.need_send_cid_control_frames() || self.streams.need_send_stream_frames() || self.spaces.need_send_buffered_frames()) @@ -3499,6 +3507,14 @@ impl Connection { }) } + /// Send a Ping frame for keep-alive. + /// + /// If `path_addr` is `None`, a Ping frame will be sent on each active path. + /// Otherwise, a Ping frame will be on the specified path. + pub fn ping(&mut self, path_addr: Option) -> Result<()> { + self.paths.mark_ping(path_addr) + } + /// Client add a new path on the connection. pub fn add_path(&mut self, local_addr: SocketAddr, remote_addr: SocketAddr) -> Result { if self.is_server { @@ -5940,6 +5956,46 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn ping() -> Result<()> { + let mut client_config = TestPair::new_test_config(false)?; + client_config.enable_dplpmtud(false); + client_config.local_transport_params = TransportParams { + max_idle_timeout: 15000, + ..TransportParams::default() + }; + let mut server_config = TestPair::new_test_config(true)?; + server_config.enable_dplpmtud(false); + server_config.local_transport_params = TransportParams { + max_idle_timeout: 15000, + ..TransportParams::default() + }; + let mut test_pair = TestPair::new(&mut client_config, &mut server_config)?; + test_pair.handshake()?; + + // Move both connections to idle state + test_pair.move_forward()?; + + // Enable qlog for Server + let slog = NamedTempFile::new().unwrap(); + let mut sfile = slog.reopen().unwrap(); + test_pair + .server + .set_qlog(Box::new(slog), "title".into(), "desc".into()); + + // Client send a Ping frame + test_pair.client.ping(None)?; + let packets = TestPair::conn_packets_out(&mut test_pair.client)?; + TestPair::conn_packets_in(&mut test_pair.server, packets.clone())?; + + let mut slog_content = String::new(); + sfile.read_to_string(&mut slog_content).unwrap(); + assert_eq!(slog_content.contains("quic:packet_received"), true); + assert_eq!(slog_content.contains("frame_type\":\"ping"), true); + + Ok(()) + } + #[test] fn conn_basic_operations() -> Result<()> { let mut test_pair = TestPair::new_with_zero_cid()?; diff --git a/src/connection/path.rs b/src/connection/path.rs index 87c8a9aa..c5238f63 100644 --- a/src/connection/path.rs +++ b/src/connection/path.rs @@ -28,6 +28,7 @@ use super::timer; use crate::connection::SpaceId; use crate::error::Error; use crate::multipath_scheduler::MultipathScheduler; +use crate::FourTuple; use crate::PathStats; use crate::RecoveryConfig; use crate::Result; @@ -92,6 +93,9 @@ pub struct Path { /// The current pmtu probing state of the path. pub(super) dplpmtud: Dplpmtud, + /// Whether a Ping frame should be sent on the path. + pub(super) need_send_ping: bool, + /// Trace id. trace_id: String, @@ -140,6 +144,7 @@ impl Path { peer_verified_local_address: false, anti_ampl_limit: 0, dplpmtud, + need_send_ping: false, trace_id: trace_id.to_string(), space_id: SpaceId::Data, is_abandon: false, @@ -607,6 +612,33 @@ impl PathMap { left } + /// Schedule a Ping frame on the specified path or all active paths. + pub fn mark_ping(&mut self, path_addr: Option) -> Result<()> { + // If multipath is not enabled, schedule a Ping frame on the current + // active path. + if !self.is_multipath { + self.get_active_mut()?.need_send_ping = true; + return Ok(()); + } + + // If multipath is enabled, schedule a Ping frame on the specified path + // or all the active paths. + if let Some(a) = path_addr { + let pid = match self.get_path_id(&(a.local, a.remote)) { + Some(pid) => pid, + None => return Ok(()), + }; + self.get_mut(pid)?.need_send_ping = true; + return Ok(()); + } + for (_, path) in self.paths.iter_mut() { + if path.active() { + path.need_send_ping = true; + } + } + Ok(()) + } + /// Promote to multipath mode. pub fn enable_multipath(&mut self) { self.is_multipath = true; diff --git a/src/ffi.rs b/src/ffi.rs index 987e01d0..d9fda126 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1048,6 +1048,35 @@ pub extern "C" fn quic_conn_early_data_reason( } } +/// Send a Ping frame on the active path(s) for keep-alive. +#[no_mangle] +pub extern "C" fn quic_conn_ping(conn: &mut Connection) -> c_int { + match conn.ping(None) { + Ok(_) => return 0, + Err(e) => return e.to_errno() as c_int, + } +} + +/// Send a Ping frame on the specified path for keep-alive. +/// The API is only applicable to multipath quic connections. +#[no_mangle] +pub extern "C" fn quic_conn_ping_path( + conn: &mut Connection, + local: &sockaddr, + local_len: socklen_t, + remote: &sockaddr, + remote_len: socklen_t, +) -> c_int { + let addr = FourTuple { + local: sock_addr_from_c(local, local_len), + remote: sock_addr_from_c(remote, remote_len), + }; + match conn.ping(Some(addr)) { + Ok(_) => return 0, + Err(e) => return e.to_errno() as c_int, + } +} + /// Add a new path on the client connection. #[no_mangle] pub extern "C" fn quic_conn_add_path(