dfw/
types.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//! The types in this module make up the structure of the configuration-file(s).
11//!
12//! # Example
13//!
14//! The following is an examplary TOML configuration, which will be parsed into this modules types.
15//!
16//! ```
17//! # use dfw::nftables::Nftables;
18//! # use dfw::types::*;
19//! # use toml;
20//! # toml::from_str::<DFW<Nftables>>(r#"
21//! [global_defaults]
22//! external_network_interfaces = "eth0"
23//!
24//! [backend_defaults]
25//! custom_tables = { name = "filter", chains = ["input", "forward"]}
26//!
27//! [backend_defaults.initialization]
28//! rules = [
29//!     "add table inet custom",
30//! ]
31//!
32//! [container_to_container]
33//! default_policy = "drop"
34//!
35//! [[container_to_container.rules]]
36//! network = "common_network"
37//! src_container = "container_a"
38//! dst_container = "container_b"
39//! verdict = "accept"
40//!
41//! [container_to_wider_world]
42//! default_policy = "accept"
43//!
44//! [[container_to_container.rules]]
45//! network = "other_network"
46//! src_container = "container_c"
47//! verdict = "drop"
48//!
49//! [wider_world_to_container]
50//!
51//! [[wider_world_to_container.rules]]
52//! network = "common_network"
53//! dst_container = "container_a"
54//! expose_port = [80, 443]
55//!
56//! [container_dnat]
57//!
58//! [[container_dnat.rules]]
59//! src_network = "common_network"
60//! src_container = "container_a"
61//! dst_network = "other_network"
62//! dst_container = "container_c"
63//! expose_port = { host_port = 8080, container_port = 80, family = "tcp" }
64//! # "#).unwrap();
65//! ```
66
67use crate::{de::*, nftables, FirewallBackend, Process};
68use derive_builder::Builder;
69use serde::Deserialize;
70use std::str::FromStr;
71use strum::{Display, EnumString};
72
73const DEFAULT_PROTOCOL: &str = "tcp";
74
75/// `DFW` is the parent type defining the complete configuration used by DFW to build up the
76/// firewall rules.
77///
78/// Every section is optional.
79#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
80#[serde(deny_unknown_fields)]
81pub struct DFW<B>
82where
83    B: FirewallBackend,
84    DFW<B>: Process<B>,
85{
86    /// The `defaults` configuration section.
87    ///
88    /// You can leave this section unspecified.
89    #[serde(default, alias = "defaults")]
90    pub global_defaults: GlobalDefaults,
91    /// The `backend_defaults` configuration section
92    #[serde(default)]
93    pub backend_defaults: Option<B::Defaults>,
94    /// # This field is **DEPRECATED!**
95    ///
96    /// Provide the custom tables in the nftables backend-defaults section instead. (This field will
97    /// be removed with release 2.0.0.)
98    ///
99    /// Please consult the [firewall-backend documentation] if you want to know how to use this
100    /// field.
101    ///
102    /// [firewall-backend documentation]: ../nftables/types/struct.Defaults.html#structfield.initialization
103    #[deprecated(
104        since = "1.2.0",
105        note = "Provide the initialization in the nftables backend-defaults section instead. This \
106                field will be removed with release 2.0.0."
107    )]
108    pub initialization: Option<nftables::types::Initialization>,
109    /// The `container_to_container` configuration section
110    pub container_to_container: Option<ContainerToContainer>,
111    /// The `container_to_wider_world` configuration section
112    pub container_to_wider_world: Option<ContainerToWiderWorld>,
113    /// The `container_to_host` configuration section
114    pub container_to_host: Option<ContainerToHost>,
115    /// The `wider_world_to_container` configuration section
116    pub wider_world_to_container: Option<WiderWorldToContainer>,
117    /// The `container_dnat` configuration section
118    pub container_dnat: Option<ContainerDNAT>,
119}
120
121/// The default configuration section, used by DFW for rule processing.
122#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
123#[serde(deny_unknown_fields)]
124pub struct GlobalDefaults {
125    /// This defines the external network interfaces of the host to consider during building the
126    /// rules. The value can be non-existent, a string, or a sequence of strings.
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// # use dfw::types::*;
132    /// # use toml;
133    /// # toml::from_str::<GlobalDefaults>(r#"
134    /// external_network_interfaces = "eth0"
135    /// # "#).unwrap();
136    /// # toml::from_str::<GlobalDefaults>(r#"
137    /// external_network_interfaces = ["eth0", "eth1"]
138    /// # "#).unwrap();
139    /// ```
140    #[serde(default, deserialize_with = "option_string_or_seq_string")]
141    pub external_network_interfaces: Option<Vec<String>>,
142
143    /// This defines whether the default Docker bridge (usually `docker0`) is allowed to access host
144    /// resources.
145    ///
146    /// This field is optional and will be set to "accept" by default.
147    ///
148    /// For non-default Docker bridges this is controlled within the [container-to-host section].
149    ///
150    /// [container-to-host section]: struct.ContainerToHostRule.html
151    #[serde(default)]
152    pub default_docker_bridge_to_host_policy: ChainPolicy,
153
154    /// # This field is **DEPRECATED!**
155    ///
156    /// Provide the custom tables in the nftables backend-defaults section instead.
157    /// (This field will be removed with release 2.0.0.)
158    ///
159    /// Please consult the [firewall-backend documentation] if you want to know how to use this
160    /// field.
161    ///
162    /// [firewall-backend documentation]: ../nftables/types/struct.Defaults.html#structfield.custom_tables
163    #[deprecated(
164        since = "1.2.0",
165        note = "Provide the custom tables in the nftables backend-defaults section instead. This \
166                field will be removed with release 2.0.0."
167    )]
168    #[serde(default, deserialize_with = "option_struct_or_seq_struct")]
169    pub custom_tables: Option<Vec<nftables::types::Table>>,
170}
171
172/// The container-to-container section, defining how containers can communicate amongst each other.
173#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
174#[serde(deny_unknown_fields)]
175pub struct ContainerToContainer {
176    /// The `default_policy` defines the default for when there is not a specific rule.
177    ///
178    /// # Filtering traffic within the same bridge
179    ///
180    /// Depending on how your host is configured, traffic whose origin and destination interface are
181    /// the same bridge is _not_ filtered by the kernel netfilter module. This means that this
182    /// default policy is not honored for traffic between containers that are on the same Docker
183    /// network, but only for traffic that traverses two bridges.
184    ///
185    /// If your kernel has the `br_netfilter` kernel-module available, you can set the sysctl
186    /// `net.bridge.bridge-nf-call-iptables` to `1` to have the netfilter-module act on traffic
187    /// within the same bridge, too. You can set this value temporarily like this:
188    ///
189    /// ```text
190    /// sysctl net.bridge.bridge-nf-call-iptables=1
191    /// ```
192    ///
193    /// To permanently set this configuration, take a look at `man sysctl.d` and `man sysctl.conf`.
194    pub default_policy: ChainPolicy,
195    /// Configure whether traffic between containers within the same network should be allowed or
196    /// not.
197    ///
198    /// This option is more specific than [`default_policy`], applying to traffic between containers
199    /// on the same network, rather than also applying across networks. This option has precedence
200    /// over the [`default_policy`] for traffic on the same network.
201    ///
202    /// ## Example
203    ///
204    /// Setting the `same_network_verdict` to `accept` will allow all traffic between containers on
205    /// the same network to pass, regardless of the [`default_policy`] configured:
206    ///
207    /// ```
208    /// # use dfw::nftables::Nftables;
209    /// # use dfw::types::*;
210    /// # use toml;
211    /// # toml::from_str::<DFW<Nftables>>(r#"
212    /// [container_to_container]
213    /// default_policy = "drop"
214    /// same_network_verdict = "accept"
215    /// # "#).unwrap();
216    /// ```
217    ///
218    /// If you want to allow traffic between containers on the same networks in general, but want to
219    /// restrict traffic on some networks, you can additionally add [container-to-container rules]
220    /// to disallow traffic between containers for the desired networks:
221    ///
222    /// ```
223    /// # use dfw::nftables::Nftables;
224    /// # use dfw::types::*;
225    /// # use toml;
226    /// # toml::from_str::<DFW<Nftables>>(r#"
227    /// [container_to_container]
228    /// default_policy = "drop"
229    /// same_network_verdict = "accept"
230    ///
231    /// [[container_to_container.rules]]
232    /// network = "restricted_network"
233    /// verdict = "reject"
234    /// # "#).unwrap();
235    /// ```
236    ///
237    /// [container-to-container rules]: struct.ContainerToContainerRule.html
238    ///
239    /// ## Host configuration
240    ///
241    /// Depending on how your host is configured, traffic whose origin and destination interface are
242    /// the same bridge (network) is _not_ filtered by the kernel netfilter module.
243    ///
244    /// This means that this verdict is only honored if your kernel has the `br_netfilter`
245    /// kernel-module available and the sysctl `net.bridge.bridge-nf-call-iptables` is set to `1`.
246    /// Otherwise traffic between containers on the same network will always be allowed.
247    ///
248    /// You can set the sysctl-value temporarily like this:
249    ///
250    /// ```text
251    /// sysctl net.bridge.bridge-nf-call-iptables=1
252    /// ```
253    ///
254    /// To permanently set this configuration, take a look at `man sysctl.d` and `man sysctl.conf`.
255    ///
256    /// [`default_policy`]: #structfield.default_policy
257    pub same_network_verdict: Option<RuleVerdict>,
258    /// An optional list of rules, see
259    /// [`ContainerToContainerRule`](struct.ContainerToContainerRule.html).
260    ///
261    /// # Example
262    ///
263    /// The easiest way to define the rules is using TOMLs [arrays of tables][toml-aot]:
264    ///
265    /// ```
266    /// # use dfw::nftables::Nftables;
267    /// # use dfw::types::*;
268    /// # use toml;
269    /// # toml::from_str::<DFW<Nftables>>(r#"
270    /// [container_to_container]
271    /// default_policy = "drop"
272    ///
273    /// [[container_to_container.rules]]
274    /// ## first rule here
275    /// # network = ""
276    /// # verdict = "accept"
277    /// [[container_to_container.rules]]
278    /// ## second rule here
279    /// # network = ""
280    /// # verdict = "accept"
281    /// # "#).unwrap();
282    /// ```
283    ///
284    /// [toml-aot]:
285    ///  https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#array-of-tables
286    pub rules: Option<Vec<ContainerToContainerRule>>,
287}
288
289/// Definition for a rule to be used in the container-to-container section.
290#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
291#[serde(deny_unknown_fields)]
292pub struct ContainerToContainerRule {
293    /// Common network between the source container and the destination container to apply the rule
294    /// to.
295    pub network: String,
296    /// Source container to apply the rule to.
297    pub src_container: Option<String>,
298    /// Destination container to apply the rule to.
299    pub dst_container: Option<String>,
300    /// Additional match-string, which will be added to the nftables command.
301    pub matches: Option<String>,
302    /// Verdict for rule (accept, drop or reject).
303    #[serde(alias = "action")]
304    pub verdict: RuleVerdict,
305}
306
307/// The container-to-wider-world section, defining how containers can communicate with the wider
308/// world.
309#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
310#[serde(deny_unknown_fields)]
311pub struct ContainerToWiderWorld {
312    /// The `default_policy` defines the default for when there is not a specific rule.
313    pub default_policy: RuleVerdict,
314    /// An optional list of rules, see
315    /// [`ContainerToWiderWorldRule`](struct.ContainerToWiderWorldRule.html).
316    ///
317    /// # Example
318    ///
319    /// The easiest way to define the rules is using TOMLs [arrays of tables][toml-aot]:
320    ///
321    /// ```
322    /// # use dfw::nftables::Nftables;
323    /// # use dfw::types::*;
324    /// # use toml;
325    /// # toml::from_str::<DFW<Nftables>>(r#"
326    /// [container_to_wider_world]
327    /// default_policy = "drop"
328    ///
329    /// [[container_to_wider_world.rules]]
330    /// ## first rule here
331    /// # verdict = "accept"
332    /// [[container_to_wider_world.rules]]
333    /// ## second rule here
334    /// # verdict = "accept"
335    /// # "#).unwrap();
336    /// ```
337    ///
338    /// [toml-aot]:
339    ///  https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#array-of-tables
340    pub rules: Option<Vec<ContainerToWiderWorldRule>>,
341}
342
343/// Definition for a rule to be used in the container-to-wider-world section.
344#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
345#[serde(deny_unknown_fields)]
346pub struct ContainerToWiderWorldRule {
347    /// Network of the source container to apply the rule to.
348    pub network: Option<String>,
349    /// Source container to apply the rule to.
350    pub src_container: Option<String>,
351    /// Additional match-string, which will be added to the nftables command.
352    pub matches: Option<String>,
353    /// Verdict for rule (accept, drop or reject).
354    #[serde(alias = "action")]
355    pub verdict: RuleVerdict,
356    /// Specific external network interface to target.
357    pub external_network_interface: Option<String>,
358}
359
360/// The container-to-host section, defining how containers can communicate with the host.
361#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
362#[serde(deny_unknown_fields)]
363pub struct ContainerToHost {
364    /// The `default_policy` defines the default for when there is not a specific rule.
365    pub default_policy: RuleVerdict,
366    /// An optional list of rules, see
367    /// [`ContainerToHostRule`](struct.ContainerToHostRule.html).
368    ///
369    /// # Example
370    ///
371    /// The easiest way to define the rules is using TOMLs [arrays of tables][toml-aot]:
372    ///
373    /// ```
374    /// # use dfw::nftables::Nftables;
375    /// # use dfw::types::*;
376    /// # use toml;
377    /// # toml::from_str::<DFW<Nftables>>(r#"
378    /// [container_to_host]
379    /// default_policy = "drop"
380    ///
381    /// [[container_to_host.rules]]
382    /// ## first rule here
383    /// # network = ""
384    /// # verdict = "accept"
385    /// [[container_to_host.rules]]
386    /// ## second rule here
387    /// # network = ""
388    /// # verdict = "accept"
389    /// # "#).unwrap();
390    /// ```
391    ///
392    /// [toml-aot]:
393    ///  https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#array-of-tables
394    pub rules: Option<Vec<ContainerToHostRule>>,
395}
396
397/// Definition for a rule to be used in the container-to-host section.
398#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
399#[serde(deny_unknown_fields)]
400pub struct ContainerToHostRule {
401    /// Network of the source container to apply the rule to.
402    pub network: String,
403    /// Source container to apply the rule to.
404    pub src_container: Option<String>,
405    /// Additional match-string, which will be added to the nftables command.
406    pub matches: Option<String>,
407    /// Verdict for rule (accept, drop or reject).
408    #[serde(alias = "action")]
409    pub verdict: RuleVerdict,
410}
411
412/// The wider-world-to-container section, defining how containers can reached from the wider world.
413#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
414#[serde(deny_unknown_fields)]
415pub struct WiderWorldToContainer {
416    /// An optional list of rules, see
417    /// [`WiderWorldToContainerRule`](struct.WiderWorldToContainerRule.html).
418    ///
419    /// # Example
420    ///
421    /// The easiest way to define the rules is using TOMLs [arrays of tables][toml-aot]:
422    ///
423    /// ```
424    /// # use dfw::nftables::Nftables;
425    /// # use dfw::types::*;
426    /// # use toml;
427    /// # toml::from_str::<DFW<Nftables>>(r#"
428    /// [[wider_world_to_container.rules]]
429    /// ## first rule here
430    /// # network = ""
431    /// # dst_container = ""
432    /// # expose_port = 0
433    /// [[wider_world_to_container.rules]]
434    /// ## second rule here
435    /// # network = ""
436    /// # dst_container = ""
437    /// # expose_port = 0
438    /// # "#).unwrap();
439    /// ```
440    ///
441    /// [toml-aot]:
442    ///  https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#array-of-tables
443    pub rules: Option<Vec<WiderWorldToContainerRule>>,
444}
445
446/// Definition for a rule to be used in the wider-world-to-container section.
447#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
448#[serde(deny_unknown_fields)]
449pub struct WiderWorldToContainerRule {
450    /// Network of the destination container to apply the rule to.
451    pub network: String,
452
453    /// Destination container to apply the rule to.
454    pub dst_container: String,
455
456    /// Ports to apply the rule to.
457    ///
458    /// Defined as:
459    ///
460    /// * a single integer
461    ///
462    /// * a single string
463    ///
464    /// * a single struct
465    ///
466    /// * a list of integers
467    ///
468    /// * a list of strings
469    ///
470    /// * a list of structs
471    ///
472    /// # Example
473    ///
474    /// All of the following are legal TOML fragments:
475    ///
476    /// ```
477    /// # use dfw::types::*;
478    /// # use toml;
479    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
480    /// # network = ""
481    /// # dst_container = ""
482    /// expose_port = 80
483    /// # "#).unwrap();
484    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
485    /// # network = ""
486    /// # dst_container = ""
487    /// expose_port = [80, 443]
488    /// # "#).unwrap();
489    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
490    /// # network = ""
491    /// # dst_container = ""
492    /// expose_port = "53/udp"
493    /// # "#).unwrap();
494    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
495    /// # network = ""
496    /// # dst_container = ""
497    /// expose_port = ["80/tcp", "53/udp"]
498    /// # "#).unwrap();
499    ///
500    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
501    /// ## The following four all result in the same definition
502    /// # network = ""
503    /// # dst_container = ""
504    /// expose_port = { host_port = 8080 }
505    /// # "#).unwrap();
506    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
507    /// # network = ""
508    /// # dst_container = ""
509    /// expose_port = { host_port = 8080, container_port = 8080 }
510    /// # "#).unwrap();
511    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
512    /// # network = ""
513    /// # dst_container = ""
514    /// expose_port = { host_port = 8080, family = "tcp" }
515    /// # "#).unwrap();
516    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
517    /// # network = ""
518    /// # dst_container = ""
519    /// expose_port = { host_port = 8080, container_port = 8080, family = "tcp" }
520    /// # "#).unwrap();
521    ///
522    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
523    /// # network = ""
524    /// # dst_container = ""
525    /// expose_port = [
526    ///     { host_port = 80 },
527    ///     { host_port = 53, family = "udp" },
528    ///     { host_port = 443, container_port = 8443 },
529    /// ]
530    /// # "#).unwrap();
531    /// ```
532    #[serde(deserialize_with = "single_or_seq_string_or_struct")]
533    pub expose_port: Vec<ExposePort>,
534
535    /// Specific external network interface to target.
536    pub external_network_interface: Option<String>,
537
538    /// Configure if the container should be exposed via IPv6, too. _(Default: true)_.
539    ///
540    /// # Example
541    ///
542    /// ```
543    /// # use dfw::types::*;
544    /// # use toml;
545    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
546    /// # network = ""
547    /// # dst_container = ""
548    /// # expose_port = 0
549    /// expose_via_ipv6 = false
550    /// # "#).unwrap();
551    ///
552    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
553    /// # network = ""
554    /// # dst_container = ""
555    /// # expose_port = 0
556    /// expose_via_ipv6 = false
557    /// # "#).unwrap();
558    /// ```
559    #[serde(default = "default_wwtcr_expose_via_ipv6")]
560    pub expose_via_ipv6: bool,
561
562    /// Source CIDRs (IPv4) to which incoming traffic should be restricted.
563    ///
564    /// This can be:
565    ///
566    /// * a single string
567    ///
568    /// * a list of strings
569    ///
570    /// There is no validation whether the provided CIDRs are actually valid.
571    ///
572    /// # Example
573    ///
574    /// All of the following are legal TOML fragments:
575    ///
576    /// ```
577    /// # use dfw::types::*;
578    /// # use toml;
579    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
580    /// # network = ""
581    /// # dst_container = ""
582    /// # expose_port = 0
583    /// source_cidr_v4 = "127.0.0.0/8"
584    /// # "#).unwrap();
585    ///
586    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
587    /// # network = ""
588    /// # dst_container = ""
589    /// # expose_port = 0
590    /// source_cidr_v4 = ["127.0.0.0/8", "192.0.2.1/32"]
591    /// # "#).unwrap();
592    /// ```
593    #[serde(
594        default,
595        deserialize_with = "option_string_or_seq_string",
596        alias = "source_cidr"
597    )]
598    pub source_cidr_v4: Option<Vec<String>>,
599
600    /// Source CIDRs (IPv6) to which incoming traffic should be restricted.
601    ///
602    /// This can be:
603    ///
604    /// * a single string
605    ///
606    /// * a list of strings
607    ///
608    /// There is no validation whether the provided CIDRs are actually valid.
609    ///
610    /// # Example
611    ///
612    /// All of the following are legal TOML fragments:
613    ///
614    /// ```
615    /// # use dfw::types::*;
616    /// # use toml;
617    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
618    /// # network = ""
619    /// # dst_container = ""
620    /// # expose_port = 0
621    /// source_cidr_v6 = "fe80::/10"
622    /// # "#).unwrap();
623    ///
624    /// # toml::from_str::<WiderWorldToContainerRule>(r#"
625    /// # network = ""
626    /// # dst_container = ""
627    /// # expose_port = 0
628    /// source_cidr_v6 = ["fe80::/10", "2001:db8::/32"]
629    /// # "#).unwrap();
630    /// ```
631    #[serde(default, deserialize_with = "option_string_or_seq_string")]
632    pub source_cidr_v6: Option<Vec<String>>,
633}
634
635fn default_wwtcr_expose_via_ipv6() -> bool {
636    true
637}
638
639/// Struct to hold a port definition to expose on the host/between containers.
640#[derive(Deserialize, Debug, Clone, Default, Builder, PartialEq, Eq, Hash)]
641#[serde(deny_unknown_fields)]
642pub struct ExposePort {
643    /// Port the `container_port` should be exposed to on the host.
644    #[builder(field(public))]
645    pub host_port: u16,
646
647    /// Port the `host_port` should map to into the container.
648    #[builder(field(public), default = "self.default_container_port()")]
649    pub container_port: Option<u16>,
650
651    /// Family of the exposed port.
652    ///
653    /// Can be left blank, `tcp` will be used as default.
654    #[serde(default = "default_expose_port_family")]
655    #[builder(field(public), default = "self.default_family()")]
656    pub family: String,
657}
658
659impl ExposePortBuilder {
660    fn client_and_host_port(&mut self, value: &str) -> Result<&mut Self, String> {
661        let split: Vec<&str> = value.split(':').collect();
662        match split.len() {
663            1 => self.host_port = Some(split[0].parse().map_err(|e| format!("{}", e))?),
664            2 => {
665                self.host_port = Some(split[0].parse().map_err(|e| format!("{}", e))?);
666                self.container_port = Some(Some(split[1].parse().map_err(|e| format!("{}", e))?));
667            }
668            _ => return Err(format!("port string has invalid format '{}'", value)),
669        }
670        Ok(self)
671    }
672
673    fn default_container_port(&self) -> Option<u16> {
674        None
675    }
676
677    fn default_family(&self) -> String {
678        DEFAULT_PROTOCOL.to_owned()
679    }
680}
681
682impl FromStr for ExposePort {
683    type Err = String;
684
685    /// Convert a formatted string into a [`ExposePort`](struct.ExposePort.html).
686    ///
687    /// The string has to be in the format `<HOST_PORT>[:<CONTAINER_PORT>]/<FAMILY>`, i.e.
688    /// `80:8080/tcp`. If you don't specify the container-port, it is assumed to be identical to the
689    /// host-port.
690    ///
691    /// # Example
692    ///
693    /// ```
694    /// # use dfw::types::ExposePort;
695    /// let port: ExposePort = "80".parse().unwrap();
696    /// assert_eq!(port.host_port, 80);
697    /// assert_eq!(port.container_port, None);
698    /// assert_eq!(port.family, "tcp");
699    /// ```
700    ///
701    /// ```
702    /// # use dfw::types::ExposePort;
703    /// let port: ExposePort = "53/udp".parse().unwrap();
704    /// assert_eq!(port.host_port, 53);
705    /// assert_eq!(port.container_port, None);
706    /// assert_eq!(port.family, "udp");
707    /// ```
708    ///
709    /// ```
710    /// # use dfw::types::ExposePort;
711    /// let port: ExposePort = "80:8080/tcp".parse().unwrap();
712    /// assert_eq!(port.host_port, 80);
713    /// assert_eq!(port.container_port, Some(8080));
714    /// assert_eq!(port.family, "tcp");
715    /// ```
716    fn from_str(s: &str) -> Result<Self, Self::Err> {
717        let split: Vec<&str> = s.split('/').collect();
718        Ok(match split.len() {
719            1 => ExposePortBuilder::default()
720                .client_and_host_port(split[0])?
721                .build()
722                .map_err(|error| format!("{}", error))?,
723            2 => ExposePortBuilder::default()
724                .client_and_host_port(split[0])?
725                .family(split[1].to_owned())
726                .build()
727                .map_err(|error| format!("{}", error))?,
728            _ => return Err(format!("port string has invalid format '{}'", s)),
729        })
730    }
731}
732
733/// The container-DNAT section, defining how containers can communicate with each other over
734/// non-common networks.
735#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
736#[serde(deny_unknown_fields)]
737pub struct ContainerDNAT {
738    /// An optional list of rules, see
739    /// [`ContainerDNATRule`](struct.ContainerDNATRule.html).
740    ///
741    /// # Example
742    ///
743    /// The easiest way to define the rules is using TOMLs [arrays of tables][toml-aot]:
744    ///
745    /// ```
746    /// # use dfw::nftables::Nftables;
747    /// # use dfw::types::*;
748    /// # use toml;
749    /// # toml::from_str::<DFW<Nftables>>(r#"
750    /// [[container_dnat.rules]]
751    /// ## first rule here
752    /// # dst_network = ""
753    /// # dst_container = ""
754    /// # expose_port = 0
755    /// [[container_dnat.rules]]
756    /// ## second rule here
757    /// # dst_network = ""
758    /// # dst_container = ""
759    /// # expose_port = 0
760    /// # "#).unwrap();
761    /// ```
762    ///
763    /// [toml-aot]:
764    ///  https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#array-of-tables
765    pub rules: Option<Vec<ContainerDNATRule>>,
766}
767
768/// Definition for a rule to be used in the container-DNAT section.
769#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
770#[serde(deny_unknown_fields)]
771pub struct ContainerDNATRule {
772    /// Network of the source container to apply the rule to.
773    pub src_network: Option<String>,
774
775    /// Source container to apply the rule to.
776    pub src_container: Option<String>,
777
778    /// Network of the destination container to apply the rule to.
779    pub dst_network: String,
780
781    /// Destination container to apply the rule to.
782    pub dst_container: String,
783
784    /// Ports to apply the rule to.
785    ///
786    /// Defined as:
787    ///
788    /// * a single integer
789    ///
790    /// * a single string
791    ///
792    /// * a single struct
793    ///
794    /// * a list of integers
795    ///
796    /// * a list of strings
797    ///
798    /// * a list of structs
799    ///
800    /// # Example
801    ///
802    /// All of the following are legal TOML fragments:
803    ///
804    /// ```
805    /// # use dfw::types::*;
806    /// # use toml;
807    /// # toml::from_str::<ContainerDNATRule>(r#"
808    /// # dst_network = ""
809    /// # dst_container = ""
810    /// expose_port = 80
811    /// # "#).unwrap();
812    /// # toml::from_str::<ContainerDNATRule>(r#"
813    /// # dst_network = ""
814    /// # dst_container = ""
815    /// expose_port = [80, 443]
816    /// # "#).unwrap();
817    /// # toml::from_str::<ContainerDNATRule>(r#"
818    /// # dst_network = ""
819    /// # dst_container = ""
820    /// expose_port = "53/udp"
821    /// # "#).unwrap();
822    /// # toml::from_str::<ContainerDNATRule>(r#"
823    /// # dst_network = ""
824    /// # dst_container = ""
825    /// expose_port = ["80/tcp", "53/udp"]
826    /// # "#).unwrap();
827    ///
828    /// # toml::from_str::<ContainerDNATRule>(r#"
829    /// ## The following four all result in the same definition
830    /// # dst_network = ""
831    /// # dst_container = ""
832    /// expose_port = { host_port = 8080 }
833    /// # "#).unwrap();
834    /// # toml::from_str::<ContainerDNATRule>(r#"
835    /// # dst_network = ""
836    /// # dst_container = ""
837    /// expose_port = { host_port = 8080, container_port = 8080 }
838    /// # "#).unwrap();
839    /// # toml::from_str::<ContainerDNATRule>(r#"
840    /// # dst_network = ""
841    /// # dst_container = ""
842    /// expose_port = { host_port = 8080, family = "tcp" }
843    /// # "#).unwrap();
844    /// # toml::from_str::<ContainerDNATRule>(r#"
845    /// # dst_network = ""
846    /// # dst_container = ""
847    /// expose_port = { host_port = 8080, container_port = 8080, family = "tcp" }
848    /// # "#).unwrap();
849    ///
850    /// # toml::from_str::<ContainerDNATRule>(r#"
851    /// # dst_network = ""
852    /// # dst_container = ""
853    /// expose_port = [
854    ///     { host_port = 80 },
855    ///     { host_port = 53, family = "udp" },
856    ///     { host_port = 443, container_port = 8443 },
857    /// ]
858    /// # "#).unwrap();
859    /// ```
860    #[serde(deserialize_with = "single_or_seq_string_or_struct")]
861    pub expose_port: Vec<ExposePort>,
862}
863
864fn default_expose_port_family() -> String {
865    DEFAULT_PROTOCOL.to_owned()
866}
867
868/// Representation of chain policies.
869///
870/// ## Attribution
871///
872/// Parts of the documentation have been taken from
873/// <https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains>.
874#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Display, EnumString)]
875#[serde(rename_all = "lowercase")]
876#[strum(serialize_all = "snake_case")]
877pub enum ChainPolicy {
878    /// The accept verdict means that the packet will keep traversing the network stack.
879    #[strum(to_string = "accept", serialize = "ACCEPT")]
880    #[serde(alias = "ACCEPT")]
881    #[default]
882    Accept,
883    /// The drop verdict means that the packet is discarded if the packet reaches the end of the
884    /// base chain.
885    #[strum(to_string = "drop", serialize = "DROP")]
886    #[serde(alias = "DROP")]
887    Drop,
888}
889
890impl slog::Value for ChainPolicy {
891    fn serialize(
892        &self,
893        record: &slog::Record,
894        key: slog::Key,
895        serializer: &mut dyn slog::Serializer,
896    ) -> slog::Result {
897        self.to_string().serialize(record, key, serializer)
898    }
899}
900
901/// Representation of rule policies.
902///
903/// ## Attribution
904///
905/// Parts of the documentation have been taken from
906/// <https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains>.
907#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Display, EnumString)]
908#[serde(rename_all = "lowercase")]
909#[strum(serialize_all = "snake_case")]
910pub enum RuleVerdict {
911    /// The accept verdict means that the packet will keep traversing the network stack.
912    #[serde(alias = "ACCEPT")]
913    #[strum(to_string = "accept", serialize = "ACCEPT")]
914    #[default]
915    Accept,
916    /// The drop verdict means that the packet is discarded if the packet reaches the end of the
917    /// base chain.
918    #[serde(alias = "DROP")]
919    #[strum(to_string = "drop", serialize = "DROP")]
920    Drop,
921    /// The reject verdict means that the packet is responded to with an ICMP message stating that
922    /// it was rejected.
923    #[serde(alias = "REJECT")]
924    #[strum(to_string = "reject", serialize = "REJECT")]
925    Reject,
926}
927
928impl slog::Value for RuleVerdict {
929    fn serialize(
930        &self,
931        record: &slog::Record,
932        key: slog::Key,
933        serializer: &mut dyn slog::Serializer,
934    ) -> slog::Result {
935        self.to_string().serialize(record, key, serializer)
936    }
937}
938#[cfg(test)]
939mod test {
940    use super::{ChainPolicy, RuleVerdict};
941    use std::str::FromStr;
942
943    #[test]
944    fn chainpolicy_fromstr() {
945        assert_eq!(ChainPolicy::Accept, FromStr::from_str("accept").unwrap());
946        assert_eq!(ChainPolicy::Accept, FromStr::from_str("ACCEPT").unwrap());
947        assert_eq!(ChainPolicy::Drop, FromStr::from_str("drop").unwrap());
948        assert_eq!(ChainPolicy::Drop, FromStr::from_str("DROP").unwrap());
949    }
950
951    #[test]
952    fn chainpolicy_tostring() {
953        assert_eq!("accept", &ChainPolicy::Accept.to_string());
954        assert_eq!("drop", &ChainPolicy::Drop.to_string());
955    }
956
957    #[test]
958    fn ruleverdict_fromstr() {
959        assert_eq!(RuleVerdict::Accept, FromStr::from_str("accept").unwrap());
960        assert_eq!(RuleVerdict::Accept, FromStr::from_str("ACCEPT").unwrap());
961        assert_eq!(RuleVerdict::Drop, FromStr::from_str("drop").unwrap());
962        assert_eq!(RuleVerdict::Drop, FromStr::from_str("DROP").unwrap());
963        assert_eq!(RuleVerdict::Reject, FromStr::from_str("reject").unwrap());
964        assert_eq!(RuleVerdict::Reject, FromStr::from_str("REJECT").unwrap());
965    }
966
967    #[test]
968    fn ruleverdict_tostring() {
969        assert_eq!("accept", &RuleVerdict::Accept.to_string());
970        assert_eq!("drop", &RuleVerdict::Drop.to_string());
971        assert_eq!("reject", &RuleVerdict::Reject.to_string());
972    }
973}