freenet_test_network/
builder.rs

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