1use std::hash::{Hash, Hasher};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 ip::{is_ipv6_addr, sanitize_ip_network, AddressFamily},
9 ErrorKind, InterfaceIpAddr, InterfaceType, NmstateError,
10};
11
12const ROUTE_RULE_DEFAULT_PRIORIRY: i64 = 30000;
13
14#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[non_exhaustive]
16#[serde(deny_unknown_fields)]
17pub struct RouteRules {
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub config: Option<Vec<RouteRuleEntry>>,
35}
36
37impl RouteRules {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn is_empty(&self) -> bool {
43 self.config.is_none()
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "kebab-case")]
49#[non_exhaustive]
50#[derive(Default)]
51pub enum RouteRuleState {
52 #[default]
54 Absent,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58#[serde(rename_all = "kebab-case")]
59#[non_exhaustive]
60#[serde(deny_unknown_fields)]
61pub struct RouteRuleEntry {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub family: Option<AddressFamily>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub state: Option<RouteRuleState>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub ip_from: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub ip_to: Option<String>,
80 #[serde(
81 skip_serializing_if = "Option::is_none",
82 default,
83 deserialize_with = "crate::deserializer::option_i64_or_string"
84 )]
85 pub priority: Option<i64>,
88 #[serde(
89 skip_serializing_if = "Option::is_none",
90 rename = "route-table",
91 default,
92 deserialize_with = "crate::deserializer::option_u32_or_string"
93 )]
94 pub table_id: Option<u32>,
97 #[serde(
98 skip_serializing_if = "Option::is_none",
99 default,
100 deserialize_with = "crate::deserializer::option_u32_or_string",
101 serialize_with = "crate::serializer::option_u32_as_hex"
102 )]
103 pub fwmark: Option<u32>,
105 #[serde(
106 skip_serializing_if = "Option::is_none",
107 default,
108 deserialize_with = "crate::deserializer::option_u32_or_string",
109 serialize_with = "crate::serializer::option_u32_as_hex"
110 )]
111 pub fwmask: Option<u32>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub action: Option<RouteRuleAction>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub iif: Option<String>,
119 #[serde(
124 skip_serializing_if = "Option::is_none",
125 alias = "suppress_prefixlength"
126 )]
127 pub suppress_prefix_length: Option<u32>,
128}
129
130impl RouteRuleEntry {
131 pub const USE_DEFAULT_PRIORITY: i64 = -1;
133 pub const USE_DEFAULT_ROUTE_TABLE: u32 = 0;
135 pub const DEFAULR_ROUTE_TABLE_ID: u32 = 254;
137
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 fn validate_ip_from_to(&self) -> Result<(), NmstateError> {
143 if self.ip_from.is_none()
144 && self.ip_to.is_none()
145 && self.family.is_none()
146 {
147 let e = NmstateError::new(
148 ErrorKind::InvalidArgument,
149 format!(
150 "Neither ip-from, ip-to nor family is defined '{self}'"
151 ),
152 );
153 log::error!("{e}");
154 return Err(e);
155 } else if let Some(family) = self.family {
156 if let Some(ip_from) = self.ip_from.as_ref() {
157 if is_ipv6_addr(ip_from.as_str())
158 != matches!(family, AddressFamily::IPv6)
159 {
160 let e = NmstateError::new(
161 ErrorKind::InvalidArgument,
162 format!(
163 "The ip-from format mismatches with the family \
164 set '{self}'"
165 ),
166 );
167 log::error!("{e}");
168 return Err(e);
169 }
170 }
171 if let Some(ip_to) = self.ip_to.as_ref() {
172 if is_ipv6_addr(ip_to.as_str())
173 != matches!(family, AddressFamily::IPv6)
174 {
175 let e = NmstateError::new(
176 ErrorKind::InvalidArgument,
177 format!(
178 "The ip-to format mismatches with the family set \
179 {self}"
180 ),
181 );
182 log::error!("{e}");
183 return Err(e);
184 }
185 }
186 }
187 Ok(())
188 }
189
190 fn validate_fwmark_and_fwmask(&self) -> Result<(), NmstateError> {
191 if self.fwmark.is_none() && self.fwmask.is_some() {
192 let e = NmstateError::new(
193 ErrorKind::InvalidArgument,
194 format!(
195 "fwmask is present but fwmark is not defined or is zero \
196 {self:?}"
197 ),
198 );
199 log::error!("{e}");
200 return Err(e);
201 }
202 Ok(())
203 }
204
205 pub(crate) fn is_absent(&self) -> bool {
206 matches!(self.state, Some(RouteRuleState::Absent))
207 }
208
209 pub(crate) fn is_ipv6(&self) -> bool {
210 self.family.as_ref() == Some(&AddressFamily::IPv6)
211 || self.ip_from.as_ref().map(|i| is_ipv6_addr(i.as_str()))
212 == Some(true)
213 || self.ip_to.as_ref().map(|i| is_ipv6_addr(i.as_str()))
214 == Some(true)
215 }
216
217 pub(crate) fn is_match(&self, other: &Self) -> bool {
218 if let Some(ip_from) = self.ip_from.as_deref() {
219 if !ip_from.is_empty() {
220 let ip_from = if !ip_from.contains('/') {
221 match InterfaceIpAddr::try_from(ip_from) {
222 Ok(i) => i.to_string(),
223 Err(e) => {
224 log::error!("{e}");
225 return false;
226 }
227 }
228 } else {
229 ip_from.to_string()
230 };
231 if other.ip_from != Some(ip_from) {
232 return false;
233 }
234 } else if other.ip_from.as_deref().map(|s| s.is_empty())
235 == Some(false)
236 {
237 return false;
240 }
241 }
242 if let Some(ip_to) = self.ip_to.as_deref() {
243 if !ip_to.is_empty() {
244 let ip_to = if !ip_to.contains('/') {
245 match InterfaceIpAddr::try_from(ip_to) {
246 Ok(ref i) => i.to_string(),
247 Err(e) => {
248 log::error!("{e}");
249 return false;
250 }
251 }
252 } else {
253 ip_to.to_string()
254 };
255 if other.ip_to != Some(ip_to) {
256 return false;
257 }
258 } else if other.ip_to.as_deref().map(|s| s.is_empty())
259 == Some(false)
260 {
261 return false;
264 }
265 }
266 if self.family.is_some()
267 && other.family.is_some()
268 && self.family != other.family
269 {
270 return false;
271 }
272
273 if self.priority.is_some()
274 && self.priority != Some(RouteRuleEntry::USE_DEFAULT_PRIORITY)
275 && self.priority != other.priority
276 && !(self.priority == Some(0) && other.priority.is_none())
277 {
278 return false;
279 }
280 if self.table_id.is_some()
281 && self.table_id != Some(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE)
282 && self.table_id != other.table_id
283 {
284 return false;
285 }
286 if self.fwmark.is_some()
287 && self.fwmark.unwrap_or(0) != other.fwmark.unwrap_or(0)
288 {
289 return false;
290 }
291 if self.fwmask.is_some()
292 && self.fwmask.unwrap_or(0) != other.fwmask.unwrap_or(0)
293 {
294 return false;
295 }
296 if self.iif.is_some() && self.iif != other.iif {
297 return false;
298 }
299 if self.action.is_some() && self.action != other.action {
300 return false;
301 }
302 if self.suppress_prefix_length.is_some()
303 && self.suppress_prefix_length != other.suppress_prefix_length
304 {
305 return false;
306 }
307 true
308 }
309
310 fn sort_key(
313 &self,
314 ) -> (bool, bool, u32, &str, &str, i64, u32, u32, u8, u32) {
315 (
316 !matches!(self.state, Some(RouteRuleState::Absent)),
317 {
318 if let Some(ip_from) = self.ip_from.as_ref() {
319 !is_ipv6_addr(ip_from.as_str())
320 } else if let Some(ip_to) = self.ip_to.as_ref() {
321 !is_ipv6_addr(ip_to.as_str())
322 } else if let Some(family) = self.family.as_ref() {
323 *family == AddressFamily::IPv4
324 } else {
325 log::warn!(
326 "Neither ip-from, ip-to nor family is defined, \
327 treating it a IPv4 route rule"
328 );
329 true
330 }
331 },
332 self.table_id
333 .unwrap_or(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE),
334 self.ip_from.as_deref().unwrap_or(""),
335 self.ip_to.as_deref().unwrap_or(""),
336 self.priority
337 .unwrap_or(RouteRuleEntry::USE_DEFAULT_PRIORITY),
338 self.fwmark.unwrap_or(0),
339 self.fwmask.unwrap_or(0),
340 self.action.map(u8::from).unwrap_or(0),
341 self.suppress_prefix_length.unwrap_or_default(),
342 )
343 }
344
345 pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
346 if let Some(ip) = self.ip_from.as_ref() {
347 if ip.is_empty() {
348 self.ip_from = None;
349 } else {
350 let new_ip = sanitize_ip_network(ip)?;
351 if self.family.is_none() {
352 match is_ipv6_addr(new_ip.as_str()) {
353 true => self.family = Some(AddressFamily::IPv6),
354 false => self.family = Some(AddressFamily::IPv4),
355 };
356 }
357 if ip != &new_ip {
358 log::warn!("Route rule ip-from {ip} sanitized to {new_ip}");
359 self.ip_from = Some(new_ip);
360 }
361 }
362 }
363 if let Some(ip) = self.ip_to.as_ref() {
364 if ip.is_empty() {
365 self.ip_to = None;
366 } else {
367 let new_ip = sanitize_ip_network(ip)?;
368 if self.family.is_none() {
369 match is_ipv6_addr(new_ip.as_str()) {
370 true => self.family = Some(AddressFamily::IPv6),
371 false => self.family = Some(AddressFamily::IPv4),
372 };
373 }
374 if ip != &new_ip {
375 log::warn!("Route rule ip-to {ip} sanitized to {new_ip}");
376 self.ip_to = Some(new_ip);
377 }
378 }
379 }
380 self.validate_ip_from_to()?;
381 self.validate_fwmark_and_fwmask()?;
382
383 if self.action.is_none() && self.table_id.is_none() {
384 log::info!(
385 "Route rule {self} has no action or route-table defined, \
386 using default route table 254"
387 );
388 self.table_id = Some(RouteRuleEntry::DEFAULR_ROUTE_TABLE_ID);
389 }
390
391 Ok(())
392 }
393}
394
395impl PartialEq for RouteRuleEntry {
397 fn eq(&self, other: &Self) -> bool {
398 self.sort_key() == other.sort_key()
399 }
400}
401
402impl Ord for RouteRuleEntry {
404 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
405 self.sort_key().cmp(&other.sort_key())
406 }
407}
408
409impl Eq for RouteRuleEntry {}
411
412impl PartialOrd for RouteRuleEntry {
414 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
415 Some(self.cmp(other))
416 }
417}
418
419impl Hash for RouteRuleEntry {
420 fn hash<H: Hasher>(&self, state: &mut H) {
421 self.sort_key().hash(state);
422 }
423}
424
425impl std::fmt::Display for RouteRuleEntry {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 let mut props = Vec::new();
428 if self.is_absent() {
429 props.push("state: absent".to_string());
430 }
431 if let Some(v) = self.family.as_ref() {
432 props.push(format!("family: {v}"));
433 }
434 if let Some(v) = self.ip_from.as_ref() {
435 props.push(format!("ip-from: {v}"));
436 }
437 if let Some(v) = self.ip_to.as_ref() {
438 props.push(format!("ip-to: {v}"));
439 }
440 if let Some(v) = self.priority.as_ref() {
441 props.push(format!("priority: {v}"));
442 }
443 if let Some(v) = self.table_id.as_ref() {
444 props.push(format!("route-table: {v}"));
445 }
446 if let Some(v) = self.fwmask.as_ref() {
447 props.push(format!("fwmask: {v}"));
448 }
449 if let Some(v) = self.fwmark.as_ref() {
450 props.push(format!("fwmark: {v}"));
451 }
452 if let Some(v) = self.iif.as_ref() {
453 props.push(format!("iif: {v}"));
454 }
455 if let Some(v) = self.action.as_ref() {
456 props.push(format!("action: {v}"));
457 }
458 if let Some(v) = self.suppress_prefix_length.as_ref() {
459 props.push(format!("suppress-prefix-length: {v}"));
460 }
461 write!(f, "{}", props.join(" "))
462 }
463}
464
465#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
466#[serde(rename_all = "kebab-case")]
467#[non_exhaustive]
468#[serde(deny_unknown_fields)]
469pub enum RouteRuleAction {
470 Blackhole,
471 Unreachable,
472 Prohibit,
473}
474
475impl std::fmt::Display for RouteRuleAction {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 write!(
478 f,
479 "{}",
480 match self {
481 Self::Blackhole => "blackhole",
482 Self::Unreachable => "unreachable",
483 Self::Prohibit => "prohibit",
484 }
485 )
486 }
487}
488
489const FR_ACT_BLACKHOLE: u8 = 6;
490const FR_ACT_UNREACHABLE: u8 = 7;
491const FR_ACT_PROHIBIT: u8 = 8;
492
493impl From<RouteRuleAction> for u8 {
494 fn from(v: RouteRuleAction) -> u8 {
495 match v {
496 RouteRuleAction::Blackhole => FR_ACT_BLACKHOLE,
497 RouteRuleAction::Unreachable => FR_ACT_UNREACHABLE,
498 RouteRuleAction::Prohibit => FR_ACT_PROHIBIT,
499 }
500 }
501}
502
503#[derive(Clone, Debug, Default, PartialEq, Eq)]
504pub(crate) struct MergedRouteRules {
505 pub(crate) desired: RouteRules,
506 pub(crate) current: RouteRules,
507 pub(crate) for_apply: Vec<RouteRuleEntry>,
511 pub(crate) for_verify: Vec<RouteRuleEntry>,
514}
515
516impl MergedRouteRules {
517 pub(crate) fn new(
518 desired: RouteRules,
519 current: RouteRules,
520 ) -> Result<Self, NmstateError> {
521 let mut for_apply: Vec<RouteRuleEntry> = Vec::new();
522 let mut merged_rules: Vec<RouteRuleEntry> = Vec::new();
523
524 let mut des_absent_rules: Vec<&RouteRuleEntry> = Vec::new();
525 if let Some(rules) = desired.config.as_ref() {
526 for rule in rules.as_slice().iter() {
527 if !rule.is_absent() {
528 let mut new_rule = rule.clone();
529 new_rule.sanitize()?;
530 for_apply.push(new_rule);
531 } else {
532 des_absent_rules.push(rule);
533 }
534 }
535 }
536
537 if let Some(cur_rules) = current.config.as_ref() {
538 for rule in cur_rules {
539 if des_absent_rules
540 .as_slice()
541 .iter()
542 .any(|absent_rule| absent_rule.is_match(rule))
543 {
544 let mut new_rule = rule.clone();
545 new_rule.state = Some(RouteRuleState::Absent);
546 new_rule.sanitize()?;
547 for_apply.push(new_rule);
548 } else {
549 let mut new_rule = rule.clone();
550 new_rule.sanitize()?;
551 merged_rules.push(new_rule);
552 }
553 }
554 }
555
556 for rule in for_apply.iter().filter(|rule| !rule.is_absent()) {
557 if !merged_rules.iter().any(|mer_rule| rule.is_match(mer_rule)) {
558 merged_rules.push(rule.clone());
559 }
560 }
561
562 let for_verify = for_apply.clone();
563
564 set_auto_priority(for_apply.as_mut_slice(), merged_rules.as_slice());
565
566 Ok(Self {
567 desired,
568 current,
569 for_apply,
570 for_verify,
571 })
572 }
573
574 pub(crate) fn remove_rules_to_ignored_ifaces(
575 &mut self,
576 ignored_ifaces: &[(String, InterfaceType)],
577 ) {
578 let ignored_ifaces: Vec<&str> = ignored_ifaces
579 .iter()
580 .filter(|(_, t)| !t.is_userspace())
581 .map(|(n, _)| n.as_str())
582 .collect();
583
584 self.for_apply.retain(|rule| {
585 if let Some(iif) = rule.iif.as_ref() {
586 !ignored_ifaces.contains(&iif.as_str())
587 } else {
588 true
589 }
590 })
591 }
592
593 pub(crate) fn is_changed(&self) -> bool {
594 (!self.desired.is_empty())
595 && (self.for_apply
596 != self.current.config.clone().unwrap_or_default())
597 }
598}
599
600fn set_auto_priority(
605 for_apply: &mut [RouteRuleEntry],
606 merged: &[RouteRuleEntry],
607) {
608 let mut max_priority = get_max_rule_priority(merged);
609 if max_priority < ROUTE_RULE_DEFAULT_PRIORIRY - 1 {
610 max_priority = ROUTE_RULE_DEFAULT_PRIORIRY - 1;
611 }
612
613 for rule in for_apply.iter_mut().filter(|r| {
614 !r.is_absent()
615 && (r.priority.is_none()
616 || r.priority == Some(RouteRuleEntry::USE_DEFAULT_PRIORITY))
617 }) {
618 let cur_prio =
619 merged.iter().find_map(|cur_rule| match cur_rule.priority {
620 Some(RouteRuleEntry::USE_DEFAULT_PRIORITY) => None,
621 Some(n) => rule.is_match(cur_rule).then_some(n),
622 None => None,
623 });
624 if cur_prio.is_some() {
625 rule.priority = cur_prio;
626 } else {
627 max_priority += 1;
628 rule.priority = Some(max_priority);
629 }
630 }
631}
632
633fn get_max_rule_priority(rules: &[RouteRuleEntry]) -> i64 {
634 rules
635 .iter()
636 .map(|r| r.priority.unwrap_or_default())
637 .max()
638 .unwrap_or_default()
639}