Skip to main content

netget/client/wireguard/
mod.rs

1//! WireGuard VPN client implementation
2pub mod actions;
3
4pub use actions::WireguardClientProtocol;
5
6use anyhow::{Context, Result};
7use defguard_wireguard_rs::{host::Peer, key::Key, InterfaceConfiguration, WireguardInterfaceApi};
8use std::collections::HashMap;
9use std::net::{IpAddr, Ipv4Addr, SocketAddr};
10use std::sync::{Arc, LazyLock};
11use std::time::Duration;
12use tokio::sync::{mpsc, RwLock};
13use tokio::time;
14use tracing::{debug, error, info, warn};
15
16// Platform-specific WGApi type
17#[cfg(target_os = "macos")]
18type WGApi = defguard_wireguard_rs::WGApi<defguard_wireguard_rs::Userspace>;
19
20#[cfg(not(target_os = "macos"))]
21type WGApi = defguard_wireguard_rs::WGApi<defguard_wireguard_rs::Kernel>;
22
23use crate::client::llm_budget::call_llm_for_client;
24use crate::client::wireguard::actions::{
25    WIREGUARD_CLIENT_CONNECTED_EVENT, WIREGUARD_CLIENT_DISCONNECTED_EVENT,
26};
27use crate::llm::ollama_client::OllamaClient;
28use crate::protocol::Event;
29use crate::state::app_state::AppState;
30use crate::state::{ClientId, ClientStatus};
31
32/// Commands that can be sent to WireGuard client
33#[derive(Debug)]
34pub enum WireguardCommand {
35    GetStatus(tokio::sync::oneshot::Sender<Result<serde_json::Value>>),
36    Disconnect,
37}
38
39/// Global storage for WireGuard client command channels
40static WIREGUARD_CLIENTS: LazyLock<
41    Arc<RwLock<HashMap<ClientId, mpsc::UnboundedSender<WireguardCommand>>>>,
42> = LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
43
44/// WireGuard client startup parameters
45#[derive(Debug, Clone)]
46pub struct WireguardClientParams {
47    /// Server's public key (base64 encoded)
48    pub server_public_key: String,
49    /// Server endpoint (IP:port)
50    pub server_endpoint: String,
51    /// Client's VPN IP address (e.g., "10.20.30.2/32")
52    pub client_address: String,
53    /// IPs to route through VPN (e.g., ["0.0.0.0/0"] for all traffic)
54    pub allowed_ips: Vec<String>,
55    /// Keepalive interval in seconds (0 to disable)
56    pub keepalive: Option<u16>,
57    /// Private key (optional - will be generated if not provided)
58    pub private_key: Option<String>,
59}
60
61/// What applying one action to the WireGuard interface actually did.
62///
63/// WireGuard never yields [`crate::state::client_handles::ClientSendOutcome::Sent`]:
64/// NetGet implements none of the WireGuard protocol and writes no packets — it
65/// orchestrates `defguard_wireguard_rs`, which drives the kernel module (or, on
66/// macOS, an external `wireguard-go`). Every verb this client has reads interface
67/// state or tears the interface down, so `Executed` with a specific reading is the
68/// only truthful answer.
69pub enum WireguardApplied {
70    /// The action ran; the string describes what it read or did.
71    Executed(String),
72    /// The interface was torn down.
73    Disconnected,
74}
75
76/// WireGuard client
77pub struct WireguardClient {
78    interface_name: String,
79    wgapi: Arc<RwLock<WGApi>>,
80    _private_key: String,
81    public_key: String,
82    server_public_key: Key,
83    server_public_key_str: String,
84    server_endpoint: String,
85    client_address: String,
86    allowed_ips: Vec<String>,
87}
88
89impl WireguardClient {
90    /// Connect to a WireGuard VPN server with LLM integration
91    pub async fn connect_with_llm_actions(
92        remote_addr: String, // Format: "server_ip:port"
93        llm_client: OllamaClient,
94        app_state: Arc<AppState>,
95        status_tx: mpsc::UnboundedSender<String>,
96        client_id: ClientId,
97        params: WireguardClientParams,
98    ) -> Result<SocketAddr> {
99        info!(
100            "WireGuard client {} connecting to server {} (endpoint: {})",
101            client_id, remote_addr, params.server_endpoint
102        );
103
104        // Generate or use provided keypair
105        let private_key = if let Some(key) = params.private_key {
106            Key::decode(&key).context("Failed to decode private key")?
107        } else {
108            Key::generate()
109        };
110        let public_key = private_key.public_key();
111
112        let private_key_b64 = private_key.to_string();
113        let public_key_b64 = public_key.to_string();
114
115        info!("WireGuard client public key: {}", public_key_b64);
116        let _ = status_tx.send(format!(
117            "[CLIENT] WireGuard client {} public key: {}",
118            client_id, public_key_b64
119        ));
120
121        // Platform-specific interface naming
122        #[cfg(target_os = "macos")]
123        let interface_name = "utun20".to_string();
124
125        #[cfg(not(target_os = "macos"))]
126        let interface_name = format!("netget_wg_client{}", client_id);
127
128        // Create WireGuard interface
129        let ifname = interface_name.clone();
130
131        let wgapi = WGApi::new(ifname)
132            .context("Failed to create WireGuard interface (requires elevated privileges)")?;
133
134        info!("Created WireGuard interface: {}", interface_name);
135        let _ = status_tx.send(format!("[CLIENT] Created interface: {}", interface_name));
136
137        // Parse client address (e.g., "10.20.30.2/32")
138        use defguard_wireguard_rs::net::IpAddrMask;
139        let client_addr_mask: IpAddrMask = params
140            .client_address
141            .parse()
142            .context("Invalid client address")?;
143
144        // Parse server endpoint
145        let server_endpoint_addr: SocketAddr = params
146            .server_endpoint
147            .parse()
148            .context("Invalid server endpoint")?;
149
150        // Parse server public key
151        let server_key =
152            Key::decode(&params.server_public_key).context("Invalid server public key")?;
153
154        // Parse listen port from remote_addr (use client_id as offset from base port)
155        let listen_port = 51820u16.wrapping_add((client_id.as_u32() % 1000) as u16);
156
157        // Configure interface
158        let config = InterfaceConfiguration {
159            name: interface_name.clone(),
160            prvkey: private_key_b64.clone(),
161            addresses: vec![client_addr_mask],
162            port: listen_port as u32,
163            peers: vec![Peer {
164                public_key: server_key.clone(),
165                preshared_key: None,
166                protocol_version: None,
167                endpoint: Some(server_endpoint_addr),
168                last_handshake: None,
169                tx_bytes: 0,
170                rx_bytes: 0,
171                persistent_keepalive_interval: params.keepalive,
172                allowed_ips: params
173                    .allowed_ips
174                    .iter()
175                    .map(|ip| ip.parse())
176                    .collect::<Result<Vec<_>, _>>()
177                    .context("Invalid allowed IPs")?,
178            }],
179            #[cfg(target_family = "unix")]
180            mtu: None,
181        };
182
183        wgapi
184            .configure_interface(&config)
185            .context("Failed to configure WireGuard interface")?;
186
187        info!("WireGuard interface configured");
188        let _ = status_tx.send(format!(
189            "[CLIENT] Interface configured: {} → {}",
190            interface_name, params.server_endpoint
191        ));
192
193        // Update client state
194        app_state
195            .update_client_status(client_id, ClientStatus::Connected)
196            .await;
197        let _ = status_tx.send(format!("[CLIENT] WireGuard client {} connected", client_id));
198        let _ = status_tx.send("__UPDATE_UI__".to_string());
199
200        // Wrap wgapi in Arc<RwLock>
201        let wgapi_arc = Arc::new(RwLock::new(wgapi));
202
203        // Create client instance
204        let client = Arc::new(WireguardClient {
205            interface_name: interface_name.clone(),
206            wgapi: wgapi_arc.clone(),
207            _private_key: private_key_b64,
208            public_key: public_key_b64.clone(),
209            server_public_key: server_key.clone(),
210            server_public_key_str: params.server_public_key.clone(),
211            server_endpoint: params.server_endpoint.clone(),
212            client_address: params.client_address.clone(),
213            allowed_ips: params.allowed_ips.clone(),
214        });
215
216        // Create command channel
217        let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel();
218
219        // Store command channel in global map
220        WIREGUARD_CLIENTS.write().await.insert(client_id, cmd_tx);
221
222        // Command channel for injected actions (the dashboard's [ send ]).
223        // Registered BEFORE the connected-event LLM call below: a dashboard-created
224        // client defaults to a `*` -> manual rule, so that call can park for minutes
225        // waiting for a human, and the operator must be able to query or tear down
226        // the tunnel while it waits.
227        let command_rx =
228            crate::client::command_support::register_command_channel(&app_state, client_id).await;
229        let cmd_task = tokio::spawn({
230            let client = client.clone();
231            let app_state = app_state.clone();
232            let status_tx = status_tx.clone();
233            async move {
234                Self::command_loop(command_rx, client, client_id, app_state, status_tx).await;
235            }
236        });
237        app_state.register_client_task(client_id, cmd_task).await;
238
239        // Spawn monitoring loop
240        let app_state_for_cleanup = app_state.clone();
241        let app_state_clone = app_state.clone();
242        let status_tx_clone = status_tx.clone();
243        let llm_client_clone = llm_client.clone();
244        let wgapi_monitor = wgapi_arc.clone();
245        let server_key_clone = server_key.clone();
246        let client_clone = client.clone();
247
248        // Registered with AppState so stop_client can abort this task —
249        // dropping a JoinHandle only detaches it in Tokio.
250        let task_registrar = app_state.clone();
251        let task_handle = tokio::spawn(async move {
252            Self::monitoring_loop(
253                client_id,
254                client_clone,
255                wgapi_monitor,
256                server_key_clone,
257                &mut cmd_rx,
258                app_state_clone,
259                status_tx_clone,
260                llm_client_clone,
261            )
262            .await;
263
264            // Clean up on exit. The command handle goes too: the monitoring loop is
265            // what tracks liveness, so once it is gone the dashboard must stop
266            // offering [ send ].
267            WIREGUARD_CLIENTS.write().await.remove(&client_id);
268            app_state_for_cleanup.remove_client_handle(client_id).await;
269        });
270        task_registrar
271            .register_client_task(client_id, task_handle)
272            .await;
273
274        // Call LLM with connected event
275        let event = Event::new(
276            &WIREGUARD_CLIENT_CONNECTED_EVENT,
277            serde_json::json!({
278                "server_endpoint": params.server_endpoint,
279                "client_public_key": public_key_b64,
280                "client_address": params.client_address,
281            }),
282        );
283
284        // Get client instruction and memory
285        let instruction = app_state
286            .get_instruction_for_client(client_id)
287            .await
288            .unwrap_or_default();
289        let memory = app_state
290            .get_memory_for_client(client_id)
291            .await
292            .unwrap_or_default();
293
294        // Create protocol instance for LLM call
295        let protocol = Arc::new(crate::client::wireguard::WireguardClientProtocol::new());
296
297        // Call LLM
298        match call_llm_for_client(
299            &llm_client,
300            &app_state,
301            client_id.to_string(),
302            &instruction,
303            &memory,
304            Some(&event),
305            protocol.as_ref(),
306            &status_tx,
307        )
308        .await
309        {
310            Ok(result) => {
311                debug!("LLM response for connected event: {:?}", result);
312                // Update memory if provided
313                if let Some(new_memory) = result.memory_updates {
314                    app_state.set_memory_for_client(client_id, new_memory).await;
315                }
316                // ...and carry out what it decided. This used to log the response and
317                // execute none of it, which is the "asks the model and throws the answer
318                // away" defect: a client told to check the tunnel and hang up if it is not
319                // established did neither.
320                Self::apply_llm_actions(
321                    result.actions,
322                    &client,
323                    client_id,
324                    &app_state,
325                    &status_tx,
326                    "wireguard_connected",
327                )
328                .await;
329            }
330            Err(e) => {
331                error!("Failed to call LLM for connected event: {}", e);
332            }
333        }
334
335        // Return a dummy SocketAddr (WireGuard is UDP-based)
336        Ok(SocketAddr::new(
337            IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
338            listen_port,
339        ))
340    }
341
342    /// Drain injected commands until the channel closes (the client was removed,
343    /// which drops the handle) or an injected `disconnect` tears the tunnel down.
344    ///
345    /// The generic `command_support::handle_stream_client_command` cannot serve this
346    /// client: it owns no socket at all. Everything goes through
347    /// [`Self::apply_wireguard_action`], and the outcome is recorded and replied the
348    /// way the generic arm does it.
349    async fn command_loop(
350        mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
351        client: Arc<WireguardClient>,
352        client_id: ClientId,
353        app_state: Arc<AppState>,
354        status_tx: mpsc::UnboundedSender<String>,
355    ) {
356        use crate::llm::actions::protocol_trait::Protocol;
357        use crate::state::client_handles::ClientSendOutcome;
358        use crate::state::AccessLogOwner;
359
360        let protocol = crate::client::wireguard::WireguardClientProtocol::new();
361
362        while let Some(command) = command_rx.recv().await {
363            let action = command.action.clone();
364
365            let outcome: anyhow::Result<ClientSendOutcome> = match Self::apply_wireguard_action(
366                &client,
367                action.clone(),
368                client_id,
369                &app_state,
370                &status_tx,
371            )
372            .await
373            {
374                Ok(WireguardApplied::Executed(detail)) => {
375                    Ok(ClientSendOutcome::Executed { detail })
376                }
377                Ok(WireguardApplied::Disconnected) => Ok(ClientSendOutcome::Disconnected),
378                Err(e) => Ok(ClientSendOutcome::Rejected {
379                    error: e.to_string(),
380                }),
381            };
382
383            let outcome_json = match &outcome {
384                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
385                Err(e) => serde_json::json!({"error": e.to_string()}),
386            };
387            app_state
388                .record_access_log(
389                    AccessLogOwner::Client(client_id.as_u32()),
390                    protocol.protocol_name(),
391                    None,
392                    "injected_action",
393                    action,
394                    vec![outcome_json],
395                )
396                .await;
397
398            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
399            let _ = status_tx.send("__UPDATE_UI__".to_string());
400            crate::client::command_support::reply(command, outcome);
401
402            if disconnect {
403                break;
404            }
405        }
406
407        // Every exit path lands here: drop the command handle so the dashboard stops
408        // offering [ send ] on a tunnel that is gone.
409        app_state.remove_client_handle(client_id).await;
410        let _ = status_tx.send("__UPDATE_UI__".to_string());
411    }
412
413    /// Carry one action out against the live interface.
414    ///
415    /// `Err` means the protocol rejected the action (unknown verb); a
416    /// `read_interface_data` failure is `Ok(Executed(..))` naming the error, because
417    /// the action did run.
418    async fn apply_wireguard_action(
419        client: &Arc<WireguardClient>,
420        action: serde_json::Value,
421        client_id: ClientId,
422        app_state: &Arc<AppState>,
423        status_tx: &mpsc::UnboundedSender<String>,
424    ) -> Result<WireguardApplied> {
425        use crate::llm::actions::client_trait::{Client, ClientActionResult};
426
427        let protocol = crate::client::wireguard::WireguardClientProtocol::new();
428        match protocol.execute_action(action)? {
429            ClientActionResult::Custom { name, .. } if name == "wireguard_get_status" => {
430                match client.get_status().await {
431                    Ok(status) => Ok(WireguardApplied::Executed(format!(
432                        "get_connection_status: connected={}, tx_bytes={}, rx_bytes={}, \
433                         last_handshake={}",
434                        status["connected"],
435                        status["tx_bytes"],
436                        status["rx_bytes"],
437                        status["last_handshake"],
438                    ))),
439                    Err(e) => Ok(WireguardApplied::Executed(format!(
440                        "get_connection_status failed: {e:#}"
441                    ))),
442                }
443            }
444            ClientActionResult::Custom { name, .. } if name == "wireguard_get_info" => {
445                let info = client.get_client_info();
446                Ok(WireguardApplied::Executed(format!(
447                    "get_client_info: interface={}, public_key={}, client_address={}, \
448                     server_endpoint={}",
449                    info["interface"],
450                    info["public_key"],
451                    info["client_address"],
452                    info["server_endpoint"],
453                )))
454            }
455            ClientActionResult::Custom { name, .. } => Ok(WireguardApplied::Executed(format!(
456                "custom result '{name}' has no WireGuard handler"
457            ))),
458            ClientActionResult::Disconnect => {
459                if let Err(e) = client.disconnect().await {
460                    error!(
461                        "WireGuard client {} failed to remove interface: {}",
462                        client_id, e
463                    );
464                }
465                app_state
466                    .update_client_status(client_id, ClientStatus::Disconnected)
467                    .await;
468                // Drop the command handle here rather than only in the command loop:
469                // the monitoring loop can disconnect too, and a handle left behind
470                // would offer [ send ] into a torn-down interface.
471                app_state.remove_client_handle(client_id).await;
472                let _ = status_tx.send(format!(
473                    "[CLIENT] WireGuard client {} disconnected",
474                    client_id
475                ));
476                let _ = status_tx.send("__UPDATE_UI__".to_string());
477                Ok(WireguardApplied::Disconnected)
478            }
479            other => Ok(WireguardApplied::Executed(format!(
480                "unhandled action result {other:?}"
481            ))),
482        }
483    }
484
485    /// Run every action the model answered an event with against the live interface.
486    ///
487    /// The verbs a WireGuard client has read interface state or tear the tunnel down, so
488    /// this is short - but running it at all is the point. Both event paths used to log the
489    /// response and execute nothing, so `disconnect` in reply to `wireguard_disconnected`
490    /// left the interface up.
491    async fn apply_llm_actions(
492        actions: Vec<serde_json::Value>,
493        client: &Arc<WireguardClient>,
494        client_id: ClientId,
495        app_state: &Arc<AppState>,
496        status_tx: &mpsc::UnboundedSender<String>,
497        event_id: &str,
498    ) {
499        for action in actions {
500            let described = action
501                .get("type")
502                .and_then(|t| t.as_str())
503                .unwrap_or("?")
504                .to_string();
505            match Self::apply_wireguard_action(client, action, client_id, app_state, status_tx)
506                .await
507            {
508                Ok(WireguardApplied::Executed(detail)) => {
509                    info!(
510                        "WireGuard client {} {} -> {}: {}",
511                        client_id, event_id, described, detail
512                    );
513                }
514                Ok(WireguardApplied::Disconnected) => {
515                    info!(
516                        "WireGuard client {} {} -> {}: interface torn down",
517                        client_id, event_id, described
518                    );
519                    return;
520                }
521                Err(e) => {
522                    warn!(
523                        "WireGuard client {} could not run '{}' from {}: {}",
524                        client_id, described, event_id, e
525                    );
526                    let _ = status_tx.send(format!(
527                        "[CLIENT] WireGuard client {} rejected '{}': {}",
528                        client_id, described, e
529                    ));
530                }
531            }
532        }
533    }
534
535    /// Monitoring loop to track connection status and handle commands
536    async fn monitoring_loop(
537        client_id: ClientId,
538        client: Arc<WireguardClient>,
539        wgapi: Arc<RwLock<WGApi>>,
540        server_public_key: Key,
541        cmd_rx: &mut mpsc::UnboundedReceiver<WireguardCommand>,
542        app_state: Arc<AppState>,
543        status_tx: mpsc::UnboundedSender<String>,
544        llm_client: OllamaClient,
545    ) {
546        let mut was_connected = false;
547        let mut interval = time::interval(Duration::from_secs(5));
548        let mut should_exit = false;
549
550        loop {
551            tokio::select! {
552                _ = interval.tick() => {
553                    // Continue with status monitoring
554                }
555                Some(cmd) = cmd_rx.recv() => {
556                    match cmd {
557                        WireguardCommand::GetStatus(response_tx) => {
558                            let status = client.get_status().await;
559                            let _ = response_tx.send(status);
560                        }
561                        WireguardCommand::Disconnect => {
562                            info!("WireGuard client {} received disconnect command", client_id);
563                            if let Err(e) = client.disconnect().await {
564                                error!("Failed to disconnect WireGuard client {}: {}", client_id, e);
565                            }
566                            app_state.update_client_status(client_id, ClientStatus::Disconnected).await;
567                            let _ = status_tx.send(format!("[CLIENT] WireGuard client {} disconnected", client_id));
568                            let _ = status_tx.send("__UPDATE_UI__".to_string());
569                            should_exit = true;
570                        }
571                    }
572                }
573            }
574
575            if should_exit {
576                break;
577            }
578
579            // Check if client is still active
580            if let Some(client) = app_state.get_client(client_id).await {
581                if client.status == ClientStatus::Disconnected {
582                    info!(
583                        "WireGuard client {} stopped, exiting monitor loop",
584                        client_id
585                    );
586                    break;
587                }
588            } else {
589                // Client removed from state
590                break;
591            }
592
593            // Read interface data
594            let wgapi_lock = wgapi.read().await;
595            match wgapi_lock.read_interface_data() {
596                Ok(interface_data) => {
597                    // Check server peer status
598                    if let Some(peer) = interface_data.peers.get(&server_public_key) {
599                        let is_connected = peer.last_handshake.is_some()
600                            && peer
601                                .last_handshake
602                                .unwrap()
603                                .elapsed()
604                                .unwrap_or(Duration::from_secs(999))
605                                < Duration::from_secs(180); // 3 minutes
606
607                        if is_connected && !was_connected {
608                            info!("WireGuard client {} handshake successful", client_id);
609                            let _ = status_tx.send(format!(
610                                "[CLIENT] WireGuard client {} handshake successful",
611                                client_id
612                            ));
613
614                            was_connected = true;
615                        } else if !is_connected && was_connected {
616                            warn!("WireGuard client {} lost connection to server", client_id);
617                            let _ = status_tx.send(format!(
618                                "[CLIENT] WireGuard client {} lost connection",
619                                client_id
620                            ));
621
622                            // Trigger disconnected event
623                            let event = Event::new(
624                                &WIREGUARD_CLIENT_DISCONNECTED_EVENT,
625                                serde_json::json!({
626                                    "reason": "handshake_timeout"
627                                }),
628                            );
629
630                            let instruction = app_state
631                                .get_instruction_for_client(client_id)
632                                .await
633                                .unwrap_or_default();
634                            let memory = app_state
635                                .get_memory_for_client(client_id)
636                                .await
637                                .unwrap_or_default();
638                            let protocol =
639                                Arc::new(crate::client::wireguard::WireguardClientProtocol::new());
640
641                            match call_llm_for_client(
642                                &llm_client,
643                                &app_state,
644                                client_id.to_string(),
645                                &instruction,
646                                &memory,
647                                Some(&event),
648                                protocol.as_ref(),
649                                &status_tx,
650                            )
651                            .await
652                            {
653                                Ok(result) => {
654                                    if let Some(new_memory) = result.memory_updates {
655                                        app_state
656                                            .set_memory_for_client(client_id, new_memory)
657                                            .await;
658                                    }
659                                    // Same as the connected event: the answer is acted on,
660                                    // not just counted.
661                                    Self::apply_llm_actions(
662                                        result.actions,
663                                        &client,
664                                        client_id,
665                                        &app_state,
666                                        &status_tx,
667                                        "wireguard_disconnected",
668                                    )
669                                    .await;
670                                }
671                                Err(e) => {
672                                    error!(
673                                        "WireGuard client {} LLM call for the disconnected \
674                                         event failed: {}",
675                                        client_id, e
676                                    );
677                                }
678                            }
679
680                            was_connected = false;
681                        }
682
683                        // Log stats
684                        if is_connected {
685                            debug!(
686                                "WireGuard client {} stats: tx={} rx={} last_handshake={}",
687                                client_id,
688                                peer.tx_bytes,
689                                peer.rx_bytes,
690                                peer.last_handshake
691                                    .map(|t| format!("{:?} ago", t.elapsed().unwrap_or_default()))
692                                    .unwrap_or_else(|| "never".to_string())
693                            );
694                        }
695                    } else {
696                        if was_connected {
697                            warn!("WireGuard client {} server peer disappeared", client_id);
698                            was_connected = false;
699                        }
700                    }
701                }
702                Err(e) => {
703                    error!(
704                        "Failed to read WireGuard interface data for client {}: {}",
705                        client_id, e
706                    );
707                }
708            }
709        }
710    }
711
712    /// Disconnect the client
713    pub async fn disconnect(&self) -> Result<()> {
714        info!("Disconnecting WireGuard client");
715
716        // Remove interface (this will clean up everything)
717        let wgapi = self.wgapi.read().await;
718        wgapi
719            .remove_interface()
720            .context("Failed to remove WireGuard interface")?;
721
722        Ok(())
723    }
724
725    /// Get the client's public key and configuration info
726    pub fn get_client_info(&self) -> serde_json::Value {
727        serde_json::json!({
728            "interface": self.interface_name,
729            "public_key": self.public_key,
730            "private_key": "<redacted>",
731            "client_address": self.client_address,
732            "server_endpoint": self.server_endpoint,
733            "server_public_key": self.server_public_key_str,
734            "allowed_ips": self.allowed_ips,
735        })
736    }
737
738    /// Get connection status
739    pub async fn get_status(&self) -> Result<serde_json::Value> {
740        let wgapi = self.wgapi.read().await;
741        let interface_data = wgapi
742            .read_interface_data()
743            .context("Failed to read interface data")?;
744
745        let peer = interface_data.peers.get(&self.server_public_key);
746
747        Ok(serde_json::json!({
748            "interface": self.interface_name,
749            "public_key": self.public_key,
750            "client_address": self.client_address,
751            "server_endpoint": self.server_endpoint,
752            "server_public_key": self.server_public_key_str,
753            "connected": peer.and_then(|p| p.last_handshake).is_some(),
754            "last_handshake": peer.and_then(|p| p.last_handshake)
755                .map(|t| t.elapsed().unwrap_or_default().as_secs()),
756            "tx_bytes": peer.map(|p| p.tx_bytes).unwrap_or(0),
757            "rx_bytes": peer.map(|p| p.rx_bytes).unwrap_or(0),
758            "allowed_ips": self.allowed_ips,
759        }))
760    }
761}
762
763/// Send a command to a WireGuard client
764pub async fn send_command(client_id: ClientId, command: WireguardCommand) -> Result<()> {
765    let clients = WIREGUARD_CLIENTS.read().await;
766    let tx = clients
767        .get(&client_id)
768        .context("WireGuard client not found")?;
769    tx.send(command)
770        .map_err(|_| anyhow::anyhow!("Failed to send command to WireGuard client"))?;
771    Ok(())
772}
773
774/// Get the connection status of a WireGuard client
775pub async fn get_client_status(client_id: ClientId) -> Result<serde_json::Value> {
776    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
777    send_command(client_id, WireguardCommand::GetStatus(response_tx)).await?;
778
779    match tokio::time::timeout(Duration::from_secs(5), response_rx).await {
780        Ok(Ok(status)) => status,
781        Ok(Err(_)) => Err(anyhow::anyhow!("Failed to get status from client")),
782        Err(_) => Err(anyhow::anyhow!("Timeout waiting for status response")),
783    }
784}
785
786/// Disconnect a WireGuard client
787pub async fn disconnect_client(client_id: ClientId) -> Result<()> {
788    send_command(client_id, WireguardCommand::Disconnect).await
789}