Skip to main content

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            let mut config = DockerNatConfig::default();
32
33            // Check for network emulation setting
34            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
64/// Builder for configuring and creating a test network
65pub 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, // Default: require all peers connected
94            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    /// Set the number of gateway peers
107    pub fn gateways(mut self, n: usize) -> Self {
108        self.gateways = n;
109        self
110    }
111
112    /// Set the number of regular peers
113    pub fn peers(mut self, n: usize) -> Self {
114        self.peers = n;
115        self
116    }
117
118    /// Set which freenet binary to use
119    pub fn binary(mut self, binary: FreenetBinary) -> Self {
120        self.binary = binary;
121        self
122    }
123
124    /// Set minimum connectivity ratio required (0.0 to 1.0)
125    pub fn require_connectivity(mut self, ratio: f64) -> Self {
126        self.min_connectivity = ratio;
127        self
128    }
129
130    /// Set timeout for connectivity check
131    pub fn connectivity_timeout(mut self, timeout: Duration) -> Self {
132        self.connectivity_timeout = timeout;
133        self
134    }
135
136    /// Override min connections target for all peers.
137    pub fn min_connections(mut self, min: usize) -> Self {
138        self.min_connections = Some(min);
139        self
140    }
141
142    /// Override max connections target for all peers.
143    pub fn max_connections(mut self, max: usize) -> Self {
144        self.max_connections = Some(max);
145        self
146    }
147
148    /// Add a delay between starting successive non-gateway peers.
149    pub fn start_stagger(mut self, delay: Duration) -> Self {
150        self.start_stagger = delay;
151        self
152    }
153
154    /// Preserve peer data directories in `/tmp` when network startup fails
155    pub fn preserve_temp_dirs_on_failure(mut self, preserve: bool) -> Self {
156        self.preserve_data_on_failure = preserve;
157        self
158    }
159
160    /// Preserve peer data directories in `/tmp` even when the network boots successfully.
161    pub fn preserve_temp_dirs_on_success(mut self, preserve: bool) -> Self {
162        self.preserve_data_on_success = preserve;
163        self
164    }
165
166    /// Set the location for a specific peer (by index)
167    /// Index 0 is the first gateway, subsequent indices are regular peers
168    pub fn peer_location(mut self, index: usize, location: PeerLocation) -> Self {
169        self.peer_locations.insert(index, location);
170        self
171    }
172
173    /// Set the default location for all peers not explicitly configured
174    pub fn default_location(mut self, location: PeerLocation) -> Self {
175        self.default_location = location;
176        self
177    }
178
179    /// Convenience method to set locations for multiple remote machines
180    /// Distributes peers across the provided machines in round-robin fashion
181    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    /// Set the backend for running peers (Local or DockerNat)
191    pub fn backend(mut self, backend: Backend) -> Self {
192        self.backend = backend;
193        self
194    }
195
196    /// Build and start the network (async)
197    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    /// Build network using local processes (original implementation)
205    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        // Start gateways first
222        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        // Collect gateway info for peers to connect to
236        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        // Start regular peers
248        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        // Wait for network to be ready
276        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    /// Build the network synchronously (for use in LazyLock)
320    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        // Get location for this peer
344        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        // Determine network address based on location
357        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                // Discover the public IP address of the remote machine
366                remote.discover_public_address()?
367            }
368        };
369
370        // For local peers, allocate ports locally
371        // For remote peers, use port 0 (let remote OS allocate)
372        let (ws_port, network_port) = match &location {
373            PeerLocation::Local => (get_free_port()?, get_free_port()?),
374            PeerLocation::Remote(_) => (0, 0), // Will be allocated on remote
375        };
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        // Generate a unique transport keypair for every node so identities are distinct.
388        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        // For remote gateways, we need to upload the keypair
393        // For remote regular peers, we need to upload the gateway public keys
394        if let PeerLocation::Remote(remote) = &location {
395            let remote_data_dir = remote.remote_work_dir().join(&id);
396
397            // Create remote data directory before uploading files
398            let mkdir_cmd = format!("mkdir -p {}", remote_data_dir.display());
399            remote.exec(&mkdir_cmd)?;
400
401            // Upload keypair to remote
402            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            // Upload gateway public keys for regular peers
408            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        // Build command arguments (same for local and remote)
420        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        // Add gateway addresses for regular peers
470        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            // Upload gateways.toml to remote if needed
494            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        // Environment variables
501        // Disable telemetry to avoid flooding the collector with test data
502        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            ("FREENET_TELEMETRY_ENABLED".to_string(), "false".to_string()),
510        ];
511
512        if let Some(min_conn) = self.min_connections {
513            args.push("--min-number-of-connections".to_string());
514            args.push(min_conn.to_string());
515        }
516        if let Some(max_conn) = self.max_connections {
517            args.push("--max-number-of-connections".to_string());
518            args.push(max_conn.to_string());
519        }
520
521        // Spawn process (local or remote)
522        let process: Box<dyn PeerProcess + Send> = match &location {
523            PeerLocation::Local => Box::new(process::spawn_local_peer(
524                binary_path,
525                &args,
526                &data_dir,
527                &env_vars,
528            )?),
529            PeerLocation::Remote(remote) => {
530                let remote_data_dir = remote.remote_work_dir().join(&id);
531                let local_cache_dir = run_root.join(format!("{}-cache", id));
532                std::fs::create_dir_all(&local_cache_dir)?;
533
534                Box::new(
535                    process::spawn_remote_peer(
536                        binary_path,
537                        &args,
538                        remote,
539                        &remote_data_dir,
540                        &local_cache_dir,
541                        &env_vars,
542                    )
543                    .await?,
544                )
545            }
546        };
547
548        // Give it a moment to start
549        tokio::time::sleep(Duration::from_millis(100)).await;
550
551        Ok(TestPeer {
552            id,
553            is_gateway,
554            ws_port,
555            network_port,
556            network_address,
557            data_dir,
558            process,
559            public_key_path: Some(public_key_path),
560            location,
561        })
562    }
563
564    /// Build network using Docker containers with NAT simulation
565    async fn build_docker_nat(self, config: DockerNatConfig) -> Result<TestNetwork> {
566        let binary_path = self.binary.resolve()?;
567
568        tracing::info!(
569            "Starting Docker NAT test network: {} gateways, {} peers",
570            self.gateways,
571            self.peers
572        );
573
574        let base_dir = resolve_base_dir();
575        fs::create_dir_all(&base_dir)?;
576        cleanup_old_runs(&base_dir, 5)?;
577        let run_root = create_run_directory(&base_dir)?;
578
579        let mut run_status = RunStatusGuard::new(&run_root);
580
581        // Initialize Docker backend
582        let mut docker_backend = DockerNatBackend::new(config).await.map_err(|e| {
583            run_status.mark("failure", Some(&format!("Docker init failed: {}", e)));
584            e
585        })?;
586
587        // Create public network
588        docker_backend.create_public_network().await.map_err(|e| {
589            run_status.mark(
590                "failure",
591                Some(&format!("Failed to create public network: {}", e)),
592            );
593            e
594        })?;
595
596        // Standard ports inside containers
597        let ws_port: u16 = 9000;
598        let network_port: u16 = 31337;
599
600        // Start gateways first
601        let mut gateways = Vec::new();
602        for i in 0..self.gateways {
603            let data_dir = create_peer_dir(&run_root, &format!("gw{}", i))?;
604
605            // Generate keypair locally
606            let keypair_path = data_dir.join("keypair.pem");
607            let public_key_path = data_dir.join("public_key.pem");
608            generate_keypair(&keypair_path, &public_key_path)?;
609
610            let (info, process) = docker_backend
611                .create_gateway(
612                    i,
613                    &binary_path,
614                    &keypair_path,
615                    &public_key_path,
616                    ws_port,
617                    network_port,
618                    &run_root,
619                )
620                .await
621                .map_err(|e| {
622                    let detail = format!("failed to start gateway {}: {}", i, e);
623                    run_status.mark("failure", Some(&detail));
624                    e
625                })?;
626
627            let peer = TestPeer {
628                id: format!("gw{}", i),
629                is_gateway: true,
630                ws_port: info.host_ws_port,
631                network_port: info.network_port,
632                network_address: info.public_ip.to_string(),
633                data_dir,
634                process: Box::new(process),
635                public_key_path: Some(public_key_path),
636                location: PeerLocation::Local, // Treated as local from API perspective
637            };
638            gateways.push(peer);
639        }
640
641        // Collect gateway info for peers
642        let gateway_info: Vec<_> = gateways
643            .iter()
644            .map(|gw| GatewayInfo {
645                address: format!("{}:{}", gw.network_address, network_port),
646                public_key_path: gw
647                    .public_key_path
648                    .clone()
649                    .expect("Gateway must have public key"),
650            })
651            .collect();
652
653        // Start regular peers (each behind its own NAT)
654        let mut peers = Vec::new();
655        for i in 0..self.peers {
656            let peer_index = i + self.gateways;
657            let data_dir = create_peer_dir(&run_root, &format!("peer{}", peer_index))?;
658
659            // Generate keypair locally
660            let keypair_path = data_dir.join("keypair.pem");
661            let public_key_path = data_dir.join("public_key.pem");
662            generate_keypair(&keypair_path, &public_key_path)?;
663
664            // Create gateways.toml pointing to gateway's public network address
665            let gateways_toml_path = data_dir.join("gateways.toml");
666            let mut gateways_content = String::new();
667            for gw in &gateway_info {
668                gateways_content.push_str(&format!(
669                    "[[gateways]]\n\
670                     address = {{ hostname = \"{}\" }}\n\
671                     public_key = \"/config/gw_public_key.pem\"\n\n",
672                    gw.address,
673                ));
674            }
675            std::fs::write(&gateways_toml_path, &gateways_content)?;
676
677            // Get gateway public key path if available
678            let gateway_public_key_path = gateway_info.first().map(|gw| gw.public_key_path.clone());
679
680            let (info, process) = docker_backend
681                .create_peer(
682                    peer_index,
683                    &binary_path,
684                    &keypair_path,
685                    &public_key_path,
686                    &gateways_toml_path,
687                    gateway_public_key_path.as_deref(),
688                    ws_port,
689                    network_port,
690                    &run_root,
691                )
692                .await
693                .map_err(|e| {
694                    let detail = format!("failed to start peer {}: {}", peer_index, e);
695                    run_status.mark("failure", Some(&detail));
696                    e
697                })?;
698
699            let peer = TestPeer {
700                id: format!("peer{}", peer_index),
701                is_gateway: false,
702                ws_port: info.host_ws_port,
703                network_port: info.network_port,
704                network_address: info.private_ip.to_string(),
705                data_dir,
706                process: Box::new(process),
707                public_key_path: Some(public_key_path),
708                location: PeerLocation::Local,
709            };
710            peers.push(peer);
711
712            if i + 1 < self.peers && !self.start_stagger.is_zero() {
713                tokio::time::sleep(self.start_stagger).await;
714            }
715        }
716
717        // Store Docker backend in network for cleanup
718        let network = TestNetwork::new_with_docker(
719            gateways,
720            peers,
721            self.min_connectivity,
722            run_root.clone(),
723            Some(docker_backend),
724        );
725
726        // Wait for network to be ready
727        match network
728            .wait_until_ready_with_timeout(self.connectivity_timeout)
729            .await
730        {
731            Ok(()) => {
732                if self.preserve_data_on_success {
733                    println!(
734                        "Network data directories preserved at {}",
735                        run_root.display()
736                    );
737                }
738                let detail = format!(
739                    "success: gateways={}, peers={} (Docker NAT)",
740                    self.gateways, self.peers
741                );
742                run_status.mark("success", Some(&detail));
743                Ok(network)
744            }
745            Err(err) => {
746                if let Err(log_err) = dump_recent_logs(&network) {
747                    eprintln!("Failed to dump logs after connectivity error: {}", log_err);
748                }
749                if self.preserve_data_on_failure {
750                    eprintln!(
751                        "Network data directories preserved at {}",
752                        run_root.display()
753                    );
754                }
755                let detail = err.to_string();
756                run_status.mark("failure", Some(&detail));
757                Err(err)
758            }
759        }
760    }
761}
762
763fn resolve_base_dir() -> PathBuf {
764    if let Some(path) = std::env::var_os("FREENET_TEST_NETWORK_BASE_DIR") {
765        PathBuf::from(path)
766    } else {
767        // Use system temp directory by default - works on all platforms including CI
768        std::env::temp_dir().join("freenet-test-networks")
769    }
770}
771
772fn cleanup_old_runs(base_dir: &Path, max_runs: usize) -> Result<()> {
773    let mut runs: Vec<(PathBuf, SystemTime)> = fs::read_dir(base_dir)?
774        .filter_map(|entry| {
775            let entry = entry.ok()?;
776            let file_type = entry.file_type().ok()?;
777            if !file_type.is_dir() {
778                return None;
779            }
780            let metadata = entry.metadata().ok()?;
781            let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
782            Some((entry.path(), modified))
783        })
784        .collect();
785
786    if runs.len() <= max_runs {
787        return Ok(());
788    }
789
790    runs.sort_by_key(|(_, modified)| *modified);
791    let remove_count = runs.len() - max_runs;
792    for (path, _) in runs.into_iter().take(remove_count) {
793        if let Err(err) = fs::remove_dir_all(&path) {
794            tracing::warn!(
795                ?err,
796                path = %path.display(),
797                "Failed to remove old freenet test network run directory"
798            );
799        }
800    }
801
802    Ok(())
803}
804
805fn create_run_directory(base_dir: &Path) -> Result<PathBuf> {
806    let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
807    for attempt in 0..100 {
808        let candidate = if attempt == 0 {
809            base_dir.join(&timestamp)
810        } else {
811            base_dir.join(format!("{}-{}", &timestamp, attempt))
812        };
813        if !candidate.exists() {
814            fs::create_dir_all(&candidate)?;
815            return Ok(candidate);
816        }
817    }
818
819    Err(Error::Other(anyhow::anyhow!(
820        "Unable to allocate run directory after repeated attempts"
821    )))
822}
823
824fn create_peer_dir(run_root: &Path, id: &str) -> Result<PathBuf> {
825    let dir = run_root.join(id);
826    fs::create_dir_all(&dir)?;
827    Ok(dir)
828}
829
830struct RunStatusGuard {
831    status_path: PathBuf,
832}
833
834impl RunStatusGuard {
835    fn new(run_root: &Path) -> Self {
836        let status_path = run_root.join("run_status.txt");
837        let _ = fs::write(&status_path, b"status=initializing\n");
838        Self { status_path }
839    }
840
841    fn mark(&mut self, status: &str, detail: Option<&str>) {
842        let mut content = format!("status={}", status);
843        if let Some(detail) = detail {
844            content.push('\n');
845            content.push_str("detail=");
846            content.push_str(detail);
847        }
848        content.push('\n');
849        if let Err(err) = fs::write(&self.status_path, content) {
850            tracing::warn!(
851                ?err,
852                path = %self.status_path.display(),
853                "Failed to write run status"
854            );
855        }
856    }
857}
858
859fn generate_keypair(
860    private_key_path: &std::path::Path,
861    public_key_path: &std::path::Path,
862) -> Result<()> {
863    use rand::RngCore;
864    use x25519_dalek::{PublicKey, StaticSecret};
865
866    // Generate random bytes for the secret key
867    let mut secret_bytes = [0u8; 32];
868    rand::thread_rng().fill_bytes(&mut secret_bytes);
869
870    // Create X25519 keypair - derive public key from secret
871    let secret = StaticSecret::from(secret_bytes);
872    let public = PublicKey::from(&secret);
873    drop(secret); // We save the raw bytes, not the StaticSecret
874
875    // Save secret key as hex
876    std::fs::write(private_key_path, hex::encode(secret_bytes))
877        .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write private key: {}", e)))?;
878
879    // Save public key as hex
880    std::fs::write(public_key_path, hex::encode(public.as_bytes()))
881        .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write public key: {}", e)))?;
882
883    Ok(())
884}
885
886fn dump_recent_logs(network: &TestNetwork) -> Result<()> {
887    const MAX_LOG_LINES: usize = 200;
888
889    let mut logs = network.read_logs()?;
890    let total = logs.len();
891    if total > MAX_LOG_LINES {
892        logs.drain(0..(total - MAX_LOG_LINES));
893    }
894
895    eprintln!(
896        "\n--- Network connectivity check failed; showing {} of {} log entries ---",
897        logs.len(),
898        total
899    );
900
901    for entry in logs {
902        let level = entry.level.as_deref().unwrap_or("INFO");
903        let ts_display = entry
904            .timestamp_raw
905            .clone()
906            .or_else(|| entry.timestamp.map(|ts| ts.to_rfc3339()));
907
908        if let Some(ts) = ts_display {
909            eprintln!("[{}] [{}] {}: {}", entry.peer_id, ts, level, entry.message);
910        } else {
911            eprintln!("[{}] {}: {}", entry.peer_id, level, entry.message);
912        }
913    }
914
915    eprintln!("--- End of network logs ---\n");
916
917    Ok(())
918}
919
920fn preserve_network_state(network: &TestNetwork) -> Result<PathBuf> {
921    Ok(network.run_root().to_path_buf())
922}