1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::net::SocketAddr;
13use std::pin::Pin;
14use std::sync::LazyLock;
15
16pub static WIREGUARD_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
18 EventType::new(
19 "wireguard_connected",
20 "WireGuard client successfully connected to VPN server",
21 json!({}),
22 )
23 .with_parameters(vec![
24 Parameter {
25 name: "server_endpoint".to_string(),
26 type_hint: "string".to_string(),
27 description: "VPN server endpoint (IP:port)".to_string(),
28 required: true,
29 },
30 Parameter {
31 name: "client_public_key".to_string(),
32 type_hint: "string".to_string(),
33 description: "Client's WireGuard public key".to_string(),
34 required: true,
35 },
36 Parameter {
37 name: "client_address".to_string(),
38 type_hint: "string".to_string(),
39 description: "Client's VPN IP address".to_string(),
40 required: true,
41 },
42 ])
43});
44
45pub static WIREGUARD_CLIENT_DISCONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
47 EventType::new(
48 "wireguard_disconnected",
49 "WireGuard client disconnected from VPN server",
50 json!({}),
51 )
52 .with_parameters(vec![Parameter {
53 name: "reason".to_string(),
54 type_hint: "string".to_string(),
55 description: "Disconnection reason".to_string(),
56 required: true,
57 }])
58});
59
60pub struct WireguardClientProtocol;
62
63impl WireguardClientProtocol {
64 pub fn new() -> Self {
65 Self
66 }
67}
68
69impl Protocol for WireguardClientProtocol {
71 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
72 vec![
73 ActionDefinition {
74 name: "get_connection_status".to_string(),
75 description: "Get current WireGuard connection status and statistics".to_string(),
76 parameters: vec![],
77 example: json!({
78 "type": "get_connection_status"
79 }),
80 log_template: None,
81 },
82 ActionDefinition {
83 name: "disconnect".to_string(),
84 description: "Disconnect from the WireGuard VPN server".to_string(),
85 parameters: vec![],
86 example: json!({
87 "type": "disconnect"
88 }),
89 log_template: None,
90 },
91 ActionDefinition {
92 name: "get_client_info".to_string(),
93 description: "Get WireGuard client configuration information".to_string(),
94 parameters: vec![],
95 example: json!({
96 "type": "get_client_info"
97 }),
98 log_template: None,
99 },
100 ]
101 }
102
103 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
104 vec![]
106 }
107
108 fn get_event_types(&self) -> Vec<EventType> {
109 vec![
110 WIREGUARD_CLIENT_CONNECTED_EVENT.clone(),
111 WIREGUARD_CLIENT_DISCONNECTED_EVENT.clone(),
112 ]
113 }
114
115 fn protocol_name(&self) -> &'static str {
116 "wireguard"
117 }
118
119 fn stack_name(&self) -> &'static str {
120 "VPN"
121 }
122
123 fn keywords(&self) -> Vec<&'static str> {
124 vec!["wireguard", "wg", "vpn client", "wireguard client"]
125 }
126
127 #[cfg(target_os = "macos")]
133 fn get_dependencies(&self) -> Vec<crate::protocol::dependencies::ProtocolDependency> {
134 let mut deps =
135 crate::llm::actions::protocol_trait::default_dependencies_from_privilege(self);
136 deps.push(crate::protocol::dependencies::ProtocolDependency::ToolInPath("wireguard-go"));
137 deps
138 }
139
140 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
141 use crate::protocol::metadata::{
142 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
143 };
144
145 ProtocolMetadataV2::builder()
146 .state(DevelopmentState::Experimental)
147 .implementation("defguard_wireguard_rs with kernel/userspace backend")
148 .llm_control("VPN connection control, status queries")
149 .e2e_testing("WireGuard server for client connections")
150 .privilege_requirement(PrivilegeRequirement::Root)
151 .notes("Requires root/CAP_NET_ADMIN on Linux, userspace on macOS")
152 .build()
153 }
154
155 fn description(&self) -> &'static str {
156 "WireGuard VPN client for secure tunneling"
157 }
158
159 fn example_prompt(&self) -> &'static str {
160 "Connect to WireGuard VPN at 1.2.3.4:51820 with server public key abc123..."
161 }
162
163 fn group_name(&self) -> &'static str {
164 "VPN"
165 }
166
167 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
168 vec![
169 ParameterDefinition {
170 name: "server_public_key".to_string(),
171 type_hint: "string".to_string(),
172 description: "WireGuard server's public key (base64 encoded)".to_string(),
173 required: true,
174 example: json!("xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="),
175 default: None,
176 },
177 ParameterDefinition {
178 name: "server_endpoint".to_string(),
179 type_hint: "string".to_string(),
180 description: "Server endpoint IP:port (e.g., 1.2.3.4:51820)".to_string(),
181 required: true,
182 example: json!("1.2.3.4:51820"),
183 default: None,
184 },
185 ParameterDefinition {
186 name: "client_address".to_string(),
187 type_hint: "string".to_string(),
188 description: "Client's VPN IP address with CIDR (e.g., 10.20.30.2/32)".to_string(),
189 required: true,
190 example: json!("10.20.30.2/32"),
191 default: None,
192 },
193 ParameterDefinition {
194 name: "allowed_ips".to_string(),
195 type_hint: "array".to_string(),
196 description: "IP ranges to route through VPN (default: 0.0.0.0/0 for all traffic)"
197 .to_string(),
198 required: false,
199 example: json!(["0.0.0.0/0", "::/0"]),
200 default: None,
201 },
202 ParameterDefinition {
203 name: "keepalive".to_string(),
204 type_hint: "integer".to_string(),
205 description: "Persistent keepalive interval in seconds (optional)".to_string(),
206 required: false,
207 example: json!(25),
208 default: None,
209 },
210 ParameterDefinition {
211 name: "private_key".to_string(),
212 type_hint: "string".to_string(),
213 description: "Client private key (base64). If not provided, will be generated."
214 .to_string(),
215 required: false,
216 example: json!("YAnz5TF+lXXJte14tji3zlMNftft3YEPi775qQV8mno="),
217 default: None,
218 },
219 ]
220 }
221 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
222 use crate::llm::actions::StartupExamples;
223 use serde_json::json;
224
225 StartupExamples::new(
226 json!({
228 "type": "open_client",
229 "remote_addr": "1.2.3.4:51820",
230 "base_stack": "wireguard",
231 "instruction": "Connect to VPN and check connection status",
232 "startup_params": {
233 "server_public_key": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=",
234 "server_endpoint": "1.2.3.4:51820",
235 "client_address": "10.20.30.2/32"
236 }
237 }),
238 json!({
240 "type": "open_client",
241 "remote_addr": "1.2.3.4:51820",
242 "base_stack": "wireguard",
243 "startup_params": {
244 "server_public_key": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=",
245 "server_endpoint": "1.2.3.4:51820",
246 "client_address": "10.20.30.2/32"
247 },
248 "event_handlers": [{
249 "event_pattern": "wireguard_connected",
250 "handler": {
251 "type": "script",
252 "language": "python",
253 "code": "<wireguard_client_handler>"
254 }
255 }]
256 }),
257 json!({
259 "type": "open_client",
260 "remote_addr": "1.2.3.4:51820",
261 "base_stack": "wireguard",
262 "startup_params": {
263 "server_public_key": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=",
264 "server_endpoint": "1.2.3.4:51820",
265 "client_address": "10.20.30.2/32",
266 "keepalive": 25
267 },
268 "event_handlers": [
269 {
270 "event_pattern": "wireguard_connected",
271 "handler": {
272 "type": "static",
273 "actions": [{
274 "type": "get_connection_status"
275 }]
276 }
277 }
278 ]
279 }),
280 )
281 }
282}
283
284impl Client for WireguardClientProtocol {
286 fn connect(
287 &self,
288 ctx: crate::protocol::ConnectContext,
289 ) -> Pin<Box<dyn std::future::Future<Output = Result<SocketAddr>> + Send>> {
290 Box::pin(async move {
291 use crate::client::wireguard::WireguardClient;
292
293 let params = if let Some(startup_params) = &ctx.startup_params {
295 crate::client::wireguard::WireguardClientParams {
297 server_public_key: startup_params.get_string("server_public_key")?,
298 server_endpoint: startup_params.get_string("server_endpoint")?,
299 client_address: startup_params.get_string("client_address")?,
300 allowed_ips: startup_params
301 .get_optional_array("allowed_ips")?
302 .map(|arr| {
303 arr.iter()
304 .filter_map(|v| v.as_str().map(|s| s.to_string()))
305 .collect()
306 })
307 .unwrap_or_else(|| vec!["0.0.0.0/0".to_string()]),
308 keepalive: startup_params
309 .get_optional_u64("keepalive")?
310 .map(|k| u16::try_from(k).context("WireGuard keepalive exceeds u16"))
311 .transpose()?,
312 private_key: startup_params.get_optional_string("private_key")?,
313 }
314 } else {
315 return Err(anyhow::anyhow!(
316 "Missing startup parameters for WireGuard client"
317 ));
318 };
319
320 WireguardClient::connect_with_llm_actions(
321 ctx.remote_addr,
322 ctx.llm_client,
323 ctx.state,
324 ctx.status_tx,
325 ctx.client_id,
326 params,
327 )
328 .await
329 })
330 }
331
332 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
333 let action_type = action
334 .get("type")
335 .and_then(|v| v.as_str())
336 .context("Missing 'type' field in action")?;
337
338 match action_type {
339 "get_connection_status" => Ok(ClientActionResult::Custom {
340 name: "wireguard_get_status".to_string(),
341 data: json!({}),
342 }),
343 "disconnect" => Ok(ClientActionResult::Disconnect),
344 "get_client_info" => Ok(ClientActionResult::Custom {
345 name: "wireguard_get_info".to_string(),
346 data: json!({}),
347 }),
348 _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
349 }
350 }
351}