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 peer_private_ip = Ipv4Addr::new(base[0], base[1].wrapping_add(peer_index as u8), 0, 2);
526
527 let dnat_rules = if std::env::var("FREENET_TEST_FULL_CONE_NAT").is_ok() {
543 format!(
546 "iptables -t nat -A PREROUTING -i $PUBLIC_IF -p udp --dport 31337 -j DNAT --to-destination {}:31337 && \
547 echo 'Full Cone NAT: DNAT rule added for port 31337 -> {}:31337' && ",
548 peer_private_ip, peer_private_ip
549 )
550 } else if std::env::var("FREENET_TEST_SYMMETRIC_NAT").is_ok() {
551 format!(
554 "iptables -t nat -A POSTROUTING -o $PUBLIC_IF -p udp -j MASQUERADE --random && \
555 echo 'Symmetric NAT: Random port mapping enabled (hole punching will fail)' && "
556 )
557 } else {
558 format!(
574 "echo 'Port-Restricted Cone NAT: EIM + port-restricted filtering' && \
575 iptables -t nat -A PREROUTING -i $PUBLIC_IF -p udp --dport 31337 -j DNAT --to-destination {}:31337 && \
576 iptables -t nat -A POSTROUTING -o $PUBLIC_IF -p udp --sport 31337 -j SNAT --to-source $PUBLIC_IP:31337 && ",
577 peer_private_ip
578 )
579 };
580
581 let router_config = Config {
582 image: Some("alpine:latest".to_string()),
583 hostname: Some(router_name.clone()),
584 cmd: Some(vec![
585 "sh".to_string(),
586 "-c".to_string(),
587 format!(
592 "apk add --no-cache iptables iproute2 > /dev/null 2>&1 && \
593 PUBLIC_IF=$(ip -o addr show | grep '{}' | awk '{{print $2}}') && \
594 PRIVATE_IF=$(ip -o addr show | grep '{}' | awk '{{print $2}}') && \
595 PUBLIC_IP=$(ip -o addr show dev $PUBLIC_IF | awk '/inet / {{split($4,a,\"/\"); print a[1]}}') && \
596 echo \"Public interface: $PUBLIC_IF ($PUBLIC_IP), Private interface: $PRIVATE_IF\" && \
597 {}iptables -t nat -A POSTROUTING -o $PUBLIC_IF -j MASQUERADE && \
598 iptables -A FORWARD -i $PRIVATE_IF -o $PUBLIC_IF -j ACCEPT && \
599 iptables -A FORWARD -i $PUBLIC_IF -o $PRIVATE_IF -j ACCEPT && \
600 echo 'NAT router ready' && \
601 tail -f /dev/null",
602 public_pattern, private_pattern, dnat_rules
603 ),
604 ]),
605 host_config: Some(HostConfig {
606 cap_add: Some(vec!["NET_ADMIN".to_string()]),
607 sysctls: Some(HashMap::from([
608 ("net.ipv4.ip_forward".to_string(), "1".to_string()),
609 ])),
610 ..Default::default()
611 }),
612 ..Default::default()
613 };
614
615 let router_id = self
616 .docker
617 .create_container(
618 Some(CreateContainerOptions {
619 name: router_name.clone(),
620 ..Default::default()
621 }),
622 router_config,
623 )
624 .await
625 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create NAT router: {}", e)))?
626 .id;
627
628 self.containers.push(router_id.clone());
629
630 let _ = self
632 .docker
633 .disconnect_network(
634 "bridge",
635 bollard::network::DisconnectNetworkOptions {
636 container: router_id.clone(),
637 force: true,
638 },
639 )
640 .await;
641
642 self.docker
644 .connect_network(
645 public_network_id,
646 bollard::network::ConnectNetworkOptions {
647 container: router_id.clone(),
648 endpoint_config: bollard::secret::EndpointSettings {
649 ipam_config: Some(bollard::secret::EndpointIpamConfig {
650 ipv4_address: Some(router_public_ip.to_string()),
651 ..Default::default()
652 }),
653 ..Default::default()
654 },
655 },
656 )
657 .await
658 .map_err(|e| {
659 Error::Other(anyhow::anyhow!(
660 "Failed to connect router to public network: {}",
661 e
662 ))
663 })?;
664
665 self.docker
667 .connect_network(
668 &network_id,
669 bollard::network::ConnectNetworkOptions {
670 container: router_id.clone(),
671 endpoint_config: bollard::secret::EndpointSettings {
672 ipam_config: Some(bollard::secret::EndpointIpamConfig {
673 ipv4_address: Some(router_private_ip.to_string()),
674 ..Default::default()
675 }),
676 ..Default::default()
677 },
678 },
679 )
680 .await
681 .map_err(|e| {
682 Error::Other(anyhow::anyhow!(
683 "Failed to connect router to private network: {}",
684 e
685 ))
686 })?;
687
688 self.docker
690 .start_container(&router_id, None::<StartContainerOptions<String>>)
691 .await
692 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start NAT router: {}", e)))?;
693
694 tokio::time::sleep(Duration::from_secs(2)).await;
696
697 tracing::info!(
698 "Created NAT network {} with router {} (public: {}, private: {})",
699 network_name,
700 router_name,
701 router_public_ip,
702 router_private_ip
703 );
704
705 Ok((network_id, router_id, router_public_ip))
706 }
707
708 pub async fn ensure_base_image(&self) -> Result<String> {
710 let image_name = "freenet-test-peer:latest";
711
712 if self.docker.inspect_image(image_name).await.is_ok() {
714 tracing::debug!("Base image {} already exists", image_name);
715 return Ok(image_name.to_string());
716 }
717
718 tracing::info!("Building base image {}...", image_name);
719
720 let dockerfile = r#"
722FROM ubuntu:24.04
723RUN apt-get update && \
724 apt-get install -y --no-install-recommends \
725 libssl3 \
726 ca-certificates \
727 iproute2 \
728 && rm -rf /var/lib/apt/lists/*
729RUN mkdir -p /data /config
730WORKDIR /app
731"#;
732
733 let mut tar_builder = tar::Builder::new(Vec::new());
735 let mut header = tar::Header::new_gnu();
736 header.set_path("Dockerfile")?;
737 header.set_size(dockerfile.len() as u64);
738 header.set_mode(0o644);
739 header.set_cksum();
740 tar_builder.append(&header, dockerfile.as_bytes())?;
741 let tar_data = tar_builder.into_inner()?;
742
743 let options = BuildImageOptions {
745 dockerfile: "Dockerfile",
746 t: image_name,
747 rm: true,
748 ..Default::default()
749 };
750
751 let mut build_stream = self
752 .docker
753 .build_image(options, None, Some(tar_data.into()));
754
755 while let Some(result) = build_stream.next().await {
756 match result {
757 Ok(info) => {
758 if let Some(stream) = info.stream {
759 tracing::debug!("Build: {}", stream.trim());
760 }
761 if let Some(error) = info.error {
762 return Err(Error::Other(anyhow::anyhow!(
763 "Image build error: {}",
764 error
765 )));
766 }
767 }
768 Err(e) => {
769 return Err(Error::Other(anyhow::anyhow!("Image build failed: {}", e)));
770 }
771 }
772 }
773
774 tracing::info!("Built base image {}", image_name);
775 Ok(image_name.to_string())
776 }
777
778 pub async fn copy_binary_to_container(
780 &self,
781 container_id: &str,
782 binary_path: &Path,
783 ) -> Result<()> {
784 let binary_data = std::fs::read(binary_path)?;
786
787 let mut tar_builder = tar::Builder::new(Vec::new());
789 let mut header = tar::Header::new_gnu();
790 header.set_path("freenet")?;
791 header.set_size(binary_data.len() as u64);
792 header.set_mode(0o755);
793 header.set_cksum();
794 tar_builder.append(&header, binary_data.as_slice())?;
795 let tar_data = tar_builder.into_inner()?;
796
797 self.docker
799 .upload_to_container(
800 container_id,
801 Some(UploadToContainerOptions {
802 path: "/app",
803 ..Default::default()
804 }),
805 tar_data.into(),
806 )
807 .await
808 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy binary: {}", e)))?;
809
810 Ok(())
811 }
812
813 pub async fn create_gateway(
815 &mut self,
816 index: usize,
817 binary_path: &Path,
818 keypair_path: &Path,
819 public_key_path: &Path,
820 ws_port: u16,
821 network_port: u16,
822 run_root: &Path,
823 ) -> Result<(DockerPeerInfo, DockerProcess)> {
824 let container_name = format!("{}-gw-{}", self.config.name_prefix, index);
825 let image = self.ensure_base_image().await?;
826
827 let public_network_id = self
828 .public_network_id
829 .as_ref()
830 .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
831
832 let gateway_ip = Ipv4Addr::new(
834 self.config.public_subnet.ip().octets()[0],
835 self.config.public_subnet.ip().octets()[1],
836 0,
837 10 + index as u8,
838 );
839
840 let config = Config {
842 image: Some(image),
843 hostname: Some(container_name.clone()),
844 exposed_ports: Some(HashMap::from([(
845 format!("{}/tcp", ws_port),
846 HashMap::new(),
847 )])),
848 host_config: Some(HostConfig {
849 port_bindings: Some(HashMap::from([(
850 format!("{}/tcp", ws_port),
851 Some(vec![PortBinding {
852 host_ip: Some("0.0.0.0".to_string()),
853 host_port: None, }]),
855 )])),
856 cap_add: Some(vec!["NET_ADMIN".to_string()]),
857 ..Default::default()
858 }),
859 env: Some(vec![
860 format!("RUST_LOG={}", std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())),
861 "RUST_BACKTRACE=1".to_string(),
862 ]),
863 cmd: Some(vec![
864 "/app/freenet".to_string(),
865 "network".to_string(),
866 "--data-dir".to_string(),
867 "/data".to_string(),
868 "--config-dir".to_string(),
869 "/config".to_string(),
870 "--ws-api-address".to_string(),
871 "0.0.0.0".to_string(),
872 "--ws-api-port".to_string(),
873 ws_port.to_string(),
874 "--network-address".to_string(),
875 "0.0.0.0".to_string(),
876 "--network-port".to_string(),
877 network_port.to_string(),
878 "--public-network-address".to_string(),
879 gateway_ip.to_string(),
880 "--public-network-port".to_string(),
881 network_port.to_string(),
882 "--is-gateway".to_string(),
883 "--skip-load-from-network".to_string(),
884 "--transport-keypair".to_string(),
885 "/config/keypair.pem".to_string(),
886 ]),
887 ..Default::default()
888 };
889
890 let container_id = self
891 .docker
892 .create_container(
893 Some(CreateContainerOptions {
894 name: container_name.clone(),
895 ..Default::default()
896 }),
897 config,
898 )
899 .await
900 .map_err(|e| {
901 Error::Other(anyhow::anyhow!("Failed to create gateway container: {}", e))
902 })?
903 .id;
904
905 self.containers.push(container_id.clone());
906
907 self.docker
909 .connect_network(
910 public_network_id,
911 bollard::network::ConnectNetworkOptions {
912 container: container_id.clone(),
913 endpoint_config: bollard::secret::EndpointSettings {
914 ipam_config: Some(bollard::secret::EndpointIpamConfig {
915 ipv4_address: Some(gateway_ip.to_string()),
916 ..Default::default()
917 }),
918 ..Default::default()
919 },
920 },
921 )
922 .await
923 .map_err(|e| {
924 Error::Other(anyhow::anyhow!(
925 "Failed to connect gateway to network: {}",
926 e
927 ))
928 })?;
929
930 self.copy_binary_to_container(&container_id, binary_path)
932 .await?;
933 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem")
934 .await?;
935 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem")
936 .await?;
937
938 self.docker
940 .start_container(&container_id, None::<StartContainerOptions<String>>)
941 .await
942 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start gateway: {}", e)))?;
943
944 let host_ws_port = self
946 .get_container_host_port(&container_id, ws_port)
947 .await?;
948
949 let info = DockerPeerInfo {
950 container_id: container_id.clone(),
951 container_name: container_name.clone(),
952 private_ip: gateway_ip, public_ip: gateway_ip,
954 host_ws_port,
955 network_port,
956 is_gateway: true,
957 nat_router_id: None,
958 };
959
960 self.peer_containers.insert(index, info.clone());
961
962 let local_log_cache = run_root.join(format!("gw{}", index)).join("peer.log");
963
964 tracing::info!(
965 "Created gateway {} at {} (ws: localhost:{})",
966 container_name,
967 gateway_ip,
968 host_ws_port
969 );
970
971 Ok((
972 info,
973 DockerProcess {
974 docker: self.docker.clone(),
975 container_id,
976 container_name,
977 local_log_cache,
978 },
979 ))
980 }
981
982 pub async fn create_peer(
984 &mut self,
985 index: usize,
986 binary_path: &Path,
987 keypair_path: &Path,
988 public_key_path: &Path,
989 gateways_toml_path: &Path,
990 gateway_public_key_path: Option<&Path>,
991 ws_port: u16,
992 network_port: u16,
993 run_root: &Path,
994 ) -> Result<(DockerPeerInfo, DockerProcess)> {
995 let container_name = format!("{}-peer-{}", self.config.name_prefix, index);
996 let image = self.ensure_base_image().await?;
997
998 let (nat_network_id, router_id, router_public_ip) = self.create_nat_network(index).await?;
1000
1001 let base = self.config.private_subnet_base.octets();
1003 let private_ip = Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 2);
1004
1005 let config = Config {
1007 image: Some(image),
1008 hostname: Some(container_name.clone()),
1009 exposed_ports: Some(HashMap::from([(
1010 format!("{}/tcp", ws_port),
1011 HashMap::new(),
1012 )])),
1013 host_config: Some(HostConfig {
1014 port_bindings: Some(HashMap::from([(
1015 format!("{}/tcp", ws_port),
1016 Some(vec![PortBinding {
1017 host_ip: Some("0.0.0.0".to_string()),
1018 host_port: None, }]),
1020 )])),
1021 cap_add: Some(vec!["NET_ADMIN".to_string()]),
1022 ..Default::default()
1023 }),
1024 env: Some(vec![
1025 format!("RUST_LOG={}", std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())),
1026 "RUST_BACKTRACE=1".to_string(),
1027 ]),
1028 cmd: Some(vec![
1029 "/app/freenet".to_string(),
1030 "network".to_string(),
1031 "--data-dir".to_string(),
1032 "/data".to_string(),
1033 "--config-dir".to_string(),
1034 "/config".to_string(),
1035 "--ws-api-address".to_string(),
1036 "0.0.0.0".to_string(),
1037 "--ws-api-port".to_string(),
1038 ws_port.to_string(),
1039 "--network-address".to_string(),
1040 "0.0.0.0".to_string(),
1041 "--network-port".to_string(),
1042 network_port.to_string(),
1043 "--skip-load-from-network".to_string(),
1045 "--transport-keypair".to_string(),
1046 "/config/keypair.pem".to_string(),
1047 ]),
1048 ..Default::default()
1049 };
1050
1051 let container_id = self
1052 .docker
1053 .create_container(
1054 Some(CreateContainerOptions {
1055 name: container_name.clone(),
1056 ..Default::default()
1057 }),
1058 config,
1059 )
1060 .await
1061 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create peer container: {}", e)))?
1062 .id;
1063
1064 self.containers.push(container_id.clone());
1065
1066 self.docker
1069 .connect_network(
1070 &nat_network_id,
1071 bollard::network::ConnectNetworkOptions {
1072 container: container_id.clone(),
1073 endpoint_config: bollard::secret::EndpointSettings {
1074 ipam_config: Some(bollard::secret::EndpointIpamConfig {
1075 ipv4_address: Some(private_ip.to_string()),
1076 ..Default::default()
1077 }),
1078 gateway: Some(
1079 Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 1)
1080 .to_string(),
1081 ),
1082 ..Default::default()
1083 },
1084 },
1085 )
1086 .await
1087 .map_err(|e| {
1088 Error::Other(anyhow::anyhow!(
1089 "Failed to connect peer to NAT network: {}",
1090 e
1091 ))
1092 })?;
1093
1094 self.copy_binary_to_container(&container_id, binary_path)
1096 .await?;
1097 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem")
1098 .await?;
1099 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem")
1100 .await?;
1101 self.copy_file_to_container(&container_id, gateways_toml_path, "/config/gateways.toml")
1102 .await?;
1103
1104 if let Some(gw_pubkey_path) = gateway_public_key_path {
1106 self.copy_file_to_container(&container_id, gw_pubkey_path, "/config/gw_public_key.pem")
1107 .await?;
1108 }
1109
1110 self.docker
1112 .start_container(&container_id, None::<StartContainerOptions<String>>)
1113 .await
1114 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start peer: {}", e)))?;
1115
1116 let host_ws_port = self
1118 .get_container_host_port(&container_id, ws_port)
1119 .await?;
1120
1121 let router_gateway = Ipv4Addr::new(base[0], base[1].wrapping_add(index as u8), 0, 254);
1124 let public_subnet = self.config.public_subnet;
1125 self.exec_in_container(
1126 &container_id,
1127 &[
1128 "sh",
1129 "-c",
1130 &format!("ip route add {} via {}", public_subnet, router_gateway),
1131 ],
1132 )
1133 .await?;
1134
1135 let info = DockerPeerInfo {
1136 container_id: container_id.clone(),
1137 container_name: container_name.clone(),
1138 private_ip,
1139 public_ip: router_public_ip,
1140 host_ws_port,
1141 network_port,
1142 is_gateway: false,
1143 nat_router_id: Some(router_id),
1144 };
1145
1146 self.peer_containers.insert(index, info.clone());
1147
1148 let local_log_cache = run_root.join(format!("peer{}", index)).join("peer.log");
1149
1150 tracing::info!(
1151 "Created peer {} at {} behind NAT {} (ws: localhost:{})",
1152 container_name,
1153 private_ip,
1154 router_public_ip,
1155 host_ws_port
1156 );
1157
1158 Ok((
1159 info,
1160 DockerProcess {
1161 docker: self.docker.clone(),
1162 container_id,
1163 container_name,
1164 local_log_cache,
1165 },
1166 ))
1167 }
1168
1169 pub async fn copy_file_to_container_pub(
1171 &self,
1172 container_id: &str,
1173 local_path: &Path,
1174 container_path: &str,
1175 ) -> Result<()> {
1176 self.copy_file_to_container(container_id, local_path, container_path)
1177 .await
1178 }
1179
1180 async fn copy_file_to_container(
1182 &self,
1183 container_id: &str,
1184 local_path: &Path,
1185 container_path: &str,
1186 ) -> Result<()> {
1187 let file_data = std::fs::read(local_path)?;
1188 let file_name = Path::new(container_path)
1189 .file_name()
1190 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
1191 .to_str()
1192 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid file name")))?;
1193
1194 let dir_path = Path::new(container_path)
1195 .parent()
1196 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
1197 .to_str()
1198 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid directory path")))?;
1199
1200 let mut tar_builder = tar::Builder::new(Vec::new());
1202 let mut header = tar::Header::new_gnu();
1203 header.set_path(file_name)?;
1204 header.set_size(file_data.len() as u64);
1205 header.set_mode(0o644);
1206 header.set_cksum();
1207 tar_builder.append(&header, file_data.as_slice())?;
1208 let tar_data = tar_builder.into_inner()?;
1209
1210 self.docker
1211 .upload_to_container(
1212 container_id,
1213 Some(UploadToContainerOptions {
1214 path: dir_path,
1215 ..Default::default()
1216 }),
1217 tar_data.into(),
1218 )
1219 .await
1220 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy file: {}", e)))?;
1221
1222 Ok(())
1223 }
1224
1225 async fn exec_in_container(&self, container_id: &str, cmd: &[&str]) -> Result<String> {
1227 let exec = self
1228 .docker
1229 .create_exec(
1230 container_id,
1231 CreateExecOptions {
1232 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
1233 attach_stdout: Some(true),
1234 attach_stderr: Some(true),
1235 ..Default::default()
1236 },
1237 )
1238 .await
1239 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create exec: {}", e)))?;
1240
1241 let output = self
1242 .docker
1243 .start_exec(&exec.id, None)
1244 .await
1245 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start exec: {}", e)))?;
1246
1247 let mut result = String::new();
1248 if let StartExecResults::Attached { mut output, .. } = output {
1249 while let Some(Ok(msg)) = output.next().await {
1250 match msg {
1251 LogOutput::StdOut { message } | LogOutput::StdErr { message } => {
1252 result.push_str(&String::from_utf8_lossy(&message));
1253 }
1254 _ => {}
1255 }
1256 }
1257 }
1258
1259 Ok(result)
1260 }
1261
1262 pub async fn cleanup(&mut self) -> Result<()> {
1264 tracing::info!("Cleaning up Docker NAT resources...");
1265
1266 for container_id in self.containers.drain(..) {
1268 let _ = self
1269 .docker
1270 .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
1271 .await;
1272 let _ = self
1273 .docker
1274 .remove_container(
1275 &container_id,
1276 Some(RemoveContainerOptions {
1277 force: true,
1278 ..Default::default()
1279 }),
1280 )
1281 .await;
1282 }
1283
1284 for network_id in self.networks.drain(..) {
1286 let _ = self.docker.remove_network(&network_id).await;
1287 }
1288
1289 self.peer_containers.clear();
1290 self.public_network_id = None;
1291
1292 Ok(())
1293 }
1294
1295 pub fn get_peer_info(&self, index: usize) -> Option<&DockerPeerInfo> {
1297 self.peer_containers.get(&index)
1298 }
1299
1300 pub async fn dump_iptables_counters(&self) -> Result<std::collections::HashMap<usize, String>> {
1306 let mut results = std::collections::HashMap::new();
1307
1308 for (&peer_index, peer_info) in &self.peer_containers {
1309 if let Some(router_id) = &peer_info.nat_router_id {
1310 let mut output = String::new();
1311
1312 output.push_str("=== NAT table ===\n");
1314 match self.exec_in_container(router_id, &["iptables", "-t", "nat", "-nvL"]).await {
1315 Ok(s) => output.push_str(&s),
1316 Err(e) => output.push_str(&format!("Error: {}\n", e)),
1317 }
1318
1319 output.push_str("\n=== FORWARD chain ===\n");
1321 match self.exec_in_container(router_id, &["iptables", "-nvL", "FORWARD"]).await {
1322 Ok(s) => output.push_str(&s),
1323 Err(e) => output.push_str(&format!("Error: {}\n", e)),
1324 }
1325
1326 results.insert(peer_index, output);
1327 }
1328 }
1329
1330 Ok(results)
1331 }
1332
1333 pub async fn dump_conntrack_table(&self) -> Result<std::collections::HashMap<usize, String>> {
1338 let mut results = std::collections::HashMap::new();
1339
1340 for (&peer_index, peer_info) in &self.peer_containers {
1341 if let Some(router_id) = &peer_info.nat_router_id {
1342 let _ = self.exec_in_container(
1344 router_id,
1345 &["apk", "add", "--no-cache", "conntrack-tools"]
1346 ).await;
1347
1348 match self.exec_in_container(router_id, &["conntrack", "-L", "-p", "udp"]).await {
1350 Ok(s) if s.trim().is_empty() => {
1351 results.insert(peer_index, "(no UDP conntrack entries)".to_string());
1352 }
1353 Ok(s) => {
1354 results.insert(peer_index, s);
1355 }
1356 Err(e) => {
1357 results.insert(peer_index, format!("Error: {}", e));
1358 }
1359 }
1360 }
1361 }
1362
1363 Ok(results)
1364 }
1365
1366 pub async fn dump_peer_routes(&self) -> Result<std::collections::HashMap<usize, String>> {
1370 let mut results = std::collections::HashMap::new();
1371
1372 for (&peer_index, peer_info) in &self.peer_containers {
1373 if peer_info.nat_router_id.is_some() {
1374 match self.exec_in_container(&peer_info.container_id, &["ip", "route"]).await {
1376 Ok(s) => { results.insert(peer_index, s); }
1377 Err(e) => { results.insert(peer_index, format!("Error: {}", e)); }
1378 }
1379 }
1380 }
1381
1382 Ok(results)
1383 }
1384
1385 async fn get_container_host_port(&self, container_id: &str, container_port: u16) -> Result<u16> {
1392 let info = self
1393 .docker
1394 .inspect_container(container_id, None)
1395 .await
1396 .map_err(|e| {
1397 Error::Other(anyhow::anyhow!(
1398 "Failed to inspect container for port allocation: {}",
1399 e
1400 ))
1401 })?;
1402
1403 let port_key = format!("{}/tcp", container_port);
1404
1405 let host_port = info
1406 .network_settings
1407 .and_then(|ns| ns.ports)
1408 .and_then(|ports| ports.get(&port_key).cloned())
1409 .flatten()
1410 .and_then(|bindings| bindings.first().cloned())
1411 .and_then(|binding| binding.host_port)
1412 .and_then(|port_str| port_str.parse::<u16>().ok())
1413 .ok_or_else(|| {
1414 Error::Other(anyhow::anyhow!(
1415 "Failed to get allocated host port for container {} port {}",
1416 container_id,
1417 container_port
1418 ))
1419 })?;
1420
1421 Ok(host_port)
1422 }
1423}
1424
1425impl Drop for DockerNatBackend {
1426 fn drop(&mut self) {
1427 if self.config.cleanup_on_drop {
1428 tracing::info!("Cleaning up Docker NAT backend resources...");
1429
1430 let docker = self.docker.clone();
1432 let containers = std::mem::take(&mut self.containers);
1433 let networks = std::mem::take(&mut self.networks);
1434
1435 let cleanup = async {
1439 let container_futures = containers.into_iter().map(|container_id| {
1441 let docker = docker.clone();
1442 async move {
1443 if let Err(e) = docker
1444 .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
1445 .await
1446 {
1447 tracing::debug!("Failed to stop container {}: {}", container_id, e);
1448 }
1449 if let Err(e) = docker
1450 .remove_container(
1451 &container_id,
1452 Some(RemoveContainerOptions {
1453 force: true,
1454 ..Default::default()
1455 }),
1456 )
1457 .await
1458 {
1459 tracing::debug!("Failed to remove container {}: {}", container_id, e);
1460 }
1461 }
1462 });
1463
1464 futures::future::join_all(container_futures).await;
1466
1467 for network_id in networks {
1469 if let Err(e) = docker.remove_network(&network_id).await {
1470 tracing::debug!("Failed to remove network {}: {}", network_id, e);
1471 }
1472 }
1473
1474 tracing::info!("Docker NAT backend cleanup complete");
1475 };
1476
1477 if let Ok(handle) = tokio::runtime::Handle::try_current() {
1480 tokio::task::block_in_place(|| {
1481 handle.block_on(cleanup);
1482 });
1483 } else if let Ok(rt) = tokio::runtime::Runtime::new() {
1484 rt.block_on(cleanup);
1485 } else {
1486 tracing::error!("Failed to create runtime for cleanup");
1487 }
1488 }
1489 }
1490}