1use 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; const LOOPBACK_IFACE_NAME: &str = "lo";
19
20#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
21#[non_exhaustive]
22#[serde(deny_unknown_fields)]
23pub struct Routes {
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub running: Option<Vec<RouteEntry>>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub config: Option<Vec<RouteEntry>>,
60}
61
62impl Routes {
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 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 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 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 #[default]
195 Absent,
196 Ignore,
198}
199
200#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201#[serde(rename_all = "kebab-case")]
202#[non_exhaustive]
203#[serde(deny_unknown_fields)]
204pub struct RouteEntry {
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub state: Option<RouteState>,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub destination: Option<String>,
213 #[serde(
214 skip_serializing_if = "Option::is_none",
215 rename = "next-hop-interface"
216 )]
217 pub next_hop_iface: Option<String>,
222 #[serde(
223 skip_serializing_if = "Option::is_none",
224 rename = "next-hop-address"
225 )]
226 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 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 pub table_id: Option<u32>,
247
248 #[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 #[serde(skip_serializing_if = "Option::is_none")]
259 pub route_type: Option<RouteType>,
260 #[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 pub source: Option<String>,
271 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 fn sort_key(&self) -> (Vec<bool>, Vec<&str>, Vec<u32>) {
438 (
439 vec![
440 !matches!(self.state, Some(RouteState::Absent)),
442 !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
569impl PartialEq for RouteEntry {
571 fn eq(&self, other: &Self) -> bool {
572 self.sort_key() == other.sort_key()
573 }
574}
575
576impl Ord for RouteEntry {
578 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
579 self.sort_key().cmp(&other.sort_key())
580 }
581}
582
583impl Eq for RouteEntry {}
585
586impl 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 pub(crate) merged: HashMap<String, Vec<RouteEntry>>,
663 pub(crate) route_changed_ifaces: Vec<String>,
664 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 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 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 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 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
886fn 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}