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)]
108 delete: bool,
109 },
110 #[command(name = "policy-route")]
112 PolicyRoute {
113 #[arg(long)]
115 src_ip: String,
116 #[arg(long)]
118 via: String,
119 #[arg(long, default_value = "100")]
121 table: u32,
122 #[arg(long)]
124 delete: bool,
125 },
126 #[command(name = "ls")]
128 List {
129 #[arg(
131 value_name = "CONTAINER_ID",
132 add = ArgValueCompleter::new(crate::completions::complete_container_id)
133 )]
134 container_id: Option<String>,
135 },
136 #[command(name = "clear")]
138 Clear,
139 #[command(name = "docker")]
141 Docker {
142 #[command(subcommand)]
143 cmd: DockerCommand,
144 },
145 #[command(name = "save")]
147 Save,
148 #[command(name = "fwd")]
150 Fwd,
151 #[command(name = "daemon")]
153 Daemon {
154 #[arg(long, default_value = STATE)]
156 state: PathBuf,
157 #[arg(long, default_value = DAEMON_SOCK)]
159 socket: PathBuf,
160 #[arg(long, default_value = PKG_NAME)]
162 socket_group: String,
163 },
164 #[command(name = "install")]
166 Install {
167 #[arg(long, default_value = PKG_NAME)]
169 group: String,
170 #[arg(long, default_value = lab_ops_lab_lib::consts::LABOPS_BIN)]
172 binary: String,
173 },
174}
175
176#[derive(Subcommand, Debug)]
178pub enum DockerCommand {
179 #[command(name = "add")]
181 Add {
182 #[arg(
184 value_name = "CONTAINER_ID",
185 add = ArgValueCompleter::new(crate::completions::complete_container_id)
186 )]
187 container_id: String,
188 #[arg(value_name = "MAPPING")]
191 mapping: Option<String>,
192 #[arg(
194 long,
195 add = ArgValueCompleter::new(crate::completions::complete_container_id)
196 )]
197 name: Option<String>,
198 },
199 #[command(name = "rm")]
201 Remove {
202 #[arg(
204 value_name = "CONTAINER_ID",
205 add = ArgValueCompleter::new(crate::completions::complete_container_id)
206 )]
207 container_id: Option<String>,
208 #[arg(value_name = "PORT[/PROTO]")]
210 port: Option<String>,
211 #[arg(long)]
213 all: bool,
214 #[arg(long)]
216 id: Option<u64>,
217 #[arg(
219 long,
220 add = ArgValueCompleter::new(crate::completions::complete_container_id)
221 )]
222 name: Option<String>,
223 },
224 #[command(name = "remap")]
226 Remap {
227 #[arg(
229 value_name = "CONTAINER_ID",
230 add = ArgValueCompleter::new(crate::completions::complete_container_id)
231 )]
232 container_id: String,
233 #[arg(value_name = "OLD_PORT:NEW_PORT")]
235 mapping: String,
236 },
237}
238
239pub async fn run_cli(cli: Cli, use_color: bool) -> Result<()> {
241 let socket = cli.socket;
242 let json = cli.json;
243
244 match cli.command {
245 NatMapCommand::Dnat {
246 ext_ip,
247 int_ip,
248 proto,
249 ports,
250 ext_if,
251 delete,
252 no_masquerade,
253 } => {
254 handle_dnat(
255 ext_ip,
256 int_ip,
257 proto,
258 ports,
259 ext_if,
260 delete,
261 no_masquerade,
262 &socket,
263 )
264 .await?;
265 }
266 NatMapCommand::Snat {
267 int_ip,
268 ext_if,
269 ext_ip,
270 delete,
271 } => {
272 handle_snat(int_ip, ext_if, ext_ip, delete, &socket).await?;
273 }
274 NatMapCommand::Hairpin {
275 ext_ip,
276 int_ip,
277 proto,
278 ports,
279 delete,
280 } => {
281 handle_hairpin(ext_ip, int_ip, proto, ports, delete, &socket).await?;
282 }
283 NatMapCommand::PolicyRoute {
284 src_ip,
285 via,
286 table,
287 delete,
288 } => {
289 handle_policy_route(src_ip, via, table, delete, &socket).await?;
290 }
291 NatMapCommand::List { container_id } => {
292 handle_list(&socket, container_id, json, use_color).await?;
293 }
294 NatMapCommand::Clear => {
295 handle_clear(&socket).await?;
296 }
297 NatMapCommand::Docker { cmd } => match cmd {
298 DockerCommand::Add {
299 container_id,
300 mapping,
301 name,
302 } => {
303 add(container_id, mapping, name, &socket, json).await?;
304 }
305 DockerCommand::Remove {
306 container_id,
307 port,
308 all,
309 id,
310 name,
311 } => {
312 remove(container_id, port, all, id, name, &socket, json).await?;
313 }
314 DockerCommand::Remap {
315 container_id,
316 mapping,
317 } => {
318 remap(container_id, mapping, &socket, json).await?;
319 }
320 },
321 NatMapCommand::Save => {
322 Command::new("sh")
323 .arg("-c")
324 .arg("iptables-save > /etc/iptables/rules.v4")
325 .status()?;
326 }
327 NatMapCommand::Fwd => {
328 let status = Command::new("sysctl")
329 .arg("-w")
330 .arg("net.ipv4.ip_forward=1")
331 .status()?;
332 if !status.success() {
333 color_eyre::eyre::bail!("Failed to enable IP forwarding");
334 }
335 }
336 NatMapCommand::Daemon {
337 state,
338 socket,
339 socket_group,
340 } => {
341 Daemon::new(socket, state, socket_group)
342 .await?
343 .run()
344 .await?;
345 }
346 NatMapCommand::Install { binary, group } => {
347 crate::install::install_systemd(&binary, &group)?;
348 }
349 }
350 Ok(())
351}