remote/lib.rs
1//! Remote copy protocol and networking for distributed file operations
2//!
3//! This crate provides the networking layer and protocol definitions for remote file copying
4//! in the RCP tools suite. It enables efficient distributed copying between remote hosts using
5//! SSH for orchestration and QUIC for high-performance data transfer.
6//!
7//! # Overview
8//!
9//! The remote copy system uses a three-node architecture:
10//!
11//! ```text
12//! Master (rcp)
13//! ├── SSH → Source Host (rcpd)
14//! │ └── QUIC → Master (control)
15//! │ └── QUIC Server (waits for Destination)
16//! └── SSH → Destination Host (rcpd)
17//! └── QUIC → Master (control)
18//! └── QUIC Client → Source (data transfer)
19//! ```
20//!
21//! ## Connection Flow
22//!
23//! 1. **Initialization**: Master starts `rcpd` processes on source and destination via SSH
24//! 2. **Control Connections**: Both `rcpd` processes connect back to Master via QUIC
25//! 3. **Address Exchange**: Source starts QUIC server and sends its address to Master
26//! 4. **Direct Connection**: Master forwards address to Destination, which connects to Source
27//! 5. **Data Transfer**: Files flow directly from Source to Destination (not through Master)
28//!
29//! This design ensures efficient data transfer while allowing the Master to coordinate
30//! operations and monitor progress.
31//!
32//! # Key Components
33//!
34//! ## SSH Session Management
35//!
36//! The [`SshSession`] type represents an SSH connection to a remote host and is used to:
37//! - Launch `rcpd` daemons on remote hosts
38//! - Configure connection parameters (user, host, port)
39//!
40//! ## QUIC Networking
41//!
42//! QUIC protocol provides:
43//! - Multiplexed streams over a single connection
44//! - Built-in encryption and authentication
45//! - Efficient data transfer with congestion control
46//!
47//! Key functions:
48//! - [`get_server_with_port_ranges`] - Create QUIC server endpoint with optional port restrictions
49//! - [`get_client_with_port_ranges_and_pinning`] - Create secure QUIC client with certificate pinning
50//! - [`get_endpoint_addr`] - Get the local address of an endpoint
51//!
52//! ## Port Range Configuration
53//!
54//! The [`port_ranges`] module allows restricting QUIC to specific port ranges, useful for
55//! firewall-restricted environments:
56//!
57//! ```rust,no_run
58//! # use remote::get_server_with_port_ranges;
59//! // bind to ports in the 8000-8999 range with default timeouts
60//! // idle_timeout: 10 seconds, keep_alive: 1 second
61//! let (endpoint, cert_fingerprint) = get_server_with_port_ranges(
62//! Some("8000-8999"),
63//! 10, // idle_timeout_sec
64//! 1, // keep_alive_interval_sec
65//! )?;
66//! # Ok::<(), anyhow::Error>(())
67//! ```
68//!
69//! ## Protocol Messages
70//!
71//! The [`protocol`] module defines the message types exchanged between nodes:
72//! - `MasterHello` - Master → rcpd configuration
73//! - `SourceMasterHello` - Source → Master address information
74//! - `RcpdResult` - rcpd → Master operation results
75//! - `TracingHello` - rcpd → Master tracing initialization
76//!
77//! ## Stream Communication
78//!
79//! The [`streams`] module provides high-level abstractions over QUIC streams:
80//! - Bidirectional streams for request/response communication
81//! - Unidirectional streams for tracing and logging
82//! - Object serialization/deserialization using bincode
83//!
84//! ## Remote Tracing
85//!
86//! The [`tracelog`] module enables distributed logging and progress tracking:
87//! - Forward tracing events from remote `rcpd` processes to Master
88//! - Aggregate progress information across multiple remote operations
89//! - Display unified progress for distributed operations
90//!
91//! # Security Model
92//!
93//! The remote copy system implements a defense-in-depth security model using SSH for authentication
94//! and certificate pinning for QUIC connection integrity. This provides protection against
95//! man-in-the-middle (MITM) attacks while maintaining ease of deployment.
96//!
97//! ## Authentication & Authorization
98//!
99//! **SSH is the security perimeter**: All remote operations begin with SSH authentication.
100//! - Initial access control is handled entirely by SSH
101//! - Users must be authenticated and authorized via SSH before any QUIC connections are established
102//! - SSH configuration (keys, permissions, etc.) determines who can initiate remote copies
103//!
104//! ## Transport Encryption & Integrity
105//!
106//! **QUIC with TLS 1.3**: All data transfer uses QUIC protocol built on TLS 1.3
107//! - Provides encryption for data confidentiality
108//! - Ensures data integrity through cryptographic authentication
109//! - Built-in protection against replay attacks
110//!
111//! ## Trust Bootstrap via Certificate Pinning
112//!
113//! **Two secured QUIC connections** in every remote copy operation:
114//!
115//! ### 1. Master ← rcpd (Control Connection)
116//! ```text
117//! Master (rcp) Remote Host (rcpd)
118//! | |
119//! | 1. SSH connection established |
120//! |<--------------------------------->|
121//! | 2. Master generates self-signed |
122//! | cert, computes SHA-256 |
123//! | fingerprint |
124//! | |
125//! | 3. Launch rcpd via SSH with |
126//! | fingerprint as argument |
127//! |---------------------------------->|
128//! | |
129//! | 4. rcpd validates Master's cert |
130//! | against received fingerprint |
131//! |<---(QUIC + cert pinning)----------|
132//! ```
133//!
134//! - Master generates ephemeral self-signed certificate at startup
135//! - Certificate fingerprint (SHA-256) is passed to rcpd via SSH command-line arguments
136//! - rcpd validates Master's certificate by computing its fingerprint and comparing
137//! - Connection fails if fingerprints don't match (MITM protection)
138//!
139//! ### 2. Source → Destination (Data Transfer Connection)
140//! ```text
141//! Source (rcpd) Destination (rcpd)
142//! | |
143//! | 1. Source generates self-signed |
144//! | cert, computes SHA-256 |
145//! | fingerprint |
146//! | |
147//! | 2. Send fingerprint + address |
148//! | to Master via secure channel |
149//! |---------------------------------->|
150//! | Master |
151//! | | |
152//! | 3. Master forwards | |
153//! | to Destination | |
154//! | |----------->|
155//! | |
156//! | 4. Destination validates Source's |
157//! | cert against received |
158//! | fingerprint |
159//! |<---(QUIC + cert pinning)----------|
160//! ```
161//!
162//! - Source generates ephemeral self-signed certificate
163//! - Fingerprint is sent to Master over already-secured Master←Source connection
164//! - Master forwards fingerprint to Destination over already-secured Master←Destination connection
165//! - Destination validates Source's certificate against fingerprint
166//! - Direct Source→Destination connection established only after successful validation
167//!
168//! ## SSH as Secure Out-of-Band Channel
169//!
170//! **Key insight**: SSH provides a secure, authenticated channel for bootstrapping QUIC trust
171//!
172//! - Certificate fingerprints are transmitted through SSH (Master→rcpd command-line arguments)
173//! - SSH connection is already authenticated and encrypted
174//! - This creates a "chain of trust":
175//! 1. User trusts SSH (proven by successful authentication)
176//! 2. SSH carries the certificate fingerprint securely
177//! 3. QUIC connection validates against that fingerprint
178//! 4. Therefore, QUIC connection is trustworthy
179//!
180//! ## Attack Resistance
181//!
182//! ### ✅ Protected Against
183//!
184//! - **Man-in-the-Middle (MITM)**: Certificate pinning prevents attackers from impersonating endpoints
185//! - **Replay Attacks**: TLS 1.3 in QUIC provides built-in replay protection
186//! - **Eavesdropping**: All data encrypted with TLS 1.3
187//! - **Tampering**: Cryptographic integrity checks prevent data modification
188//! - **Unauthorized Access**: SSH authentication is required before any operations
189//!
190//! ### ⚠️ Threat Model Assumptions
191//!
192//! - **SSH is secure**: The security model depends on SSH being properly configured and uncompromised
193//! - **Certificate fingerprints are short-lived**: Ephemeral certificates are generated per-session
194//! - **Trusted network for Master**: The machine running Master (rcp) should be trusted
195//!
196//! ## Best Practices
197//!
198//! 1. **Secure SSH Configuration**: Use key-based authentication, disable password auth
199//! 2. **Keep Systems Updated**: Ensure SSH, TLS libraries, and QUIC implementations are current
200//! 3. **Network Segmentation**: Run remote copies on trusted network segments when possible
201//! 4. **Monitor Logs**: Certificate validation failures indicate potential security issues
202//!
203//! # Network Troubleshooting
204//!
205//! Common failure scenarios and their handling:
206//!
207//! ## SSH Connection Fails
208//! - **Cause**: Host unreachable, authentication failure
209//! - **Timeout**: ~30s (SSH default)
210//! - **Error**: Standard SSH error messages
211//!
212//! ## rcpd Cannot Connect to Master
213//! - **Cause**: Firewall blocks QUIC, network routing issue
214//! - **Timeout**: Configurable via `--remote-copy-conn-timeout-sec` (default: 15s)
215//! - **Solution**: Check firewall rules for QUIC ports
216//!
217//! ## Destination Cannot Connect to Source
218//! - **Cause**: Firewall blocks direct connection between hosts
219//! - **Timeout**: Configurable (default: 15s)
220//! - **Solution**: Use `--quic-port-ranges` to specify allowed ports, configure firewall
221//!
222//! For detailed troubleshooting, see the repository's `docs/network_connectivity.md`.
223//!
224//! # Examples
225//!
226//! ## Starting a Remote Copy Daemon
227//!
228//! ```rust,no_run
229//! use remote::{SshSession, protocol::RcpdConfig, start_rcpd};
230//! use std::net::SocketAddr;
231//!
232//! # async fn example() -> anyhow::Result<()> {
233//! let session = SshSession {
234//! user: Some("user".to_string()),
235//! host: "example.com".to_string(),
236//! port: None,
237//! };
238//!
239//! let config = RcpdConfig {
240//! verbose: 0,
241//! fail_early: false,
242//! max_workers: 4,
243//! max_blocking_threads: 512,
244//! max_open_files: None,
245//! ops_throttle: 0,
246//! iops_throttle: 0,
247//! chunk_size: 1024 * 1024,
248//! dereference: false,
249//! overwrite: false,
250//! overwrite_compare: String::new(),
251//! debug_log_prefix: None,
252//! quic_port_ranges: None,
253//! quic_idle_timeout_sec: 10,
254//! quic_keep_alive_interval_sec: 1,
255//! progress: false,
256//! progress_delay: None,
257//! remote_copy_conn_timeout_sec: 15,
258//! master_cert_fingerprint: Vec::new(),
259//! };
260//! let master_addr: SocketAddr = "192.168.1.100:5000".parse()?;
261//! let server_name = "master-server";
262//!
263//! let process = start_rcpd(&config, &session, &master_addr, server_name).await?;
264//! # Ok(())
265//! # }
266//! ```
267//!
268//! ## Creating a QUIC Server with Port Ranges
269//!
270//! ```rust,no_run
271//! use remote::{get_server_with_port_ranges, get_endpoint_addr};
272//!
273//! # fn example() -> anyhow::Result<()> {
274//! // create server restricted to ports 8000-8999
275//! // timeouts: 10s idle, 1s keep-alive (CLI defaults)
276//! let (endpoint, _cert_fingerprint) = get_server_with_port_ranges(
277//! Some("8000-8999"),
278//! 10, // idle_timeout_sec
279//! 1, // keep_alive_interval_sec
280//! )?;
281//! let addr = get_endpoint_addr(&endpoint)?;
282//! println!("Server listening on: {}", addr);
283//! # Ok(())
284//! # }
285//! ```
286//!
287//! # Module Organization
288//!
289//! - [`port_ranges`] - Port range parsing and UDP socket binding
290//! - [`protocol`] - Protocol message definitions and serialization
291//! - [`streams`] - QUIC stream wrappers with typed message passing
292//! - [`tracelog`] - Remote tracing and progress aggregation
293
294use anyhow::{anyhow, Context};
295use rand::Rng;
296use tracing::instrument;
297
298pub mod port_ranges;
299pub mod protocol;
300pub mod streams;
301pub mod tracelog;
302
303/// Configuration for QUIC connections
304#[derive(Debug, Clone)]
305pub struct QuicConfig {
306 /// Port ranges to use for QUIC connections (e.g., "8000-8999,9000-9999")
307 pub port_ranges: Option<String>,
308 /// Maximum idle time before closing connection (seconds)
309 pub idle_timeout_sec: u64,
310 /// Interval for keep-alive packets (seconds)
311 pub keep_alive_interval_sec: u64,
312 /// Connection timeout for remote operations (seconds)
313 pub conn_timeout_sec: u64,
314}
315
316impl Default for QuicConfig {
317 fn default() -> Self {
318 Self {
319 port_ranges: None,
320 idle_timeout_sec: 10,
321 keep_alive_interval_sec: 1,
322 conn_timeout_sec: 15,
323 }
324 }
325}
326
327impl QuicConfig {
328 /// Create QuicConfig with custom timeout values
329 pub fn with_timeouts(
330 idle_timeout_sec: u64,
331 keep_alive_interval_sec: u64,
332 conn_timeout_sec: u64,
333 ) -> Self {
334 Self {
335 port_ranges: None,
336 idle_timeout_sec,
337 keep_alive_interval_sec,
338 conn_timeout_sec,
339 }
340 }
341
342 /// Set port ranges
343 pub fn with_port_ranges(mut self, ranges: impl Into<String>) -> Self {
344 self.port_ranges = Some(ranges.into());
345 self
346 }
347}
348
349#[derive(Debug, PartialEq)]
350pub struct SshSession {
351 pub user: Option<String>,
352 pub host: String,
353 pub port: Option<u16>,
354}
355
356impl SshSession {
357 pub fn local() -> Self {
358 Self {
359 user: None,
360 host: "localhost".to_string(),
361 port: None,
362 }
363 }
364}
365
366async fn setup_ssh_session(
367 session: &SshSession,
368) -> anyhow::Result<std::sync::Arc<openssh::Session>> {
369 let host = session.host.as_str();
370 let destination = match (session.user.as_deref(), session.port) {
371 (Some(user), Some(port)) => format!("ssh://{user}@{host}:{port}"),
372 (None, Some(port)) => format!("ssh://{}:{}", session.host, port),
373 (Some(user), None) => format!("ssh://{user}@{host}"),
374 (None, None) => format!("ssh://{host}"),
375 };
376 tracing::debug!("Connecting to SSH destination: {}", destination);
377 let session = std::sync::Arc::new(
378 openssh::Session::connect(destination, openssh::KnownHosts::Accept)
379 .await
380 .context("Failed to establish SSH connection")?,
381 );
382 Ok(session)
383}
384
385#[instrument]
386pub async fn wait_for_rcpd_process(
387 process: openssh::Child<std::sync::Arc<openssh::Session>>,
388) -> anyhow::Result<()> {
389 tracing::info!("Waiting on rcpd server on: {:?}", process);
390 // wait for process to exit with a timeout and capture output
391 let output = tokio::time::timeout(
392 std::time::Duration::from_secs(10),
393 process.wait_with_output(),
394 )
395 .await
396 .context("Timeout waiting for rcpd process to exit")?
397 .context("Failed to wait for rcpd process")?;
398 if !output.status.success() {
399 let stdout = String::from_utf8_lossy(&output.stdout);
400 let stderr = String::from_utf8_lossy(&output.stderr);
401 tracing::error!(
402 "rcpd command failed on remote host, status code: {:?}\nstdout:\n{}\nstderr:\n{}",
403 output.status.code(),
404 stdout,
405 stderr
406 );
407 return Err(anyhow!(
408 "rcpd command failed on remote host, status code: {:?}",
409 output.status.code(),
410 ));
411 }
412 // log stderr even on success if there's any output (might contain warnings)
413 if !output.stderr.is_empty() {
414 let stderr = String::from_utf8_lossy(&output.stderr);
415 tracing::debug!("rcpd stderr output:\n{}", stderr);
416 }
417 Ok(())
418}
419
420#[instrument]
421pub async fn start_rcpd(
422 rcpd_config: &protocol::RcpdConfig,
423 session: &SshSession,
424 master_addr: &std::net::SocketAddr,
425 master_server_name: &str,
426) -> anyhow::Result<openssh::Child<std::sync::Arc<openssh::Session>>> {
427 tracing::info!("Starting rcpd server on: {:?}", session);
428 let session = setup_ssh_session(session).await?;
429 // Run rcpd command remotely
430 let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
431 let bin_dir = current_exe
432 .parent()
433 .context("Failed to get parent directory of current executable")?;
434 tracing::debug!("Running rcpd from: {:?}", bin_dir);
435 let rcpd_args = rcpd_config.to_args();
436 tracing::debug!("rcpd arguments: {:?}", rcpd_args);
437 let mut cmd = session.arc_command(format!("{}/rcpd", bin_dir.display()));
438 cmd.arg("--master-addr")
439 .arg(master_addr.to_string())
440 .arg("--server-name")
441 .arg(master_server_name)
442 .args(rcpd_args);
443 // capture stdout and stderr so we can read them later
444 cmd.stdout(openssh::Stdio::piped());
445 cmd.stderr(openssh::Stdio::piped());
446 tracing::info!("Will run remotely: {cmd:?}");
447 cmd.spawn().await.context("Failed to spawn rcpd command")
448}
449
450/// Compute SHA-256 fingerprint of a DER-encoded certificate
451fn compute_cert_fingerprint(cert_der: &[u8]) -> ring::digest::Digest {
452 ring::digest::digest(&ring::digest::SHA256, cert_der)
453}
454
455/// Configure QUIC server with a self-signed certificate
456/// Returns the server config and the SHA-256 fingerprint of the certificate
457fn configure_server(
458 idle_timeout_sec: u64,
459 keep_alive_interval_sec: u64,
460) -> anyhow::Result<(quinn::ServerConfig, Vec<u8>)> {
461 tracing::info!(
462 "Configuring QUIC server (idle_timeout={}s, keep_alive={}s)",
463 idle_timeout_sec,
464 keep_alive_interval_sec
465 );
466 let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])?;
467 let key_der = cert.serialize_private_key_der();
468 let cert_der = cert.serialize_der()?;
469 let fingerprint = compute_cert_fingerprint(&cert_der);
470 let fingerprint_vec = fingerprint.as_ref().to_vec();
471 tracing::debug!(
472 "Generated certificate with fingerprint: {}",
473 hex::encode(&fingerprint_vec)
474 );
475 let key = rustls::PrivateKey(key_der);
476 let cert = rustls::Certificate(cert_der);
477 let mut server_config = quinn::ServerConfig::with_single_cert(vec![cert], key)
478 .context("Failed to create server config")?;
479 // configure transport timeouts for connection liveness detection
480 let mut transport_config = quinn::TransportConfig::default();
481 transport_config.max_idle_timeout(Some(
482 std::time::Duration::from_secs(idle_timeout_sec)
483 .try_into()
484 .context("Failed to convert idle timeout to VarInt")?,
485 ));
486 transport_config.keep_alive_interval(Some(std::time::Duration::from_secs(
487 keep_alive_interval_sec,
488 )));
489 server_config.transport_config(std::sync::Arc::new(transport_config));
490 Ok((server_config, fingerprint_vec))
491}
492
493#[instrument]
494pub fn get_server_with_port_ranges(
495 port_ranges: Option<&str>,
496 idle_timeout_sec: u64,
497 keep_alive_interval_sec: u64,
498) -> anyhow::Result<(quinn::Endpoint, Vec<u8>)> {
499 let (server_config, cert_fingerprint) =
500 configure_server(idle_timeout_sec, keep_alive_interval_sec)?;
501 let socket = if let Some(ranges_str) = port_ranges {
502 let ranges = port_ranges::PortRanges::parse(ranges_str)?;
503 ranges.bind_udp_socket(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED))?
504 } else {
505 // default behavior: bind to any available port
506 std::net::UdpSocket::bind("0.0.0.0:0")?
507 };
508 let endpoint = quinn::Endpoint::new(
509 quinn::EndpointConfig::default(),
510 Some(server_config),
511 socket,
512 std::sync::Arc::new(quinn::TokioRuntime),
513 )
514 .context("Failed to create QUIC endpoint")?;
515 Ok((endpoint, cert_fingerprint))
516}
517
518// certificate verifier that validates against a pinned certificate fingerprint
519// This prevents MITM attacks by ensuring we're connecting to the expected server
520struct PinnedCertVerifier {
521 expected_fingerprint: Vec<u8>,
522}
523
524impl PinnedCertVerifier {
525 fn new(expected_fingerprint: Vec<u8>) -> Self {
526 Self {
527 expected_fingerprint,
528 }
529 }
530}
531
532impl rustls::client::ServerCertVerifier for PinnedCertVerifier {
533 fn verify_server_cert(
534 &self,
535 end_entity: &rustls::Certificate,
536 _intermediates: &[rustls::Certificate],
537 _server_name: &rustls::ServerName,
538 _scts: &mut dyn Iterator<Item = &[u8]>,
539 _ocsp_response: &[u8],
540 _now: std::time::SystemTime,
541 ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
542 let received_fingerprint = compute_cert_fingerprint(&end_entity.0);
543 if received_fingerprint.as_ref() == self.expected_fingerprint.as_slice() {
544 tracing::debug!(
545 "Certificate fingerprint validated successfully: {}",
546 hex::encode(&self.expected_fingerprint)
547 );
548 Ok(rustls::client::ServerCertVerified::assertion())
549 } else {
550 tracing::error!(
551 "Certificate fingerprint mismatch! Expected: {}, Got: {}",
552 hex::encode(&self.expected_fingerprint),
553 hex::encode(received_fingerprint)
554 );
555 Err(rustls::Error::InvalidCertificate(
556 rustls::CertificateError::Other(std::sync::Arc::new(std::io::Error::new(
557 std::io::ErrorKind::InvalidData,
558 format!(
559 "Certificate fingerprint mismatch (expected {}, got {})",
560 hex::encode(&self.expected_fingerprint),
561 hex::encode(received_fingerprint)
562 ),
563 ))),
564 ))
565 }
566 }
567}
568
569fn get_local_ip() -> anyhow::Result<std::net::IpAddr> {
570 let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
571 socket.connect("8.8.8.8:80")?;
572 Ok(socket.local_addr()?.ip())
573}
574
575#[instrument]
576pub fn get_endpoint_addr(endpoint: &quinn::Endpoint) -> anyhow::Result<std::net::SocketAddr> {
577 // endpoint is bound to 0.0.0.0 so we need to get the local IP address
578 let local_ip = get_local_ip().context("Failed to get local IP address")?;
579 let endpoint_addr = endpoint.local_addr()?;
580 Ok(std::net::SocketAddr::new(local_ip, endpoint_addr.port()))
581}
582
583#[instrument]
584pub fn get_random_server_name() -> String {
585 rand::thread_rng()
586 .sample_iter(&rand::distributions::Alphanumeric)
587 .take(20)
588 .map(char::from)
589 .collect()
590}
591
592#[instrument]
593pub fn get_client_with_port_ranges_and_pinning(
594 port_ranges: Option<&str>,
595 cert_fingerprint: Vec<u8>,
596 idle_timeout_sec: u64,
597 keep_alive_interval_sec: u64,
598) -> anyhow::Result<quinn::Endpoint> {
599 tracing::info!(
600 "Creating QUIC client with certificate pinning (fingerprint: {}, idle_timeout={}s, keep_alive={}s)",
601 hex::encode(&cert_fingerprint),
602 idle_timeout_sec,
603 keep_alive_interval_sec
604 );
605 // create a crypto backend with certificate pinning
606 let crypto = rustls::ClientConfig::builder()
607 .with_safe_defaults()
608 .with_custom_certificate_verifier(std::sync::Arc::new(PinnedCertVerifier::new(
609 cert_fingerprint,
610 )))
611 .with_no_client_auth();
612 create_client_endpoint(
613 port_ranges,
614 crypto,
615 idle_timeout_sec,
616 keep_alive_interval_sec,
617 )
618}
619
620// helper function to create client endpoint with given crypto config
621fn create_client_endpoint(
622 port_ranges: Option<&str>,
623 crypto: rustls::ClientConfig,
624 idle_timeout_sec: u64,
625 keep_alive_interval_sec: u64,
626) -> anyhow::Result<quinn::Endpoint> {
627 // create QUIC client config with timeouts
628 let mut client_config = quinn::ClientConfig::new(std::sync::Arc::new(crypto));
629 let mut transport_config = quinn::TransportConfig::default();
630 transport_config.max_idle_timeout(Some(
631 std::time::Duration::from_secs(idle_timeout_sec)
632 .try_into()
633 .context("Failed to convert idle timeout to VarInt")?,
634 ));
635 transport_config.keep_alive_interval(Some(std::time::Duration::from_secs(
636 keep_alive_interval_sec,
637 )));
638 client_config.transport_config(std::sync::Arc::new(transport_config));
639 let socket = if let Some(ranges_str) = port_ranges {
640 let ranges = port_ranges::PortRanges::parse(ranges_str)?;
641 ranges.bind_udp_socket(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED))?
642 } else {
643 // default behavior: bind to any available port
644 std::net::UdpSocket::bind("0.0.0.0:0")?
645 };
646 // create and configure endpoint
647 let mut endpoint = quinn::Endpoint::new(
648 quinn::EndpointConfig::default(),
649 None, // No server config for client
650 socket,
651 std::sync::Arc::new(quinn::TokioRuntime),
652 )
653 .context("Failed to create QUIC endpoint")?;
654 endpoint.set_default_client_config(client_config);
655 Ok(endpoint)
656}
657
658#[cfg(test)]
659pub mod test_defaults {
660 //! Test-only constants for QUIC timeout defaults
661 //! These should not be used in production code - all production code should
662 //! receive timeout values from CLI arguments
663
664 /// Default QUIC idle timeout in seconds for tests
665 pub const DEFAULT_QUIC_IDLE_TIMEOUT_SEC: u64 = 10;
666
667 /// Default QUIC keep-alive interval in seconds for tests
668 pub const DEFAULT_QUIC_KEEP_ALIVE_INTERVAL_SEC: u64 = 1;
669}