lab_ops_natmap/
policy_route.rs1use std::process::Command;
2
3use color_eyre::Result;
4use color_eyre::eyre::WrapErr;
5
6use crate::models::PolicyRouteConfig;
7
8pub struct PolicyRouteManager;
9
10impl PolicyRouteManager {
11 pub fn new() -> Self {
12 Self
13 }
14
15 fn check_rule_exists(&self, config: &PolicyRouteConfig) -> Result<bool> {
16 let output = Command::new("ip")
17 .args(["rule", "show"])
18 .output()
19 .wrap_err("Failed to execute ip rule show")?;
20
21 let stdout = String::from_utf8_lossy(&output.stdout);
22 let expected = format!("from {} lookup {}", config.src_ip, config.table);
24 Ok(stdout.contains(&expected))
25 }
26
27 fn check_route_exists(&self, config: &PolicyRouteConfig) -> Result<bool> {
28 let output = Command::new("ip")
29 .args(["route", "show", "table", &config.table.to_string()])
30 .output()
31 .wrap_err("Failed to execute ip route show")?;
32
33 let stdout = String::from_utf8_lossy(&output.stdout);
34 let expected = format!("default via {}", config.via);
35 Ok(stdout.contains(&expected))
36 }
37
38 fn route_line_in_table(&self, route_line: &str, table: u32) -> Result<bool> {
40 let output = Command::new("ip")
41 .args(["route", "show", "table", &table.to_string()])
42 .output()?;
43 let stdout = String::from_utf8_lossy(&output.stdout);
44 Ok(stdout.lines().any(|l| l.trim() == route_line.trim()))
45 }
46
47 fn get_cloneable_routes(&self) -> Result<Vec<String>> {
51 let output = Command::new("ip")
52 .args(["route", "show", "table", "main"])
53 .output()?;
54 let routes = String::from_utf8_lossy(&output.stdout)
55 .lines()
56 .filter(|l| {
57 let t = l.trim();
58 !t.is_empty()
59 && !t.starts_with("default ")
60 && !t.starts_with("broadcast ")
61 && !t.starts_with("local ")
62 && !t.starts_with("unreachable ")
63 && !t.starts_with("fe80::")
64 && !t.starts_with("ff00::")
65 })
66 .map(|l| l.trim().to_string())
67 .filter(|l| !l.is_empty())
68 .collect();
69 Ok(routes)
70 }
71
72 pub fn install(&self, config: &PolicyRouteConfig) -> Result<()> {
73 if !self.check_route_exists(config)? {
74 let status = Command::new("ip")
75 .args([
76 "route",
77 "add",
78 "default",
79 "via",
80 &config.via,
81 "table",
82 &config.table.to_string(),
83 ])
84 .status()
85 .wrap_err("Failed to execute ip route add")?;
86
87 if !status.success() {
88 color_eyre::eyre::bail!("ip route add failed with status: {}", status);
89 }
90 }
91
92 if !self.check_rule_exists(config)? {
93 let status = Command::new("ip")
94 .args([
95 "rule",
96 "add",
97 "from",
98 &config.src_ip,
99 "table",
100 &config.table.to_string(),
101 ])
102 .status()
103 .wrap_err("Failed to execute ip rule add")?;
104
105 if !status.success() {
106 color_eyre::eyre::bail!("ip rule add failed with status: {}", status);
107 }
108 }
109
110 let table = config.table;
114 for route_line in self.get_cloneable_routes()? {
115 if !self.route_line_in_table(&route_line, table)? {
116 let status = Command::new("sh")
117 .args(["-c", &format!("ip route add {route_line} table {table}")])
118 .status()
119 .wrap_err("Failed to execute ip route add")?;
120 if !status.success() {
121 tracing::warn!("failed to clone route to table {table}: {route_line}");
122 }
123 }
124 }
125
126 Ok(())
127 }
128
129 pub fn remove(&self, config: &PolicyRouteConfig) -> Result<()> {
130 let _ = Command::new("ip")
132 .args([
133 "rule",
134 "del",
135 "from",
136 &config.src_ip,
137 "table",
138 &config.table.to_string(),
139 ])
140 .status();
141
142 let _ = Command::new("ip")
143 .args([
144 "route",
145 "del",
146 "default",
147 "via",
148 &config.via,
149 "table",
150 &config.table.to_string(),
151 ])
152 .status();
153
154 Ok(())
155 }
156
157 pub fn flush_all(&self, policy_routes: &[PolicyRouteConfig]) -> Result<()> {
158 for config in policy_routes {
159 self.remove(config)?;
160 }
161 Ok(())
162 }
163}