Skip to main content

dfw/iptables/
mod.rs

1// Copyright Pit Kleyersburg <pitkley@googlemail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified or distributed
8// except according to those terms.
9
10//! This module implements the iptables backend for DFW.
11
12use crate::{errors::*, FirewallBackend, ProcessContext};
13use failure::format_err;
14use slog::info;
15use std::{
16    collections::BTreeMap,
17    io::{BufWriter, Write},
18    process::{Command, Stdio},
19    str,
20};
21use strum::EnumDiscriminants;
22
23mod process;
24mod rule;
25pub mod types;
26
27const DFW_FORWARD_CHAIN: &str = "DFWRS_FORWARD";
28const DFW_INPUT_CHAIN: &str = "DFWRS_INPUT";
29const DFW_POSTROUTING_CHAIN: &str = "DFWRS_POSTROUTING";
30const DFW_PREROUTING_CHAIN: &str = "DFWRS_PREROUTING";
31
32const COMMAND_IPTABLES_RESTORE: &str = "iptables-restore";
33const COMMAND_IP6TABLES_RESTORE: &str = "ip6tables-restore";
34
35type Table = String;
36type Chain = String;
37type Policy = String;
38type Rule = String;
39
40/// Marker struct to implement iptables as a firewall backend.
41#[derive(Debug)]
42pub struct Iptables;
43impl FirewallBackend for Iptables {
44    type Rule = IptablesRule;
45    type Defaults = types::Defaults;
46
47    fn apply(rules: Vec<Self::Rule>, ctx: &ProcessContext<Self>) -> Result<()> {
48        if ctx.dry_run {
49            info!(ctx.logger, "Performing dry-run, will not update any rules");
50        } else {
51            info!(
52                ctx.logger,
53                "Applying IPv4 rules (using {})", COMMAND_IPTABLES_RESTORE
54            );
55            Self::restore(IptablesRuleDiscriminants::V4, rules.clone())?;
56            info!(
57                ctx.logger,
58                "Applying IPv6 rules (using {})", COMMAND_IP6TABLES_RESTORE
59            );
60            Self::restore(IptablesRuleDiscriminants::V6, rules)?;
61        }
62        Ok(())
63    }
64}
65
66impl Iptables {
67    fn restore(
68        rule_discriminant: IptablesRuleDiscriminants,
69        rules: Vec<IptablesRule>,
70    ) -> Result<()> {
71        let command = match rule_discriminant {
72            IptablesRuleDiscriminants::V4 => COMMAND_IPTABLES_RESTORE,
73            IptablesRuleDiscriminants::V6 => COMMAND_IP6TABLES_RESTORE,
74        };
75        let mut process = Command::new(command)
76            .stdin(Stdio::piped())
77            .stdout(Stdio::piped())
78            .stderr(Stdio::piped())
79            .spawn()?;
80
81        // Get process stdin, write format as expected by iptables-restore
82        match process.stdin.as_mut() {
83            Some(s) => Self::write_rules(rules, rule_discriminant, s)?,
84            None => return Err(format_err!("cannot get stdin of {}", command)),
85        }
86
87        // Check exit status of command
88        let output = process.wait_with_output()?;
89        if output.status.success() {
90            Ok(())
91        } else {
92            Err(format_err!(
93                "{} failed: '{}'",
94                command,
95                str::from_utf8(&output.stderr).unwrap_or("").trim(),
96            ))
97        }
98    }
99
100    /// Retrieve the current text that would be passed to `iptables-restore` as a vector of lines.
101    pub fn get_rules(
102        rules: Vec<IptablesRule>,
103        rule_discriminant: IptablesRuleDiscriminants,
104    ) -> Vec<String> {
105        // Create a writer for around a vector
106        let mut w = BufWriter::new(Vec::new());
107        // Write the rules into the writer (and hence into the vector)
108        Self::write_rules(rules, rule_discriminant, &mut w).unwrap();
109        // Retrieve the vector from the writer
110        let v = w.into_inner().unwrap();
111        // Transform the `Vec<u8>` into `&str` (this can happen unsafely because the input provided
112        // comes from DFW and is UTF-8)
113        let s = unsafe { str::from_utf8_unchecked(&v) };
114
115        // Trim whitespace, split on newlines, make owned and collect into `Vec<String>`
116        s.trim().split('\n').map(|e| e.to_owned()).collect()
117    }
118
119    /// Write the rules in iptables-restore format to a given writer.
120    ///
121    /// (Used internally by `apply` and in tests to verify correct output.)
122    fn write_rules<W: Write>(
123        rules: Vec<IptablesRule>,
124        rule_discriminant: IptablesRuleDiscriminants,
125        w: &mut W,
126    ) -> Result<()> {
127        #[allow(clippy::type_complexity)]
128        let mut rule_map: BTreeMap<Table, BTreeMap<Chain, (Option<Policy>, Vec<Rule>)>> =
129            BTreeMap::new();
130        for rule in rules {
131            if rule_discriminant != (&rule).into() {
132                continue;
133            }
134            match rule.policy_or_rule() {
135                PolicyOrRule::Policy {
136                    table,
137                    chain,
138                    policy,
139                } => {
140                    rule_map
141                        .entry(table.to_owned())
142                        .or_default()
143                        .entry(chain.to_owned())
144                        .or_insert_with(|| (None, Vec::new()))
145                        .0 = Some(policy.to_owned());
146                }
147                PolicyOrRule::Rule {
148                    table,
149                    chain,
150                    value,
151                } => {
152                    rule_map
153                        .entry(table.to_owned())
154                        .or_default()
155                        .entry(chain.to_owned())
156                        .or_insert_with(|| (None, Vec::new()))
157                        .1
158                        .push(value.to_owned());
159                }
160            }
161        }
162
163        for (table, chains) in rule_map.iter() {
164            writeln!(w, "*{}", table)?;
165            for (chain, (policy, _)) in chains.iter() {
166                if let Some(policy) = policy {
167                    writeln!(w, ":{} {} [0:0]", chain, policy)?;
168                }
169            }
170            for (_, rules) in chains.values() {
171                for rule in rules {
172                    writeln!(w, "{}", rule)?;
173                }
174            }
175            writeln!(w, "COMMIT")?;
176        }
177
178        Ok(())
179    }
180}
181
182/// Rule representation for iptables firewall backend.
183#[derive(Debug, Clone, EnumDiscriminants)]
184pub enum IptablesRule {
185    /// IPv4
186    V4(PolicyOrRule),
187    /// IPv6
188    V6(PolicyOrRule),
189}
190
191impl IptablesRule {
192    fn policy_or_rule(&self) -> &PolicyOrRule {
193        match self {
194            Self::V4(policy_or_rule) => policy_or_rule,
195            Self::V6(policy_or_rule) => policy_or_rule,
196        }
197    }
198
199    pub(crate) fn from_discriminant(
200        discriminant: IptablesRuleDiscriminants,
201        policy_or_rule: PolicyOrRule,
202    ) -> Self {
203        match discriminant {
204            IptablesRuleDiscriminants::V4 => IptablesRule::V4(policy_or_rule),
205            IptablesRuleDiscriminants::V6 => IptablesRule::V6(policy_or_rule),
206        }
207    }
208}
209
210/// Policy or rule representation for iptables firewall backend.
211#[derive(Debug, Clone)]
212pub enum PolicyOrRule {
213    /// Rule specifying a chain policy.
214    Policy {
215        /// Associated iptables table.
216        table: String,
217        /// Associated iptables chain.
218        chain: String,
219        /// Policy to set.
220        policy: String,
221    },
222    /// Actual filter rule that will be added to a chain.
223    Rule {
224        /// Associated iptables table.
225        table: String,
226        /// Associated iptables chain.
227        chain: String,
228        /// The rule itself in valid iptables syntax.
229        value: String,
230    },
231}