1#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct DefaultRoute {
6 pub if_index: u32,
7 pub metric: u32,
8 pub tun: bool,
9 pub alias: String,
10 pub desc: String,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum Placement {
15 Open,
16 InsideExisting { if_index: u32, alias: String, desc: String },
17 WaitExitTun,
18}
19
20pub fn nested_locked(env_nested: bool, routes: &[DefaultRoute], ever_nested: bool) -> bool {
21 env_nested || ever_nested || routes.iter().any(|r| r.tun)
22}
23
24pub fn classify(routes: &[DefaultRoute], nested: bool) -> Placement {
25 let best = routes.iter().min_by_key(|route| route.metric);
26 match best {
27 Some(best) if best.tun => Placement::InsideExisting {
28 if_index: best.if_index,
29 alias: best.alias.clone(),
30 desc: best.desc.clone(),
31 },
32 Some(_) | None if nested => Placement::WaitExitTun,
33 Some(_) | None => Placement::Open,
34 }
35}
36
37pub fn opens_adapter(placement: &Placement) -> bool {
38 matches!(placement, Placement::Open)
39}
40
41pub fn parse_route_lines(text: &str) -> Vec<DefaultRoute> {
42 text.lines()
43 .filter_map(|line| {
44 let line = line.trim();
45 if line.is_empty() {
46 return None;
47 }
48 let (head, desc) = match line.split_once('\t') {
49 Some((h, d)) => (h, d.trim().to_owned()),
50 None => (line, "-".to_owned()),
51 };
52 let mut parts = head.split_whitespace();
53 let if_index = parts.next()?.parse().ok()?;
54 let metric = parts.next()?.parse().ok()?;
55 let tun = parts.next()? == "1";
56 let alias = {
57 let rest: Vec<&str> = parts.collect();
58 if rest.is_empty() {
59 "-".to_owned()
60 } else {
61 rest.join(" ")
62 }
63 };
64 Some(DefaultRoute {
65 if_index,
66 metric,
67 tun,
68 alias,
69 desc: if desc.is_empty() { "-".to_owned() } else { desc },
70 })
71 })
72 .collect()
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 fn route(if_index: u32, metric: u32, tun: bool, alias: &str) -> DefaultRoute {
80 DefaultRoute {
81 if_index,
82 metric,
83 tun,
84 alias: alias.to_owned(),
85 desc: "-".to_owned(),
86 }
87 }
88
89 #[test]
90 fn classify_opens_when_not_nested_and_default_is_not_tun() {
91 assert_eq!(classify(&[route(5, 25, false, "eth0")], false), Placement::Open);
92 }
93
94 #[test]
95 fn classify_waits_when_nested_and_default_is_not_tun() {
96 assert_eq!(classify(&[route(5, 25, false, "eth0")], true), Placement::WaitExitTun);
97 assert!(nested_locked(false, &[route(5, 25, false, "eth0")], true));
98 }
99
100 #[test]
101 fn classify_uses_lowest_metric_tun() {
102 let routes = [route(12, 256, false, "eth0"), route(40, 0, true, "tun0")];
103 assert_eq!(
104 classify(&routes, false),
105 Placement::InsideExisting {
106 if_index: 40,
107 alias: "tun0".to_owned(),
108 desc: "-".to_owned(),
109 }
110 );
111 assert!(nested_locked(false, &routes, false));
112 }
113
114 #[test]
115 fn nested_locked_by_env_or_any_tun() {
116 assert!(nested_locked(true, &[], false));
117 assert!(!nested_locked(false, &[route(1, 1, false, "eth")], false));
118 assert!(nested_locked(false, &[route(1, 1, true, "tun")], false));
119 }
120
121 #[test]
122 fn opens_adapter_only_for_open() {
123 assert!(opens_adapter(&Placement::Open));
124 assert!(!opens_adapter(&Placement::WaitExitTun));
125 assert!(!opens_adapter(&Placement::InsideExisting {
126 if_index: 1,
127 alias: "t".into(),
128 desc: "d".into(),
129 }));
130 }
131
132 #[test]
133 fn route_lines_parse_alias_and_desc() {
134 let routes = parse_route_lines("15 0 1 nic 2\tWireGuard Tunnel\n9 256 0 nic0\n");
135 assert_eq!(routes[0].alias, "nic 2");
136 assert_eq!(routes[0].desc, "WireGuard Tunnel");
137 assert!(routes[0].tun);
138 assert!(!routes[1].tun);
139 assert_eq!(routes[1].desc, "-");
140 }
141}