1use super::super::{
5 expr::Expr,
6 types::{ChainType, Family, Hook, Policy, Priority, Rule},
7};
8
9#[derive(Debug, Clone, Default)]
15pub struct NftablesConfig {
16 pub(crate) tables: Vec<DeclaredTable>,
17}
18
19impl NftablesConfig {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn table<F>(mut self, name: impl Into<String>, family: Family, f: F) -> Self
30 where
31 F: FnOnce(DeclaredTableBuilder) -> DeclaredTableBuilder,
32 {
33 let builder = DeclaredTableBuilder::new(name.into(), family);
34 let built = f(builder);
35 self.tables.push(built.into_table());
36 self
37 }
38
39 pub fn tables(&self) -> &[DeclaredTable] {
41 &self.tables
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.tables.is_empty()
47 }
48}
49
50#[cfg_attr(feature = "serde", derive(serde::Serialize))]
57#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
58#[derive(Debug, Clone)]
59pub struct DeclaredTable {
60 pub(crate) name: String,
61 pub(crate) family: Family,
62 pub(crate) flags: u32,
63 pub(crate) chains: Vec<DeclaredChain>,
64 pub(crate) rules: Vec<DeclaredRule>,
65 pub(crate) flowtables: Vec<DeclaredFlowtable>,
66}
67
68impl DeclaredTable {
69 pub fn name(&self) -> &str {
71 &self.name
72 }
73 pub fn family(&self) -> Family {
75 self.family
76 }
77 pub fn flags(&self) -> u32 {
80 self.flags
81 }
82 pub fn chains(&self) -> &[DeclaredChain] {
83 &self.chains
84 }
85 pub fn rules(&self) -> &[DeclaredRule] {
86 &self.rules
87 }
88 pub fn flowtables(&self) -> &[DeclaredFlowtable] {
89 &self.flowtables
90 }
91}
92
93pub struct DeclaredTableBuilder {
96 name: String,
97 family: Family,
98 flags: u32,
99 chains: Vec<DeclaredChain>,
100 rules: Vec<DeclaredRule>,
101 flowtables: Vec<DeclaredFlowtable>,
102}
103
104impl DeclaredTableBuilder {
105 fn new(name: String, family: Family) -> Self {
106 Self {
107 name,
108 family,
109 flags: 0,
110 chains: Vec::new(),
111 rules: Vec::new(),
112 flowtables: Vec::new(),
113 }
114 }
115
116 pub fn flags(mut self, flags: u32) -> Self {
121 self.flags = flags;
122 self
123 }
124
125 pub fn persist(mut self, on: bool) -> Self {
127 if on {
128 self.flags |= super::super::NFT_TABLE_F_PERSIST;
129 } else {
130 self.flags &= !super::super::NFT_TABLE_F_PERSIST;
131 }
132 self
133 }
134
135 pub fn chain<F>(mut self, name: impl Into<String>, f: F) -> Self
138 where
139 F: FnOnce(DeclaredChainBuilder) -> DeclaredChainBuilder,
140 {
141 let builder = DeclaredChainBuilder::new(name.into());
142 self.chains.push(f(builder).into_chain());
143 self
144 }
145
146 pub fn rule<F>(mut self, chain: impl AsRef<str>, f: F) -> Self
151 where
152 F: FnOnce(Rule) -> Rule,
153 {
154 let rule = Rule::new(&self.name, chain.as_ref()).family(self.family);
155 self.rules.push(DeclaredRule {
156 table: self.name.clone(),
157 chain: chain.as_ref().to_string(),
158 family: self.family,
159 handle_key: None,
160 body: f(rule),
161 });
162 self
163 }
164
165 pub fn rule_keyed<F>(
169 mut self,
170 chain: impl AsRef<str>,
171 key: impl Into<String>,
172 f: F,
173 ) -> Self
174 where
175 F: FnOnce(Rule) -> Rule,
176 {
177 let rule = Rule::new(&self.name, chain.as_ref()).family(self.family);
178 self.rules.push(DeclaredRule {
179 table: self.name.clone(),
180 chain: chain.as_ref().to_string(),
181 family: self.family,
182 handle_key: Some(key.into()),
183 body: f(rule),
184 });
185 self
186 }
187
188 pub fn flowtable<F>(mut self, name: impl Into<String>, f: F) -> Self
191 where
192 F: FnOnce(DeclaredFlowtableBuilder) -> DeclaredFlowtableBuilder,
193 {
194 let builder = DeclaredFlowtableBuilder::new(name.into());
195 self.flowtables.push(f(builder).into_flowtable(self.family, &self.name));
196 self
197 }
198
199 fn into_table(self) -> DeclaredTable {
200 DeclaredTable {
201 name: self.name,
202 family: self.family,
203 flags: self.flags,
204 chains: self.chains,
205 rules: self.rules,
206 flowtables: self.flowtables,
207 }
208 }
209}
210
211#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
219#[derive(Debug, Clone)]
220pub struct DeclaredChain {
221 pub(crate) name: String,
222 pub(crate) hook: Option<Hook>,
223 pub(crate) priority: Option<Priority>,
224 pub(crate) policy: Option<Policy>,
225 pub(crate) chain_type: Option<ChainType>,
226 pub(crate) device: Option<String>,
227}
228
229impl DeclaredChain {
230 pub fn name(&self) -> &str {
231 &self.name
232 }
233 pub fn hook(&self) -> Option<Hook> {
234 self.hook
235 }
236 pub fn priority(&self) -> Option<Priority> {
237 self.priority
238 }
239 pub fn policy(&self) -> Option<Policy> {
240 self.policy
241 }
242 pub fn chain_type(&self) -> Option<ChainType> {
243 self.chain_type
244 }
245 pub fn device(&self) -> Option<&str> {
246 self.device.as_deref()
247 }
248
249 pub fn is_base(&self) -> bool {
252 self.hook.is_some()
253 }
254}
255
256pub struct DeclaredChainBuilder {
257 name: String,
258 hook: Option<Hook>,
259 priority: Option<Priority>,
260 policy: Option<Policy>,
261 chain_type: Option<ChainType>,
262 device: Option<String>,
263}
264
265impl DeclaredChainBuilder {
266 fn new(name: String) -> Self {
267 Self {
268 name,
269 hook: None,
270 priority: None,
271 policy: None,
272 chain_type: None,
273 device: None,
274 }
275 }
276
277 pub fn hook(mut self, hook: Hook) -> Self {
280 self.hook = Some(hook);
281 self
282 }
283
284 pub fn priority(mut self, p: Priority) -> Self {
286 self.priority = Some(p);
287 self
288 }
289
290 pub fn policy(mut self, p: Policy) -> Self {
294 self.policy = Some(p);
295 self
296 }
297
298 pub fn chain_type(mut self, ct: ChainType) -> Self {
305 self.chain_type = Some(ct);
306 self
307 }
308
309 pub fn device(mut self, dev: impl Into<String>) -> Self {
315 self.device = Some(dev.into());
316 self
317 }
318
319 fn into_chain(self) -> DeclaredChain {
320 DeclaredChain {
321 name: self.name,
322 hook: self.hook,
323 priority: self.priority,
324 policy: self.policy,
325 chain_type: self.chain_type,
326 device: self.device,
327 }
328 }
329}
330
331#[cfg_attr(feature = "serde", derive(serde::Serialize))]
345#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
346#[derive(Debug, Clone)]
347pub struct DeclaredRule {
348 pub(crate) table: String,
349 pub(crate) chain: String,
350 pub(crate) family: Family,
351 pub(crate) handle_key: Option<String>,
352 #[cfg_attr(feature = "serde", serde(skip))]
358 pub(crate) body: Rule,
359}
360
361impl DeclaredRule {
362 pub fn table(&self) -> &str {
363 &self.table
364 }
365 pub fn chain(&self) -> &str {
366 &self.chain
367 }
368 pub fn family(&self) -> Family {
369 self.family
370 }
371 pub fn handle_key(&self) -> Option<&str> {
372 self.handle_key.as_deref()
373 }
374 pub fn body(&self) -> &Rule {
375 &self.body
376 }
377 pub fn exprs(&self) -> &[Expr] {
380 &self.body.exprs
381 }
382}
383
384#[cfg_attr(feature = "serde", derive(serde::Serialize))]
390#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
391#[derive(Debug, Clone)]
392pub struct DeclaredFlowtable {
393 pub(crate) family: Family,
394 pub(crate) table: String,
395 pub(crate) name: String,
396 pub(crate) devs: Vec<String>,
397 pub(crate) priority: i32,
398 pub(crate) flags: u32,
399}
400
401impl DeclaredFlowtable {
402 pub fn name(&self) -> &str {
403 &self.name
404 }
405 pub fn family(&self) -> Family {
406 self.family
407 }
408 pub fn table(&self) -> &str {
409 &self.table
410 }
411 pub fn devs(&self) -> &[String] {
412 &self.devs
413 }
414 pub fn priority(&self) -> i32 {
415 self.priority
416 }
417 pub fn flags(&self) -> u32 {
418 self.flags
419 }
420}
421
422pub struct DeclaredFlowtableBuilder {
423 name: String,
424 devs: Vec<String>,
425 priority: i32,
426 flags: u32,
427}
428
429impl DeclaredFlowtableBuilder {
430 fn new(name: String) -> Self {
431 Self {
432 name,
433 devs: Vec::new(),
434 priority: 0,
435 flags: 0,
436 }
437 }
438
439 pub fn device(mut self, dev: impl Into<String>) -> Self {
440 self.devs.push(dev.into());
441 self
442 }
443
444 pub fn priority(mut self, p: i32) -> Self {
445 self.priority = p;
446 self
447 }
448
449 pub fn hw_offload(mut self, on: bool) -> Self {
450 if on {
451 self.flags |= super::super::NFT_FLOWTABLE_HW_OFFLOAD;
452 } else {
453 self.flags &= !super::super::NFT_FLOWTABLE_HW_OFFLOAD;
454 }
455 self
456 }
457
458 pub fn counter(mut self, on: bool) -> Self {
459 if on {
460 self.flags |= super::super::NFT_FLOWTABLE_COUNTER;
461 } else {
462 self.flags &= !super::super::NFT_FLOWTABLE_COUNTER;
463 }
464 self
465 }
466
467 fn into_flowtable(self, family: Family, table: &str) -> DeclaredFlowtable {
468 DeclaredFlowtable {
469 family,
470 table: table.to_string(),
471 name: self.name,
472 devs: self.devs,
473 priority: self.priority,
474 flags: self.flags,
475 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::netlink::nftables::NFT_TABLE_F_PERSIST;
483
484 #[test]
485 fn empty_config_has_no_tables() {
486 let cfg = NftablesConfig::new();
487 assert!(cfg.is_empty());
488 assert_eq!(cfg.tables().len(), 0);
489 }
490
491 #[test]
492 fn declarative_composition_round_trips_to_struct_fields() {
493 let cfg = NftablesConfig::new().table("filter", Family::Inet, |t| {
494 t.persist(true)
495 .chain("input", |c| {
496 c.hook(Hook::Input)
497 .priority(Priority::Filter)
498 .policy(Policy::Drop)
499 })
500 .rule("input", |r| r)
501 .rule_keyed("input", "allow-icmp", |r| r)
502 .flowtable("ft", |f| f.device("eth0").hw_offload(true))
503 });
504
505 assert_eq!(cfg.tables().len(), 1);
506 let t = &cfg.tables()[0];
507 assert_eq!(t.name(), "filter");
508 assert_eq!(t.family(), Family::Inet);
509 assert!(t.flags() & NFT_TABLE_F_PERSIST != 0);
510
511 assert_eq!(t.chains().len(), 1);
512 let c = &t.chains()[0];
513 assert_eq!(c.name(), "input");
514 assert!(c.is_base());
515 assert!(c.hook().is_some());
516 assert_eq!(c.policy(), Some(Policy::Drop));
517
518 assert_eq!(t.rules().len(), 2);
519 assert_eq!(t.rules()[0].chain(), "input");
520 assert!(t.rules()[0].handle_key().is_none());
521 assert_eq!(t.rules()[1].handle_key(), Some("allow-icmp"));
522
523 assert_eq!(t.flowtables().len(), 1);
524 let f = &t.flowtables()[0];
525 assert_eq!(f.name(), "ft");
526 assert_eq!(f.devs(), &["eth0"]);
527 assert!(f.flags() & super::super::super::NFT_FLOWTABLE_HW_OFFLOAD != 0);
528 }
529
530 #[test]
531 fn flowtable_carries_owning_table_and_family() {
532 let cfg = NftablesConfig::new().table("nat", Family::Ip, |t| {
533 t.flowtable("ft1", |f| f.device("eth0"))
534 });
535 let ft = &cfg.tables()[0].flowtables()[0];
536 assert_eq!(ft.table(), "nat");
537 assert_eq!(ft.family(), Family::Ip);
538 }
539
540 #[test]
541 fn persist_flag_toggles_off() {
542 let cfg = NftablesConfig::new().table("filter", Family::Inet, |t| {
543 t.persist(true).persist(false)
544 });
545 assert_eq!(cfg.tables()[0].flags() & NFT_TABLE_F_PERSIST, 0);
546 }
547
548 #[test]
551 fn declared_chain_type_round_trips_to_struct() {
552 let cfg = NftablesConfig::new().table("nat", Family::Inet, |t| {
553 t.chain("postrouting", |c| {
554 c.hook(Hook::Postrouting)
555 .priority(Priority::SrcNat)
556 .chain_type(ChainType::Nat)
557 })
558 });
559 let chain = cfg.tables().first().unwrap().chains().first().unwrap();
560 assert_eq!(chain.chain_type(), Some(ChainType::Nat));
561 assert_eq!(chain.device(), None);
562 assert!(chain.is_base());
563 }
564
565 #[test]
566 fn declared_chain_device_round_trips_to_struct() {
567 let cfg = NftablesConfig::new().table("ft", Family::Netdev, |t| {
568 t.chain("ingress", |c| {
569 c.hook(Hook::NetdevIngress)
570 .priority(Priority::Filter)
571 .chain_type(ChainType::Filter)
572 .device("eth0")
573 })
574 });
575 let chain = cfg.tables().first().unwrap().chains().first().unwrap();
576 assert_eq!(chain.chain_type(), Some(ChainType::Filter));
577 assert_eq!(chain.device(), Some("eth0"));
578 }
579
580 #[test]
581 fn declared_chain_omits_chain_type_and_device_by_default() {
582 let cfg = NftablesConfig::new().table("filter", Family::Inet, |t| {
583 t.chain("input", |c| {
584 c.hook(Hook::Input)
585 .priority(Priority::Filter)
586 .policy(Policy::Drop)
587 })
588 });
589 let chain = cfg.tables().first().unwrap().chains().first().unwrap();
590 assert_eq!(chain.chain_type(), None);
591 assert_eq!(chain.device(), None);
592 assert_eq!(chain.policy(), Some(Policy::Drop));
593 }
594}