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            (
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        // Spawn process (local or remote)
525        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        // Give it a moment to start
552        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    /// Build network using Docker containers with NAT simulation
568    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        // Initialize Docker backend
585        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        // Create public network
591        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        // Standard ports inside containers
600        let ws_port: u16 = 9000;
601        let network_port: u16 = 31337;
602
603        // Start gateways first
604        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            // Generate keypair locally
609            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, // Treated as local from API perspective
640            };
641            gateways.push(peer);
642        }
643
644        // Collect gateway info for peers
645        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        // Start regular peers (each behind its own NAT)
657        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            // Generate keypair locally
663            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            // Create gateways.toml pointing to gateway's public network address
668            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            // Get gateway public key path if available
681            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        // Store Docker backend in network for cleanup
721        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        // Wait for network to be ready
730        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(&timestamp)
814        } else {
815            base_dir.join(format!("{}-{}", &timestamp, 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    // Generate random bytes for the secret key
871    let mut secret_bytes = [0u8; 32];
872    rand::thread_rng().fill_bytes(&mut secret_bytes);
873
874    // Create X25519 keypair - derive public key from secret
875    let secret = StaticSecret::from(secret_bytes);
876    let public = PublicKey::from(&secret);
877    drop(secret); // We save the raw bytes, not the StaticSecret
878
879    // Save secret key as hex
880    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    // Save public key as hex
884    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}