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