Skip to main content

gate4agent_node_core/
kit_wireguard_placement.rs

1//! Default-route classification for Windows WireGuard placement.
2//! Pure logic, tested on every target.
3
4#[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}