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 ip::{is_ipv6_addr, sanitize_ip_network},
12 ErrorKind, InterfaceType, MergedInterfaces, NmstateError,
13};
14
15const DEFAULT_TABLE_ID: u32 = 254; const LOOPBACK_IFACE_NAME: &str = "lo";
17
18#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[non_exhaustive]
20#[serde(deny_unknown_fields)]
21pub struct Routes {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub running: Option<Vec<RouteEntry>>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub config: Option<Vec<RouteEntry>>,
58}
59
60impl Routes {
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 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 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 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)]
127pub struct RouteEntry {
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub state: Option<RouteState>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub destination: Option<String>,
136 #[serde(
137 skip_serializing_if = "Option::is_none",
138 rename = "next-hop-interface"
139 )]
140 pub next_hop_iface: Option<String>,
145 #[serde(
146 skip_serializing_if = "Option::is_none",
147 rename = "next-hop-address"
148 )]
149 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 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 pub table_id: Option<u32>,
170
171 #[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 #[serde(skip_serializing_if = "Option::is_none")]
182 pub route_type: Option<RouteType>,
183 #[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 pub source: Option<String>,
194 #[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 #[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 #[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 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 fn sort_key(&self) -> (Vec<bool>, Vec<&str>, Vec<u32>) {
321 (
322 vec![
323 !matches!(self.state, Some(RouteState::Absent)),
325 !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
450impl PartialEq for RouteEntry {
452 fn eq(&self, other: &Self) -> bool {
453 self.sort_key() == other.sort_key()
454 }
455}
456
457impl Ord for RouteEntry {
459 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
460 self.sort_key().cmp(&other.sort_key())
461 }
462}
463
464impl Eq for RouteEntry {}
466
467impl 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 pub(crate) merged: HashMap<String, Vec<RouteEntry>>,
532 pub(crate) route_changed_ifaces: Vec<String>,
533 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 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 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 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 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
749fn 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}