nmstate/
route.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::{hash_map::Entry, HashMap, HashSet};
4use std::hash::{Hash, Hasher};
5use std::net::Ipv4Addr;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    ifaces::InterfaceNameSearch,
12    ip::{is_ipv6_addr, sanitize_ip_network},
13    ErrorKind, InterfaceType, MergedInterfaces, NmstateError,
14};
15
16const DEFAULT_TABLE_ID: u32 = 254; // main route table ID
17const LOOPBACK_IFACE_NAME: &str = "lo";
18
19#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[non_exhaustive]
21#[serde(deny_unknown_fields)]
22/// IP routing status
23pub struct Routes {
24    #[serde(skip_serializing_if = "Option::is_none")]
25    /// Running effected routes containing route from universe or link scope,
26    /// and only from these protocols:
27    ///  * boot (often used by `iproute` command)
28    ///  * static
29    ///  * ra
30    ///  * dhcp
31    ///  * mrouted
32    ///  * keepalived
33    ///  * babel
34    ///
35    /// Ignored when applying.
36    pub running: Option<Vec<RouteEntry>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    /// Static routes containing route from universe or link scope,
39    /// and only from these protocols:
40    ///  * boot (often used by `iproute` command)
41    ///  * static
42    ///
43    /// When applying, `None` means preserve current routes.
44    /// This property is not overriding but adding specified routes to
45    /// existing routes. To delete a route entry, please [RouteEntry.state] as
46    /// [RouteState::Absent]. Any property of absent [RouteEntry] set to
47    /// `None` means wildcard. For example, this [crate::NetworkState] could
48    /// remove all routes next hop to interface eth1(showing in yaml):
49    /// ```yaml
50    /// routes:
51    ///   config:
52    ///   - next-hop-interface: eth1
53    ///     state: absent
54    /// ```
55    ///
56    /// To change a route entry, you need to delete old one and add new one(can
57    /// be in single transaction).
58    pub config: Option<Vec<RouteEntry>>,
59}
60
61impl Routes {
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// TODO: hide it, internal only
67    pub fn is_empty(&self) -> bool {
68        self.running.is_none() && self.config.is_none()
69    }
70
71    pub fn validate(&self) -> Result<(), NmstateError> {
72        // All desire non-absent route should have next hop interface except
73        // for route with route type `Blackhole`, `Unreachable`, `Prohibit`.
74        if let Some(config_routes) = self.config.as_ref() {
75            for route in config_routes.iter() {
76                if !route.is_absent() {
77                    if !route.is_unicast()
78                        && (route.next_hop_iface.is_some()
79                            && route.next_hop_iface
80                                != Some(LOOPBACK_IFACE_NAME.to_string())
81                            || route.next_hop_addr.is_some())
82                    {
83                        return Err(NmstateError::new(
84                            ErrorKind::InvalidArgument,
85                            format!(
86                                "A {:?} Route cannot have a next hop : \
87                                 {route:?}",
88                                route.route_type.unwrap()
89                            ),
90                        ));
91                    } else if route.next_hop_iface.is_none()
92                        && route.is_unicast()
93                    {
94                        return Err(NmstateError::new(
95                            ErrorKind::NotImplementedError,
96                            format!(
97                                "Route with empty next hop interface is not \
98                                 supported: {route:?}"
99                            ),
100                        ));
101                    }
102                }
103                validate_route_dst(route)?;
104            }
105        }
106        Ok(())
107    }
108
109    pub(crate) fn remove_ignored_routes(&mut self) {
110        for rts in [self.running.as_mut(), self.config.as_mut()]
111            .into_iter()
112            .flatten()
113        {
114            rts.retain(|rt| !rt.is_ignore());
115        }
116    }
117
118    pub(crate) fn resolve_next_hop_iface_ref(
119        &mut self,
120        merged_ifaces: &MergedInterfaces,
121    ) -> Result<(), NmstateError> {
122        let iface_name_search = InterfaceNameSearch::new(merged_ifaces);
123
124        if let Some(config_routes) = self.config.as_mut() {
125            for route in config_routes.iter_mut() {
126                let new_iface_name = if let Some(next_hop_iface) =
127                    route.next_hop_iface.as_ref()
128                {
129                    let kernel_names = iface_name_search.get(next_hop_iface);
130                    // Prefer kernel name as port name
131                    if kernel_names.contains(&next_hop_iface.as_str()) {
132                        continue;
133                    }
134                    if kernel_names.is_empty() && !route.is_absent() {
135                        if merged_ifaces.ignored_ifaces.iter().any(
136                            |(name, iface_type)| {
137                                name == next_hop_iface
138                                    && !iface_type.is_userspace()
139                            },
140                        ) {
141                            return Err(NmstateError::new(
142                                ErrorKind::InvalidArgument,
143                                format!(
144                                    "Route '{}': next hop interface {} is \
145                                     marked as ignored",
146                                    route,
147                                    next_hop_iface.as_str()
148                                ),
149                            ));
150                        } else {
151                            return Err(NmstateError::new(
152                                ErrorKind::InvalidArgument,
153                                format!(
154                                    "Route '{}': next hop interface {} not \
155                                     found",
156                                    route,
157                                    next_hop_iface.as_str()
158                                ),
159                            ));
160                        }
161                    } else if kernel_names.len() > 1 {
162                        return Err(NmstateError::new(
163                            ErrorKind::InvalidArgument,
164                            format!(
165                                "Route '{}' defined with next hop interface \
166                                 {} but multiple interfaces are sharing this \
167                                 profile name",
168                                route,
169                                next_hop_iface.as_str()
170                            ),
171                        ));
172                    } else {
173                        kernel_names.first().map(|s| s.to_string())
174                    }
175                } else {
176                    None
177                };
178                if let Some(new_iface_name) = new_iface_name {
179                    route.next_hop_iface.replace(new_iface_name);
180                }
181            }
182        }
183        Ok(())
184    }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "kebab-case")]
189#[non_exhaustive]
190#[derive(Default)]
191pub enum RouteState {
192    /// Mark a route entry as absent to remove it.
193    #[default]
194    Absent,
195    /// Mark a route as ignored
196    Ignore,
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
200#[serde(rename_all = "kebab-case")]
201#[non_exhaustive]
202#[serde(deny_unknown_fields)]
203/// Route entry
204pub struct RouteEntry {
205    #[serde(skip_serializing_if = "Option::is_none")]
206    /// Only used for delete route when applying.
207    pub state: Option<RouteState>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    /// Route destination address or network
210    /// Mandatory for every non-absent routes.
211    pub destination: Option<String>,
212    #[serde(
213        skip_serializing_if = "Option::is_none",
214        rename = "next-hop-interface"
215    )]
216    /// Route next hop interface name.
217    /// Serialize and deserialize to/from `next-hop-interface`.
218    /// Mandatory for every non-absent routes except for route with
219    /// route type `Blackhole`, `Unreachable`, `Prohibit`.
220    pub next_hop_iface: Option<String>,
221    #[serde(
222        skip_serializing_if = "Option::is_none",
223        rename = "next-hop-address"
224    )]
225    /// Route next hop IP address.
226    /// Serialize and deserialize to/from `next-hop-address`.
227    /// When setting this as empty string for absent route, it will only delete
228    /// routes __without__ `next-hop-address`.
229    pub next_hop_addr: Option<String>,
230    #[serde(
231        skip_serializing_if = "Option::is_none",
232        default,
233        deserialize_with = "crate::deserializer::option_i64_or_string"
234    )]
235    /// Route metric. [RouteEntry::USE_DEFAULT_METRIC] for default
236    /// setting of network backend.
237    pub metric: Option<i64>,
238    #[serde(
239        skip_serializing_if = "Option::is_none",
240        default,
241        deserialize_with = "crate::deserializer::option_u32_or_string"
242    )]
243    /// Route table id. [RouteEntry::USE_DEFAULT_ROUTE_TABLE] for main
244    /// route table 254.
245    pub table_id: Option<u32>,
246
247    /// ECMP(Equal-Cost Multi-Path) route weight
248    /// The valid range of this property is 1-256.
249    #[serde(
250        skip_serializing_if = "Option::is_none",
251        default,
252        deserialize_with = "crate::deserializer::option_u16_or_string"
253    )]
254    pub weight: Option<u16>,
255    /// Route type
256    /// Serialize and deserialize to/from `route-type`.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub route_type: Option<RouteType>,
259    /// Congestion window clamp
260    #[serde(
261        skip_serializing_if = "Option::is_none",
262        default,
263        deserialize_with = "crate::deserializer::option_u32_or_string"
264    )]
265    pub cwnd: Option<u32>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    /// Route source defines which IP address should be used as the source
268    /// for packets routed via a specific route
269    pub source: Option<String>,
270    /// Initial congestion window size
271    #[serde(
272        skip_serializing_if = "Option::is_none",
273        default,
274        deserialize_with = "crate::deserializer::option_u32_or_string"
275    )]
276    pub initcwnd: Option<u32>,
277    /// Initial receive window size
278    #[serde(
279        skip_serializing_if = "Option::is_none",
280        default,
281        deserialize_with = "crate::deserializer::option_u32_or_string"
282    )]
283    pub initrwnd: Option<u32>,
284    /// MTU
285    #[serde(
286        skip_serializing_if = "Option::is_none",
287        default,
288        deserialize_with = "crate::deserializer::option_u32_or_string"
289    )]
290    pub mtu: Option<u32>,
291    /// Enable quickack will disable disables delayed acknowledgments.
292    #[serde(
293        skip_serializing_if = "Option::is_none",
294        default,
295        deserialize_with = "crate::deserializer::option_bool_or_string"
296    )]
297    pub quickack: Option<bool>,
298    /// Maximal Segment Size to advertise for TCP connections
299    #[serde(
300        skip_serializing_if = "Option::is_none",
301        default,
302        deserialize_with = "crate::deserializer::option_u32_or_string"
303    )]
304    pub advmss: Option<u32>,
305    /// Store the routes to the route table specified VRF bind to.
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub vrf_name: Option<String>,
308}
309
310#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
311#[serde(rename_all = "kebab-case")]
312#[non_exhaustive]
313#[serde(deny_unknown_fields)]
314pub enum RouteType {
315    Blackhole,
316    Unreachable,
317    Prohibit,
318}
319
320impl std::fmt::Display for RouteType {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        write!(
323            f,
324            "{}",
325            match self {
326                Self::Blackhole => "blackhole",
327                Self::Unreachable => "unreachable",
328                Self::Prohibit => "prohibit",
329            }
330        )
331    }
332}
333
334const RTN_UNICAST: u8 = 1;
335const RTN_BLACKHOLE: u8 = 6;
336const RTN_UNREACHABLE: u8 = 7;
337const RTN_PROHIBIT: u8 = 8;
338
339impl From<RouteType> for u8 {
340    fn from(v: RouteType) -> u8 {
341        match v {
342            RouteType::Blackhole => RTN_BLACKHOLE,
343            RouteType::Unreachable => RTN_UNREACHABLE,
344            RouteType::Prohibit => RTN_PROHIBIT,
345        }
346    }
347}
348
349impl RouteEntry {
350    pub const USE_DEFAULT_METRIC: i64 = -1;
351    pub const USE_DEFAULT_ROUTE_TABLE: u32 = 0;
352
353    pub fn new() -> Self {
354        Self::default()
355    }
356
357    pub(crate) fn is_absent(&self) -> bool {
358        matches!(self.state, Some(RouteState::Absent))
359    }
360
361    pub(crate) fn is_ignore(&self) -> bool {
362        matches!(self.state, Some(RouteState::Ignore))
363    }
364
365    /// Whether the desired route (self) matches with another
366    /// metric is ignored.
367    pub(crate) fn is_match(&self, other: &Self) -> bool {
368        if self.destination.as_ref().is_some()
369            && self.destination.as_deref() != Some("")
370            && self.destination != other.destination
371        {
372            return false;
373        }
374        if self.next_hop_iface.as_ref().is_some()
375            && self.next_hop_iface != other.next_hop_iface
376        {
377            return false;
378        }
379
380        if self.next_hop_addr.as_ref().is_some()
381            && self.next_hop_addr != other.next_hop_addr
382        {
383            return false;
384        }
385        if self.table_id.is_some()
386            && self.table_id != Some(RouteEntry::USE_DEFAULT_ROUTE_TABLE)
387            && self.table_id != other.table_id
388        {
389            return false;
390        }
391        if self.weight.is_some() && self.weight != other.weight {
392            return false;
393        }
394        if self.route_type.is_some() && self.route_type != other.route_type {
395            return false;
396        }
397        if self.cwnd.is_some() && self.cwnd != other.cwnd {
398            return false;
399        }
400        if self.source.as_ref().is_some() && self.source != other.source {
401            return false;
402        }
403        if self.initcwnd.is_some() && self.initcwnd != other.initcwnd {
404            return false;
405        }
406        if self.initrwnd.is_some() && self.initrwnd != other.initrwnd {
407            return false;
408        }
409        if self.mtu.is_some() && self.mtu != other.mtu {
410            return false;
411        }
412        if self.quickack.is_some() && self.quickack != other.quickack {
413            return false;
414        }
415        if self.advmss.is_some() && self.advmss != other.advmss {
416            return false;
417        }
418        if self.vrf_name.is_some() && self.vrf_name != other.vrf_name {
419            return false;
420        }
421        true
422    }
423
424    // Return tuple of Vec of all properties with default value unwrapped.
425    // Metric is ignored
426    fn sort_key(&self) -> (Vec<bool>, Vec<&str>, Vec<u32>) {
427        (
428            vec![
429                // not_absent
430                !matches!(self.state, Some(RouteState::Absent)),
431                // is_ipv6
432                !self
433                    .destination
434                    .as_ref()
435                    .map(|d| is_ipv6_addr(d.as_str()))
436                    .unwrap_or_default(),
437                self.quickack.unwrap_or_default(),
438            ],
439            vec![
440                self.next_hop_iface
441                    .as_deref()
442                    .unwrap_or(LOOPBACK_IFACE_NAME),
443                self.destination.as_deref().unwrap_or(""),
444                self.next_hop_addr.as_deref().unwrap_or(""),
445                self.source.as_deref().unwrap_or(""),
446                self.vrf_name.as_deref().unwrap_or(""),
447            ],
448            vec![
449                self.table_id.unwrap_or(DEFAULT_TABLE_ID),
450                self.cwnd.unwrap_or_default(),
451                self.initcwnd.unwrap_or_default(),
452                self.initrwnd.unwrap_or_default(),
453                self.mtu.unwrap_or_default(),
454                self.weight.unwrap_or_default().into(),
455                self.route_type
456                    .as_ref()
457                    .map(|t| u8::from(*t))
458                    .unwrap_or_default()
459                    .into(),
460                self.advmss.unwrap_or_default(),
461            ],
462        )
463    }
464
465    pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
466        if let Some(dst) = self.destination.as_ref() {
467            if dst.is_empty() {
468                self.destination = None;
469            } else {
470                let new_dst = sanitize_ip_network(dst)?;
471                if dst != &new_dst {
472                    log::warn!(
473                        "Route destination {dst} sanitized to {new_dst}"
474                    );
475                    self.destination = Some(new_dst);
476                }
477            }
478        }
479        if let Some(via) = self.next_hop_addr.as_ref() {
480            let new_via = format!("{}", via.parse::<std::net::IpAddr>()?);
481            if via != &new_via {
482                log::warn!(
483                    "Route next-hop-address {via} sanitized to {new_via}"
484                );
485                self.next_hop_addr = Some(new_via);
486            }
487        }
488        if let Some(src) = self.source.as_ref() {
489            let new_src = format!(
490                "{}",
491                src.parse::<std::net::IpAddr>().map_err(|e| {
492                    NmstateError::new(
493                        ErrorKind::InvalidArgument,
494                        format!("Failed to parse IP address '{src}': {e}"),
495                    )
496                })?
497            );
498            if src != &new_src {
499                log::info!("Route source address {src} sanitized to {new_src}");
500                self.source = Some(new_src);
501            }
502        }
503        if let Some(weight) = self.weight {
504            if !(1..=256).contains(&weight) {
505                return Err(NmstateError::new(
506                    ErrorKind::InvalidArgument,
507                    format!(
508                        "Invalid ECMP route weight {weight}, should be in the \
509                         range of 1 to 256"
510                    ),
511                ));
512            }
513            if let Some(dst) = self.destination.as_deref() {
514                if is_ipv6_addr(dst) {
515                    return Err(NmstateError::new(
516                        ErrorKind::NotSupportedError,
517                        "IPv6 ECMP route with weight is not supported yet"
518                            .to_string(),
519                    ));
520                }
521            }
522        }
523        if let Some(cwnd) = self.cwnd {
524            if cwnd == 0 {
525                return Err(NmstateError::new(
526                    ErrorKind::InvalidArgument,
527                    "The value of 'cwnd' cannot be 0".to_string(),
528                ));
529            }
530        }
531        if self.mtu == Some(0) {
532            return Err(NmstateError::new(
533                ErrorKind::InvalidArgument,
534                "The value of 'mtu' cannot be 0".to_string(),
535            ));
536        }
537        if self.advmss == Some(0) {
538            return Err(NmstateError::new(
539                ErrorKind::InvalidArgument,
540                "The value of 'advmss' cannot be 0".to_string(),
541            ));
542        }
543        Ok(())
544    }
545
546    pub(crate) fn is_ipv6(&self) -> bool {
547        self.destination.as_ref().map(|d| is_ipv6_addr(d.as_str()))
548            == Some(true)
549    }
550
551    pub(crate) fn is_unicast(&self) -> bool {
552        self.route_type.is_none()
553            || u8::from(self.route_type.unwrap()) == RTN_UNICAST
554    }
555}
556
557// For Vec::dedup()
558impl PartialEq for RouteEntry {
559    fn eq(&self, other: &Self) -> bool {
560        self.sort_key() == other.sort_key()
561    }
562}
563
564// For Vec::sort_unstable()
565impl Ord for RouteEntry {
566    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
567        self.sort_key().cmp(&other.sort_key())
568    }
569}
570
571// For ord
572impl Eq for RouteEntry {}
573
574// For ord
575impl PartialOrd for RouteEntry {
576    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
577        Some(self.cmp(other))
578    }
579}
580
581impl Hash for RouteEntry {
582    fn hash<H: Hasher>(&self, state: &mut H) {
583        self.sort_key().hash(state);
584    }
585}
586
587impl std::fmt::Display for RouteEntry {
588    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589        let mut props = Vec::new();
590        if self.is_absent() {
591            props.push("state: absent".to_string());
592        }
593        if let Some(v) = self.destination.as_ref() {
594            props.push(format!("destination: {v}"));
595        }
596        if let Some(v) = self.next_hop_iface.as_ref() {
597            props.push(format!("next-hop-interface: {v}"));
598        }
599        if let Some(v) = self.next_hop_addr.as_ref() {
600            props.push(format!("next-hop-address: {v}"));
601        }
602        if let Some(v) = self.source.as_ref() {
603            props.push(format!("source: {v}"));
604        }
605        if let Some(v) = self.metric.as_ref() {
606            props.push(format!("metric: {v}"));
607        }
608        if let Some(v) = self.table_id.as_ref() {
609            props.push(format!("table-id: {v}"));
610        }
611        if let Some(v) = self.weight {
612            props.push(format!("weight: {v}"));
613        }
614        if let Some(v) = self.cwnd {
615            props.push(format!("cwnd: {v}"));
616        }
617        if let Some(v) = self.initcwnd {
618            props.push(format!("initcwnd: {v}"));
619        }
620        if let Some(v) = self.initrwnd {
621            props.push(format!("initrwnd: {v}"));
622        }
623        if let Some(v) = self.mtu {
624            props.push(format!("mtu: {v}"));
625        }
626        if let Some(v) = self.quickack {
627            props.push(format!("quickack: {v}"));
628        }
629        if let Some(v) = self.advmss {
630            props.push(format!("advmss: {v}"));
631        }
632        if let Some(v) = self.vrf_name.as_ref() {
633            props.push(format!("vrf-name: {v}"));
634        }
635
636        write!(f, "{}", props.join(" "))
637    }
638}
639
640#[derive(Clone, Debug, Default, PartialEq, Eq)]
641pub(crate) struct MergedRoutes {
642    // When all routes next hop to a interface are all marked as absent,
643    // the `MergedRoutes.merged` will not have entry for this interface, but
644    // interface name is found in `MergedRoutes.route_changed_ifaces`.
645    // For backend use incremental route changes, please use
646    // `MergedRoutes.changed_routes`.
647    pub(crate) merged: HashMap<String, Vec<RouteEntry>>,
648    pub(crate) route_changed_ifaces: Vec<String>,
649    // The `changed_routes` contains changed routes including those been marked
650    // as absent. Not including desired route equal to current route.
651    pub(crate) changed_routes: Vec<RouteEntry>,
652    pub(crate) desired: Routes,
653    pub(crate) current: Routes,
654}
655
656impl MergedRoutes {
657    pub(crate) fn new(
658        mut desired: Routes,
659        current: Routes,
660        merged_ifaces: &MergedInterfaces,
661    ) -> Result<Self, NmstateError> {
662        desired.remove_ignored_routes();
663        desired.validate()?;
664        desired.resolve_next_hop_iface_ref(merged_ifaces)?;
665        desired.resolve_vrf_name(merged_ifaces)?;
666
667        let mut desired_routes = Vec::new();
668        if let Some(rts) = desired.config.as_ref() {
669            for rt in rts {
670                let mut rt = rt.clone();
671                rt.sanitize()?;
672                desired_routes.push(rt);
673            }
674        }
675
676        let mut changed_ifaces: HashSet<&str> = HashSet::new();
677        let mut changed_routes: HashSet<RouteEntry> = HashSet::new();
678
679        let ifaces_marked_as_absent: Vec<&str> = merged_ifaces
680            .kernel_ifaces
681            .values()
682            .filter(|i| i.merged.is_absent())
683            .map(|i| i.merged.name())
684            .collect();
685
686        let ifaces_with_ipv4_disabled: Vec<&str> = merged_ifaces
687            .kernel_ifaces
688            .values()
689            .filter(|i| !i.merged.base_iface().is_ipv4_enabled())
690            .map(|i| i.merged.name())
691            .collect();
692
693        let ifaces_with_ipv6_disabled: Vec<&str> = merged_ifaces
694            .kernel_ifaces
695            .values()
696            .filter(|i| !i.merged.base_iface().is_ipv6_enabled())
697            .map(|i| i.merged.name())
698            .collect();
699
700        // Interface has route added.
701        for rt in desired_routes
702            .as_slice()
703            .iter()
704            .filter(|rt| !rt.is_absent())
705        {
706            if let Some(via) = rt.next_hop_iface.as_ref() {
707                if ifaces_marked_as_absent.contains(&via.as_str()) {
708                    return Err(NmstateError::new(
709                        ErrorKind::InvalidArgument,
710                        format!(
711                            "The next hop interface of desired Route '{rt}' \
712                             has been marked as absent"
713                        ),
714                    ));
715                }
716                if rt.is_ipv6()
717                    && ifaces_with_ipv6_disabled.contains(&via.as_str())
718                {
719                    return Err(NmstateError::new(
720                        ErrorKind::InvalidArgument,
721                        format!(
722                            "The next hop interface of desired Route '{rt}' \
723                             has been marked as IPv6 disabled"
724                        ),
725                    ));
726                }
727                if (!rt.is_ipv6())
728                    && ifaces_with_ipv4_disabled.contains(&via.as_str())
729                {
730                    return Err(NmstateError::new(
731                        ErrorKind::InvalidArgument,
732                        format!(
733                            "The next hop interface of desired Route '{rt}' \
734                             has been marked as IPv4 disabled"
735                        ),
736                    ));
737                }
738                changed_ifaces.insert(via.as_str());
739            } else if rt.route_type.is_some() {
740                changed_ifaces.insert(LOOPBACK_IFACE_NAME);
741            }
742        }
743
744        // Interface has route deleted.
745        for absent_rt in
746            desired_routes.as_slice().iter().filter(|rt| rt.is_absent())
747        {
748            if let Some(cur_rts) = current.config.as_ref() {
749                for rt in cur_rts {
750                    if absent_rt.is_match(rt) {
751                        if let Some(via) = rt.next_hop_iface.as_ref() {
752                            changed_ifaces.insert(via.as_str());
753                        } else {
754                            changed_ifaces.insert(LOOPBACK_IFACE_NAME);
755                        }
756                    }
757                }
758            }
759        }
760
761        let mut merged_routes: Vec<RouteEntry> = Vec::new();
762
763        if let Some(cur_rts) = current.config.as_ref() {
764            for rt in cur_rts {
765                if let Some(via) = rt.next_hop_iface.as_ref() {
766                    // We include current route to merged_routes when it is
767                    // not marked as absent due to absent interface or disabled
768                    // ip stack or route state:absent.
769                    if ifaces_marked_as_absent.contains(&via.as_str())
770                        || (rt.is_ipv6()
771                            && ifaces_with_ipv6_disabled
772                                .contains(&via.as_str()))
773                        || (!rt.is_ipv6()
774                            && ifaces_with_ipv4_disabled
775                                .contains(&via.as_str()))
776                        || desired_routes
777                            .as_slice()
778                            .iter()
779                            .filter(|r| r.is_absent())
780                            .any(|absent_rt| absent_rt.is_match(rt))
781                    {
782                        let mut new_rt = rt.clone();
783                        new_rt.state = Some(RouteState::Absent);
784                        changed_routes.insert(new_rt);
785                    } else {
786                        merged_routes.push(rt.clone());
787                    }
788                }
789            }
790        }
791
792        // Append desired routes
793        for rt in desired_routes
794            .as_slice()
795            .iter()
796            .filter(|rt| !rt.is_absent())
797        {
798            if let Some(cur_rts) = current.config.as_ref() {
799                if !cur_rts.as_slice().iter().any(|cur_rt| cur_rt.is_match(rt))
800                {
801                    changed_routes.insert(rt.clone());
802                }
803            } else {
804                changed_routes.insert(rt.clone());
805            }
806            merged_routes.push(rt.clone());
807        }
808
809        merged_routes.sort_unstable();
810        merged_routes.dedup();
811
812        let mut merged: HashMap<String, Vec<RouteEntry>> = HashMap::new();
813
814        for rt in merged_routes {
815            if let Some(via) = rt.next_hop_iface.as_ref() {
816                let rts: &mut Vec<RouteEntry> =
817                    match merged.entry(via.to_string()) {
818                        Entry::Occupied(o) => o.into_mut(),
819                        Entry::Vacant(v) => v.insert(Vec::new()),
820                    };
821                rts.push(rt);
822            } else if rt.route_type.is_some() {
823                let rts: &mut Vec<RouteEntry> =
824                    match merged.entry(LOOPBACK_IFACE_NAME.to_string()) {
825                        Entry::Occupied(o) => o.into_mut(),
826                        Entry::Vacant(v) => v.insert(Vec::new()),
827                    };
828                rts.push(rt);
829            }
830        }
831
832        let route_changed_ifaces: Vec<String> =
833            changed_ifaces.iter().map(|i| i.to_string()).collect();
834
835        Ok(Self {
836            merged,
837            desired,
838            current,
839            route_changed_ifaces,
840            changed_routes: changed_routes.drain().collect(),
841        })
842    }
843
844    pub(crate) fn remove_routes_to_ignored_ifaces(
845        &mut self,
846        ignored_ifaces: &[(String, InterfaceType)],
847    ) {
848        let ignored_ifaces: Vec<&str> = ignored_ifaces
849            .iter()
850            .filter_map(|(n, t)| {
851                if !t.is_userspace() {
852                    Some(n.as_str())
853                } else {
854                    None
855                }
856            })
857            .collect();
858
859        for iface in ignored_ifaces.as_slice() {
860            self.merged.remove(*iface);
861        }
862        self.route_changed_ifaces
863            .retain(|n| !ignored_ifaces.contains(&n.as_str()));
864    }
865
866    pub(crate) fn is_changed(&self) -> bool {
867        !self.route_changed_ifaces.is_empty()
868    }
869}
870
871// Validating if the route destination network is valid,
872// 0.0.0.0/8 and its subnet cannot be used as the route destination network
873// for unicast route
874fn validate_route_dst(route: &RouteEntry) -> Result<(), NmstateError> {
875    if let Some(dst) = route.destination.as_deref() {
876        if !is_ipv6_addr(dst) {
877            let ip_net: Vec<&str> = dst.split('/').collect();
878            let ip_addr = Ipv4Addr::from_str(ip_net[0])?;
879            if ip_addr.octets()[0] == 0 {
880                if dst.contains('/') {
881                    let prefix = match ip_net[1].parse::<i32>() {
882                        Ok(p) => p,
883                        Err(_) => {
884                            return Err(NmstateError::new(
885                                ErrorKind::InvalidArgument,
886                                format!(
887                                    "The prefix of the route destination \
888                                     network '{dst}' is invalid"
889                                ),
890                            ));
891                        }
892                    };
893                    if prefix >= 8 && route.is_unicast() {
894                        let e = NmstateError::new(
895                            ErrorKind::InvalidArgument,
896                            "0.0.0.0/8 and its subnet cannot be used as the \
897                             route destination for unicast route, please use \
898                             the default gateway 0.0.0.0/0 instead"
899                                .to_string(),
900                        );
901                        log::error!("{e}");
902                        return Err(e);
903                    }
904                } else if route.is_unicast() {
905                    let e = NmstateError::new(
906                        ErrorKind::InvalidArgument,
907                        "0.0.0.0/8 and its subnet cannot be used as the route \
908                         destination for unicast route, please use the \
909                         default gateway 0.0.0.0/0 instead"
910                            .to_string(),
911                    );
912                    log::error!("{e}");
913                    return Err(e);
914                }
915            }
916            return Ok(());
917        }
918    }
919    Ok(())
920}