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}