freenet_test_network/
docker.rs

1//! Docker-based NAT simulation backend for testing Freenet in isolated networks.
2//!
3//! This module provides infrastructure to run Freenet peers in Docker containers
4//! behind simulated NAT routers, allowing detection of bugs that only manifest
5//! when peers are on different networks.
6
7use crate::{logs::LogEntry, process::PeerProcess, Error, Result};
8use bollard::{
9    container::{
10        Config, CreateContainerOptions, LogOutput, LogsOptions,
11        RemoveContainerOptions, StartContainerOptions, StopContainerOptions, UploadToContainerOptions,
12    },
13    exec::{CreateExecOptions, StartExecResults},
14    image::BuildImageOptions,
15    network::CreateNetworkOptions,
16    secret::{ContainerStateStatusEnum, HostConfig, Ipam, IpamConfig, PortBinding},
17    Docker,
18};
19use futures::StreamExt;
20use ipnetwork::Ipv4Network;
21use rand::Rng;
22use std::{
23    collections::HashMap,
24    net::Ipv4Addr,
25    path::{Path, PathBuf},
26    time::Duration,
27};
28
29/// Configuration for Docker NAT simulation
30#[derive(Debug, Clone)]
31pub struct DockerNatConfig {
32    /// NAT topology configuration
33    pub topology: NatTopology,
34    /// Base subnet for public network (gateway network)
35    pub public_subnet: Ipv4Network,
36    /// Base for private network subnets (each NAT gets one)
37    pub private_subnet_base: Ipv4Addr,
38    /// Whether to remove containers on drop
39    pub cleanup_on_drop: bool,
40    /// Prefix for container and network names
41    pub name_prefix: String,
42}
43
44impl Default for DockerNatConfig {
45    fn default() -> Self {
46        Self {
47            topology: NatTopology::OnePerNat,
48            public_subnet: "172.20.0.0/24".parse().unwrap(),
49            private_subnet_base: Ipv4Addr::new(10, 0, 0, 0),
50            cleanup_on_drop: true,
51            name_prefix: format!("freenet-nat-{}", rand::thread_rng().gen::<u16>()),
52        }
53    }
54}
55
56/// How peers are distributed across NAT networks
57#[derive(Debug, Clone)]
58pub enum NatTopology {
59    /// Each peer (except gateways) gets its own NAT network
60    OnePerNat,
61    /// Specific assignment of peers to NAT networks
62    Custom(Vec<NatNetwork>),
63}
64
65/// A NAT network containing one or more peers
66#[derive(Debug, Clone)]
67pub struct NatNetwork {
68    pub name: String,
69    pub peer_indices: Vec<usize>,
70    pub nat_type: NatType,
71}
72
73/// Type of NAT simulation
74#[derive(Debug, Clone, Default)]
75pub enum NatType {
76    /// Outbound MASQUERADE only - most common residential NAT
77    #[default]
78    RestrictedCone,
79    /// MASQUERADE + port forwarding for specified ports
80    FullCone { forwarded_ports: Option<Vec<u16>> },
81}
82
83/// Manages Docker resources for NAT simulation
84pub struct DockerNatBackend {
85    docker: Docker,
86    config: DockerNatConfig,
87    /// Network IDs created by this backend
88    networks: Vec<String>,
89    /// Container IDs created by this backend (NAT routers + peers)
90    containers: Vec<String>,
91    /// Mapping from peer index to container info
92    peer_containers: HashMap<usize, DockerPeerInfo>,
93    /// ID of the public network
94    public_network_id: Option<String>,
95}
96
97/// Information about a peer running in a Docker container
98#[derive(Debug, Clone)]
99pub struct DockerPeerInfo {
100    pub container_id: String,
101    pub container_name: String,
102    /// IP address on private network (behind NAT)
103    pub private_ip: Ipv4Addr,
104    /// IP address on public network (for gateways) or NAT router's public IP (for peers)
105    pub public_ip: Ipv4Addr,
106    /// Port mapped to host for WebSocket API access
107    pub host_ws_port: u16,
108    /// Network port inside container
109    pub network_port: u16,
110    /// Whether this is a gateway (not behind NAT)
111    pub is_gateway: bool,
112    /// NAT router container ID (None for gateways)
113    pub nat_router_id: Option<String>,
114}
115
116/// A peer process running in a Docker container
117pub struct DockerProcess {
118    docker: Docker,
119    container_id: String,
120    container_name: String,
121    local_log_cache: PathBuf,
122}
123
124impl PeerProcess for DockerProcess {
125    fn is_running(&self) -> bool {
126        // Use blocking runtime to check container status
127        let docker = self.docker.clone();
128        let id = self.container_id.clone();
129
130        tokio::task::block_in_place(|| {
131            tokio::runtime::Handle::current().block_on(async {
132                match docker.inspect_container(&id, None).await {
133                    Ok(info) => {
134                        info.state
135                            .and_then(|s| s.status)
136                            .map(|s| s == ContainerStateStatusEnum::RUNNING)
137                            .unwrap_or(false)
138                    }
139                    Err(_) => false,
140                }
141            })
142        })
143    }
144
145    fn kill(&mut self) -> Result<()> {
146        let docker = self.docker.clone();
147        let id = self.container_id.clone();
148
149        tokio::task::block_in_place(|| {
150            tokio::runtime::Handle::current().block_on(async {
151                // Stop container with timeout
152                let _ = docker
153                    .stop_container(&id, Some(StopContainerOptions { t: 5 }))
154                    .await;
155                Ok(())
156            })
157        })
158    }
159
160    fn log_path(&self) -> PathBuf {
161        self.local_log_cache.clone()
162    }
163
164    fn read_logs(&self) -> Result<Vec<LogEntry>> {
165        let docker = self.docker.clone();
166        let id = self.container_id.clone();
167        let cache_path = self.local_log_cache.clone();
168
169        tokio::task::block_in_place(|| {
170            tokio::runtime::Handle::current().block_on(async {
171                // Fetch logs from container
172                let options = LogsOptions::<String> {
173                    stdout: true,
174                    stderr: true,
175                    timestamps: true,
176                    ..Default::default()
177                };
178
179                let mut logs = docker.logs(&id, Some(options));
180                let mut log_content = String::new();
181
182                while let Some(log_result) = logs.next().await {
183                    match log_result {
184                        Ok(LogOutput::StdOut { message }) | Ok(LogOutput::StdErr { message }) => {
185                            log_content.push_str(&String::from_utf8_lossy(&message));
186                        }
187                        _ => {}
188                    }
189                }
190
191                // Write to cache file
192                if let Some(parent) = cache_path.parent() {
193                    std::fs::create_dir_all(parent)?;
194                }
195                std::fs::write(&cache_path, &log_content)?;
196
197                // Parse logs
198                crate::logs::read_log_file(&cache_path)
199            })
200        })
201    }
202}
203
204impl Drop for DockerProcess {
205    fn drop(&mut self) {
206        let _ = self.kill();
207    }
208}
209
210impl DockerNatBackend {
211    /// Create a new Docker NAT backend
212    pub async fn new(config: DockerNatConfig) -> Result<Self> {
213        let docker = Docker::connect_with_local_defaults()
214            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect to Docker: {}", e)))?;
215
216        // Verify Docker is accessible
217        docker
218            .ping()
219            .await
220            .map_err(|e| Error::Other(anyhow::anyhow!("Docker ping failed: {}", e)))?;
221
222        Ok(Self {
223            docker,
224            config,
225            networks: Vec::new(),
226            containers: Vec::new(),
227            peer_containers: HashMap::new(),
228            public_network_id: None,
229        })
230    }
231
232    /// Create the public network where gateways live
233    pub async fn create_public_network(&mut self) -> Result<String> {
234        let network_name = format!("{}-public", self.config.name_prefix);
235
236        let options = CreateNetworkOptions {
237            name: network_name.clone(),
238            driver: "bridge".to_string(),
239            ipam: Ipam {
240                config: Some(vec![IpamConfig {
241                    subnet: Some(self.config.public_subnet.to_string()),
242                    ..Default::default()
243                }]),
244                ..Default::default()
245            },
246            ..Default::default()
247        };
248
249        let response = self.docker
250            .create_network(options)
251            .await
252            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create public network: {}", e)))?;
253
254        let network_id = response.id;
255
256        self.networks.push(network_id.clone());
257        self.public_network_id = Some(network_id.clone());
258
259        tracing::info!("Created public network: {} ({})", network_name, network_id);
260        Ok(network_id)
261    }
262
263    /// Create a private network behind NAT for a peer
264    pub async fn create_nat_network(&mut self, peer_index: usize) -> Result<(String, String, Ipv4Addr)> {
265        // Create private network
266        let network_name = format!("{}-nat-{}", self.config.name_prefix, peer_index);
267        let subnet = Ipv4Network::new(
268            Ipv4Addr::new(10, peer_index as u8 + 1, 0, 0),
269            24,
270        ).map_err(|e| Error::Other(anyhow::anyhow!("Invalid subnet: {}", e)))?;
271
272        let options = CreateNetworkOptions {
273            name: network_name.clone(),
274            driver: "bridge".to_string(),
275            internal: true, // No direct external access
276            ipam: Ipam {
277                config: Some(vec![IpamConfig {
278                    subnet: Some(subnet.to_string()),
279                    ..Default::default()
280                }]),
281                ..Default::default()
282            },
283            ..Default::default()
284        };
285
286        let response = self.docker
287            .create_network(options)
288            .await
289            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create NAT network: {}", e)))?;
290
291        let network_id = response.id;
292        self.networks.push(network_id.clone());
293
294        // Create NAT router container
295        let router_name = format!("{}-router-{}", self.config.name_prefix, peer_index);
296        let public_network_id = self.public_network_id.as_ref()
297            .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
298
299        // NAT router IP addresses
300        let router_public_ip = Ipv4Addr::new(
301            self.config.public_subnet.ip().octets()[0],
302            self.config.public_subnet.ip().octets()[1],
303            0,
304            100 + peer_index as u8,
305        );
306        // Use .254 for router to avoid conflict with Docker's default gateway at .1
307        let router_private_ip = Ipv4Addr::new(10, peer_index as u8 + 1, 0, 254);
308
309        // Create router container with iptables NAT rules
310        // Create without network first, then connect to both networks before starting
311        let router_config = Config {
312            image: Some("alpine:latest".to_string()),
313            hostname: Some(router_name.clone()),
314            cmd: Some(vec![
315                "sh".to_string(),
316                "-c".to_string(),
317                // Set up NAT (IP forwarding enabled via sysctl in host_config)
318                "apk add --no-cache iptables > /dev/null 2>&1 && \
319                 iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE && \
320                 iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT && \
321                 iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT && \
322                 echo 'NAT router ready' && \
323                 tail -f /dev/null".to_string(),
324            ]),
325            host_config: Some(HostConfig {
326                cap_add: Some(vec!["NET_ADMIN".to_string()]),
327                sysctls: Some(HashMap::from([
328                    ("net.ipv4.ip_forward".to_string(), "1".to_string()),
329                ])),
330                ..Default::default()
331            }),
332            ..Default::default()
333        };
334
335        let router_id = self.docker
336            .create_container(
337                Some(CreateContainerOptions { name: router_name.clone(), ..Default::default() }),
338                router_config,
339            )
340            .await
341            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create NAT router: {}", e)))?
342            .id;
343
344        self.containers.push(router_id.clone());
345
346        // Disconnect from default bridge network
347        let _ = self.docker.disconnect_network(
348            "bridge",
349            bollard::network::DisconnectNetworkOptions {
350                container: router_id.clone(),
351                force: true,
352            },
353        ).await;
354
355        // Connect router to public network first (eth0)
356        self.docker
357            .connect_network(
358                public_network_id,
359                bollard::network::ConnectNetworkOptions {
360                    container: router_id.clone(),
361                    endpoint_config: bollard::secret::EndpointSettings {
362                        ipam_config: Some(bollard::secret::EndpointIpamConfig {
363                            ipv4_address: Some(router_public_ip.to_string()),
364                            ..Default::default()
365                        }),
366                        ..Default::default()
367                    },
368                },
369            )
370            .await
371            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect router to public network: {}", e)))?;
372
373        // Connect router to private network (eth1)
374        self.docker
375            .connect_network(
376                &network_id,
377                bollard::network::ConnectNetworkOptions {
378                    container: router_id.clone(),
379                    endpoint_config: bollard::secret::EndpointSettings {
380                        ipam_config: Some(bollard::secret::EndpointIpamConfig {
381                            ipv4_address: Some(router_private_ip.to_string()),
382                            ..Default::default()
383                        }),
384                        ..Default::default()
385                    },
386                },
387            )
388            .await
389            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect router to private network: {}", e)))?;
390
391        // Start the router
392        self.docker
393            .start_container(&router_id, None::<StartContainerOptions<String>>)
394            .await
395            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start NAT router: {}", e)))?;
396
397        // Wait for router to be ready
398        tokio::time::sleep(Duration::from_secs(2)).await;
399
400        tracing::info!(
401            "Created NAT network {} with router {} (public: {}, private: {})",
402            network_name, router_name, router_public_ip, router_private_ip
403        );
404
405        Ok((network_id, router_id, router_public_ip))
406    }
407
408    /// Build the base Freenet peer Docker image
409    pub async fn ensure_base_image(&self) -> Result<String> {
410        let image_name = "freenet-test-peer:latest";
411
412        // Check if image already exists
413        if self.docker.inspect_image(image_name).await.is_ok() {
414            tracing::debug!("Base image {} already exists", image_name);
415            return Ok(image_name.to_string());
416        }
417
418        tracing::info!("Building base image {}...", image_name);
419
420        // Create a minimal Dockerfile - use Ubuntu 24.04 to match host glibc version
421        let dockerfile = r#"
422FROM ubuntu:24.04
423RUN apt-get update && \
424    apt-get install -y --no-install-recommends \
425        libssl3 \
426        ca-certificates \
427        iproute2 \
428        && rm -rf /var/lib/apt/lists/*
429RUN mkdir -p /data /config
430WORKDIR /app
431"#;
432
433        // Create tar archive with Dockerfile
434        let mut tar_builder = tar::Builder::new(Vec::new());
435        let mut header = tar::Header::new_gnu();
436        header.set_path("Dockerfile")?;
437        header.set_size(dockerfile.len() as u64);
438        header.set_mode(0o644);
439        header.set_cksum();
440        tar_builder.append(&header, dockerfile.as_bytes())?;
441        let tar_data = tar_builder.into_inner()?;
442
443        // Build image
444        let options = BuildImageOptions {
445            dockerfile: "Dockerfile",
446            t: image_name,
447            rm: true,
448            ..Default::default()
449        };
450
451        let mut build_stream = self.docker.build_image(options, None, Some(tar_data.into()));
452
453        while let Some(result) = build_stream.next().await {
454            match result {
455                Ok(info) => {
456                    if let Some(stream) = info.stream {
457                        tracing::debug!("Build: {}", stream.trim());
458                    }
459                    if let Some(error) = info.error {
460                        return Err(Error::Other(anyhow::anyhow!("Image build error: {}", error)));
461                    }
462                }
463                Err(e) => {
464                    return Err(Error::Other(anyhow::anyhow!("Image build failed: {}", e)));
465                }
466            }
467        }
468
469        tracing::info!("Built base image {}", image_name);
470        Ok(image_name.to_string())
471    }
472
473    /// Copy binary into a container
474    pub async fn copy_binary_to_container(
475        &self,
476        container_id: &str,
477        binary_path: &Path,
478    ) -> Result<()> {
479        // Read binary
480        let binary_data = std::fs::read(binary_path)?;
481
482        // Create tar archive with the binary
483        let mut tar_builder = tar::Builder::new(Vec::new());
484        let mut header = tar::Header::new_gnu();
485        header.set_path("freenet")?;
486        header.set_size(binary_data.len() as u64);
487        header.set_mode(0o755);
488        header.set_cksum();
489        tar_builder.append(&header, binary_data.as_slice())?;
490        let tar_data = tar_builder.into_inner()?;
491
492        // Upload to container
493        self.docker
494            .upload_to_container(
495                container_id,
496                Some(UploadToContainerOptions {
497                    path: "/app",
498                    ..Default::default()
499                }),
500                tar_data.into(),
501            )
502            .await
503            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy binary: {}", e)))?;
504
505        Ok(())
506    }
507
508    /// Create a gateway container (on public network, no NAT)
509    pub async fn create_gateway(
510        &mut self,
511        index: usize,
512        binary_path: &Path,
513        keypair_path: &Path,
514        public_key_path: &Path,
515        ws_port: u16,
516        network_port: u16,
517        run_root: &Path,
518    ) -> Result<(DockerPeerInfo, DockerProcess)> {
519        let container_name = format!("{}-gw-{}", self.config.name_prefix, index);
520        let image = self.ensure_base_image().await?;
521
522        let public_network_id = self.public_network_id.as_ref()
523            .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
524
525        // Gateway IP on public network
526        let gateway_ip = Ipv4Addr::new(
527            self.config.public_subnet.ip().octets()[0],
528            self.config.public_subnet.ip().octets()[1],
529            0,
530            10 + index as u8,
531        );
532
533        // Allocate host port for WS API
534        let host_ws_port = crate::peer::get_free_port()?;
535
536        // Create container
537        let config = Config {
538            image: Some(image),
539            hostname: Some(container_name.clone()),
540            exposed_ports: Some(HashMap::from([
541                (format!("{}/tcp", ws_port), HashMap::new()),
542            ])),
543            host_config: Some(HostConfig {
544                port_bindings: Some(HashMap::from([
545                    (
546                        format!("{}/tcp", ws_port),
547                        Some(vec![PortBinding {
548                            host_ip: Some("0.0.0.0".to_string()),
549                            host_port: Some(host_ws_port.to_string()),
550                        }]),
551                    ),
552                ])),
553                cap_add: Some(vec!["NET_ADMIN".to_string()]),
554                ..Default::default()
555            }),
556            env: Some(vec![
557                "RUST_LOG=info".to_string(),
558                "RUST_BACKTRACE=1".to_string(),
559            ]),
560            cmd: Some(vec![
561                "/app/freenet".to_string(),
562                "network".to_string(),
563                "--data-dir".to_string(), "/data".to_string(),
564                "--config-dir".to_string(), "/config".to_string(),
565                "--ws-api-address".to_string(), "0.0.0.0".to_string(),
566                "--ws-api-port".to_string(), ws_port.to_string(),
567                "--network-address".to_string(), "0.0.0.0".to_string(),
568                "--network-port".to_string(), network_port.to_string(),
569                "--public-network-address".to_string(), gateway_ip.to_string(),
570                "--public-network-port".to_string(), network_port.to_string(),
571                "--is-gateway".to_string(),
572                "--skip-load-from-network".to_string(),
573                "--transport-keypair".to_string(), "/config/keypair.pem".to_string(),
574            ]),
575            ..Default::default()
576        };
577
578        let container_id = self.docker
579            .create_container(
580                Some(CreateContainerOptions { name: container_name.clone(), ..Default::default() }),
581                config,
582            )
583            .await
584            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create gateway container: {}", e)))?
585            .id;
586
587        self.containers.push(container_id.clone());
588
589        // Connect to public network with specific IP
590        self.docker
591            .connect_network(
592                public_network_id,
593                bollard::network::ConnectNetworkOptions {
594                    container: container_id.clone(),
595                    endpoint_config: bollard::secret::EndpointSettings {
596                        ipam_config: Some(bollard::secret::EndpointIpamConfig {
597                            ipv4_address: Some(gateway_ip.to_string()),
598                            ..Default::default()
599                        }),
600                        ..Default::default()
601                    },
602                },
603            )
604            .await
605            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect gateway to network: {}", e)))?;
606
607        // Copy binary and keys into container
608        self.copy_binary_to_container(&container_id, binary_path).await?;
609        self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem").await?;
610        self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem").await?;
611
612        // Start container
613        self.docker
614            .start_container(&container_id, None::<StartContainerOptions<String>>)
615            .await
616            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start gateway: {}", e)))?;
617
618        let info = DockerPeerInfo {
619            container_id: container_id.clone(),
620            container_name: container_name.clone(),
621            private_ip: gateway_ip, // Gateways don't have private IP
622            public_ip: gateway_ip,
623            host_ws_port,
624            network_port,
625            is_gateway: true,
626            nat_router_id: None,
627        };
628
629        self.peer_containers.insert(index, info.clone());
630
631        let local_log_cache = run_root.join(format!("gw{}", index)).join("peer.log");
632
633        tracing::info!(
634            "Created gateway {} at {} (ws: localhost:{})",
635            container_name, gateway_ip, host_ws_port
636        );
637
638        Ok((info, DockerProcess {
639            docker: self.docker.clone(),
640            container_id,
641            container_name,
642            local_log_cache,
643        }))
644    }
645
646    /// Create a peer container behind NAT
647    pub async fn create_peer(
648        &mut self,
649        index: usize,
650        binary_path: &Path,
651        keypair_path: &Path,
652        public_key_path: &Path,
653        gateways_toml_path: &Path,
654        gateway_public_key_path: Option<&Path>,
655        ws_port: u16,
656        network_port: u16,
657        run_root: &Path,
658    ) -> Result<(DockerPeerInfo, DockerProcess)> {
659        let container_name = format!("{}-peer-{}", self.config.name_prefix, index);
660        let image = self.ensure_base_image().await?;
661
662        // Create NAT network for this peer
663        let (nat_network_id, router_id, router_public_ip) = self.create_nat_network(index).await?;
664
665        // Peer's private IP (behind NAT)
666        let private_ip = Ipv4Addr::new(10, index as u8 + 1, 0, 2);
667
668        // Allocate host port for WS API
669        let host_ws_port = crate::peer::get_free_port()?;
670
671        // Create container
672        let config = Config {
673            image: Some(image),
674            hostname: Some(container_name.clone()),
675            exposed_ports: Some(HashMap::from([
676                (format!("{}/tcp", ws_port), HashMap::new()),
677            ])),
678            host_config: Some(HostConfig {
679                port_bindings: Some(HashMap::from([
680                    (
681                        format!("{}/tcp", ws_port),
682                        Some(vec![PortBinding {
683                            host_ip: Some("0.0.0.0".to_string()),
684                            host_port: Some(host_ws_port.to_string()),
685                        }]),
686                    ),
687                ])),
688                cap_add: Some(vec!["NET_ADMIN".to_string()]),
689                ..Default::default()
690            }),
691            env: Some(vec![
692                "RUST_LOG=info".to_string(),
693                "RUST_BACKTRACE=1".to_string(),
694            ]),
695            cmd: Some(vec![
696                "/app/freenet".to_string(),
697                "network".to_string(),
698                "--data-dir".to_string(), "/data".to_string(),
699                "--config-dir".to_string(), "/config".to_string(),
700                "--ws-api-address".to_string(), "0.0.0.0".to_string(),
701                "--ws-api-port".to_string(), ws_port.to_string(),
702                "--network-address".to_string(), "0.0.0.0".to_string(),
703                "--network-port".to_string(), network_port.to_string(),
704                // Don't set public address - let Freenet discover it via gateway
705                "--skip-load-from-network".to_string(),
706                "--transport-keypair".to_string(), "/config/keypair.pem".to_string(),
707            ]),
708            ..Default::default()
709        };
710
711        let container_id = self.docker
712            .create_container(
713                Some(CreateContainerOptions { name: container_name.clone(), ..Default::default() }),
714                config,
715            )
716            .await
717            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create peer container: {}", e)))?
718            .id;
719
720        self.containers.push(container_id.clone());
721
722        // Connect to NAT network with specific IP
723        self.docker
724            .connect_network(
725                &nat_network_id,
726                bollard::network::ConnectNetworkOptions {
727                    container: container_id.clone(),
728                    endpoint_config: bollard::secret::EndpointSettings {
729                        ipam_config: Some(bollard::secret::EndpointIpamConfig {
730                            ipv4_address: Some(private_ip.to_string()),
731                            ..Default::default()
732                        }),
733                        gateway: Some(Ipv4Addr::new(10, index as u8 + 1, 0, 1).to_string()),
734                        ..Default::default()
735                    },
736                },
737            )
738            .await
739            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect peer to NAT network: {}", e)))?;
740
741        // Copy binary and keys into container
742        self.copy_binary_to_container(&container_id, binary_path).await?;
743        self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem").await?;
744        self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem").await?;
745        self.copy_file_to_container(&container_id, gateways_toml_path, "/config/gateways.toml").await?;
746
747        // Copy gateway public key if provided
748        if let Some(gw_pubkey_path) = gateway_public_key_path {
749            self.copy_file_to_container(&container_id, gw_pubkey_path, "/config/gw_public_key.pem").await?;
750        }
751
752        // Start container
753        self.docker
754            .start_container(&container_id, None::<StartContainerOptions<String>>)
755            .await
756            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start peer: {}", e)))?;
757
758        // Configure routing through NAT router (.254 is our router's private IP)
759        let router_gateway = Ipv4Addr::new(10, index as u8 + 1, 0, 254);
760        self.exec_in_container(
761            &container_id,
762            &["sh", "-c", &format!(
763                "ip route del default 2>/dev/null; ip route add default via {}",
764                router_gateway
765            )],
766        ).await?;
767
768        let info = DockerPeerInfo {
769            container_id: container_id.clone(),
770            container_name: container_name.clone(),
771            private_ip,
772            public_ip: router_public_ip,
773            host_ws_port,
774            network_port,
775            is_gateway: false,
776            nat_router_id: Some(router_id),
777        };
778
779        self.peer_containers.insert(index, info.clone());
780
781        let local_log_cache = run_root.join(format!("peer{}", index)).join("peer.log");
782
783        tracing::info!(
784            "Created peer {} at {} behind NAT {} (ws: localhost:{})",
785            container_name, private_ip, router_public_ip, host_ws_port
786        );
787
788        Ok((info, DockerProcess {
789            docker: self.docker.clone(),
790            container_id,
791            container_name,
792            local_log_cache,
793        }))
794    }
795
796    /// Copy a file into a container (public version)
797    pub async fn copy_file_to_container_pub(
798        &self,
799        container_id: &str,
800        local_path: &Path,
801        container_path: &str,
802    ) -> Result<()> {
803        self.copy_file_to_container(container_id, local_path, container_path).await
804    }
805
806    /// Copy a file into a container
807    async fn copy_file_to_container(
808        &self,
809        container_id: &str,
810        local_path: &Path,
811        container_path: &str,
812    ) -> Result<()> {
813        let file_data = std::fs::read(local_path)?;
814        let file_name = Path::new(container_path)
815            .file_name()
816            .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
817            .to_str()
818            .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid file name")))?;
819
820        let dir_path = Path::new(container_path)
821            .parent()
822            .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
823            .to_str()
824            .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid directory path")))?;
825
826        // Create tar archive
827        let mut tar_builder = tar::Builder::new(Vec::new());
828        let mut header = tar::Header::new_gnu();
829        header.set_path(file_name)?;
830        header.set_size(file_data.len() as u64);
831        header.set_mode(0o644);
832        header.set_cksum();
833        tar_builder.append(&header, file_data.as_slice())?;
834        let tar_data = tar_builder.into_inner()?;
835
836        self.docker
837            .upload_to_container(
838                container_id,
839                Some(UploadToContainerOptions {
840                    path: dir_path,
841                    ..Default::default()
842                }),
843                tar_data.into(),
844            )
845            .await
846            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy file: {}", e)))?;
847
848        Ok(())
849    }
850
851    /// Execute a command in a container
852    async fn exec_in_container(&self, container_id: &str, cmd: &[&str]) -> Result<String> {
853        let exec = self.docker
854            .create_exec(
855                container_id,
856                CreateExecOptions {
857                    cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
858                    attach_stdout: Some(true),
859                    attach_stderr: Some(true),
860                    ..Default::default()
861                },
862            )
863            .await
864            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create exec: {}", e)))?;
865
866        let output = self.docker
867            .start_exec(&exec.id, None)
868            .await
869            .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start exec: {}", e)))?;
870
871        let mut result = String::new();
872        if let StartExecResults::Attached { mut output, .. } = output {
873            while let Some(Ok(msg)) = output.next().await {
874                match msg {
875                    LogOutput::StdOut { message } | LogOutput::StdErr { message } => {
876                        result.push_str(&String::from_utf8_lossy(&message));
877                    }
878                    _ => {}
879                }
880            }
881        }
882
883        Ok(result)
884    }
885
886    /// Clean up all Docker resources created by this backend
887    pub async fn cleanup(&mut self) -> Result<()> {
888        tracing::info!("Cleaning up Docker NAT resources...");
889
890        // Stop and remove containers
891        for container_id in self.containers.drain(..) {
892            let _ = self.docker
893                .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
894                .await;
895            let _ = self.docker
896                .remove_container(
897                    &container_id,
898                    Some(RemoveContainerOptions { force: true, ..Default::default() }),
899                )
900                .await;
901        }
902
903        // Remove networks
904        for network_id in self.networks.drain(..) {
905            let _ = self.docker.remove_network(&network_id).await;
906        }
907
908        self.peer_containers.clear();
909        self.public_network_id = None;
910
911        Ok(())
912    }
913
914    /// Get peer info by index
915    pub fn get_peer_info(&self, index: usize) -> Option<&DockerPeerInfo> {
916        self.peer_containers.get(&index)
917    }
918}
919
920impl Drop for DockerNatBackend {
921    fn drop(&mut self) {
922        if self.config.cleanup_on_drop {
923            // Spawn cleanup in background since Drop is sync
924            let docker = self.docker.clone();
925            let containers = std::mem::take(&mut self.containers);
926            let networks = std::mem::take(&mut self.networks);
927
928            std::thread::spawn(move || {
929                let rt = tokio::runtime::Runtime::new().unwrap();
930                rt.block_on(async {
931                    for container_id in containers {
932                        let _ = docker
933                            .stop_container(&container_id, Some(StopContainerOptions { t: 1 }))
934                            .await;
935                        let _ = docker
936                            .remove_container(
937                                &container_id,
938                                Some(RemoveContainerOptions { force: true, ..Default::default() }),
939                            )
940                            .await;
941                    }
942                    for network_id in networks {
943                        let _ = docker.remove_network(&network_id).await;
944                    }
945                });
946            });
947        }
948    }
949}