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 iproute2 > /dev/null 2>&1 && \
322 PUBLIC_IF=$(ip -o addr show | grep '172\\.20\\.' | awk '{print $2}') && \
323 PRIVATE_IF=$(ip -o addr show | grep ' 10\\.' | awk '{print $2}') && \
324 echo \"Public interface: $PUBLIC_IF, Private interface: $PRIVATE_IF\" && \
325 iptables -t nat -A POSTROUTING -o $PUBLIC_IF -j MASQUERADE && \
326 iptables -A FORWARD -i $PRIVATE_IF -o $PUBLIC_IF -j ACCEPT && \
327 iptables -A FORWARD -i $PUBLIC_IF -o $PRIVATE_IF -m state --state RELATED,ESTABLISHED -j ACCEPT && \
328 echo 'NAT router ready' && \
329 tail -f /dev/null".to_string(),
330 ]),
331 host_config: Some(HostConfig {
332 cap_add: Some(vec!["NET_ADMIN".to_string()]),
333 sysctls: Some(HashMap::from([
334 ("net.ipv4.ip_forward".to_string(), "1".to_string()),
335 ])),
336 ..Default::default()
337 }),
338 ..Default::default()
339 };
340
341 let router_id = self.docker
342 .create_container(
343 Some(CreateContainerOptions { name: router_name.clone(), ..Default::default() }),
344 router_config,
345 )
346 .await
347 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create NAT router: {}", e)))?
348 .id;
349
350 self.containers.push(router_id.clone());
351
352 let _ = self.docker.disconnect_network(
354 "bridge",
355 bollard::network::DisconnectNetworkOptions {
356 container: router_id.clone(),
357 force: true,
358 },
359 ).await;
360
361 self.docker
363 .connect_network(
364 public_network_id,
365 bollard::network::ConnectNetworkOptions {
366 container: router_id.clone(),
367 endpoint_config: bollard::secret::EndpointSettings {
368 ipam_config: Some(bollard::secret::EndpointIpamConfig {
369 ipv4_address: Some(router_public_ip.to_string()),
370 ..Default::default()
371 }),
372 ..Default::default()
373 },
374 },
375 )
376 .await
377 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect router to public network: {}", e)))?;
378
379 self.docker
381 .connect_network(
382 &network_id,
383 bollard::network::ConnectNetworkOptions {
384 container: router_id.clone(),
385 endpoint_config: bollard::secret::EndpointSettings {
386 ipam_config: Some(bollard::secret::EndpointIpamConfig {
387 ipv4_address: Some(router_private_ip.to_string()),
388 ..Default::default()
389 }),
390 ..Default::default()
391 },
392 },
393 )
394 .await
395 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect router to private network: {}", e)))?;
396
397 self.docker
399 .start_container(&router_id, None::<StartContainerOptions<String>>)
400 .await
401 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start NAT router: {}", e)))?;
402
403 tokio::time::sleep(Duration::from_secs(2)).await;
405
406 tracing::info!(
407 "Created NAT network {} with router {} (public: {}, private: {})",
408 network_name, router_name, router_public_ip, router_private_ip
409 );
410
411 Ok((network_id, router_id, router_public_ip))
412 }
413
414 pub async fn ensure_base_image(&self) -> Result<String> {
416 let image_name = "freenet-test-peer:latest";
417
418 if self.docker.inspect_image(image_name).await.is_ok() {
420 tracing::debug!("Base image {} already exists", image_name);
421 return Ok(image_name.to_string());
422 }
423
424 tracing::info!("Building base image {}...", image_name);
425
426 let dockerfile = r#"
428FROM ubuntu:24.04
429RUN apt-get update && \
430 apt-get install -y --no-install-recommends \
431 libssl3 \
432 ca-certificates \
433 iproute2 \
434 && rm -rf /var/lib/apt/lists/*
435RUN mkdir -p /data /config
436WORKDIR /app
437"#;
438
439 let mut tar_builder = tar::Builder::new(Vec::new());
441 let mut header = tar::Header::new_gnu();
442 header.set_path("Dockerfile")?;
443 header.set_size(dockerfile.len() as u64);
444 header.set_mode(0o644);
445 header.set_cksum();
446 tar_builder.append(&header, dockerfile.as_bytes())?;
447 let tar_data = tar_builder.into_inner()?;
448
449 let options = BuildImageOptions {
451 dockerfile: "Dockerfile",
452 t: image_name,
453 rm: true,
454 ..Default::default()
455 };
456
457 let mut build_stream = self.docker.build_image(options, None, Some(tar_data.into()));
458
459 while let Some(result) = build_stream.next().await {
460 match result {
461 Ok(info) => {
462 if let Some(stream) = info.stream {
463 tracing::debug!("Build: {}", stream.trim());
464 }
465 if let Some(error) = info.error {
466 return Err(Error::Other(anyhow::anyhow!("Image build error: {}", error)));
467 }
468 }
469 Err(e) => {
470 return Err(Error::Other(anyhow::anyhow!("Image build failed: {}", e)));
471 }
472 }
473 }
474
475 tracing::info!("Built base image {}", image_name);
476 Ok(image_name.to_string())
477 }
478
479 pub async fn copy_binary_to_container(
481 &self,
482 container_id: &str,
483 binary_path: &Path,
484 ) -> Result<()> {
485 let binary_data = std::fs::read(binary_path)?;
487
488 let mut tar_builder = tar::Builder::new(Vec::new());
490 let mut header = tar::Header::new_gnu();
491 header.set_path("freenet")?;
492 header.set_size(binary_data.len() as u64);
493 header.set_mode(0o755);
494 header.set_cksum();
495 tar_builder.append(&header, binary_data.as_slice())?;
496 let tar_data = tar_builder.into_inner()?;
497
498 self.docker
500 .upload_to_container(
501 container_id,
502 Some(UploadToContainerOptions {
503 path: "/app",
504 ..Default::default()
505 }),
506 tar_data.into(),
507 )
508 .await
509 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy binary: {}", e)))?;
510
511 Ok(())
512 }
513
514 pub async fn create_gateway(
516 &mut self,
517 index: usize,
518 binary_path: &Path,
519 keypair_path: &Path,
520 public_key_path: &Path,
521 ws_port: u16,
522 network_port: u16,
523 run_root: &Path,
524 ) -> Result<(DockerPeerInfo, DockerProcess)> {
525 let container_name = format!("{}-gw-{}", self.config.name_prefix, index);
526 let image = self.ensure_base_image().await?;
527
528 let public_network_id = self.public_network_id.as_ref()
529 .ok_or_else(|| Error::Other(anyhow::anyhow!("Public network not created yet")))?;
530
531 let gateway_ip = Ipv4Addr::new(
533 self.config.public_subnet.ip().octets()[0],
534 self.config.public_subnet.ip().octets()[1],
535 0,
536 10 + index as u8,
537 );
538
539 let host_ws_port = crate::peer::get_free_port()?;
541
542 let config = Config {
544 image: Some(image),
545 hostname: Some(container_name.clone()),
546 exposed_ports: Some(HashMap::from([
547 (format!("{}/tcp", ws_port), HashMap::new()),
548 ])),
549 host_config: Some(HostConfig {
550 port_bindings: Some(HashMap::from([
551 (
552 format!("{}/tcp", ws_port),
553 Some(vec![PortBinding {
554 host_ip: Some("0.0.0.0".to_string()),
555 host_port: Some(host_ws_port.to_string()),
556 }]),
557 ),
558 ])),
559 cap_add: Some(vec!["NET_ADMIN".to_string()]),
560 ..Default::default()
561 }),
562 env: Some(vec![
563 "RUST_LOG=info".to_string(),
564 "RUST_BACKTRACE=1".to_string(),
565 ]),
566 cmd: Some(vec![
567 "/app/freenet".to_string(),
568 "network".to_string(),
569 "--data-dir".to_string(), "/data".to_string(),
570 "--config-dir".to_string(), "/config".to_string(),
571 "--ws-api-address".to_string(), "0.0.0.0".to_string(),
572 "--ws-api-port".to_string(), ws_port.to_string(),
573 "--network-address".to_string(), "0.0.0.0".to_string(),
574 "--network-port".to_string(), network_port.to_string(),
575 "--public-network-address".to_string(), gateway_ip.to_string(),
576 "--public-network-port".to_string(), network_port.to_string(),
577 "--is-gateway".to_string(),
578 "--skip-load-from-network".to_string(),
579 "--transport-keypair".to_string(), "/config/keypair.pem".to_string(),
580 ]),
581 ..Default::default()
582 };
583
584 let container_id = self.docker
585 .create_container(
586 Some(CreateContainerOptions { name: container_name.clone(), ..Default::default() }),
587 config,
588 )
589 .await
590 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create gateway container: {}", e)))?
591 .id;
592
593 self.containers.push(container_id.clone());
594
595 self.docker
597 .connect_network(
598 public_network_id,
599 bollard::network::ConnectNetworkOptions {
600 container: container_id.clone(),
601 endpoint_config: bollard::secret::EndpointSettings {
602 ipam_config: Some(bollard::secret::EndpointIpamConfig {
603 ipv4_address: Some(gateway_ip.to_string()),
604 ..Default::default()
605 }),
606 ..Default::default()
607 },
608 },
609 )
610 .await
611 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect gateway to network: {}", e)))?;
612
613 self.copy_binary_to_container(&container_id, binary_path).await?;
615 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem").await?;
616 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem").await?;
617
618 self.docker
620 .start_container(&container_id, None::<StartContainerOptions<String>>)
621 .await
622 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start gateway: {}", e)))?;
623
624 let info = DockerPeerInfo {
625 container_id: container_id.clone(),
626 container_name: container_name.clone(),
627 private_ip: gateway_ip, public_ip: gateway_ip,
629 host_ws_port,
630 network_port,
631 is_gateway: true,
632 nat_router_id: None,
633 };
634
635 self.peer_containers.insert(index, info.clone());
636
637 let local_log_cache = run_root.join(format!("gw{}", index)).join("peer.log");
638
639 tracing::info!(
640 "Created gateway {} at {} (ws: localhost:{})",
641 container_name, gateway_ip, host_ws_port
642 );
643
644 Ok((info, DockerProcess {
645 docker: self.docker.clone(),
646 container_id,
647 container_name,
648 local_log_cache,
649 }))
650 }
651
652 pub async fn create_peer(
654 &mut self,
655 index: usize,
656 binary_path: &Path,
657 keypair_path: &Path,
658 public_key_path: &Path,
659 gateways_toml_path: &Path,
660 gateway_public_key_path: Option<&Path>,
661 ws_port: u16,
662 network_port: u16,
663 run_root: &Path,
664 ) -> Result<(DockerPeerInfo, DockerProcess)> {
665 let container_name = format!("{}-peer-{}", self.config.name_prefix, index);
666 let image = self.ensure_base_image().await?;
667
668 let (nat_network_id, router_id, router_public_ip) = self.create_nat_network(index).await?;
670
671 let private_ip = Ipv4Addr::new(10, index as u8 + 1, 0, 2);
673
674 let host_ws_port = crate::peer::get_free_port()?;
676
677 let config = Config {
679 image: Some(image),
680 hostname: Some(container_name.clone()),
681 exposed_ports: Some(HashMap::from([
682 (format!("{}/tcp", ws_port), HashMap::new()),
683 ])),
684 host_config: Some(HostConfig {
685 port_bindings: Some(HashMap::from([
686 (
687 format!("{}/tcp", ws_port),
688 Some(vec![PortBinding {
689 host_ip: Some("0.0.0.0".to_string()),
690 host_port: Some(host_ws_port.to_string()),
691 }]),
692 ),
693 ])),
694 cap_add: Some(vec!["NET_ADMIN".to_string()]),
695 ..Default::default()
696 }),
697 env: Some(vec![
698 "RUST_LOG=info".to_string(),
699 "RUST_BACKTRACE=1".to_string(),
700 ]),
701 cmd: Some(vec![
702 "/app/freenet".to_string(),
703 "network".to_string(),
704 "--data-dir".to_string(), "/data".to_string(),
705 "--config-dir".to_string(), "/config".to_string(),
706 "--ws-api-address".to_string(), "0.0.0.0".to_string(),
707 "--ws-api-port".to_string(), ws_port.to_string(),
708 "--network-address".to_string(), "0.0.0.0".to_string(),
709 "--network-port".to_string(), network_port.to_string(),
710 "--skip-load-from-network".to_string(),
712 "--transport-keypair".to_string(), "/config/keypair.pem".to_string(),
713 ]),
714 ..Default::default()
715 };
716
717 let container_id = self.docker
718 .create_container(
719 Some(CreateContainerOptions { name: container_name.clone(), ..Default::default() }),
720 config,
721 )
722 .await
723 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create peer container: {}", e)))?
724 .id;
725
726 self.containers.push(container_id.clone());
727
728 self.docker
731 .connect_network(
732 &nat_network_id,
733 bollard::network::ConnectNetworkOptions {
734 container: container_id.clone(),
735 endpoint_config: bollard::secret::EndpointSettings {
736 ipam_config: Some(bollard::secret::EndpointIpamConfig {
737 ipv4_address: Some(private_ip.to_string()),
738 ..Default::default()
739 }),
740 gateway: Some(Ipv4Addr::new(10, index as u8 + 1, 0, 1).to_string()),
741 ..Default::default()
742 },
743 },
744 )
745 .await
746 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to connect peer to NAT network: {}", e)))?;
747
748 self.copy_binary_to_container(&container_id, binary_path).await?;
750 self.copy_file_to_container(&container_id, keypair_path, "/config/keypair.pem").await?;
751 self.copy_file_to_container(&container_id, public_key_path, "/config/public_key.pem").await?;
752 self.copy_file_to_container(&container_id, gateways_toml_path, "/config/gateways.toml").await?;
753
754 if let Some(gw_pubkey_path) = gateway_public_key_path {
756 self.copy_file_to_container(&container_id, gw_pubkey_path, "/config/gw_public_key.pem").await?;
757 }
758
759 self.docker
761 .start_container(&container_id, None::<StartContainerOptions<String>>)
762 .await
763 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start peer: {}", e)))?;
764
765 let router_gateway = Ipv4Addr::new(10, index as u8 + 1, 0, 254);
768 self.exec_in_container(
769 &container_id,
770 &["sh", "-c", &format!(
771 "ip route add 172.20.0.0/24 via {}",
772 router_gateway
773 )],
774 ).await?;
775
776 let info = DockerPeerInfo {
777 container_id: container_id.clone(),
778 container_name: container_name.clone(),
779 private_ip,
780 public_ip: router_public_ip,
781 host_ws_port,
782 network_port,
783 is_gateway: false,
784 nat_router_id: Some(router_id),
785 };
786
787 self.peer_containers.insert(index, info.clone());
788
789 let local_log_cache = run_root.join(format!("peer{}", index)).join("peer.log");
790
791 tracing::info!(
792 "Created peer {} at {} behind NAT {} (ws: localhost:{})",
793 container_name, private_ip, router_public_ip, host_ws_port
794 );
795
796 Ok((info, DockerProcess {
797 docker: self.docker.clone(),
798 container_id,
799 container_name,
800 local_log_cache,
801 }))
802 }
803
804 pub async fn copy_file_to_container_pub(
806 &self,
807 container_id: &str,
808 local_path: &Path,
809 container_path: &str,
810 ) -> Result<()> {
811 self.copy_file_to_container(container_id, local_path, container_path).await
812 }
813
814 async fn copy_file_to_container(
816 &self,
817 container_id: &str,
818 local_path: &Path,
819 container_path: &str,
820 ) -> Result<()> {
821 let file_data = std::fs::read(local_path)?;
822 let file_name = Path::new(container_path)
823 .file_name()
824 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
825 .to_str()
826 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid file name")))?;
827
828 let dir_path = Path::new(container_path)
829 .parent()
830 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid container path")))?
831 .to_str()
832 .ok_or_else(|| Error::Other(anyhow::anyhow!("Invalid directory path")))?;
833
834 let mut tar_builder = tar::Builder::new(Vec::new());
836 let mut header = tar::Header::new_gnu();
837 header.set_path(file_name)?;
838 header.set_size(file_data.len() as u64);
839 header.set_mode(0o644);
840 header.set_cksum();
841 tar_builder.append(&header, file_data.as_slice())?;
842 let tar_data = tar_builder.into_inner()?;
843
844 self.docker
845 .upload_to_container(
846 container_id,
847 Some(UploadToContainerOptions {
848 path: dir_path,
849 ..Default::default()
850 }),
851 tar_data.into(),
852 )
853 .await
854 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to copy file: {}", e)))?;
855
856 Ok(())
857 }
858
859 async fn exec_in_container(&self, container_id: &str, cmd: &[&str]) -> Result<String> {
861 let exec = self.docker
862 .create_exec(
863 container_id,
864 CreateExecOptions {
865 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
866 attach_stdout: Some(true),
867 attach_stderr: Some(true),
868 ..Default::default()
869 },
870 )
871 .await
872 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to create exec: {}", e)))?;
873
874 let output = self.docker
875 .start_exec(&exec.id, None)
876 .await
877 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to start exec: {}", e)))?;
878
879 let mut result = String::new();
880 if let StartExecResults::Attached { mut output, .. } = output {
881 while let Some(Ok(msg)) = output.next().await {
882 match msg {
883 LogOutput::StdOut { message } | LogOutput::StdErr { message } => {
884 result.push_str(&String::from_utf8_lossy(&message));
885 }
886 _ => {}
887 }
888 }
889 }
890
891 Ok(result)
892 }
893
894 pub async fn cleanup(&mut self) -> Result<()> {
896 tracing::info!("Cleaning up Docker NAT resources...");
897
898 for container_id in self.containers.drain(..) {
900 let _ = self.docker
901 .stop_container(&container_id, Some(StopContainerOptions { t: 2 }))
902 .await;
903 let _ = self.docker
904 .remove_container(
905 &container_id,
906 Some(RemoveContainerOptions { force: true, ..Default::default() }),
907 )
908 .await;
909 }
910
911 for network_id in self.networks.drain(..) {
913 let _ = self.docker.remove_network(&network_id).await;
914 }
915
916 self.peer_containers.clear();
917 self.public_network_id = None;
918
919 Ok(())
920 }
921
922 pub fn get_peer_info(&self, index: usize) -> Option<&DockerPeerInfo> {
924 self.peer_containers.get(&index)
925 }
926}
927
928impl Drop for DockerNatBackend {
929 fn drop(&mut self) {
930 if self.config.cleanup_on_drop {
931 let docker = self.docker.clone();
933 let containers = std::mem::take(&mut self.containers);
934 let networks = std::mem::take(&mut self.networks);
935
936 std::thread::spawn(move || {
937 let rt = tokio::runtime::Runtime::new().unwrap();
938 rt.block_on(async {
939 for container_id in containers {
940 let _ = docker
941 .stop_container(&container_id, Some(StopContainerOptions { t: 1 }))
942 .await;
943 let _ = docker
944 .remove_container(
945 &container_id,
946 Some(RemoveContainerOptions { force: true, ..Default::default() }),
947 )
948 .await;
949 }
950 for network_id in networks {
951 let _ = docker.remove_network(&network_id).await;
952 }
953 });
954 });
955 }
956 }
957}