Skip to main content

gate4agent_node_core/
wg_status.rs

1//! Process-wide WireGuard health for `/health` and logs. No secrets.
2
3use std::sync::atomic::{AtomicI32, AtomicU64, AtomicU8, Ordering};
4
5/// 0=off 1=down 2=waiting_exit_tun 3=up 4=rebinding
6const ST_OFF: u8 = 0;
7const ST_DOWN: u8 = 1;
8const ST_WAIT: u8 = 2;
9const ST_UP: u8 = 3;
10const ST_REBIND: u8 = 4;
11
12static STATE: AtomicU8 = AtomicU8::new(ST_OFF);
13static HANDSHAKE_AGE_SECS: AtomicU64 = AtomicU64::new(u64::MAX);
14static UDP_OUT: AtomicU64 = AtomicU64::new(0);
15static UDP_IN: AtomicU64 = AtomicU64::new(0);
16static SEND_FAIL: AtomicU64 = AtomicU64::new(0);
17static LAST_ERR: AtomicI32 = AtomicI32::new(0);
18static EXIT_IF: AtomicU64 = AtomicU64::new(0);
19static REBIND_COUNT: AtomicU64 = AtomicU64::new(0);
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct WgStatus {
23    pub state: String,
24    pub exit_if_index: Option<u32>,
25    pub handshake_age_secs: Option<u64>,
26    pub udp_out: u64,
27    pub udp_in: u64,
28    pub send_fail: u64,
29    pub last_err: i32,
30    pub rebind_count: u64,
31}
32
33impl WgStatus {
34    pub fn snapshot() -> Self {
35        let state = match STATE.load(Ordering::Relaxed) {
36            ST_DOWN => "down",
37            ST_WAIT => "waiting_exit_tun",
38            ST_UP => "up",
39            ST_REBIND => "rebinding",
40            _ => "off",
41        };
42        let age = HANDSHAKE_AGE_SECS.load(Ordering::Relaxed);
43        let ifx = EXIT_IF.load(Ordering::Relaxed);
44        Self {
45            state: state.to_owned(),
46            exit_if_index: if ifx == 0 {
47                None
48            } else {
49                Some(ifx as u32)
50            },
51            handshake_age_secs: if age == u64::MAX { None } else { Some(age) },
52            udp_out: UDP_OUT.load(Ordering::Relaxed),
53            udp_in: UDP_IN.load(Ordering::Relaxed),
54            send_fail: SEND_FAIL.load(Ordering::Relaxed),
55            last_err: LAST_ERR.load(Ordering::Relaxed),
56            rebind_count: REBIND_COUNT.load(Ordering::Relaxed),
57        }
58    }
59}
60
61pub(crate) fn set_off() {
62    STATE.store(ST_OFF, Ordering::Relaxed);
63}
64
65pub(crate) fn set_down() {
66    STATE.store(ST_DOWN, Ordering::Relaxed);
67    HANDSHAKE_AGE_SECS.store(u64::MAX, Ordering::Relaxed);
68}
69
70pub(crate) fn set_waiting() {
71    STATE.store(ST_WAIT, Ordering::Relaxed);
72}
73
74pub(crate) fn set_up() {
75    STATE.store(ST_UP, Ordering::Relaxed);
76}
77
78pub(crate) fn set_rebinding() {
79    STATE.store(ST_REBIND, Ordering::Relaxed);
80}
81
82pub(crate) fn set_exit_if(if_index: u32) {
83    EXIT_IF.store(u64::from(if_index), Ordering::Relaxed);
84}
85
86pub(crate) fn bump_rebind() {
87    REBIND_COUNT.fetch_add(1, Ordering::Relaxed);
88}
89
90pub(crate) fn publish_stats(
91    udp_out: u64,
92    udp_in: u64,
93    send_fail: u64,
94    last_err: i32,
95    handshake_age_secs: Option<u64>,
96) {
97    UDP_OUT.store(udp_out, Ordering::Relaxed);
98    UDP_IN.store(udp_in, Ordering::Relaxed);
99    SEND_FAIL.store(send_fail, Ordering::Relaxed);
100    LAST_ERR.store(last_err, Ordering::Relaxed);
101    HANDSHAKE_AGE_SECS.store(handshake_age_secs.unwrap_or(u64::MAX), Ordering::Relaxed);
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn snapshot_roundtrip_states() {
110        set_down();
111        assert_eq!(WgStatus::snapshot().state, "down");
112        set_waiting();
113        assert_eq!(WgStatus::snapshot().state, "waiting_exit_tun");
114        set_up();
115        publish_stats(3, 2, 1, -5, Some(12));
116        set_exit_if(40);
117        bump_rebind();
118        let s = WgStatus::snapshot();
119        assert_eq!(s.state, "up");
120        assert_eq!(s.udp_out, 3);
121        assert_eq!(s.udp_in, 2);
122        assert_eq!(s.send_fail, 1);
123        assert_eq!(s.last_err, -5);
124        assert_eq!(s.handshake_age_secs, Some(12));
125        assert_eq!(s.exit_if_index, Some(40));
126        assert!(s.rebind_count >= 1);
127        set_off();
128    }
129}