1use async_trait::async_trait;
2use std::process::Command;
3use tracing::info;
4
5use super::{NetworkIsolator, ProxyListener};
6
7pub struct LinuxNetnsIsolator {
8 pub namespace_name: String,
9}
10
11#[async_trait]
12impl NetworkIsolator for LinuxNetnsIsolator {
13 async fn setup(&self) -> anyhow::Result<()> {
14 if !cfg!(target_os = "linux") {
15 anyhow::bail!("Linux netns isolator is only supported on Linux");
16 }
17
18 info!("Setting up network namespace: {}", self.namespace_name);
19
20 run_cmd("ip", &["netns", "add", &self.namespace_name])?;
21
22 let veth_host = format!(
23 "veth-{}-h",
24 &self.namespace_name[..std::cmp::min(self.namespace_name.len(), 4)]
25 );
26 let veth_guest = format!(
27 "veth-{}-g",
28 &self.namespace_name[..std::cmp::min(self.namespace_name.len(), 4)]
29 );
30
31 run_cmd(
32 "ip",
33 &[
34 "link",
35 "add",
36 &veth_host,
37 "type",
38 "veth",
39 "peer",
40 "name",
41 &veth_guest,
42 ],
43 )?;
44
45 run_cmd(
46 "ip",
47 &["link", "set", &veth_guest, "netns", &self.namespace_name],
48 )?;
49
50 run_cmd("ip", &["addr", "add", "10.0.0.1/24", "dev", &veth_host])?;
51 run_cmd("ip", &["link", "set", &veth_host, "up"])?;
52
53 run_cmd(
54 "ip",
55 &[
56 "netns",
57 "exec",
58 &self.namespace_name,
59 "ip",
60 "addr",
61 "add",
62 "10.0.0.2/24",
63 "dev",
64 &veth_guest,
65 ],
66 )?;
67 run_cmd(
68 "ip",
69 &[
70 "netns",
71 "exec",
72 &self.namespace_name,
73 "ip",
74 "link",
75 "set",
76 &veth_guest,
77 "up",
78 ],
79 )?;
80
81 run_cmd(
82 "ip",
83 &[
84 "netns",
85 "exec",
86 &self.namespace_name,
87 "ip",
88 "route",
89 "add",
90 "default",
91 "via",
92 "10.0.0.1",
93 ],
94 )?;
95
96 run_cmd(
97 "ip",
98 &[
99 "netns",
100 "exec",
101 &self.namespace_name,
102 "iptables",
103 "-t",
104 "nat",
105 "-A",
106 "OUTPUT",
107 "-p",
108 "udp",
109 "--dport",
110 "53",
111 "-j",
112 "DNAT",
113 "--to-destination",
114 "10.0.0.1:53",
115 ],
116 )?;
117
118 Ok(())
119 }
120
121 async fn teardown(&self) -> anyhow::Result<()> {
122 if !cfg!(target_os = "linux") {
123 return Ok(());
124 }
125
126 info!("Tearing down network namespace: {}", self.namespace_name);
127 let _ = run_cmd("ip", &["netns", "delete", &self.namespace_name]);
128 Ok(())
129 }
130
131 async fn spawn_proxy(
132 &self,
133 target_ip: &str,
134 target_port: u16,
135 ) -> anyhow::Result<(ProxyListener, tokio::task::JoinHandle<()>)> {
136 let tcp_isolator = super::tcp::TcpProxyIsolator;
137 let (proxy, handle) = tcp_isolator.spawn_proxy(target_ip, target_port).await?;
138
139 tokio::spawn(async move {
140 if let Ok(socket) = tokio::net::UdpSocket::bind("10.0.0.1:53").await {
141 tracing::info!("DNS Interceptor listening on 10.0.0.1:53");
142 let mut buf = [0u8; 512];
143 while let Ok((size, peer)) = socket.recv_from(&mut buf).await {
144 tracing::info!("Intercepted DNS query from {} size {}", peer, size);
145 if size >= 12 {
146 if let Ok(upstream) = tokio::net::UdpSocket::bind("0.0.0.0:0").await {
147 let _ = upstream.send_to(&buf[..size], "8.8.8.8:53").await;
148 let mut resp_buf = [0u8; 512];
149 if let Ok((resp_size, _)) = tokio::time::timeout(
150 std::time::Duration::from_secs(2),
151 upstream.recv_from(&mut resp_buf),
152 )
153 .await
154 .unwrap_or(Err(std::io::Error::new(
155 std::io::ErrorKind::TimedOut,
156 "timeout",
157 ))) {
158 let _ = socket.send_to(&resp_buf[..resp_size], peer).await;
159 }
160 }
161 }
162 }
163 }
164 });
165
166 Ok((proxy, handle))
167 }
168}
169
170fn run_cmd(cmd: &str, args: &[&str]) -> anyhow::Result<()> {
171 let status = Command::new(cmd).args(args).status()?;
172 if !status.success() {
173 anyhow::bail!(
174 "Command `{} {:?}` failed with status: {}",
175 cmd,
176 args,
177 status
178 );
179 }
180 Ok(())
181}