dfw/nftables/
rule.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
10use crate::{errors::*, nftables::DFW_MARK, types::RuleVerdict};
11use derive_builder::Builder;
12use failure::bail;
13
14#[derive(Debug, Clone, Builder)]
15#[builder(derive(Debug), pattern = "mutable", build_fn(skip))]
16#[allow(dead_code)]
17pub(crate) struct Rule {
18    #[builder(setter(into))]
19    pub in_interface: String,
20    #[builder(setter(into))]
21    pub out_interface: String,
22    #[builder(setter(into))]
23    pub source_address: String,
24    #[builder(setter(into))]
25    pub destination_address: String,
26    #[builder(setter(into))]
27    pub source_address_v6: String,
28    #[builder(setter(into))]
29    pub destination_address_v6: String,
30    #[builder(setter(into))]
31    pub protocol: String,
32    #[builder(setter(into))]
33    pub source_port: String,
34    #[builder(setter(into))]
35    pub destination_port: String,
36    #[builder(setter(into))]
37    pub matches: String,
38    #[builder(setter(into))]
39    pub comment: String,
40    #[builder(setter(into))]
41    pub verdict: RuleVerdict,
42    #[builder(setter(into))]
43    pub dnat: String,
44}
45
46impl RuleBuilder {
47    pub(crate) fn build(&self) -> Result<String> {
48        let mut args: Vec<String> = Vec::new();
49
50        // Handle protocol matches
51        if self.source_port.is_some() || self.destination_port.is_some() {
52            args.push(self.protocol.clone().unwrap_or_else(|| "tcp".to_owned()));
53            if let Some(source_port) = &self.source_port {
54                args.push("sport".to_owned());
55                args.push(source_port.to_owned());
56            }
57            if let Some(destination_port) = &self.destination_port {
58                args.push("dport".to_owned());
59                args.push(destination_port.to_owned());
60            }
61        }
62
63        // Handle `ip` matches
64        if let Some(source_address) = &self.source_address {
65            args.push("ip".to_owned());
66            args.push("saddr".to_owned());
67            args.push(source_address.to_owned());
68        }
69        if let Some(destination_address) = &self.destination_address {
70            args.push("ip".to_owned());
71            args.push("daddr".to_owned());
72            args.push(destination_address.to_owned());
73        }
74
75        // Handle `ip6` matches
76        if let Some(source_address) = &self.source_address_v6 {
77            args.push("ip6".to_owned());
78            args.push("saddr".to_owned());
79            args.push(source_address.to_owned());
80        }
81        if let Some(destination_address) = &self.destination_address_v6 {
82            args.push("ip6".to_owned());
83            args.push("daddr".to_owned());
84            args.push(destination_address.to_owned());
85        }
86
87        // Handle interface-matches
88        if self.in_interface.is_some() || self.out_interface.is_some() {
89            args.push("meta".to_owned());
90            if let Some(in_interface) = &self.in_interface {
91                args.push("iifname".to_owned());
92                args.push(in_interface.to_owned());
93            }
94            if let Some(out_interface) = &self.out_interface {
95                args.push("oifname".to_owned());
96                args.push(out_interface.to_owned());
97            }
98        }
99
100        // Bail if none of the above was initialized
101        if args.is_empty() {
102            bail!("one of `{source,destination}_{port,address{,_v6}}`, `{in,out}_interface` must be initialized");
103        }
104
105        // Unconditionally set mark
106        args.push("meta".to_owned());
107        args.push("mark".to_owned());
108        args.push("set".to_owned());
109        args.push(DFW_MARK.to_owned());
110
111        if let Some(matches) = &self.matches {
112            args.push(matches.to_owned());
113        }
114
115        if let Some(verdict) = &self.verdict {
116            args.push(verdict.to_string());
117        } else if let Some(dnat) = &self.dnat {
118            args.push("dnat".to_owned());
119            args.push(dnat.to_owned());
120        }
121
122        if let Some(comment) = &self.comment {
123            args.push(format!(r#"comment "{}""#, comment));
124        }
125
126        Ok(args.join(" "))
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133
134    #[test]
135    fn builder_should_fail() {
136        let rule = RuleBuilder::default();
137        assert!(rule.build().is_err());
138    }
139
140    #[test]
141    fn builder_should_succeed() {
142        let mut rule = RuleBuilder::default();
143        rule.source_port("1");
144        assert!(rule.build().is_ok());
145    }
146}