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_snat;
20use crate::command::remap;
21use crate::command::remove;
22use crate::consts::DAEMON_SOCK;
23use crate::consts::PKG_NAME;
24use crate::consts::STATE;
25use crate::daemon::Daemon;
26
27#[derive(Parser, Debug)]
29#[command(
30 name = PKG_NAME,
31 about = "Manage iptables NAT rules (static VMs & dynamic Docker)"
32)]
33pub struct Cli {
34 #[arg(long, default_value = DAEMON_SOCK, global = true)]
36 pub socket: PathBuf,
37
38 #[arg(long, global = true)]
40 pub json: bool,
41
42 #[command(subcommand)]
43 pub command: NatMapCommand,
44}
45
46#[derive(Subcommand, Debug)]
48pub enum NatMapCommand {
49 #[command(name = "dnat")]
51 Dnat {
52 #[arg(long)]
54 ext_ip: String,
55 #[arg(long)]
57 int_ip: String,
58 #[arg(long, default_value = "tcp")]
60 proto: String,
61 #[arg(long)]
63 ports: String,
64 #[arg(long)]
66 ext_if: Option<String>,
67 #[arg(long)]
69 delete: bool,
70 },
71 #[command(name = "snat")]
73 Snat {
74 #[arg(long)]
76 int_ip: String,
77 #[arg(long)]
79 ext_if: String,
80 #[arg(long)]
82 ext_ip: String,
83 #[arg(long)]
85 delete: bool,
86 },
87 #[command(name = "hairpin")]
89 Hairpin {
90 #[arg(long)]
92 ext_ip: String,
93 #[arg(long)]
95 int_ip: String,
96 #[arg(long, default_value = "tcp")]
98 proto: String,
99 #[arg(long)]
101 ports: String,
102 #[arg(long)]
104 delete: bool,
105 },
106 #[command(name = "ls")]
108 List {
109 #[arg(
111 value_name = "CONTAINER_ID",
112 add = ArgValueCompleter::new(crate::completions::complete_container_id)
113 )]
114 container_id: Option<String>,
115 },
116 #[command(name = "clear")]
118 Clear,
119 #[command(name = "docker")]
121 Docker {
122 #[command(subcommand)]
123 cmd: DockerCommand,
124 },
125 #[command(name = "save")]
127 Save,
128 #[command(name = "fwd")]
130 Fwd,
131 #[command(name = "daemon")]
133 Daemon {
134 #[arg(long, default_value = STATE)]
136 state: PathBuf,
137 #[arg(long, default_value = DAEMON_SOCK)]
139 socket: PathBuf,
140 #[arg(long, default_value = PKG_NAME)]
142 socket_group: String,
143 },
144 #[command(name = "install")]
146 Install {
147 #[arg(long, default_value = PKG_NAME)]
149 group: String,
150 #[arg(long, default_value = lab_ops_lab_lib::consts::LABOPS_BIN)]
152 binary: String,
153 },
154}
155
156#[derive(Subcommand, Debug)]
158pub enum DockerCommand {
159 #[command(name = "add")]
161 Add {
162 #[arg(
164 value_name = "CONTAINER_ID",
165 add = ArgValueCompleter::new(crate::completions::complete_container_id)
166 )]
167 container_id: String,
168 #[arg(value_name = "MAPPING")]
171 mapping: Option<String>,
172 #[arg(
174 long,
175 add = ArgValueCompleter::new(crate::completions::complete_container_id)
176 )]
177 name: Option<String>,
178 },
179 #[command(name = "rm")]
181 Remove {
182 #[arg(
184 value_name = "CONTAINER_ID",
185 add = ArgValueCompleter::new(crate::completions::complete_container_id)
186 )]
187 container_id: Option<String>,
188 #[arg(value_name = "PORT[/PROTO]")]
190 port: Option<String>,
191 #[arg(long)]
193 all: bool,
194 #[arg(long)]
196 id: Option<u64>,
197 #[arg(
199 long,
200 add = ArgValueCompleter::new(crate::completions::complete_container_id)
201 )]
202 name: Option<String>,
203 },
204 #[command(name = "remap")]
206 Remap {
207 #[arg(
209 value_name = "CONTAINER_ID",
210 add = ArgValueCompleter::new(crate::completions::complete_container_id)
211 )]
212 container_id: String,
213 #[arg(value_name = "OLD_PORT:NEW_PORT")]
215 mapping: String,
216 },
217}
218
219pub async fn run_cli(cli: Cli, use_color: bool) -> Result<()> {
221 let socket = cli.socket;
222 let json = cli.json;
223
224 match cli.command {
225 NatMapCommand::Dnat {
226 ext_ip,
227 int_ip,
228 proto,
229 ports,
230 ext_if,
231 delete,
232 } => {
233 handle_dnat(ext_ip, int_ip, proto, ports, ext_if, delete, &socket).await?;
234 }
235 NatMapCommand::Snat {
236 int_ip,
237 ext_if,
238 ext_ip,
239 delete,
240 } => {
241 handle_snat(int_ip, ext_if, ext_ip, delete, &socket).await?;
242 }
243 NatMapCommand::Hairpin {
244 ext_ip,
245 int_ip,
246 proto,
247 ports,
248 delete,
249 } => {
250 handle_hairpin(ext_ip, int_ip, proto, ports, delete, &socket).await?;
251 }
252 NatMapCommand::List { container_id } => {
253 handle_list(&socket, container_id, json, use_color).await?;
254 }
255 NatMapCommand::Clear => {
256 handle_clear(&socket).await?;
257 }
258 NatMapCommand::Docker { cmd } => match cmd {
259 DockerCommand::Add {
260 container_id,
261 mapping,
262 name,
263 } => {
264 add(container_id, mapping, name, &socket, json).await?;
265 }
266 DockerCommand::Remove {
267 container_id,
268 port,
269 all,
270 id,
271 name,
272 } => {
273 remove(container_id, port, all, id, name, &socket, json).await?;
274 }
275 DockerCommand::Remap {
276 container_id,
277 mapping,
278 } => {
279 remap(container_id, mapping, &socket, json).await?;
280 }
281 },
282 NatMapCommand::Save => {
283 Command::new("sh")
284 .arg("-c")
285 .arg("iptables-save > /etc/iptables/rules.v4")
286 .status()?;
287 }
288 NatMapCommand::Fwd => {
289 let status = Command::new("sysctl")
290 .arg("-w")
291 .arg("net.ipv4.ip_forward=1")
292 .status()?;
293 if !status.success() {
294 color_eyre::eyre::bail!("Failed to enable IP forwarding");
295 }
296 }
297 NatMapCommand::Daemon {
298 state,
299 socket,
300 socket_group,
301 } => {
302 Daemon::new(socket, state, socket_group)
303 .await?
304 .run()
305 .await?;
306 }
307 NatMapCommand::Install { binary, group } => {
308 crate::install::install_systemd(&binary, &group)?;
309 }
310 }
311 Ok(())
312}