nmstate/
route_rule.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::hash::{Hash, Hasher};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    ip::{is_ipv6_addr, sanitize_ip_network, AddressFamily},
9    ErrorKind, InterfaceIpAddr, InterfaceType, NmstateError,
10};
11
12const ROUTE_RULE_DEFAULT_PRIORIRY: i64 = 30000;
13
14#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[non_exhaustive]
16#[serde(deny_unknown_fields)]
17/// Routing rules
18pub struct RouteRules {
19    #[serde(skip_serializing_if = "Option::is_none")]
20    /// When applying, `None` means preserve existing route rules.
21    /// Nmstate is using partial editing for route rule, which means
22    /// desired route rules only append to existing instead of overriding.
23    /// To delete any route rule, please set [crate::RouteRuleEntry.state] to
24    /// [RouteRuleState::Absent]. Any property set to None in absent route rule
25    /// means wildcard. For example, this [crate::NetworkState] will delete all
26    /// route rule looking up route table 500:
27    /// ```yml
28    /// ---
29    /// route-rules:
30    ///   config:
31    ///     - state: absent
32    ///       route-table: 500
33    /// ```
34    pub config: Option<Vec<RouteRuleEntry>>,
35}
36
37impl RouteRules {
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.config.is_none()
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "kebab-case")]
49#[non_exhaustive]
50#[derive(Default)]
51pub enum RouteRuleState {
52    /// Used for delete route rule
53    #[default]
54    Absent,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58#[serde(rename_all = "kebab-case")]
59#[non_exhaustive]
60#[serde(deny_unknown_fields)]
61pub struct RouteRuleEntry {
62    /// Indicate the address family of the route rule.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub family: Option<AddressFamily>,
65    /// Indicate this is normal route rule or absent route rule.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub state: Option<RouteRuleState>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    /// Source prefix to match.
70    /// Serialize and deserialize to/from `ip-from`.
71    /// When setting to empty string in absent route rule, it will only delete
72    /// route rule __without__ `ip-from`.
73    pub ip_from: Option<String>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    /// Destination prefix to match.
76    /// Serialize and deserialize to/from `ip-to`.
77    /// When setting to empty string in absent route rule, it will only delete
78    /// route rule __without__ `ip-to`.
79    pub ip_to: Option<String>,
80    #[serde(
81        skip_serializing_if = "Option::is_none",
82        default,
83        deserialize_with = "crate::deserializer::option_i64_or_string"
84    )]
85    /// Priority of this route rule.
86    /// Bigger number means lower priority.
87    pub priority: Option<i64>,
88    #[serde(
89        skip_serializing_if = "Option::is_none",
90        rename = "route-table",
91        default,
92        deserialize_with = "crate::deserializer::option_u32_or_string"
93    )]
94    /// The routing table ID to lookup if the rule selector matches.
95    /// Serialize and deserialize to/from `route-table`.
96    pub table_id: Option<u32>,
97    #[serde(
98        skip_serializing_if = "Option::is_none",
99        default,
100        deserialize_with = "crate::deserializer::option_u32_or_string",
101        serialize_with = "crate::serializer::option_u32_as_hex"
102    )]
103    /// Select the fwmark value to match
104    pub fwmark: Option<u32>,
105    #[serde(
106        skip_serializing_if = "Option::is_none",
107        default,
108        deserialize_with = "crate::deserializer::option_u32_or_string",
109        serialize_with = "crate::serializer::option_u32_as_hex"
110    )]
111    /// Select the fwmask value to match
112    pub fwmask: Option<u32>,
113    /// Actions for matching packages.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub action: Option<RouteRuleAction>,
116    /// Incoming interface.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub iif: Option<String>,
119    /// Prefix length of suppressor.
120    /// Can deserialize from `suppress-prefix-length` or
121    /// `suppress_prefixlength`.
122    /// Serialize into `suppress-prefix-length`.
123    #[serde(
124        skip_serializing_if = "Option::is_none",
125        alias = "suppress_prefixlength"
126    )]
127    pub suppress_prefix_length: Option<u32>,
128}
129
130impl RouteRuleEntry {
131    /// Let network backend choose the default priority.
132    pub const USE_DEFAULT_PRIORITY: i64 = -1;
133    /// Use main route table 254.
134    pub const USE_DEFAULT_ROUTE_TABLE: u32 = 0;
135    /// Default route table main(254).
136    pub const DEFAULR_ROUTE_TABLE_ID: u32 = 254;
137
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    fn validate_ip_from_to(&self) -> Result<(), NmstateError> {
143        if self.ip_from.is_none()
144            && self.ip_to.is_none()
145            && self.family.is_none()
146        {
147            let e = NmstateError::new(
148                ErrorKind::InvalidArgument,
149                format!(
150                    "Neither ip-from, ip-to nor family is defined '{self}'"
151                ),
152            );
153            log::error!("{e}");
154            return Err(e);
155        } else if let Some(family) = self.family {
156            if let Some(ip_from) = self.ip_from.as_ref() {
157                if is_ipv6_addr(ip_from.as_str())
158                    != matches!(family, AddressFamily::IPv6)
159                {
160                    let e = NmstateError::new(
161                        ErrorKind::InvalidArgument,
162                        format!(
163                            "The ip-from format mismatches with the family \
164                             set '{self}'"
165                        ),
166                    );
167                    log::error!("{e}");
168                    return Err(e);
169                }
170            }
171            if let Some(ip_to) = self.ip_to.as_ref() {
172                if is_ipv6_addr(ip_to.as_str())
173                    != matches!(family, AddressFamily::IPv6)
174                {
175                    let e = NmstateError::new(
176                        ErrorKind::InvalidArgument,
177                        format!(
178                            "The ip-to format mismatches with the family set \
179                             {self}"
180                        ),
181                    );
182                    log::error!("{e}");
183                    return Err(e);
184                }
185            }
186        }
187        Ok(())
188    }
189
190    fn validate_fwmark_and_fwmask(&self) -> Result<(), NmstateError> {
191        if self.fwmark.is_none() && self.fwmask.is_some() {
192            let e = NmstateError::new(
193                ErrorKind::InvalidArgument,
194                format!(
195                    "fwmask is present but fwmark is not defined or is zero \
196                     {self:?}"
197                ),
198            );
199            log::error!("{e}");
200            return Err(e);
201        }
202        Ok(())
203    }
204
205    pub(crate) fn is_absent(&self) -> bool {
206        matches!(self.state, Some(RouteRuleState::Absent))
207    }
208
209    pub(crate) fn is_ipv6(&self) -> bool {
210        self.family.as_ref() == Some(&AddressFamily::IPv6)
211            || self.ip_from.as_ref().map(|i| is_ipv6_addr(i.as_str()))
212                == Some(true)
213            || self.ip_to.as_ref().map(|i| is_ipv6_addr(i.as_str()))
214                == Some(true)
215    }
216
217    pub(crate) fn is_match(&self, other: &Self) -> bool {
218        if let Some(ip_from) = self.ip_from.as_deref() {
219            if !ip_from.is_empty() {
220                let ip_from = if !ip_from.contains('/') {
221                    match InterfaceIpAddr::try_from(ip_from) {
222                        Ok(i) => i.to_string(),
223                        Err(e) => {
224                            log::error!("{e}");
225                            return false;
226                        }
227                    }
228                } else {
229                    ip_from.to_string()
230                };
231                if other.ip_from != Some(ip_from) {
232                    return false;
233                }
234            } else if other.ip_from.as_deref().map(|s| s.is_empty())
235                == Some(false)
236            {
237                // Use desire 'ip_from: ""' means it should only match empty
238                // ip_from
239                return false;
240            }
241        }
242        if let Some(ip_to) = self.ip_to.as_deref() {
243            if !ip_to.is_empty() {
244                let ip_to = if !ip_to.contains('/') {
245                    match InterfaceIpAddr::try_from(ip_to) {
246                        Ok(ref i) => i.to_string(),
247                        Err(e) => {
248                            log::error!("{e}");
249                            return false;
250                        }
251                    }
252                } else {
253                    ip_to.to_string()
254                };
255                if other.ip_to != Some(ip_to) {
256                    return false;
257                }
258            } else if other.ip_to.as_deref().map(|s| s.is_empty())
259                == Some(false)
260            {
261                // Use desire 'ip_to: ""' means it should only match empty
262                // ip_to
263                return false;
264            }
265        }
266        if self.family.is_some()
267            && other.family.is_some()
268            && self.family != other.family
269        {
270            return false;
271        }
272
273        if self.priority.is_some()
274            && self.priority != Some(RouteRuleEntry::USE_DEFAULT_PRIORITY)
275            && self.priority != other.priority
276            && !(self.priority == Some(0) && other.priority.is_none())
277        {
278            return false;
279        }
280        if self.table_id.is_some()
281            && self.table_id != Some(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE)
282            && self.table_id != other.table_id
283        {
284            return false;
285        }
286        if self.fwmark.is_some()
287            && self.fwmark.unwrap_or(0) != other.fwmark.unwrap_or(0)
288        {
289            return false;
290        }
291        if self.fwmask.is_some()
292            && self.fwmask.unwrap_or(0) != other.fwmask.unwrap_or(0)
293        {
294            return false;
295        }
296        if self.iif.is_some() && self.iif != other.iif {
297            return false;
298        }
299        if self.action.is_some() && self.action != other.action {
300            return false;
301        }
302        if self.suppress_prefix_length.is_some()
303            && self.suppress_prefix_length != other.suppress_prefix_length
304        {
305            return false;
306        }
307        true
308    }
309
310    // Return tuple of (no_absent, is_ipv4, table_id, ip_from,
311    // ip_to, priority, fwmark, fwmask, action, suppress_prefix_length)
312    fn sort_key(
313        &self,
314    ) -> (bool, bool, u32, &str, &str, i64, u32, u32, u8, u32) {
315        (
316            !matches!(self.state, Some(RouteRuleState::Absent)),
317            {
318                if let Some(ip_from) = self.ip_from.as_ref() {
319                    !is_ipv6_addr(ip_from.as_str())
320                } else if let Some(ip_to) = self.ip_to.as_ref() {
321                    !is_ipv6_addr(ip_to.as_str())
322                } else if let Some(family) = self.family.as_ref() {
323                    *family == AddressFamily::IPv4
324                } else {
325                    log::warn!(
326                        "Neither ip-from, ip-to nor family is defined, \
327                         treating it a IPv4 route rule"
328                    );
329                    true
330                }
331            },
332            self.table_id
333                .unwrap_or(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE),
334            self.ip_from.as_deref().unwrap_or(""),
335            self.ip_to.as_deref().unwrap_or(""),
336            self.priority
337                .unwrap_or(RouteRuleEntry::USE_DEFAULT_PRIORITY),
338            self.fwmark.unwrap_or(0),
339            self.fwmask.unwrap_or(0),
340            self.action.map(u8::from).unwrap_or(0),
341            self.suppress_prefix_length.unwrap_or_default(),
342        )
343    }
344
345    pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
346        if let Some(ip) = self.ip_from.as_ref() {
347            if ip.is_empty() {
348                self.ip_from = None;
349            } else {
350                let new_ip = sanitize_ip_network(ip)?;
351                if self.family.is_none() {
352                    match is_ipv6_addr(new_ip.as_str()) {
353                        true => self.family = Some(AddressFamily::IPv6),
354                        false => self.family = Some(AddressFamily::IPv4),
355                    };
356                }
357                if ip != &new_ip {
358                    log::warn!("Route rule ip-from {ip} sanitized to {new_ip}");
359                    self.ip_from = Some(new_ip);
360                }
361            }
362        }
363        if let Some(ip) = self.ip_to.as_ref() {
364            if ip.is_empty() {
365                self.ip_to = None;
366            } else {
367                let new_ip = sanitize_ip_network(ip)?;
368                if self.family.is_none() {
369                    match is_ipv6_addr(new_ip.as_str()) {
370                        true => self.family = Some(AddressFamily::IPv6),
371                        false => self.family = Some(AddressFamily::IPv4),
372                    };
373                }
374                if ip != &new_ip {
375                    log::warn!("Route rule ip-to {ip} sanitized to {new_ip}");
376                    self.ip_to = Some(new_ip);
377                }
378            }
379        }
380        self.validate_ip_from_to()?;
381        self.validate_fwmark_and_fwmask()?;
382
383        if self.action.is_none() && self.table_id.is_none() {
384            log::info!(
385                "Route rule {self} has no action or route-table defined, \
386                 using default route table 254"
387            );
388            self.table_id = Some(RouteRuleEntry::DEFAULR_ROUTE_TABLE_ID);
389        }
390
391        Ok(())
392    }
393}
394
395// For Vec::dedup()
396impl PartialEq for RouteRuleEntry {
397    fn eq(&self, other: &Self) -> bool {
398        self.sort_key() == other.sort_key()
399    }
400}
401
402// For Vec::sort_unstable()
403impl Ord for RouteRuleEntry {
404    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
405        self.sort_key().cmp(&other.sort_key())
406    }
407}
408
409// For ord
410impl Eq for RouteRuleEntry {}
411
412// For ord
413impl PartialOrd for RouteRuleEntry {
414    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
415        Some(self.cmp(other))
416    }
417}
418
419impl Hash for RouteRuleEntry {
420    fn hash<H: Hasher>(&self, state: &mut H) {
421        self.sort_key().hash(state);
422    }
423}
424
425impl std::fmt::Display for RouteRuleEntry {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        let mut props = Vec::new();
428        if self.is_absent() {
429            props.push("state: absent".to_string());
430        }
431        if let Some(v) = self.family.as_ref() {
432            props.push(format!("family: {v}"));
433        }
434        if let Some(v) = self.ip_from.as_ref() {
435            props.push(format!("ip-from: {v}"));
436        }
437        if let Some(v) = self.ip_to.as_ref() {
438            props.push(format!("ip-to: {v}"));
439        }
440        if let Some(v) = self.priority.as_ref() {
441            props.push(format!("priority: {v}"));
442        }
443        if let Some(v) = self.table_id.as_ref() {
444            props.push(format!("route-table: {v}"));
445        }
446        if let Some(v) = self.fwmask.as_ref() {
447            props.push(format!("fwmask: {v}"));
448        }
449        if let Some(v) = self.fwmark.as_ref() {
450            props.push(format!("fwmark: {v}"));
451        }
452        if let Some(v) = self.iif.as_ref() {
453            props.push(format!("iif: {v}"));
454        }
455        if let Some(v) = self.action.as_ref() {
456            props.push(format!("action: {v}"));
457        }
458        if let Some(v) = self.suppress_prefix_length.as_ref() {
459            props.push(format!("suppress-prefix-length: {v}"));
460        }
461        write!(f, "{}", props.join(" "))
462    }
463}
464
465#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
466#[serde(rename_all = "kebab-case")]
467#[non_exhaustive]
468#[serde(deny_unknown_fields)]
469pub enum RouteRuleAction {
470    Blackhole,
471    Unreachable,
472    Prohibit,
473}
474
475impl std::fmt::Display for RouteRuleAction {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        write!(
478            f,
479            "{}",
480            match self {
481                Self::Blackhole => "blackhole",
482                Self::Unreachable => "unreachable",
483                Self::Prohibit => "prohibit",
484            }
485        )
486    }
487}
488
489const FR_ACT_BLACKHOLE: u8 = 6;
490const FR_ACT_UNREACHABLE: u8 = 7;
491const FR_ACT_PROHIBIT: u8 = 8;
492
493impl From<RouteRuleAction> for u8 {
494    fn from(v: RouteRuleAction) -> u8 {
495        match v {
496            RouteRuleAction::Blackhole => FR_ACT_BLACKHOLE,
497            RouteRuleAction::Unreachable => FR_ACT_UNREACHABLE,
498            RouteRuleAction::Prohibit => FR_ACT_PROHIBIT,
499        }
500    }
501}
502
503#[derive(Clone, Debug, Default, PartialEq, Eq)]
504pub(crate) struct MergedRouteRules {
505    pub(crate) desired: RouteRules,
506    pub(crate) current: RouteRules,
507    // The `for_apply` will hold two type of route rule:
508    //  * Desired route rules
509    //  * Current route rules been marked as absent
510    pub(crate) for_apply: Vec<RouteRuleEntry>,
511    // The `for_verify` hold the same data as `for_apply` except the
512    // auto set priority.
513    pub(crate) for_verify: Vec<RouteRuleEntry>,
514}
515
516impl MergedRouteRules {
517    pub(crate) fn new(
518        desired: RouteRules,
519        current: RouteRules,
520    ) -> Result<Self, NmstateError> {
521        let mut for_apply: Vec<RouteRuleEntry> = Vec::new();
522        let mut merged_rules: Vec<RouteRuleEntry> = Vec::new();
523
524        let mut des_absent_rules: Vec<&RouteRuleEntry> = Vec::new();
525        if let Some(rules) = desired.config.as_ref() {
526            for rule in rules.as_slice().iter() {
527                if !rule.is_absent() {
528                    let mut new_rule = rule.clone();
529                    new_rule.sanitize()?;
530                    for_apply.push(new_rule);
531                } else {
532                    des_absent_rules.push(rule);
533                }
534            }
535        }
536
537        if let Some(cur_rules) = current.config.as_ref() {
538            for rule in cur_rules {
539                if des_absent_rules
540                    .as_slice()
541                    .iter()
542                    .any(|absent_rule| absent_rule.is_match(rule))
543                {
544                    let mut new_rule = rule.clone();
545                    new_rule.state = Some(RouteRuleState::Absent);
546                    new_rule.sanitize()?;
547                    for_apply.push(new_rule);
548                } else {
549                    let mut new_rule = rule.clone();
550                    new_rule.sanitize()?;
551                    merged_rules.push(new_rule);
552                }
553            }
554        }
555
556        for rule in for_apply.iter().filter(|rule| !rule.is_absent()) {
557            if !merged_rules.iter().any(|mer_rule| rule.is_match(mer_rule)) {
558                merged_rules.push(rule.clone());
559            }
560        }
561
562        let for_verify = for_apply.clone();
563
564        set_auto_priority(for_apply.as_mut_slice(), merged_rules.as_slice());
565
566        Ok(Self {
567            desired,
568            current,
569            for_apply,
570            for_verify,
571        })
572    }
573
574    pub(crate) fn remove_rules_to_ignored_ifaces(
575        &mut self,
576        ignored_ifaces: &[(String, InterfaceType)],
577    ) {
578        let ignored_ifaces: Vec<&str> = ignored_ifaces
579            .iter()
580            .filter(|(_, t)| !t.is_userspace())
581            .map(|(n, _)| n.as_str())
582            .collect();
583
584        self.for_apply.retain(|rule| {
585            if let Some(iif) = rule.iif.as_ref() {
586                !ignored_ifaces.contains(&iif.as_str())
587            } else {
588                true
589            }
590        })
591    }
592
593    pub(crate) fn is_changed(&self) -> bool {
594        (!self.desired.is_empty())
595            && (self.for_apply
596                != self.current.config.clone().unwrap_or_default())
597    }
598}
599
600/// Set a proper priority on rules with USE_DEFAULT_PRIORITY: if a matching
601/// rule already existed, use its priority so we don't create a new one. If
602/// not, use an increasing priority number so we don't create rules with the
603/// same priority.
604fn set_auto_priority(
605    for_apply: &mut [RouteRuleEntry],
606    merged: &[RouteRuleEntry],
607) {
608    let mut max_priority = get_max_rule_priority(merged);
609    if max_priority < ROUTE_RULE_DEFAULT_PRIORIRY - 1 {
610        max_priority = ROUTE_RULE_DEFAULT_PRIORIRY - 1;
611    }
612
613    for rule in for_apply.iter_mut().filter(|r| {
614        !r.is_absent()
615            && (r.priority.is_none()
616                || r.priority == Some(RouteRuleEntry::USE_DEFAULT_PRIORITY))
617    }) {
618        let cur_prio =
619            merged.iter().find_map(|cur_rule| match cur_rule.priority {
620                Some(RouteRuleEntry::USE_DEFAULT_PRIORITY) => None,
621                Some(n) => rule.is_match(cur_rule).then_some(n),
622                None => None,
623            });
624        if cur_prio.is_some() {
625            rule.priority = cur_prio;
626        } else {
627            max_priority += 1;
628            rule.priority = Some(max_priority);
629        }
630    }
631}
632
633fn get_max_rule_priority(rules: &[RouteRuleEntry]) -> i64 {
634    rules
635        .iter()
636        .map(|r| r.priority.unwrap_or_default())
637        .max()
638        .unwrap_or_default()
639}