1use 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; const LOOPBACK_IFACE_NAME: &str = "lo";
18
19#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[non_exhaustive]
21#[serde(deny_unknown_fields)]
22pub struct Routes {
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub running: Option<Vec<RouteEntry>>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub config: Option<Vec<RouteEntry>>,
59}
60
61impl Routes {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 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 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 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 #[default]
194 Absent,
195 Ignore,
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
200#[serde(rename_all = "kebab-case")]
201#[non_exhaustive]
202#[serde(deny_unknown_fields)]
203pub struct RouteEntry {
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub state: Option<RouteState>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub destination: Option<String>,
212 #[serde(
213 skip_serializing_if = "Option::is_none",
214 rename = "next-hop-interface"
215 )]
216 pub next_hop_iface: Option<String>,
221 #[serde(
222 skip_serializing_if = "Option::is_none",
223 rename = "next-hop-address"
224 )]
225 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 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 pub table_id: Option<u32>,
246
247 #[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 #[serde(skip_serializing_if = "Option::is_none")]
258 pub route_type: Option<RouteType>,
259 #[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 pub source: Option<String>,
270 #[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 #[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 #[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 #[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 #[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 #[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 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 fn sort_key(&self) -> (Vec<bool>, Vec<&str>, Vec<u32>) {
427 (
428 vec![
429 !matches!(self.state, Some(RouteState::Absent)),
431 !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
557impl PartialEq for RouteEntry {
559 fn eq(&self, other: &Self) -> bool {
560 self.sort_key() == other.sort_key()
561 }
562}
563
564impl Ord for RouteEntry {
566 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
567 self.sort_key().cmp(&other.sort_key())
568 }
569}
570
571impl Eq for RouteEntry {}
573
574impl 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 pub(crate) merged: HashMap<String, Vec<RouteEntry>>,
648 pub(crate) route_changed_ifaces: Vec<String>,
649 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 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 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 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 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
871fn 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}