1pub 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#[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#[derive(Debug)]
34pub enum WireguardCommand {
35 GetStatus(tokio::sync::oneshot::Sender<Result<serde_json::Value>>),
36 Disconnect,
37}
38
39static WIREGUARD_CLIENTS: LazyLock<
41 Arc<RwLock<HashMap<ClientId, mpsc::UnboundedSender<WireguardCommand>>>>,
42> = LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
43
44#[derive(Debug, Clone)]
46pub struct WireguardClientParams {
47 pub server_public_key: String,
49 pub server_endpoint: String,
51 pub client_address: String,
53 pub allowed_ips: Vec<String>,
55 pub keepalive: Option<u16>,
57 pub private_key: Option<String>,
59}
60
61pub enum WireguardApplied {
70 Executed(String),
72 Disconnected,
74}
75
76pub 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 pub async fn connect_with_llm_actions(
92 remote_addr: String, 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 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 #[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 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 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 let server_endpoint_addr: SocketAddr = params
146 .server_endpoint
147 .parse()
148 .context("Invalid server endpoint")?;
149
150 let server_key =
152 Key::decode(¶ms.server_public_key).context("Invalid server public key")?;
153
154 let listen_port = 51820u16.wrapping_add((client_id.as_u32() % 1000) as u16);
156
157 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 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 let wgapi_arc = Arc::new(RwLock::new(wgapi));
202
203 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 let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel();
218
219 WIREGUARD_CLIENTS.write().await.insert(client_id, cmd_tx);
221
222 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 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 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 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 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 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 let protocol = Arc::new(crate::client::wireguard::WireguardClientProtocol::new());
296
297 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 if let Some(new_memory) = result.memory_updates {
314 app_state.set_memory_for_client(client_id, new_memory).await;
315 }
316 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 Ok(SocketAddr::new(
337 IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
338 listen_port,
339 ))
340 }
341
342 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 app_state.remove_client_handle(client_id).await;
410 let _ = status_tx.send("__UPDATE_UI__".to_string());
411 }
412
413 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 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 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 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 }
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 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 break;
591 }
592
593 let wgapi_lock = wgapi.read().await;
595 match wgapi_lock.read_interface_data() {
596 Ok(interface_data) => {
597 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); 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 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 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 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 pub async fn disconnect(&self) -> Result<()> {
714 info!("Disconnecting WireGuard client");
715
716 let wgapi = self.wgapi.read().await;
718 wgapi
719 .remove_interface()
720 .context("Failed to remove WireGuard interface")?;
721
722 Ok(())
723 }
724
725 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 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
763pub 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
774pub 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
786pub async fn disconnect_client(client_id: ClientId) -> Result<()> {
788 send_command(client_id, WireguardCommand::Disconnect).await
789}