Skip to main content

netget/client/wireguard/
actions.rs

1//! WireGuard client protocol actions implementation
2
3use 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
16/// WireGuard client connected event
17pub 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
45/// WireGuard client disconnected event
46pub 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
60/// WireGuard client protocol action handler
61pub struct WireguardClientProtocol;
62
63impl WireguardClientProtocol {
64    pub fn new() -> Self {
65        Self
66    }
67}
68
69// Implement Protocol trait (base trait for all protocols)
70impl 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        // WireGuard doesn't have sync actions (no immediate response to network events)
105        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    /// On macOS `defguard_wireguard_rs` brings the interface up by running `wireguard-go`.
128    ///
129    /// Same reasoning as the server half (`src/server/wireguard/actions.rs`): the kernel
130    /// implements WireGuard on Linux, FreeBSD and Windows, macOS does not, and there defguard
131    /// shells out to a userspace binary that must be installed separately.
132    #[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            // LLM mode: LLM controls VPN connection and status
227            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            // Script mode: Code-based VPN event handling
239            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            // Static mode: Fixed VPN connection (stays connected)
258            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
284// Implement Client trait (client-specific functionality)
285impl 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            // Parse startup params - should be in JSON format in startup_params
294            let params = if let Some(startup_params) = &ctx.startup_params {
295                // Get required parameters using StartupParams accessors
296                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}