1use crate::{
2 binary::FreenetBinary,
3 docker::{DockerNatBackend, DockerNatConfig},
4 network::TestNetwork,
5 peer::{get_free_port, TestPeer},
6 process::{self, PeerProcess},
7 remote::{PeerLocation, RemoteMachine},
8 Error, Result,
9};
10use chrono::Utc;
11use std::collections::HashMap;
12use std::fs;
13use std::net::Ipv4Addr;
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use std::time::{Duration, SystemTime};
17
18#[derive(Debug, Clone)]
20pub enum Backend {
21 Local,
23 DockerNat(DockerNatConfig),
25}
26
27impl Default for Backend {
28 fn default() -> Self {
29 if std::env::var("FREENET_TEST_DOCKER_NAT").is_ok() {
31 let mut config = DockerNatConfig::default();
32
33 if let Ok(emulation) = std::env::var("FREENET_TEST_NETWORK_EMULATION") {
35 config.network_emulation = match emulation.to_lowercase().as_str() {
36 "lan" => Some(crate::docker::NetworkEmulation::lan()),
37 "regional" => Some(crate::docker::NetworkEmulation::regional()),
38 "intercontinental" => Some(crate::docker::NetworkEmulation::intercontinental()),
39 "high_latency" => Some(crate::docker::NetworkEmulation::high_latency()),
40 "challenging" => Some(crate::docker::NetworkEmulation::challenging()),
41 other => {
42 tracing::warn!(
43 "Unknown FREENET_TEST_NETWORK_EMULATION value '{}', ignoring. \
44 Valid options: lan, regional, intercontinental, high_latency, challenging",
45 other
46 );
47 None
48 }
49 };
50 }
51
52 Backend::DockerNat(config)
53 } else {
54 Backend::Local
55 }
56 }
57}
58
59struct GatewayInfo {
60 address: String,
61 public_key_path: PathBuf,
62}
63
64pub struct NetworkBuilder {
66 gateways: usize,
67 peers: usize,
68 binary: FreenetBinary,
69 min_connectivity: f64,
70 connectivity_timeout: Duration,
71 preserve_data_on_failure: bool,
72 preserve_data_on_success: bool,
73 peer_locations: HashMap<usize, PeerLocation>,
74 default_location: PeerLocation,
75 min_connections: Option<usize>,
76 max_connections: Option<usize>,
77 start_stagger: Duration,
78 backend: Backend,
79}
80
81impl Default for NetworkBuilder {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl NetworkBuilder {
88 pub fn new() -> Self {
89 Self {
90 gateways: 1,
91 peers: 3,
92 binary: FreenetBinary::default(),
93 min_connectivity: 1.0, connectivity_timeout: Duration::from_secs(30),
95 preserve_data_on_failure: false,
96 preserve_data_on_success: false,
97 peer_locations: HashMap::new(),
98 default_location: PeerLocation::Local,
99 min_connections: None,
100 max_connections: None,
101 start_stagger: Duration::from_millis(500),
102 backend: Backend::default(),
103 }
104 }
105
106 pub fn gateways(mut self, n: usize) -> Self {
108 self.gateways = n;
109 self
110 }
111
112 pub fn peers(mut self, n: usize) -> Self {
114 self.peers = n;
115 self
116 }
117
118 pub fn binary(mut self, binary: FreenetBinary) -> Self {
120 self.binary = binary;
121 self
122 }
123
124 pub fn require_connectivity(mut self, ratio: f64) -> Self {
126 self.min_connectivity = ratio;
127 self
128 }
129
130 pub fn connectivity_timeout(mut self, timeout: Duration) -> Self {
132 self.connectivity_timeout = timeout;
133 self
134 }
135
136 pub fn min_connections(mut self, min: usize) -> Self {
138 self.min_connections = Some(min);
139 self
140 }
141
142 pub fn max_connections(mut self, max: usize) -> Self {
144 self.max_connections = Some(max);
145 self
146 }
147
148 pub fn start_stagger(mut self, delay: Duration) -> Self {
150 self.start_stagger = delay;
151 self
152 }
153
154 pub fn preserve_temp_dirs_on_failure(mut self, preserve: bool) -> Self {
156 self.preserve_data_on_failure = preserve;
157 self
158 }
159
160 pub fn preserve_temp_dirs_on_success(mut self, preserve: bool) -> Self {
162 self.preserve_data_on_success = preserve;
163 self
164 }
165
166 pub fn peer_location(mut self, index: usize, location: PeerLocation) -> Self {
169 self.peer_locations.insert(index, location);
170 self
171 }
172
173 pub fn default_location(mut self, location: PeerLocation) -> Self {
175 self.default_location = location;
176 self
177 }
178
179 pub fn distribute_across_remotes(mut self, machines: Vec<RemoteMachine>) -> Self {
182 let total_peers = self.gateways + self.peers;
183 for (idx, machine) in (0..total_peers).zip(machines.iter().cycle()) {
184 self.peer_locations
185 .insert(idx, PeerLocation::Remote(machine.clone()));
186 }
187 self
188 }
189
190 pub fn backend(mut self, backend: Backend) -> Self {
192 self.backend = backend;
193 self
194 }
195
196 pub async fn build(self) -> Result<TestNetwork> {
198 match self.backend.clone() {
199 Backend::Local => self.build_local().await,
200 Backend::DockerNat(config) => self.build_docker_nat(config).await,
201 }
202 }
203
204 async fn build_local(self) -> Result<TestNetwork> {
206 let binary_path = self.binary.resolve()?;
207
208 tracing::info!(
209 "Starting test network: {} gateways, {} peers",
210 self.gateways,
211 self.peers
212 );
213
214 let base_dir = resolve_base_dir();
215 fs::create_dir_all(&base_dir)?;
216 cleanup_old_runs(&base_dir, 5)?;
217 let run_root = create_run_directory(&base_dir)?;
218
219 let mut run_status = RunStatusGuard::new(&run_root);
220
221 let mut gateways = Vec::new();
223 for i in 0..self.gateways {
224 let peer = match self.start_peer(&binary_path, i, true, &run_root).await {
225 Ok(peer) => peer,
226 Err(err) => {
227 let detail = format!("failed to start gateway {i}: {err}");
228 run_status.mark("failure", Some(&detail));
229 return Err(err);
230 }
231 };
232 gateways.push(peer);
233 }
234
235 let gateway_info: Vec<_> = gateways
237 .iter()
238 .map(|gw| GatewayInfo {
239 address: format!("{}:{}", gw.network_address, gw.network_port),
240 public_key_path: gw
241 .public_key_path
242 .clone()
243 .expect("Gateway must have public key"),
244 })
245 .collect();
246
247 let mut peers = Vec::new();
249 for i in 0..self.peers {
250 let peer = match self
251 .start_peer_with_gateways(
252 &binary_path,
253 i + self.gateways,
254 false,
255 &gateway_info,
256 &run_root,
257 )
258 .await
259 {
260 Ok(peer) => peer,
261 Err(err) => {
262 let detail = format!("failed to start peer {}: {}", i + self.gateways, err);
263 run_status.mark("failure", Some(&detail));
264 return Err(err);
265 }
266 };
267 peers.push(peer);
268 if i + 1 < self.peers && !self.start_stagger.is_zero() {
269 tokio::time::sleep(self.start_stagger).await;
270 }
271 }
272
273 let network = TestNetwork::new(gateways, peers, self.min_connectivity, run_root.clone());
274
275 match network
277 .wait_until_ready_with_timeout(self.connectivity_timeout)
278 .await
279 {
280 Ok(()) => {
281 if self.preserve_data_on_success {
282 match preserve_network_state(&network) {
283 Ok(path) => {
284 println!("Network data directories preserved at {}", path.display());
285 }
286 Err(err) => {
287 eprintln!(
288 "Failed to preserve network data directories after success: {}",
289 err
290 );
291 }
292 }
293 }
294 let detail = format!("success: gateways={}, peers={}", self.gateways, self.peers);
295 run_status.mark("success", Some(&detail));
296 Ok(network)
297 }
298 Err(err) => {
299 if let Err(log_err) = dump_recent_logs(&network) {
300 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
301 }
302 if self.preserve_data_on_failure {
303 match preserve_network_state(&network) {
304 Ok(path) => {
305 eprintln!("Network data directories preserved at {}", path.display());
306 }
307 Err(copy_err) => {
308 eprintln!("Failed to preserve network data directories: {}", copy_err);
309 }
310 }
311 }
312 let detail = err.to_string();
313 run_status.mark("failure", Some(&detail));
314 Err(err)
315 }
316 }
317 }
318
319 pub fn build_sync(self) -> Result<TestNetwork> {
321 tokio::runtime::Runtime::new()?.block_on(self.build())
322 }
323
324 async fn start_peer(
325 &self,
326 binary_path: &PathBuf,
327 index: usize,
328 is_gateway: bool,
329 run_root: &Path,
330 ) -> Result<TestPeer> {
331 self.start_peer_with_gateways(binary_path, index, is_gateway, &[], run_root)
332 .await
333 }
334
335 async fn start_peer_with_gateways(
336 &self,
337 binary_path: &PathBuf,
338 index: usize,
339 is_gateway: bool,
340 gateway_info: &[GatewayInfo],
341 run_root: &Path,
342 ) -> Result<TestPeer> {
343 let location = self
345 .peer_locations
346 .get(&index)
347 .cloned()
348 .unwrap_or_else(|| self.default_location.clone());
349
350 let id = if is_gateway {
351 format!("gw{}", index)
352 } else {
353 format!("peer{}", index)
354 };
355
356 let network_address = match &location {
358 PeerLocation::Local => {
359 let addr_index = index as u32;
360 let second_octet = ((addr_index / 256) % 254 + 1) as u8;
361 let third_octet = (addr_index % 256) as u8;
362 Ipv4Addr::new(127, second_octet, third_octet, 1).to_string()
363 }
364 PeerLocation::Remote(remote) => {
365 remote.discover_public_address()?
367 }
368 };
369
370 let (ws_port, network_port) = match &location {
373 PeerLocation::Local => (get_free_port()?, get_free_port()?),
374 PeerLocation::Remote(_) => (0, 0), };
376
377 let data_dir = create_peer_dir(run_root, &id)?;
378
379 tracing::debug!(
380 "Starting {} {} - ws:{} net:{}",
381 if is_gateway { "gateway" } else { "peer" },
382 id,
383 ws_port,
384 network_port
385 );
386
387 let keypair_path = data_dir.join("keypair.pem");
389 let public_key_path = data_dir.join("public_key.pem");
390 generate_keypair(&keypair_path, &public_key_path)?;
391
392 if let PeerLocation::Remote(remote) = &location {
395 let remote_data_dir = remote.remote_work_dir().join(&id);
396
397 let mkdir_cmd = format!("mkdir -p {}", remote_data_dir.display());
399 remote.exec(&mkdir_cmd)?;
400
401 let remote_keypair = remote_data_dir.join("keypair.pem");
403 let remote_pubkey = remote_data_dir.join("public_key.pem");
404 remote.scp_upload(&keypair_path, remote_keypair.to_str().unwrap())?;
405 remote.scp_upload(&public_key_path, remote_pubkey.to_str().unwrap())?;
406
407 if !is_gateway {
409 for gw in gateway_info {
410 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
411 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
412 })?;
413 let remote_gw_pubkey = remote_data_dir.join(gw_pubkey_name);
414 remote.scp_upload(&gw.public_key_path, remote_gw_pubkey.to_str().unwrap())?;
415 }
416 }
417 }
418
419 let mut args = vec![
421 "network".to_string(),
422 "--data-dir".to_string(),
423 match &location {
424 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
425 PeerLocation::Remote(remote) => remote
426 .remote_work_dir()
427 .join(&id)
428 .to_string_lossy()
429 .to_string(),
430 },
431 "--config-dir".to_string(),
432 match &location {
433 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
434 PeerLocation::Remote(remote) => remote
435 .remote_work_dir()
436 .join(&id)
437 .to_string_lossy()
438 .to_string(),
439 },
440 "--ws-api-port".to_string(),
441 ws_port.to_string(),
442 "--network-address".to_string(),
443 network_address.clone(),
444 "--network-port".to_string(),
445 network_port.to_string(),
446 "--public-network-address".to_string(),
447 network_address.clone(),
448 "--public-network-port".to_string(),
449 network_port.to_string(),
450 "--skip-load-from-network".to_string(),
451 ];
452
453 if is_gateway {
454 args.push("--is-gateway".to_string());
455 }
456
457 args.push("--transport-keypair".to_string());
458 let keypair_arg = match &location {
459 PeerLocation::Local => data_dir.join("keypair.pem").to_string_lossy().to_string(),
460 PeerLocation::Remote(remote) => remote
461 .remote_work_dir()
462 .join(&id)
463 .join("keypair.pem")
464 .to_string_lossy()
465 .to_string(),
466 };
467 args.push(keypair_arg);
468
469 if !is_gateway && !gateway_info.is_empty() {
471 let gateways_toml = data_dir.join("gateways.toml");
472 let mut content = String::new();
473 for gw in gateway_info {
474 let gw_pubkey_path = match &location {
475 PeerLocation::Local => gw.public_key_path.clone(),
476 PeerLocation::Remote(remote) => {
477 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
478 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
479 })?;
480 remote.remote_work_dir().join(&id).join(gw_pubkey_name)
481 }
482 };
483 content.push_str(&format!(
484 "[[gateways]]\n\
485 address = {{ hostname = \"{}\" }}\n\
486 public_key = \"{}\"\n\n",
487 gw.address,
488 gw_pubkey_path.display()
489 ));
490 }
491 std::fs::write(&gateways_toml, content)?;
492
493 if let PeerLocation::Remote(remote) = &location {
495 let remote_gateways_toml = remote.remote_work_dir().join(&id).join("gateways.toml");
496 remote.scp_upload(&gateways_toml, remote_gateways_toml.to_str().unwrap())?;
497 }
498 }
499
500 let env_vars = vec![
503 ("NETWORK_ADDRESS".to_string(), network_address.clone()),
504 (
505 "PUBLIC_NETWORK_ADDRESS".to_string(),
506 network_address.clone(),
507 ),
508 ("PUBLIC_NETWORK_PORT".to_string(), network_port.to_string()),
509 (
510 "FREENET_TELEMETRY_ENABLED".to_string(),
511 "false".to_string(),
512 ),
513 ];
514
515 if let Some(min_conn) = self.min_connections {
516 args.push("--min-number-of-connections".to_string());
517 args.push(min_conn.to_string());
518 }
519 if let Some(max_conn) = self.max_connections {
520 args.push("--max-number-of-connections".to_string());
521 args.push(max_conn.to_string());
522 }
523
524 let process: Box<dyn PeerProcess + Send> = match &location {
526 PeerLocation::Local => Box::new(process::spawn_local_peer(
527 binary_path,
528 &args,
529 &data_dir,
530 &env_vars,
531 )?),
532 PeerLocation::Remote(remote) => {
533 let remote_data_dir = remote.remote_work_dir().join(&id);
534 let local_cache_dir = run_root.join(format!("{}-cache", id));
535 std::fs::create_dir_all(&local_cache_dir)?;
536
537 Box::new(
538 process::spawn_remote_peer(
539 binary_path,
540 &args,
541 remote,
542 &remote_data_dir,
543 &local_cache_dir,
544 &env_vars,
545 )
546 .await?,
547 )
548 }
549 };
550
551 tokio::time::sleep(Duration::from_millis(100)).await;
553
554 Ok(TestPeer {
555 id,
556 is_gateway,
557 ws_port,
558 network_port,
559 network_address,
560 data_dir,
561 process,
562 public_key_path: Some(public_key_path),
563 location,
564 })
565 }
566
567 async fn build_docker_nat(self, config: DockerNatConfig) -> Result<TestNetwork> {
569 let binary_path = self.binary.resolve()?;
570
571 tracing::info!(
572 "Starting Docker NAT test network: {} gateways, {} peers",
573 self.gateways,
574 self.peers
575 );
576
577 let base_dir = resolve_base_dir();
578 fs::create_dir_all(&base_dir)?;
579 cleanup_old_runs(&base_dir, 5)?;
580 let run_root = create_run_directory(&base_dir)?;
581
582 let mut run_status = RunStatusGuard::new(&run_root);
583
584 let mut docker_backend = DockerNatBackend::new(config).await.map_err(|e| {
586 run_status.mark("failure", Some(&format!("Docker init failed: {}", e)));
587 e
588 })?;
589
590 docker_backend.create_public_network().await.map_err(|e| {
592 run_status.mark(
593 "failure",
594 Some(&format!("Failed to create public network: {}", e)),
595 );
596 e
597 })?;
598
599 let ws_port: u16 = 9000;
601 let network_port: u16 = 31337;
602
603 let mut gateways = Vec::new();
605 for i in 0..self.gateways {
606 let data_dir = create_peer_dir(&run_root, &format!("gw{}", i))?;
607
608 let keypair_path = data_dir.join("keypair.pem");
610 let public_key_path = data_dir.join("public_key.pem");
611 generate_keypair(&keypair_path, &public_key_path)?;
612
613 let (info, process) = docker_backend
614 .create_gateway(
615 i,
616 &binary_path,
617 &keypair_path,
618 &public_key_path,
619 ws_port,
620 network_port,
621 &run_root,
622 )
623 .await
624 .map_err(|e| {
625 let detail = format!("failed to start gateway {}: {}", i, e);
626 run_status.mark("failure", Some(&detail));
627 e
628 })?;
629
630 let peer = TestPeer {
631 id: format!("gw{}", i),
632 is_gateway: true,
633 ws_port: info.host_ws_port,
634 network_port: info.network_port,
635 network_address: info.public_ip.to_string(),
636 data_dir,
637 process: Box::new(process),
638 public_key_path: Some(public_key_path),
639 location: PeerLocation::Local, };
641 gateways.push(peer);
642 }
643
644 let gateway_info: Vec<_> = gateways
646 .iter()
647 .map(|gw| GatewayInfo {
648 address: format!("{}:{}", gw.network_address, network_port),
649 public_key_path: gw
650 .public_key_path
651 .clone()
652 .expect("Gateway must have public key"),
653 })
654 .collect();
655
656 let mut peers = Vec::new();
658 for i in 0..self.peers {
659 let peer_index = i + self.gateways;
660 let data_dir = create_peer_dir(&run_root, &format!("peer{}", peer_index))?;
661
662 let keypair_path = data_dir.join("keypair.pem");
664 let public_key_path = data_dir.join("public_key.pem");
665 generate_keypair(&keypair_path, &public_key_path)?;
666
667 let gateways_toml_path = data_dir.join("gateways.toml");
669 let mut gateways_content = String::new();
670 for gw in &gateway_info {
671 gateways_content.push_str(&format!(
672 "[[gateways]]\n\
673 address = {{ hostname = \"{}\" }}\n\
674 public_key = \"/config/gw_public_key.pem\"\n\n",
675 gw.address,
676 ));
677 }
678 std::fs::write(&gateways_toml_path, &gateways_content)?;
679
680 let gateway_public_key_path = gateway_info.first().map(|gw| gw.public_key_path.clone());
682
683 let (info, process) = docker_backend
684 .create_peer(
685 peer_index,
686 &binary_path,
687 &keypair_path,
688 &public_key_path,
689 &gateways_toml_path,
690 gateway_public_key_path.as_deref(),
691 ws_port,
692 network_port,
693 &run_root,
694 )
695 .await
696 .map_err(|e| {
697 let detail = format!("failed to start peer {}: {}", peer_index, e);
698 run_status.mark("failure", Some(&detail));
699 e
700 })?;
701
702 let peer = TestPeer {
703 id: format!("peer{}", peer_index),
704 is_gateway: false,
705 ws_port: info.host_ws_port,
706 network_port: info.network_port,
707 network_address: info.private_ip.to_string(),
708 data_dir,
709 process: Box::new(process),
710 public_key_path: Some(public_key_path),
711 location: PeerLocation::Local,
712 };
713 peers.push(peer);
714
715 if i + 1 < self.peers && !self.start_stagger.is_zero() {
716 tokio::time::sleep(self.start_stagger).await;
717 }
718 }
719
720 let network = TestNetwork::new_with_docker(
722 gateways,
723 peers,
724 self.min_connectivity,
725 run_root.clone(),
726 Some(docker_backend),
727 );
728
729 match network
731 .wait_until_ready_with_timeout(self.connectivity_timeout)
732 .await
733 {
734 Ok(()) => {
735 if self.preserve_data_on_success {
736 println!(
737 "Network data directories preserved at {}",
738 run_root.display()
739 );
740 }
741 let detail = format!(
742 "success: gateways={}, peers={} (Docker NAT)",
743 self.gateways, self.peers
744 );
745 run_status.mark("success", Some(&detail));
746 Ok(network)
747 }
748 Err(err) => {
749 if let Err(log_err) = dump_recent_logs(&network) {
750 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
751 }
752 if self.preserve_data_on_failure {
753 eprintln!(
754 "Network data directories preserved at {}",
755 run_root.display()
756 );
757 }
758 let detail = err.to_string();
759 run_status.mark("failure", Some(&detail));
760 Err(err)
761 }
762 }
763 }
764}
765
766fn resolve_base_dir() -> PathBuf {
767 if let Some(path) = std::env::var_os("FREENET_TEST_NETWORK_BASE_DIR") {
768 PathBuf::from(path)
769 } else if let Ok(home) = std::env::var("HOME") {
770 PathBuf::from(home).join("code/tmp/freenet-test-networks")
771 } else {
772 std::env::temp_dir().join("freenet-test-networks")
773 }
774}
775
776fn cleanup_old_runs(base_dir: &Path, max_runs: usize) -> Result<()> {
777 let mut runs: Vec<(PathBuf, SystemTime)> = fs::read_dir(base_dir)?
778 .filter_map(|entry| {
779 let entry = entry.ok()?;
780 let file_type = entry.file_type().ok()?;
781 if !file_type.is_dir() {
782 return None;
783 }
784 let metadata = entry.metadata().ok()?;
785 let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
786 Some((entry.path(), modified))
787 })
788 .collect();
789
790 if runs.len() <= max_runs {
791 return Ok(());
792 }
793
794 runs.sort_by_key(|(_, modified)| *modified);
795 let remove_count = runs.len() - max_runs;
796 for (path, _) in runs.into_iter().take(remove_count) {
797 if let Err(err) = fs::remove_dir_all(&path) {
798 tracing::warn!(
799 ?err,
800 path = %path.display(),
801 "Failed to remove old freenet test network run directory"
802 );
803 }
804 }
805
806 Ok(())
807}
808
809fn create_run_directory(base_dir: &Path) -> Result<PathBuf> {
810 let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
811 for attempt in 0..100 {
812 let candidate = if attempt == 0 {
813 base_dir.join(×tamp)
814 } else {
815 base_dir.join(format!("{}-{}", ×tamp, attempt))
816 };
817 if !candidate.exists() {
818 fs::create_dir_all(&candidate)?;
819 return Ok(candidate);
820 }
821 }
822
823 Err(Error::Other(anyhow::anyhow!(
824 "Unable to allocate run directory after repeated attempts"
825 )))
826}
827
828fn create_peer_dir(run_root: &Path, id: &str) -> Result<PathBuf> {
829 let dir = run_root.join(id);
830 fs::create_dir_all(&dir)?;
831 Ok(dir)
832}
833
834struct RunStatusGuard {
835 status_path: PathBuf,
836}
837
838impl RunStatusGuard {
839 fn new(run_root: &Path) -> Self {
840 let status_path = run_root.join("run_status.txt");
841 let _ = fs::write(&status_path, b"status=initializing\n");
842 Self { status_path }
843 }
844
845 fn mark(&mut self, status: &str, detail: Option<&str>) {
846 let mut content = format!("status={}", status);
847 if let Some(detail) = detail {
848 content.push('\n');
849 content.push_str("detail=");
850 content.push_str(detail);
851 }
852 content.push('\n');
853 if let Err(err) = fs::write(&self.status_path, content) {
854 tracing::warn!(
855 ?err,
856 path = %self.status_path.display(),
857 "Failed to write run status"
858 );
859 }
860 }
861}
862
863fn generate_keypair(
864 private_key_path: &std::path::Path,
865 public_key_path: &std::path::Path,
866) -> Result<()> {
867 use rand::RngCore;
868 use x25519_dalek::{PublicKey, StaticSecret};
869
870 let mut secret_bytes = [0u8; 32];
872 rand::thread_rng().fill_bytes(&mut secret_bytes);
873
874 let secret = StaticSecret::from(secret_bytes);
876 let public = PublicKey::from(&secret);
877 drop(secret); std::fs::write(private_key_path, hex::encode(secret_bytes))
881 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write private key: {}", e)))?;
882
883 std::fs::write(public_key_path, hex::encode(public.as_bytes()))
885 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write public key: {}", e)))?;
886
887 Ok(())
888}
889
890fn dump_recent_logs(network: &TestNetwork) -> Result<()> {
891 const MAX_LOG_LINES: usize = 200;
892
893 let mut logs = network.read_logs()?;
894 let total = logs.len();
895 if total > MAX_LOG_LINES {
896 logs.drain(0..(total - MAX_LOG_LINES));
897 }
898
899 eprintln!(
900 "\n--- Network connectivity check failed; showing {} of {} log entries ---",
901 logs.len(),
902 total
903 );
904
905 for entry in logs {
906 let level = entry.level.as_deref().unwrap_or("INFO");
907 let ts_display = entry
908 .timestamp_raw
909 .clone()
910 .or_else(|| entry.timestamp.map(|ts| ts.to_rfc3339()));
911
912 if let Some(ts) = ts_display {
913 eprintln!("[{}] [{}] {}: {}", entry.peer_id, ts, level, entry.message);
914 } else {
915 eprintln!("[{}] {}: {}", entry.peer_id, level, entry.message);
916 }
917 }
918
919 eprintln!("--- End of network logs ---\n");
920
921 Ok(())
922}
923
924fn preserve_network_state(network: &TestNetwork) -> Result<PathBuf> {
925 Ok(network.run_root().to_path_buf())
926}