Skip to main content

nmstate/
route.rs

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