iptables/
lib.rs

1// In the name of Allah
2
3//! Provides bindings for [iptables](https://www.netfilter.org/projects/iptables/index.html) application in Linux.
4//! This crate uses iptables binary to manipulate chains and tables.
5//! This source code is licensed under MIT license that can be found in the LICENSE file.
6//!
7//! # Example
8//! ```
9//! let ipt = iptables::new(false).unwrap();
10//! assert!(ipt.new_chain("nat", "NEWCHAINNAME").is_ok());
11//! assert!(ipt.append("nat", "NEWCHAINNAME", "-j ACCEPT").is_ok());
12//! assert!(ipt.exists("nat", "NEWCHAINNAME", "-j ACCEPT").unwrap());
13//! assert!(ipt.delete("nat", "NEWCHAINNAME", "-j ACCEPT").is_ok());
14//! assert!(ipt.delete_chain("nat", "NEWCHAINNAME").is_ok());
15//! ```
16
17pub mod error;
18
19use error::IptablesError;
20use lazy_static::lazy_static;
21use regex::{Match, Regex};
22use std::convert::From;
23use std::error::Error;
24use std::ffi::OsStr;
25use std::process::{Command, Output};
26use std::vec::Vec;
27
28// List of built-in chains taken from: man 8 iptables
29const BUILTIN_CHAINS_FILTER: &[&str] = &["INPUT", "FORWARD", "OUTPUT"];
30const BUILTIN_CHAINS_MANGLE: &[&str] = &["PREROUTING", "OUTPUT", "INPUT", "FORWARD", "POSTROUTING"];
31const BUILTIN_CHAINS_NAT: &[&str] = &["PREROUTING", "POSTROUTING", "OUTPUT"];
32const BUILTIN_CHAINS_RAW: &[&str] = &["PREROUTING", "OUTPUT"];
33const BUILTIN_CHAINS_SECURITY: &[&str] = &["INPUT", "OUTPUT", "FORWARD"];
34
35lazy_static! {
36    static ref RE_SPLIT: Regex = Regex::new(r#"["'].+?["']|[^ ]+"#).unwrap();
37}
38
39trait SplitQuoted {
40    fn split_quoted(&self) -> Vec<&str>;
41}
42
43impl SplitQuoted for str {
44    fn split_quoted(&self) -> Vec<&str> {
45        RE_SPLIT
46            // Iterate over matched segments
47            .find_iter(self)
48            // Get match as str
49            .map(|m| Match::as_str(&m))
50            // Remove any surrounding quotes (they will be reinserted by `Command`)
51            .map(|s| s.trim_matches(|c| c == '"' || c == '\''))
52            // Collect
53            .collect::<Vec<_>>()
54    }
55}
56
57fn error_from_str(msg: &str) -> Box<dyn Error> {
58    msg.into()
59}
60
61fn output_to_result(output: Output) -> Result<(), Box<dyn Error>> {
62    if !output.status.success() {
63        return Err(Box::new(IptablesError::from(output)));
64    }
65    Ok(())
66}
67
68fn get_builtin_chains(table: &str) -> Result<&[&str], Box<dyn Error>> {
69    match table {
70        "filter" => Ok(BUILTIN_CHAINS_FILTER),
71        "mangle" => Ok(BUILTIN_CHAINS_MANGLE),
72        "nat" => Ok(BUILTIN_CHAINS_NAT),
73        "raw" => Ok(BUILTIN_CHAINS_RAW),
74        "security" => Ok(BUILTIN_CHAINS_SECURITY),
75        _ => Err(error_from_str("given table is not supported by iptables")),
76    }
77}
78
79/// Contains the iptables command and shows if it supports -w and -C options.
80/// Use `new` method to create a new instance of this struct.
81pub struct IPTables {
82    /// The utility command which must be 'iptables' or 'ip6tables'.
83    pub cmd: &'static str,
84
85    /// Indicates if iptables has -C (--check) option
86    pub has_check: bool,
87
88    /// Indicates if iptables has -w (--wait) option
89    pub has_wait: bool,
90
91    /// Indicates if iptables will be run with -n (--numeric) option
92    pub is_numeric: bool,
93}
94
95/// Returns `None` because iptables only works on linux
96#[cfg(not(target_os = "linux"))]
97pub fn new(is_ipv6: bool) -> Result<IPTables, Box<dyn Error>> {
98    Err(error_from_str("iptables only works on Linux"))
99}
100
101/// Creates a new `IPTables` Result with the command of 'iptables' if `is_ipv6` is `false`, otherwise the command is 'ip6tables'.
102#[cfg(target_os = "linux")]
103pub fn new(is_ipv6: bool) -> Result<IPTables, Box<dyn Error>> {
104    let cmd = if is_ipv6 { "ip6tables" } else { "iptables" };
105
106    let version_output = Command::new(cmd).arg("--version").output()?;
107    let re = Regex::new(r"v(\d+)\.(\d+)\.(\d+)")?;
108    let version_string = String::from_utf8_lossy(version_output.stdout.as_slice());
109    let versions = re
110        .captures(&version_string)
111        .ok_or("invalid version number")?;
112    let v_major = versions
113        .get(1)
114        .ok_or("unable to get major version number")?
115        .as_str()
116        .parse::<i32>()?;
117    let v_minor = versions
118        .get(2)
119        .ok_or("unable to get minor version number")?
120        .as_str()
121        .parse::<i32>()?;
122    let v_patch = versions
123        .get(3)
124        .ok_or("unable to get patch version number")?
125        .as_str()
126        .parse::<i32>()?;
127
128    Ok(IPTables {
129        cmd,
130        has_check: (v_major > 1)
131            || (v_major == 1 && v_minor > 4)
132            || (v_major == 1 && v_minor == 4 && v_patch > 10),
133        has_wait: (v_major > 1)
134            || (v_major == 1 && v_minor > 4)
135            || (v_major == 1 && v_minor == 4 && v_patch > 19),
136        is_numeric: false,
137    })
138}
139
140impl IPTables {
141    /// Get the default policy for a table/chain.
142    pub fn get_policy(&self, table: &str, chain: &str) -> Result<String, Box<dyn Error>> {
143        let builtin_chains = get_builtin_chains(table)?;
144        if !builtin_chains.iter().as_slice().contains(&chain) {
145            return Err(error_from_str(
146                "given chain is not a default chain in the given table, can't get policy",
147            ));
148        }
149
150        let stdout = match self.is_numeric {
151            false => self.run(&["-t", table, "-L", chain])?.stdout,
152            true => self.run(&["-t", table, "-L", chain, "-n"])?.stdout,
153        };
154        let output = String::from_utf8_lossy(stdout.as_slice());
155        for item in output.trim().split('\n') {
156            let fields = item.split(' ').collect::<Vec<&str>>();
157            if fields.len() > 1 && fields[0] == "Chain" && fields[1] == chain {
158                return Ok(fields[3].replace(")", ""));
159            }
160        }
161        Err(error_from_str(
162            "could not find the default policy for table and chain",
163        ))
164    }
165
166    /// Set the default policy for a table/chain.
167    pub fn set_policy(&self, table: &str, chain: &str, policy: &str) -> Result<(), Box<dyn Error>> {
168        let builtin_chains = get_builtin_chains(table)?;
169        if !builtin_chains.iter().as_slice().contains(&chain) {
170            return Err(error_from_str(
171                "given chain is not a default chain in the given table, can't set policy",
172            ));
173        }
174
175        self.run(&["-t", table, "-P", chain, policy])
176            .and_then(output_to_result)
177    }
178
179    /// Executes a given `command` on the chain.
180    /// Returns the command output if successful.
181    pub fn execute(&self, table: &str, command: &str) -> Result<Output, Box<dyn Error>> {
182        self.run(&[&["-t", table], command.split_quoted().as_slice()].concat())
183    }
184
185    /// Checks for the existence of the `rule` in the table/chain.
186    /// Returns true if the rule exists.
187    #[cfg(target_os = "linux")]
188    pub fn exists(&self, table: &str, chain: &str, rule: &str) -> Result<bool, Box<dyn Error>> {
189        if !self.has_check {
190            return self.exists_old_version(table, chain, rule);
191        }
192
193        self.run(&[&["-t", table, "-C", chain], rule.split_quoted().as_slice()].concat())
194            .map(|output| output.status.success())
195    }
196
197    /// Checks for the existence of the `chain` in the table.
198    /// Returns true if the chain exists.
199    #[cfg(target_os = "linux")]
200    pub fn chain_exists(&self, table: &str, chain: &str) -> Result<bool, Box<dyn Error>> {
201        match self.is_numeric {
202            false => self
203                .run(&["-t", table, "-L", chain])
204                .map(|output| output.status.success()),
205            true => self
206                .run(&["-t", table, "-L", chain, "-n"])
207                .map(|output| output.status.success()),
208        }
209    }
210
211    fn exists_old_version(
212        &self,
213        table: &str,
214        chain: &str,
215        rule: &str,
216    ) -> Result<bool, Box<dyn Error>> {
217        match self.is_numeric {
218            false => self.run(&["-t", table, "-S"]).map(|output| {
219                String::from_utf8_lossy(&output.stdout).contains(&format!("-A {} {}", chain, rule))
220            }),
221            true => self.run(&["-t", table, "-S", "-n"]).map(|output| {
222                String::from_utf8_lossy(&output.stdout).contains(&format!("-A {} {}", chain, rule))
223            }),
224        }
225    }
226
227    /// Inserts `rule` in the `position` to the table/chain.
228    pub fn insert(
229        &self,
230        table: &str,
231        chain: &str,
232        rule: &str,
233        position: i32,
234    ) -> Result<(), Box<dyn Error>> {
235        self.run(
236            &[
237                &["-t", table, "-I", chain, &position.to_string()],
238                rule.split_quoted().as_slice(),
239            ]
240            .concat(),
241        )
242        .and_then(output_to_result)
243    }
244
245    /// Inserts `rule` in the `position` to the table/chain if it does not exist.
246    pub fn insert_unique(
247        &self,
248        table: &str,
249        chain: &str,
250        rule: &str,
251        position: i32,
252    ) -> Result<(), Box<dyn Error>> {
253        if self.exists(table, chain, rule)? {
254            return Err(error_from_str("the rule exists in the table/chain"));
255        }
256
257        self.insert(table, chain, rule, position)
258    }
259
260    /// Replaces `rule` in the `position` to the table/chain.
261    pub fn replace(
262        &self,
263        table: &str,
264        chain: &str,
265        rule: &str,
266        position: i32,
267    ) -> Result<(), Box<dyn Error>> {
268        self.run(
269            &[
270                &["-t", table, "-R", chain, &position.to_string()],
271                rule.split_quoted().as_slice(),
272            ]
273            .concat(),
274        )
275        .and_then(output_to_result)
276    }
277
278    /// Appends `rule` to the table/chain.
279    pub fn append(&self, table: &str, chain: &str, rule: &str) -> Result<(), Box<dyn Error>> {
280        self.run(&[&["-t", table, "-A", chain], rule.split_quoted().as_slice()].concat())
281            .and_then(output_to_result)
282    }
283
284    /// Appends `rule` to the table/chain if it does not exist.
285    pub fn append_unique(
286        &self,
287        table: &str,
288        chain: &str,
289        rule: &str,
290    ) -> Result<(), Box<dyn Error>> {
291        if self.exists(table, chain, rule)? {
292            return Err(error_from_str("the rule exists in the table/chain"));
293        }
294
295        self.append(table, chain, rule)
296    }
297
298    /// Appends or replaces `rule` to the table/chain if it does not exist.
299    pub fn append_replace(
300        &self,
301        table: &str,
302        chain: &str,
303        rule: &str,
304    ) -> Result<(), Box<dyn Error>> {
305        if self.exists(table, chain, rule)? {
306            self.delete(table, chain, rule)?;
307        }
308
309        self.append(table, chain, rule)
310    }
311
312    /// Deletes `rule` from the table/chain.
313    pub fn delete(&self, table: &str, chain: &str, rule: &str) -> Result<(), Box<dyn Error>> {
314        self.run(&[&["-t", table, "-D", chain], rule.split_quoted().as_slice()].concat())
315            .and_then(output_to_result)
316    }
317
318    /// Deletes all repetition of the `rule` from the table/chain.
319    pub fn delete_all(&self, table: &str, chain: &str, rule: &str) -> Result<(), Box<dyn Error>> {
320        while self.exists(table, chain, rule)? {
321            self.delete(table, chain, rule)?;
322        }
323
324        Ok(())
325    }
326
327    /// Lists rules in the table/chain.
328    pub fn list(&self, table: &str, chain: &str) -> Result<Vec<String>, Box<dyn Error>> {
329        match self.is_numeric {
330            false => self.get_list(&["-t", table, "-S", chain]),
331            true => self.get_list(&["-t", table, "-S", chain, "-n"]),
332        }
333    }
334
335    /// Lists rules in the table.
336    pub fn list_table(&self, table: &str) -> Result<Vec<String>, Box<dyn Error>> {
337        match self.is_numeric {
338            false => self.get_list(&["-t", table, "-S"]),
339            true => self.get_list(&["-t", table, "-S", "-n"]),
340        }
341    }
342
343    /// Lists the name of each chain in the table.
344    pub fn list_chains(&self, table: &str) -> Result<Vec<String>, Box<dyn Error>> {
345        let mut list = Vec::new();
346        let stdout = self.run(&["-t", table, "-S"])?.stdout;
347        let output = String::from_utf8_lossy(stdout.as_slice());
348        for item in output.trim().split('\n') {
349            let fields = item.split(' ').collect::<Vec<&str>>();
350            if fields.len() > 1 && (fields[0] == "-P" || fields[0] == "-N") {
351                list.push(fields[1].to_string());
352            }
353        }
354        Ok(list)
355    }
356
357    /// Creates a new user-defined chain.
358    pub fn new_chain(&self, table: &str, chain: &str) -> Result<(), Box<dyn Error>> {
359        self.run(&["-t", table, "-N", chain])
360            .and_then(output_to_result)
361    }
362
363    /// Flushes (deletes all rules) a chain.
364    pub fn flush_chain(&self, table: &str, chain: &str) -> Result<(), Box<dyn Error>> {
365        self.run(&["-t", table, "-F", chain])
366            .and_then(output_to_result)
367    }
368
369    /// Renames a chain in the table.
370    pub fn rename_chain(
371        &self,
372        table: &str,
373        old_chain: &str,
374        new_chain: &str,
375    ) -> Result<(), Box<dyn Error>> {
376        self.run(&["-t", table, "-E", old_chain, new_chain])
377            .and_then(output_to_result)
378    }
379
380    /// Deletes a user-defined chain in the table.
381    pub fn delete_chain(&self, table: &str, chain: &str) -> Result<(), Box<dyn Error>> {
382        self.run(&["-t", table, "-X", chain])
383            .and_then(output_to_result)
384    }
385
386    /// Flushes all chains in a table.
387    pub fn flush_table(&self, table: &str) -> Result<(), Box<dyn Error>> {
388        self.run(&["-t", table, "-F"]).and_then(output_to_result)
389    }
390
391    fn get_list<S: AsRef<OsStr>>(&self, args: &[S]) -> Result<Vec<String>, Box<dyn Error>> {
392        let stdout = self.run(args)?.stdout;
393        Ok(String::from_utf8_lossy(stdout.as_slice())
394            .trim()
395            .split('\n')
396            .map(String::from)
397            .collect())
398    }
399
400    /// Set whether iptables is called with the -n (--numeric) option,
401    /// to avoid host name and port name lookups
402    pub fn set_numeric(&mut self, numeric: bool) {
403        self.is_numeric = numeric;
404    }
405
406    fn run<S: AsRef<OsStr>>(&self, args: &[S]) -> Result<Output, Box<dyn Error>> {
407        let mut output_cmd = Command::new(self.cmd);
408        let output;
409
410        if self.has_wait {
411            output = output_cmd.args(args).arg("--wait").output()?;
412        } else {
413            output = output_cmd.args(args).output()?;
414        }
415
416        Ok(output)
417    }
418}