1use crate::{logs::LogEntry, process::PeerProcess, Error, Result};
8use bollard::{
9 container::{
10 Config, CreateContainerOptions, LogOutput, LogsOptions, RemoveContainerOptions,
11 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#[derive(Debug, Clone)]
31pub struct DockerNatConfig {
32 pub topology: NatTopology,
34 pub public_subnet: Ipv4Network,
36 pub private_subnet_base: Ipv4Addr,
38 pub cleanup_on_drop: bool,
40 pub name_prefix: String,
42}
43
44impl Default for DockerNatConfig {
45 fn default() -> Self {
46 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S").to_string();
48 let random_id = rand::thread_rng().gen::<u16>();
49 let name_prefix = format!("freenet-nat-{}-{}", timestamp, random_id);
50
51 let second_octet = rand::thread_rng().gen_range(16..=31);
57 let public_subnet = format!("172.{}.0.0/16", second_octet).parse().unwrap();
58
59 let private_first_octet = rand::thread_rng().gen_range(1..=250);
62
63 Self {
64 topology: NatTopology::OnePerNat,
65 public_subnet,
66 private_subnet_base: Ipv4Addr::new(10, private_first_octet, 0, 0),
67 cleanup_on_drop: true,
68 name_prefix,
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub enum NatTopology {
76 OnePerNat,
78 Custom(Vec<NatNetwork>),
80}
81
82#[derive(Debug, Clone)]
84pub struct NatNetwork {
85 pub name: String,
86 pub peer_indices: Vec<usize>,
87 pub nat_type: NatType,
88}
89
90#[derive(Debug, Clone, Default)]
92pub enum NatType {
93 #[default]
95 RestrictedCone,
96 FullCone { forwarded_ports: Option<Vec<u16>> },
98}
99
100pub struct DockerNatBackend {
102 docker: Docker,
103 config: DockerNatConfig,
104 networks: Vec<String>,
106 containers: Vec<String>,
108 peer_containers: HashMap<usize, DockerPeerInfo>,
110 public_network_id: Option<String>,
112}
113
114#[derive(Debug, Clone)]
116pub struct DockerPeerInfo {
117 pub container_id: String,
118 pub container_name: String,
119 pub private_ip: Ipv4Addr,
121 pub public_ip: Ipv4Addr,
123 pub host_ws_port: u16,
125 pub network_port: u16,
127 pub is_gateway: bool,
129 pub nat_router_id: Option<String>,
131}
132
133pub struct DockerProcess {
135 docker: Docker,
136 container_id: String,
137 container_name: String,
138 local_log_cache: PathBuf,
139}
140
141impl PeerProcess for DockerProcess {
142 fn is_running(&self) -> bool {
143 let docker = self.docker.clone();
145 let id = self.container_id.clone();
146
147 tokio::task::block_in_place(|| {
148 tokio::runtime::Handle::current().block_on(async {
149 match docker.inspect_container(&id, None).await {
150 Ok(info) => info
151 .state
152 .and_then(|s| s.status)
153 .map(|s| s == ContainerStateStatusEnum::RUNNING)
154 .unwrap_or(false),
155 Err(_) => false,
156 }
157 })
158 })
159 }
160
161 fn kill(&mut self) -> Result<()> {
162 let docker = self.docker.clone();
163 let id = self.container_id.clone();
164
165 tokio::task::block_in_place(|| {
166 tokio::runtime::Handle::current().block_on(async {
167 let _ = docker
169 .stop_container(&id, Some(StopContainerOptions { t: 5 }))
170 .await;
171 Ok(())
172 })
173 })
174 }
175
176 fn log_path(&self) -> PathBuf {
177 self.local_log_cache.clone()
178 }
179
180 fn read_logs(&self) -> Result<Vec<LogEntry>> {
181 let docker = self.docker.clone();
182 let id = self.container_id.clone();
183 let cache_path = self.local_log_cache.clone();
184
185 tokio::task::block_in_place(|| {
186 tokio::runtime::Handle::current().block_on(async {
187 let options = LogsOptions::<String> {
189 stdout: true,
190 stderr: true,
191 timestamps: true,
192 ..Default::default()
193 };
194
195 let mut logs = docker.logs(&id, Some(options));
196 let mut log_content = String::new();
197
198 while let Some(log_result) = logs.next().await {
199 match log_result {
200 Ok(LogOutput::StdOut { message }) | Ok(LogOutput::StdErr { message }) => {
201 log_content.push_str(&String::from_utf8_lossy(&message));
202 }
203 _ => {}
204 }
205 }
206
207 if let Some(parent) = cache_path.parent() {
209 std::fs::create_dir_all(parent)?;
210 }
211 std::fs::write(&cache_path, &log_content)?;
212
213 crate::logs::read_log_file(&cache_path)
215 })
216 })
217 }
218}
219
220impl Drop for DockerProcess {
221 fn drop(&mut self) {
222 let _ = self.kill();
223 }
224}
225
226impl DockerNatBackend {
227 pub async fn new(config: DockerNatConfig) -> Result<Self> {
229 let docker = Docker::connect_with_local_defaults()
230 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect to Docker: {}", e)))?;
231
232 docker
234 .ping()
235 .await
236 .map_err(|e| Error::Other(anyhow::anyhow!("Docker ping failed: {}", e)))?;
237
238 Self::cleanup_stale_resources(&docker, Duration::from_secs(10)).await?;
244
245 Ok(Self {
246 docker,
247 config,
248 networks: Vec::new(),
249 containers: Vec::new(),
250 peer_containers: HashMap::new(),
251 public_network_id: None,
252 })
253 }
254
255 async fn cleanup_stale_resources(docker: &Docker, max_age: Duration) -> Result<()> {
261 use bollard::container::ListContainersOptions;
262 use bollard::network::ListNetworksOptions;
263
264 let now = std::time::SystemTime::now();
265 let now_secs = now.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
266 let cutoff = if max_age.is_zero() {
268 i64::MAX } else {
270 now_secs - max_age.as_secs() as i64
271 };
272
273 if max_age.is_zero() {
274 tracing::debug!("Cleaning up ALL freenet-nat resources");
275 } else {
276 tracing::debug!(
277 "Cleaning up freenet-nat resources older than {} seconds",
278 max_age.as_secs()
279 );
280 }
281
282 let mut filters = HashMap::new();
284 filters.insert("name".to_string(), vec!["freenet-nat-".to_string()]);
285
286 let options = ListContainersOptions {
287 all: true,
288 filters,
289 ..Default::default()
290 };
291
292 match docker.list_containers(Some(options)).await {
293 Ok(containers) => {
294 let mut removed_count = 0;
295 for container in containers {
296 if let Some(name) = container.names.and_then(|n| n.first().cloned()) {
298 if let Some(created) = container.created {
299 if created < cutoff {
300 if let Some(id) = container.id {
301 tracing::info!(
302 "Removing stale container: {} (age: {}s)",
303 name,
304 now.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
305 as i64
306 - created
307 );
308 let _ = docker
309 .stop_container(&id, Some(StopContainerOptions { t: 2 }))
310 .await;
311 let _ = docker
312 .remove_container(
313 &id,
314 Some(RemoveContainerOptions {
315 force: true,
316 ..Default::default()
317 }),
318 )
319 .await;
320 removed_count += 1;
321 }
322 }
323 }
324 }
325 }
326 if removed_count > 0 {
327 tracing::info!("Removed {} stale container(s)", removed_count);
328 }
329 }
330 Err(e) => {
331 tracing::warn!("Failed to list containers for cleanup: {}", e);
332 }
333 }
334
335 let mut filters = HashMap::new();
337 filters.insert("name".to_string(), vec!["freenet-nat-".to_string()]);
338
339 let options = ListNetworksOptions { filters };
340
341 match docker.list_networks(Some(options)).await {
342 Ok(networks) => {
343 let mut removed_count = 0;
344 for network in networks {
345 if let Some(name) = &network.name {
346 if name.starts_with("freenet-nat-") {
347 if let Some(timestamp_str) = name.strip_prefix("freenet-nat-") {
349 let parts: Vec<&str> = timestamp_str.split('-').collect();
351 if parts.len() >= 2 {
352 let date_time = format!("{}-{}", parts[0], parts[1]);
353 if let Ok(created_time) = chrono::NaiveDateTime::parse_from_str(
354 &date_time,
355 "%Y%m%d-%H%M%S",
356 ) {
357 let created_timestamp = created_time.and_utc().timestamp();
358 if created_timestamp < cutoff {
359 if let Some(id) = &network.id {
360 tracing::info!(
361 "Removing stale network: {} (age: {}s)",
362 name,
363 now.duration_since(std::time::UNIX_EPOCH)
364 .unwrap()
365 .as_secs()
366 as i64
367 - created_timestamp
368 );
369 let _ = docker.remove_network(id).await;
370 removed_count += 1;
371 }
372 }
373 }
374 }
375 }
376 }
377 }
378 }
379 if removed_count > 0 {
380 tracing::info!("Removed {} stale network(s)", removed_count);
381 }
382 }
383 Err(e) => {
384 tracing::warn!("Failed to list networks for cleanup: {}", e);
385 }
386 }
387
388 Ok(())
389 }
390
391 pub async fn create_public_network(&mut self) -> Result<String> {
396 const MAX_SUBNET_RETRIES: usize = 10;
397
398 for attempt in 0..MAX_SUBNET_RETRIES {
399 let network_name = format!("{}-public", self.config.name_prefix);
400
401 let options = CreateNetworkOptions {
402 name: network_name.clone(),
403 driver: "bridge".to_string(),
404 ipam: Ipam {
405 config: Some(vec![IpamConfig {
406 subnet: Some(self.config.public_subnet.to_string()),
407 ..Default::default()
408 }]),
409 ..Default::default()
410 },
411 ..Default::default()
412 };
413
414 match self.docker.create_network(options).await {
415 Ok(response) => {
416 let network_id = response.id;
417 self.networks.push(network_id.clone());
418 self.public_network_id = Some(network_id.clone());
419 tracing::info!(
420 "Created public network: {} ({}) with subnet {}",
421 network_name,
422 network_id,
423 self.config.public_subnet
424 );
425 return Ok(network_id);
426 }
427 Err(e) => {
428 let error_msg = e.to_string();
429 if error_msg.contains("Pool overlaps") {
430 let old_subnet = self.config.public_subnet;
432 let new_second_octet = rand::thread_rng().gen_range(16..=31);
433 self.config.public_subnet =
434 format!("172.{}.0.0/16", new_second_octet).parse().unwrap();
435 tracing::warn!(
436 "Subnet {} conflicts with existing network, retrying with {} (attempt {}/{})",
437 old_subnet,
438 self.config.public_subnet,
439 attempt + 1,
440 MAX_SUBNET_RETRIES
441 );
442 continue;
443 }
444 return Err(Error::Other(anyhow::anyhow!(
445 "Failed to create public network: {}",
446 e
447 )));
448 }
449 }
450 }
451
452 Err(Error::Other(anyhow::anyhow!(
453 "Failed to create public network after {} attempts due to subnet conflicts. \
454 This may indicate stale Docker networks. Try running: \
455 docker network ls | grep freenet-nat | awk '{{print $1}}' | xargs -r docker network rm",
456 MAX_SUBNET_RETRIES
457 )))
458 }
459
460 pub async fn create_nat_network(
462 &mut self,
463 peer_index: usize,
464 ) -> Result<(String, String, Ipv4Addr)> {
465 let network_name = format!("{}-nat-{}", self.config.name_prefix, peer_index);
468 let base = self.config.private_subnet_base.octets();
469 let subnet = Ipv4Network::new(
470 Ipv4Addr::new(base[0], base[1].wrapping_add(peer_index as u8), 0, 0),
471 24,
472 )
473 .map_err(|e| Error::Other(anyhow::anyhow!("Invalid subnet: {}", e)))?;
474
475 let options = CreateNetworkOptions {
476 name: network_name.clone(),
477 driver: "bridge".to_string(),
478 internal: true, ipam: Ipam {
480 config: Some(vec![IpamConfig {
481 subnet: Some(subnet.to_string()),
482 ..Default::default()
483 }]),
484 ..Default::default()
485 },
486 ..Default::default()
487 };
488
489 let response =
490 self.docker.create_network(options).await.map_err(|e| {
491 Error::Other(anyhow::anyhow!("Failed to create NAT network: {}", e))
492 })?;
493
494 let network_id = response.id;
495 self.networks.push(network_id.clone());
496
497 let router_name = format!("{}-router-{}", self.config.name_prefix, peer_index);
499 let public_network_id = self
500 .public_network_id
501 .as_ref()
502 .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
503
504 let router_public_ip = Ipv4Addr::new(
509 self.config.public_subnet.ip().octets()[0],
510 self.config.public_subnet.ip().octets()[1],
511 peer_index as u8, 100, );
514 let router_private_ip =
516 Ipv4Addr::new(base[0], base[1].wrapping_add(peer_index as u8), 0, 254);
517
518 let public_octets = self.config.public_subnet.ip().octets();
522 let public_pattern = format!("172\\.{}\\.", public_octets[1]);
523 let private_pattern = format!(" {}\\.", base[0]);
524 let router_config = Config {
525 image: Some("alpine:latest".to_string()),
526 hostname: Some(router_name.clone()),
527 cmd: Some(vec![
528 "sh".to_string(),
529 "-c".to_string(),
530 format!(
535 "apk add --no-cache iptables iproute2 > /dev/null 2>&1 && \
536 PUBLIC_IF=$(ip -o addr show | grep '{}' | awk '{{print $2}}') && \
537 PRIVATE_IF=$(ip -o addr show | grep '{}' | awk '{{print $2}}') && \
538 PUBLIC_IP=$(ip -o addr show dev $PUBLIC_IF | awk '/inet / {{split($4,a,\"/\"); print a[1]}}') && \
539 echo \"Public interface: $PUBLIC_IF ($PUBLIC_IP), Private interface: $PRIVATE_IF\" && \
540 iptables -t nat -A POSTROUTING -o $PUBLIC_IF -p udp -j SNAT --to-source $PUBLIC_IP:31337 && \
541 iptables -t nat -A POSTROUTING -o $PUBLIC_IF ! -p udp -j MASQUERADE && \
542 iptables -A FORWARD -i $PRIVATE_IF -o $PUBLIC_IF -j ACCEPT && \
543 iptables -A FORWARD -i $PUBLIC_IF -o $PRIVATE_IF -j ACCEPT && \
544 echo 'Cone NAT router ready' && \
545 tail -f /dev/null",
546 public_pattern, private_pattern
547 ),
548 ]),
549 host_config: Some(HostConfig {
550 cap_add: Some(vec!["NET_ADMIN".to_string()]),
551 sysctls: Some(HashMap::from([
552 ("net.ipv4.ip_forward".to_string(), "1".to_string()),
553 ])),
554 ..Default::default()
555 }),
556 ..Default::default()
557 };
558
559 let router_id = self
560 .docker
561 .create_container(
562 Some(CreateContainerOptions {
563 name: router_name.clone(),
564 ..Default::default()
565 }),
566 router_config,
567 )
568 .await
569 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create NAT router: {}", e)))?
570 .id;
571
572 self.containers.push(router_id.clone());
573
574 let _ = self
576 .docker
577 .disconnect_network(
578 "bridge",
579 bollard::network::DisconnectNetworkOptions {
580 container: router_id.clone(),
581 force: true,
582 },
583 )
584 .await;
585
586 self.docker
588 .connect_network(
589 public_network_id,
590 bollard::network::ConnectNetworkOptions {
591 container: router_id.clone(),
592 endpoint_config: bollard::secret::EndpointSettings {
593 ipam_config: Some(bollard::secret::EndpointIpamConfig {
594 ipv4_address: Some(router_public_ip.to_string()),
595 ..Default::default()
596 }),
597 ..Default::default()
598 },
599 },
600 )
601 .await
602 .map_err(|e| {
603 Error::Other(anyhow::anyhow!(
604 "Failed to connect router to public network: {}",
605 e
606 ))
607 })?;
608
609 self.docker
611 .connect_network(
612 &network_id,
613 bollard::network::ConnectNetworkOptions {
614 container: router_id.clone(),
615 endpoint_config: bollard::secret::EndpointSettings {
616 ipam_config: Some(bollard::secret::EndpointIpamConfig {
617 ipv4_address: Some(router_private_ip.to_string()),
618 ..Default::default()
619 }),
620 ..Default::default()
621 },
622 },
623 )
624 .await
625 .map_err(|e| {
626 Error::Other(anyhow::anyhow!(
627 "Failed to connect router to private network: {}",
628 e
629 ))
630 })?;
631
632 self.docker
634 .start_container(&router_id, None::<StartContainerOptions<String>>)
635 .await
636 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start NAT router: {}", e)))?;
637
638 tokio::time::sleep(Duration::from_secs(2)).await;
640
641 tracing::info!(
642 "Created NAT network {} with router {} (public: {}, private: {})",
643 network_name,
644 router_name,
645 router_public_ip,
646 router_private_ip
647 );
648
649 Ok((network_id, router_id, router_public_ip))
650 }
651
652 pub async fn ensure_base_image(&self) -> Result<String> {
654 let image_name = "freenet-test-peer:latest";
655
656 if self.docker.inspect_image(image_name).await.is_ok() {
658 tracing::debug!("Base image {} already exists", image_name);
659 return Ok(image_name.to_string());
660 }
661
662 tracing::info!("Building base image {}...", image_name);
663
664 let dockerfile = r#"
666FROM ubuntu:24.04
667RUN apt-get update && \
668 apt-get install -y --no-install-recommends \
669 libssl3 \
670 ca-certificates \
671 iproute2 \
672 && rm -rf /var/lib/apt/lists/*
673RUN mkdir -p /data /config
674WORKDIR /app
675"#;
676
677 let mut tar_builder = tar::Builder::new(Vec::new());
679 let mut header = tar::Header::new_gnu();
680 header.set_path("Dockerfile")?;
681 header.set_size(dockerfile.len() as u64);
682 header.set_mode(0o644);
683 header.set_cksum();
684 tar_builder.append(&header, dockerfile.as_bytes())?;
685 let tar_data = tar_builder.into_inner()?;
686
687 let options = BuildImageOptions {
689 dockerfile: "Dockerfile",
690 t: image_name,
691 rm: true,
692 ..Default::default()
693 };
694
695 let mut build_stream = self
696 .docker
697 .build_image(options, None, Some(tar_data.into()));
698
699 while let Some(result) = build_stream.next().await {
700 match result {
701 Ok(info) => {
702 if let Some(stream) = info.stream {
703 tracing::debug!("Build: {}", stream.trim());
704 }
705 if let Some(error) = info.error {
706 return Err(Error::Other(anyhow::anyhow!(
707 "Image build error: {}",
708 error
709 )));
710 }
711 }
712 Err(e) => {
713 return Err(Error::Other(anyhow::anyhow!("Image build failed: {}", e)));
714 }
715 }
716 }
717
718 tracing::info!("Built base image {}", image_name);
719 Ok(image_name.to_string())
720 }
721
722 pub async fn copy_binary_to_container(
724 &self,
725 container_id: &str,
726 binary_path: &Path,
727 ) -> Result<()> {
728 let binary_data = std::fs::read(binary_path)?;
730
731 let mut tar_builder = tar::Builder::new(Vec::new());
733 let mut header = tar::Header::new_gnu();
734 header.set_path("freenet")?;
735 header.set_size(binary_data.len() as u64);
736 header.set_mode(0o755);
737 header.set_cksum();
738 tar_builder.append(&header, binary_data.as_slice())?;
739 let tar_data = tar_builder.into_inner()?;
740
741 self.docker
743 .upload_to_container(
744 container_id,
745 Some(UploadToContainerOptions {
746 path: "/app",
747 ..Default::default()
748 }),
749 tar_data.into(),
750 )
751 .await
752 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy binary: {}", e)))?;
753
754 Ok(())
755 }
756
757 pub async fn create_gateway(
759 &mut self,
760 index: usize,
761 binary_path: &Path,
762 keypair_path: &Path,
763 public_key_path: &Path,
764 ws_port: u16,
765 network_port: u16,
766 run_root: &Path,
767 ) -> Result<(DockerPeerInfo, DockerProcess)> {
768 let container_name = format!("{}-gw-{}", self.config.name_prefix, index);
769 let image = self.ensure_base_image().await?;
770
771 let public_network_id = self
772 .public_network_id
773 .as_ref()
774 .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
775
776 let gateway_ip = Ipv4Addr::new(
778 self.config.public_subnet.ip().octets()[0],
779 self.config.public_subnet.ip().octets()[1],
780 0,
781 10 + index as u8,
782 );
783
784 let host_ws_port = crate::peer::get_free_port()?;
786
787 let config = Config {
789 image: Some(image),
790 hostname: Some(container_name.clone()),
791 exposed_ports: Some(HashMap::from([(
792 format!("{}/tcp", ws_port),
793 HashMap::new(),
794 )])),
795 host_config: Some(HostConfig {
796 port_bindings: Some(HashMap::from([(
797 format!("{}/tcp", ws_port),
798 Some(vec![PortBinding {
799 host_ip: Some("0.0.0.0".to_string()),
800 host_port: Some(host_ws_port.to_string()),
801 }]),
802 )])),
803 cap_add: Some(vec!["NET_ADMIN".to_string()]),
804 ..Default::default()
805 }),
806 env: Some(vec![
807 "RUST_LOG=info".to_string(),
808 "RUST_BACKTRACE=1".to_string(),
809 ]),
810 cmd: Some(vec![
811 "/app/freenet".to_string(),
812 "network".to_string(),
813 "--data-dir".to_string(),
814 "/data".to_string(),
815 "--config-dir".to_string(),
816 "/config".to_string(),
817 "--ws-api-address".to_string(),
818 "0.0.0.0".to_string(),
819 "--ws-api-port".to_string(),
820 ws_port.to_string(),
821 "--network-address".to_string(),
822 "0.0.0.0".to_string(),
823 "--network-port".to_string(),
824 network_port.to_string(),
825 "--public-network-address".to_string(),
826 gateway_ip.to_string(),
827 "--public-network-port".to_string(),
828 network_port.to_string(),
829 "--is-gateway".to_string(),
830 "--skip-load-from-network".to_string(),
831 "--transport-keypair".to_string(),
832 "/config/keypair.pem".to_string(),
833 ]),
834 ..Default::default()
835 };
836
837 let container_id = self
838 .docker
839 .create_container(
840 Some(CreateContainerOptions {
841 name: container_name.clone(),
842 ..Default::default()
843 }),
844 config,
845 )
846 .await
847 .map_err(|e| {
848 Error::Other(anyhow::anyhow!("Failed to create gateway container: {}", e))
849 })?
850 .id;
851
852 self.containers.push(container_id.clone());
853
854 self.docker
856 .connect_network(
857 public_network_id,
858 bollard::network::ConnectNetworkOptions {
859 container: container_id.clone(),
860 endpoint_config: bollard::secret::EndpointSettings {
861 ipam_config: Some(bollard::secret::EndpointIpamConfig {
862 ipv4_address: Some(gateway_ip.to_string()),
863 ..Default::default()
864 }),
865 ..Default::default()
866 },
867 },
868 )
869 .await
870 .map_err(|e| {
871 Error::Other(anyhow::anyhow!(
872 "Failed to connect gateway to network: {}",
873 e
874 ))
875 })?;
876
877 self.copy_binary_to_container(&container_id, binary_path)
879 .await?;
880 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem")
881 .await?;
882 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem")
883 .await?;
884
885 self.docker
887 .start_container(&container_id, None::<StartContainerOptions<String>>)
888 .await
889 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start gateway: {}", e)))?;
890
891 let info = DockerPeerInfo {
892 container_id: container_id.clone(),
893 container_name: container_name.clone(),
894 private_ip: gateway_ip, public_ip: gateway_ip,
896 host_ws_port,
897 network_port,
898 is_gateway: true,
899 nat_router_id: None,
900 };
901
902 self.peer_containers.insert(index, info.clone());
903
904 let local_log_cache = run_root.join(format!("gw{}", index)).join("peer.log");
905
906 tracing::info!(
907 "Created gateway {} at {} (ws: localhost:{})",
908 container_name,
909 gateway_ip,
910 host_ws_port
911 );
912
913 Ok((
914 info,
915 DockerProcess {
916 docker: self.docker.clone(),
917 container_id,
918 container_name,
919 local_log_cache,
920 },
921 ))
922 }
923
924 pub async fn create_peer(
926 &mut self,
927 index: usize,
928 binary_path: &Path,
929 keypair_path: &Path,
930 public_key_path: &Path,
931 gateways_toml_path: &Path,
932 gateway_public_key_path: Option<&Path>,
933 ws_port: u16,
934 network_port: u16,
935 run_root: &Path,
936 ) -> Result<(DockerPeerInfo, DockerProcess)> {
937 let container_name = format!("{}-peer-{}", self.config.name_prefix, index);
938 let image = self.ensure_base_image().await?;
939
940 let (nat_network_id, router_id, router_public_ip) = self.create_nat_network(index).await?;
942
943 let base = self.config.private_subnet_base.octets();
945 let private_ip = Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 2);
946
947 let host_ws_port = crate::peer::get_free_port()?;
949
950 let config = Config {
952 image: Some(image),
953 hostname: Some(container_name.clone()),
954 exposed_ports: Some(HashMap::from([(
955 format!("{}/tcp", ws_port),
956 HashMap::new(),
957 )])),
958 host_config: Some(HostConfig {
959 port_bindings: Some(HashMap::from([(
960 format!("{}/tcp", ws_port),
961 Some(vec![PortBinding {
962 host_ip: Some("0.0.0.0".to_string()),
963 host_port: Some(host_ws_port.to_string()),
964 }]),
965 )])),
966 cap_add: Some(vec!["NET_ADMIN".to_string()]),
967 ..Default::default()
968 }),
969 env: Some(vec![
970 "RUST_LOG=info".to_string(),
971 "RUST_BACKTRACE=1".to_string(),
972 ]),
973 cmd: Some(vec![
974 "/app/freenet".to_string(),
975 "network".to_string(),
976 "--data-dir".to_string(),
977 "/data".to_string(),
978 "--config-dir".to_string(),
979 "/config".to_string(),
980 "--ws-api-address".to_string(),
981 "0.0.0.0".to_string(),
982 "--ws-api-port".to_string(),
983 ws_port.to_string(),
984 "--network-address".to_string(),
985 "0.0.0.0".to_string(),
986 "--network-port".to_string(),
987 network_port.to_string(),
988 "--skip-load-from-network".to_string(),
990 "--transport-keypair".to_string(),
991 "/config/keypair.pem".to_string(),
992 ]),
993 ..Default::default()
994 };
995
996 let container_id = self
997 .docker
998 .create_container(
999 Some(CreateContainerOptions {
1000 name: container_name.clone(),
1001 ..Default::default()
1002 }),
1003 config,
1004 )
1005 .await
1006 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create peer container: {}", e)))?
1007 .id;
1008
1009 self.containers.push(container_id.clone());
1010
1011 self.docker
1014 .connect_network(
1015 &nat_network_id,
1016 bollard::network::ConnectNetworkOptions {
1017 container: container_id.clone(),
1018 endpoint_config: bollard::secret::EndpointSettings {
1019 ipam_config: Some(bollard::secret::EndpointIpamConfig {
1020 ipv4_address: Some(private_ip.to_string()),
1021 ..Default::default()
1022 }),
1023 gateway: Some(
1024 Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 1)
1025 .to_string(),
1026 ),
1027 ..Default::default()
1028 },
1029 },
1030 )
1031 .await
1032 .map_err(|e| {
1033 Error::Other(anyhow::anyhow!(
1034 "Failed to connect peer to NAT network: {}",
1035 e
1036 ))
1037 })?;
1038
1039 self.copy_binary_to_container(&container_id, binary_path)
1041 .await?;
1042 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem")
1043 .await?;
1044 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem")
1045 .await?;
1046 self.copy_file_to_container(&container_id, gateways_toml_path, "/config/gateways.toml")
1047 .await?;
1048
1049 if let Some(gw_pubkey_path) = gateway_public_key_path {
1051 self.copy_file_to_container(&container_id, gw_pubkey_path, "/config/gw_public_key.pem")
1052 .await?;
1053 }
1054
1055 self.docker
1057 .start_container(&container_id, None::<StartContainerOptions<String>>)
1058 .await
1059 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start peer: {}", e)))?;
1060
1061 let router_gateway = Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 254);
1064 let public_subnet = self.config.public_subnet;
1065 self.exec_in_container(
1066 &container_id,
1067 &[
1068 "sh",
1069 "-c",
1070 &format!("ip route add {} via {}", public_subnet, router_gateway),
1071 ],
1072 )
1073 .await?;
1074
1075 let info = DockerPeerInfo {
1076 container_id: container_id.clone(),
1077 container_name: container_name.clone(),
1078 private_ip,
1079 public_ip: router_public_ip,
1080 host_ws_port,
1081 network_port,
1082 is_gateway: false,
1083 nat_router_id: Some(router_id),
1084 };
1085
1086 self.peer_containers.insert(index, info.clone());
1087
1088 let local_log_cache = run_root.join(format!("peer{}", index)).join("peer.log");
1089
1090 tracing::info!(
1091 "Created peer {} at {} behind NAT {} (ws: localhost:{})",
1092 container_name,
1093 private_ip,
1094 router_public_ip,
1095 host_ws_port
1096 );
1097
1098 Ok((
1099 info,
1100 DockerProcess {
1101 docker: self.docker.clone(),
1102 container_id,
1103 container_name,
1104 local_log_cache,
1105 },
1106 ))
1107 }
1108
1109 pub async fn copy_file_to_container_pub(
1111 &self,
1112 container_id: &str,
1113 local_path: &Path,
1114 container_path: &str,
1115 ) -> Result<()> {
1116 self.copy_file_to_container(container_id, local_path, container_path)
1117 .await
1118 }
1119
1120 async fn copy_file_to_container(
1122 &self,
1123 container_id: &str,
1124 local_path: &Path,
1125 container_path: &str,
1126 ) -> Result<()> {
1127 let file_data = std::fs::read(local_path)?;
1128 let file_name = Path::new(container_path)
1129 .file_name()
1130 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
1131 .to_str()
1132 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid file name")))?;
1133
1134 let dir_path = Path::new(container_path)
1135 .parent()
1136 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
1137 .to_str()
1138 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid directory path")))?;
1139
1140 let mut tar_builder = tar::Builder::new(Vec::new());
1142 let mut header = tar::Header::new_gnu();
1143 header.set_path(file_name)?;
1144 header.set_size(file_data.len() as u64);
1145 header.set_mode(0o644);
1146 header.set_cksum();
1147 tar_builder.append(&header, file_data.as_slice())?;
1148 let tar_data = tar_builder.into_inner()?;
1149
1150 self.docker
1151 .upload_to_container(
1152 container_id,
1153 Some(UploadToContainerOptions {
1154 path: dir_path,
1155 ..Default::default()
1156 }),
1157 tar_data.into(),
1158 )
1159 .await
1160 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy file: {}", e)))?;
1161
1162 Ok(())
1163 }
1164
1165 async fn exec_in_container(&self, container_id: &str, cmd: &[&str]) -> Result<String> {
1167 let exec = self
1168 .docker
1169 .create_exec(
1170 container_id,
1171 CreateExecOptions {
1172 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
1173 attach_stdout: Some(true),
1174 attach_stderr: Some(true),
1175 ..Default::default()
1176 },
1177 )
1178 .await
1179 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create exec: {}", e)))?;
1180
1181 let output = self
1182 .docker
1183 .start_exec(&exec.id, None)
1184 .await
1185 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start exec: {}", e)))?;
1186
1187 let mut result = String::new();
1188 if let StartExecResults::Attached { mut output, .. } = output {
1189 while let Some(Ok(msg)) = output.next().await {
1190 match msg {
1191 LogOutput::StdOut { message } | LogOutput::StdErr { message } => {
1192 result.push_str(&String::from_utf8_lossy(&message));
1193 }
1194 _ => {}
1195 }
1196 }
1197 }
1198
1199 Ok(result)
1200 }
1201
1202 pub async fn cleanup(&mut self) -> Result<()> {
1204 tracing::info!("Cleaning up Docker NAT resources...");
1205
1206 for container_id in self.containers.drain(..) {
1208 let _ = self
1209 .docker
1210 .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
1211 .await;
1212 let _ = self
1213 .docker
1214 .remove_container(
1215 &container_id,
1216 Some(RemoveContainerOptions {
1217 force: true,
1218 ..Default::default()
1219 }),
1220 )
1221 .await;
1222 }
1223
1224 for network_id in self.networks.drain(..) {
1226 let _ = self.docker.remove_network(&network_id).await;
1227 }
1228
1229 self.peer_containers.clear();
1230 self.public_network_id = None;
1231
1232 Ok(())
1233 }
1234
1235 pub fn get_peer_info(&self, index: usize) -> Option<&DockerPeerInfo> {
1237 self.peer_containers.get(&index)
1238 }
1239}
1240
1241impl Drop for DockerNatBackend {
1242 fn drop(&mut self) {
1243 if self.config.cleanup_on_drop {
1244 tracing::info!("Cleaning up Docker NAT backend resources...");
1245
1246 let docker = self.docker.clone();
1248 let containers = std::mem::take(&mut self.containers);
1249 let networks = std::mem::take(&mut self.networks);
1250
1251 let cleanup = async {
1255 let container_futures = containers.into_iter().map(|container_id| {
1257 let docker = docker.clone();
1258 async move {
1259 if let Err(e) = docker
1260 .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
1261 .await
1262 {
1263 tracing::debug!("Failed to stop container {}: {}", container_id, e);
1264 }
1265 if let Err(e) = docker
1266 .remove_container(
1267 &container_id,
1268 Some(RemoveContainerOptions {
1269 force: true,
1270 ..Default::default()
1271 }),
1272 )
1273 .await
1274 {
1275 tracing::debug!("Failed to remove container {}: {}", container_id, e);
1276 }
1277 }
1278 });
1279
1280 futures::future::join_all(container_futures).await;
1282
1283 for network_id in networks {
1285 if let Err(e) = docker.remove_network(&network_id).await {
1286 tracing::debug!("Failed to remove network {}: {}", network_id, e);
1287 }
1288 }
1289
1290 tracing::info!("Docker NAT backend cleanup complete");
1291 };
1292
1293 if let Ok(handle) = tokio::runtime::Handle::try_current() {
1296 tokio::task::block_in_place(|| {
1297 handle.block_on(cleanup);
1298 });
1299 } else if let Ok(rt) = tokio::runtime::Runtime::new() {
1300 rt.block_on(cleanup);
1301 } else {
1302 tracing::error!("Failed to create runtime for cleanup");
1303 }
1304 }
1305 }
1306}