1use 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#[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 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#[derive(Debug, Clone)]
58pub enum NatTopology {
59 OnePerNat,
61 Custom(Vec<NatNetwork>),
63}
64
65#[derive(Debug, Clone)]
67pub struct NatNetwork {
68 pub name: String,
69 pub peer_indices: Vec<usize>,
70 pub nat_type: NatType,
71}
72
73#[derive(Debug, Clone, Default)]
75pub enum NatType {
76 #[default]
78 RestrictedCone,
79 FullCone { forwarded_ports: Option<Vec<u16>> },
81}
82
83pub struct DockerNatBackend {
85 docker: Docker,
86 config: DockerNatConfig,
87 networks: Vec<String>,
89 containers: Vec<String>,
91 peer_containers: HashMap<usize, DockerPeerInfo>,
93 public_network_id: Option<String>,
95}
96
97#[derive(Debug, Clone)]
99pub struct DockerPeerInfo {
100 pub container_id: String,
101 pub container_name: String,
102 pub private_ip: Ipv4Addr,
104 pub public_ip: Ipv4Addr,
106 pub host_ws_port: u16,
108 pub network_port: u16,
110 pub is_gateway: bool,
112 pub nat_router_id: Option<String>,
114}
115
116pub 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 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 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 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 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 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 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 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 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 pub async fn create_nat_network(&mut self, peer_index: usize) -> Result<(String, String, Ipv4Addr)> {
265 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, 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 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 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 let router_private_ip = Ipv4Addr::new(10, peer_index as u8 + 1, 0, 254);
308
309 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 "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 let _ = self.docker.disconnect_network(
348 "bridge",
349 bollard::network::DisconnectNetworkOptions {
350 container: router_id.clone(),
351 force: true,
352 },
353 ).await;
354
355 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 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 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 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 pub async fn ensure_base_image(&self) -> Result<String> {
410 let image_name = "freenet-test-peer:latest";
411
412 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 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 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 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 pub async fn copy_binary_to_container(
475 &self,
476 container_id: &str,
477 binary_path: &Path,
478 ) -> Result<()> {
479 let binary_data = std::fs::read(binary_path)?;
481
482 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 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 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 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 let host_ws_port = crate::peer::get_free_port()?;
535
536 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 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 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 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, 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 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 let (nat_network_id, router_id, router_public_ip) = self.create_nat_network(index).await?;
664
665 let private_ip = Ipv4Addr::new(10, index as u8 + 1, 0, 2);
667
668 let host_ws_port = crate::peer::get_free_port()?;
670
671 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 "--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 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 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 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 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 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 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 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 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 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 pub async fn cleanup(&mut self) -> Result<()> {
888 tracing::info!("Cleaning up Docker NAT resources...");
889
890 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 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 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 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}