Skip to main content

dfw/
process.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 holds the types related to configuration processing and rule creation.
11
12use crate::{errors::*, types::*, util::FutureExt, FirewallBackend};
13use bollard::{
14    container::ListContainersOptions,
15    models::{ContainerSummary, Network, NetworkContainer},
16    Docker,
17};
18use failure::{bail, format_err};
19use maplit::hashmap;
20use slog::{debug, o, trace, Logger};
21use std::collections::HashMap as Map;
22
23/// This trait allows a type to define its own processing rules. It is expected to return a list
24/// of rules that can be applied with nft.
25///
26/// # Example
27///
28/// ```
29/// # use dfw::FirewallBackend;
30/// # use dfw::process::{Process, ProcessContext};
31/// # use dfw::types::DFW;
32/// # use failure::Error;
33/// struct MyBackend;
34/// impl FirewallBackend for MyBackend {
35///     type Rule = String;
36/// #    type Defaults = ();
37///
38///     fn apply(rules: Vec<String>, ctx: &ProcessContext<Self>) -> Result<(), Error> {
39///         // Write code to apply the processed rules.
40/// #        unimplemented!()
41///     }
42/// }
43/// # impl Process<MyBackend> for DFW<MyBackend> {
44/// #     fn process(&self, ctx: &ProcessContext<MyBackend>) -> Result<Option<Vec<String>>, Error> {
45/// #         unimplemented!()
46/// #     }
47/// # }
48/// struct MyType {
49///     rules: Vec<String>,
50/// }
51///
52/// impl Process<MyBackend> for MyType {
53///     fn process(&self, ctx: &ProcessContext<MyBackend>) -> Result<Option<Vec<String>>, Error> {
54///         let mut rules = Vec::new();
55///         for rule in &self.rules {
56///             rules.push(format!("add rule {}", rule));
57///         }
58///         Ok(Some(rules))
59///     }
60/// }
61/// ```
62pub trait Process<B: FirewallBackend>
63where
64    DFW<B>: Process<B>,
65{
66    /// Process the current type within the given [`ProcessContext`], returning zero or more rules
67    /// to apply with nft.
68    ///
69    /// [`ProcessContext`]: struct.ProcessContext.html
70    fn process(&self, ctx: &ProcessContext<B>) -> Result<Option<Vec<B::Rule>>>;
71}
72
73impl<B, T> Process<B> for Option<T>
74where
75    B: FirewallBackend,
76    DFW<B>: Process<B>,
77    T: Process<B>,
78{
79    fn process(&self, ctx: &ProcessContext<B>) -> Result<Option<Vec<B::Rule>>> {
80        match self {
81            Some(t) => t.process(ctx),
82            None => Ok(None),
83        }
84    }
85}
86
87impl<B, T> Process<B> for Vec<T>
88where
89    B: FirewallBackend,
90    DFW<B>: Process<B>,
91    T: Process<B>,
92{
93    fn process(&self, ctx: &ProcessContext<B>) -> Result<Option<Vec<B::Rule>>> {
94        let mut rules = Vec::new();
95        for rule in self {
96            if let Some(mut sub_rules) = rule.process(ctx)? {
97                rules.append(&mut sub_rules);
98            }
99        }
100
101        Ok(Some(rules))
102    }
103}
104
105/// Enclosing struct to manage rule processing.
106pub struct ProcessContext<'a, B>
107where
108    B: FirewallBackend,
109    DFW<B>: Process<B>,
110{
111    pub(crate) docker: &'a Docker,
112    pub(crate) dfw: &'a DFW<B>,
113    pub(crate) container_map: Map<String, ContainerSummary>,
114    pub(crate) network_map: Map<String, Network>,
115    pub(crate) external_network_interfaces: Option<Vec<String>>,
116    pub(crate) primary_external_network_interface: Option<String>,
117    pub(crate) logger: Logger,
118    pub(crate) dry_run: bool,
119}
120
121impl<'a, B> ProcessContext<'a, B>
122where
123    B: FirewallBackend,
124    DFW<B>: Process<B>,
125{
126    /// Create a new instance of `ProcessDFW` for rule processing.
127    pub fn new(
128        docker: &'a Docker,
129        dfw: &'a DFW<B>,
130        processing_options: &'a ProcessingOptions,
131        logger: &'a Logger,
132        dry_run: bool,
133    ) -> Result<ProcessContext<'a, B>> {
134        let logger = logger.new(o!());
135
136        let list_containers_options = match processing_options.container_filter {
137            ContainerFilter::All => None,
138            ContainerFilter::Running => Some(ListContainersOptions {
139                filters: hashmap! { "status" => vec!["running"]},
140                ..Default::default()
141            }),
142        };
143        let containers = docker.list_containers(list_containers_options).sync()?;
144        debug!(logger, "Got list of containers";
145               o!("containers" => format!("{:#?}", containers)));
146
147        let container_map = get_container_map(&containers);
148        trace!(logger, "Got map of containers";
149               o!("container_map" => format!("{:#?}", container_map)));
150
151        let networks = docker.list_networks::<String>(None).sync()?;
152        debug!(logger, "Got list of networks";
153               o!("networks" => format!("{:#?}", networks)));
154
155        let network_map =
156            get_network_map(&networks).ok_or_else(|| format_err!("no networks found"))?;
157        trace!(logger, "Got map of networks";
158               o!("container_map" => format!("{:#?}", container_map)));
159
160        let external_network_interfaces = dfw
161            .global_defaults
162            .external_network_interfaces
163            .as_ref()
164            .cloned();
165        let primary_external_network_interface = external_network_interfaces
166            .as_ref()
167            .and_then(|v| v.first())
168            .map(|s| s.to_owned());
169
170        Ok(ProcessContext {
171            docker,
172            dfw,
173            container_map,
174            network_map,
175            external_network_interfaces,
176            primary_external_network_interface,
177            logger,
178            dry_run,
179        })
180    }
181
182    /// Start the processing using the configuration given at creation.
183    pub fn process(&mut self) -> Result<()> {
184        let rules = Process::<B>::process(self.dfw, self)?;
185        if let Some(rules) = rules {
186            B::apply(rules, self)?;
187        }
188
189        Ok(())
190    }
191}
192
193/// Option to filter the containers to be processed
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum ContainerFilter {
196    /// Process all containers, i.e. don't filter.
197    All,
198    /// Only process running containers.
199    Running,
200}
201
202/// Options to configure the processing procedure.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct ProcessingOptions {
205    /// Option to filter the containers to be processed, see
206    /// [`ContainerFilter`](enum.ContainerFilter.html).
207    pub container_filter: ContainerFilter,
208}
209
210impl Default for ProcessingOptions {
211    fn default() -> Self {
212        ProcessingOptions {
213            container_filter: ContainerFilter::All,
214        }
215    }
216}
217
218pub(crate) fn get_bridge_name(network_id: &str) -> Result<String> {
219    if network_id.len() < 12 {
220        bail!("network has to be longer than 12 characters");
221    }
222    Ok(format!("br-{}", &network_id[..12]))
223}
224
225pub(crate) fn docker_bridge_name(network: &Network) -> Result<Option<String>> {
226    if network.driver.as_deref() != Some("bridge") {
227        return Ok(None);
228    }
229    if let Some(name) = network
230        .options
231        .as_ref()
232        .and_then(|options| options.get("com.docker.network.bridge.name"))
233        .filter(|name| !name.is_empty())
234    {
235        return Ok(Some(name.clone()));
236    }
237    network.id.as_deref().map(get_bridge_name).transpose()
238}
239
240pub(crate) struct ContainerDnatEndpoints {
241    pub source_bridges: Vec<String>,
242    pub source_ip: Option<std::net::Ipv4Addr>,
243    pub destination_ip: std::net::Ipv4Addr,
244}
245
246pub(crate) fn container_dnat_endpoints<B>(
247    rule: &ContainerDNATRule,
248    ctx: &ProcessContext<B>,
249) -> Result<Option<ContainerDnatEndpoints>>
250where
251    B: FirewallBackend,
252    DFW<B>: Process<B>,
253{
254    let (source_bridges, source_ip) = if let Some(network_name) = &rule.src_network {
255        let Some(network) = ctx.network_map.get(network_name) else {
256            return Ok(None);
257        };
258        let Some(bridge) = docker_bridge_name(network)? else {
259            return Ok(None);
260        };
261        let source_ip = if let Some(container) = &rule.src_container {
262            let Some(network_id) = network.id.as_deref() else {
263                return Ok(None);
264            };
265            let Some(ip) =
266                get_network_for_container(ctx.docker, &ctx.container_map, container, network_id)?
267                    .as_ref()
268                    .and_then(container_ipv4)
269            else {
270                return Ok(None);
271            };
272            Some(ip)
273        } else {
274            None
275        };
276        (vec![bridge], source_ip)
277    } else {
278        if rule.src_container.is_some() {
279            bail!("a container-DNAT rule with src_container requires src_network");
280        }
281        let mut bridges = Vec::new();
282        for network in ctx.network_map.values() {
283            if let Some(name) = docker_bridge_name(network)? {
284                bridges.push(name);
285            }
286        }
287        bridges.sort_unstable();
288        bridges.dedup();
289        (bridges, None)
290    };
291
292    let Some(network_id) = ctx
293        .network_map
294        .get(&rule.dst_network)
295        .and_then(|n| n.id.as_deref())
296    else {
297        return Ok(None);
298    };
299    let Some(destination_ip) = get_network_for_container(
300        ctx.docker,
301        &ctx.container_map,
302        &rule.dst_container,
303        network_id,
304    )?
305    .as_ref()
306    .and_then(container_ipv4) else {
307        return Ok(None);
308    };
309
310    Ok(Some(ContainerDnatEndpoints {
311        source_bridges,
312        source_ip,
313        destination_ip,
314    }))
315}
316
317pub(crate) fn get_network_for_container(
318    docker: &Docker,
319    container_map: &Map<String, ContainerSummary>,
320    container_name: &str,
321    network_id: &str,
322) -> Result<Option<NetworkContainer>> {
323    if let Some(container) = container_map.get(container_name) {
324        let network = match docker.inspect_network::<String>(network_id, None).sync() {
325            Ok(network) => network,
326            Err(bollard::errors::Error::DockerResponseServerError {
327                status_code: 404, ..
328            }) => return Ok(None),
329            Err(error) => return Err(error.into()),
330        };
331        Ok(network.containers.and_then(|containers| {
332            container
333                .id
334                .as_ref()
335                .and_then(|container_id| containers.get(container_id).cloned())
336        }))
337    } else {
338        Ok(None)
339    }
340}
341
342pub(crate) fn container_ipv4(endpoint: &NetworkContainer) -> Option<std::net::Ipv4Addr> {
343    endpoint
344        .ipv4_address
345        .as_deref()?
346        .split('/')
347        .next()?
348        .parse()
349        .ok()
350}
351
352pub(crate) fn get_container_map(containers: &[ContainerSummary]) -> Map<String, ContainerSummary> {
353    let mut container_map: Map<String, ContainerSummary> = Map::new();
354    for container in containers {
355        if let Some(names) = &container.names {
356            for name in names {
357                container_map.insert(
358                    name.clone().trim_start_matches('/').to_owned(),
359                    container.clone(),
360                );
361            }
362        }
363    }
364
365    container_map
366}
367
368pub(crate) fn get_network_map(networks: &[Network]) -> Option<Map<String, Network>> {
369    let mut network_map: Map<String, Network> = Map::new();
370    for network in networks {
371        if let Some(name) = &network.name {
372            network_map.insert(name.clone(), network.clone());
373        }
374    }
375
376    if network_map.is_empty() {
377        None
378    } else {
379        Some(network_map)
380    }
381}
382
383pub(crate) fn generate_marker(components: &[&str]) -> String {
384    format!("DFW-MARKER:{}", components.join(";"))
385}