1use std::path::PathBuf;
7use std::process::Command;
8
9use clap::Parser;
10use clap::Subcommand;
11use clap_complete::engine::ArgValueCompleter;
12use color_eyre::Result;
13
14use crate::command::add;
15use crate::command::handle_clear;
16use crate::command::handle_dnat;
17use crate::command::handle_hairpin;
18use crate::command::handle_list;
19use crate::command::handle_policy_route;
20use crate::command::handle_snat;
21use crate::command::remap;
22use crate::command::remove;
23use crate::consts::DAEMON_SOCK;
24use crate::consts::PKG_NAME;
25use crate::consts::STATE;
26use crate::daemon::Daemon;
27
28#[derive(Parser, Debug)]
30#[command(
31 name = PKG_NAME,
32 about = "Manage iptables NAT rules (static VMs & dynamic Docker)"
33)]
34pub struct Cli {
35 #[arg(long, default_value = DAEMON_SOCK, global = true)]
37 pub socket: PathBuf,
38
39 #[arg(long, global = true)]
41 pub json: bool,
42
43 #[command(subcommand)]
44 pub command: NatMapCommand,
45}
46
47#[derive(Subcommand, Debug)]
49pub enum NatMapCommand {
50 #[command(name = "dnat")]
52 Dnat {
53 #[arg(long)]
55 ext_ip: String,
56 #[arg(long)]
58 int_ip: String,
59 #[arg(long, default_value = "tcp")]
61 proto: String,
62 #[arg(long)]
64 ports: String,
65 #[arg(long)]
67 ext_if: Option<String>,
68 #[arg(long)]
70 delete: bool,
71 #[arg(long)]
73 no_masquerade: bool,
74 },
75 #[command(name = "snat")]
77 Snat {
78 #[arg(long)]
80 int_ip: String,
81 #[arg(long)]
83 ext_if: String,
84 #[arg(long)]
86 ext_ip: String,
87 #[arg(long)]
89 delete: bool,
90 },
91 #[command(name = "hairpin")]
93 Hairpin {
94 #[arg(long)]
96 ext_ip: String,
97 #[arg(long)]
99 int_ip: String,
100 #[arg(long, default_value = "tcp")]
102 proto: String,
103 #[arg(long)]
105 ports: String,
106 #[arg(long)]
110 lan_cidr: Option<String>,
111 #[arg(long)]
113 delete: bool,
114 },
115 #[command(name = "policy-route")]
117 PolicyRoute {
118 #[arg(long)]
120 src_ip: String,
121 #[arg(long)]
123 via: String,
124 #[arg(long, default_value = "100")]
126 table: u32,
127 #[arg(long)]
129 delete: bool,
130 },
131 #[command(name = "ls")]
133 List {
134 #[arg(
136 value_name = "CONTAINER_ID",
137 add = ArgValueCompleter::new(crate::completions::complete_container_id)
138 )]
139 container_id: Option<String>,
140 },
141 #[command(name = "clear")]
143 Clear,
144 #[command(name = "docker")]
146 Docker {
147 #[command(subcommand)]
148 cmd: DockerCommand,
149 },
150 #[command(name = "save")]
152 Save,
153 #[command(name = "fwd")]
155 Fwd,
156 #[command(name = "daemon")]
158 Daemon {
159 #[arg(long, default_value = STATE)]
161 state: PathBuf,
162 #[arg(long, default_value = DAEMON_SOCK)]
164 socket: PathBuf,
165 #[arg(long, default_value = PKG_NAME)]
167 socket_group: String,
168 },
169 #[command(name = "install")]
171 Install {
172 #[arg(long, default_value = PKG_NAME)]
174 group: String,
175 #[arg(long, default_value = lab_ops_lab_lib::consts::LABOPS_BIN)]
177 binary: String,
178 },
179}
180
181#[derive(Subcommand, Debug)]
183pub enum DockerCommand {
184 #[command(name = "add")]
186 Add {
187 #[arg(
189 value_name = "CONTAINER_ID",
190 add = ArgValueCompleter::new(crate::completions::complete_container_id)
191 )]
192 container_id: String,
193 #[arg(value_name = "MAPPING")]
196 mapping: Option<String>,
197 #[arg(
199 long,
200 add = ArgValueCompleter::new(crate::completions::complete_container_id)
201 )]
202 name: Option<String>,
203 },
204 #[command(name = "rm")]
206 Remove {
207 #[arg(
209 value_name = "CONTAINER_ID",
210 add = ArgValueCompleter::new(crate::completions::complete_container_id)
211 )]
212 container_id: Option<String>,
213 #[arg(value_name = "PORT[/PROTO]")]
215 port: Option<String>,
216 #[arg(long)]
218 all: bool,
219 #[arg(long)]
221 id: Option<u64>,
222 #[arg(
224 long,
225 add = ArgValueCompleter::new(crate::completions::complete_container_id)
226 )]
227 name: Option<String>,
228 },
229 #[command(name = "remap")]
231 Remap {
232 #[arg(
234 value_name = "CONTAINER_ID",
235 add = ArgValueCompleter::new(crate::completions::complete_container_id)
236 )]
237 container_id: String,
238 #[arg(value_name = "OLD_PORT:NEW_PORT")]
240 mapping: String,
241 },
242}
243
244pub async fn run_cli(cli: Cli, use_color: bool) -> Result<()> {
246 let socket = cli.socket;
247 let json = cli.json;
248
249 match cli.command {
250 NatMapCommand::Dnat {
251 ext_ip,
252 int_ip,
253 proto,
254 ports,
255 ext_if,
256 delete,
257 no_masquerade,
258 } => {
259 handle_dnat(
260 ext_ip,
261 int_ip,
262 proto,
263 ports,
264 ext_if,
265 delete,
266 no_masquerade,
267 &socket,
268 )
269 .await?;
270 }
271 NatMapCommand::Snat {
272 int_ip,
273 ext_if,
274 ext_ip,
275 delete,
276 } => {
277 handle_snat(int_ip, ext_if, ext_ip, delete, &socket).await?;
278 }
279 NatMapCommand::Hairpin {
280 ext_ip,
281 int_ip,
282 proto,
283 ports,
284 lan_cidr,
285 delete,
286 } => {
287 handle_hairpin(ext_ip, int_ip, proto, ports, lan_cidr, delete, &socket).await?;
288 }
289 NatMapCommand::PolicyRoute {
290 src_ip,
291 via,
292 table,
293 delete,
294 } => {
295 handle_policy_route(src_ip, via, table, delete, &socket).await?;
296 }
297 NatMapCommand::List { container_id } => {
298 handle_list(&socket, container_id, json, use_color).await?;
299 }
300 NatMapCommand::Clear => {
301 handle_clear(&socket).await?;
302 }
303 NatMapCommand::Docker { cmd } => match cmd {
304 DockerCommand::Add {
305 container_id,
306 mapping,
307 name,
308 } => {
309 add(container_id, mapping, name, &socket, json).await?;
310 }
311 DockerCommand::Remove {
312 container_id,
313 port,
314 all,
315 id,
316 name,
317 } => {
318 remove(container_id, port, all, id, name, &socket, json).await?;
319 }
320 DockerCommand::Remap {
321 container_id,
322 mapping,
323 } => {
324 remap(container_id, mapping, &socket, json).await?;
325 }
326 },
327 NatMapCommand::Save => {
328 Command::new("sh")
329 .arg("-c")
330 .arg("iptables-save > /etc/iptables/rules.v4")
331 .status()?;
332 }
333 NatMapCommand::Fwd => {
334 let status = Command::new("sysctl")
335 .arg("-w")
336 .arg("net.ipv4.ip_forward=1")
337 .status()?;
338 if !status.success() {
339 color_eyre::eyre::bail!("Failed to enable IP forwarding");
340 }
341 }
342 NatMapCommand::Daemon {
343 state,
344 socket,
345 socket_group,
346 } => {
347 Daemon::new(socket, state, socket_group)
348 .await?
349 .run()
350 .await?;
351 }
352 NatMapCommand::Install { binary, group } => {
353 crate::install::install_systemd(&binary, &group)?;
354 }
355 }
356 Ok(())
357}