1use crate::llm::actions::{
23 protocol_trait::{ActionResult, Protocol, Server},
24 ActionDefinition, Parameter,
25};
26use crate::protocol::log_template::LogTemplate;
27use crate::protocol::EventType;
28use crate::state::app_state::AppState;
29use anyhow::{Context, Result};
30use serde_json::json;
31use std::sync::LazyLock;
32
33pub const PEER_DECISION_RESULT: &str = "openvpn_peer_decision";
37
38pub const KEY_EXCHANGE_DECISION_RESULT: &str = "openvpn_key_exchange_decision";
43
44pub static OPENVPN_PEER_RESET_EVENT: LazyLock<EventType> = LazyLock::new(|| {
46 EventType::new(
47 "openvpn_peer_reset",
48 "An OpenVPN client sent a session reset (P_CONTROL_HARD_RESET_CLIENT_V1/V2) - the \
49 first packet of a handshake. Decide whether to answer it. Reply with accept_peer to \
50 send P_CONTROL_HARD_RESET_SERVER_V2 and begin tracking the peer, or reject_peer to \
51 stay silent and drop it. If you reply with neither, the peer is left unanswered. \
52 Answering starts a real TLS control channel, and the client's credentials arrive \
53 next as an openvpn_client_key_exchange event. Even an accepted peer never gets a \
54 tunnel: this server answers no PUSH_REQUEST and has no data channel.",
55 json!({
56 "type": "accept_peer",
57 "reason": "Answer the handshake and observe what the client sends next"
58 }),
59 )
60 .with_actions(vec![accept_peer_action(), reject_peer_action()])
61 .with_log_template(
65 LogTemplate::new()
66 .with_info("{peer_addr} OpenVPN reset (session {client_session_id})")
67 .with_debug(
68 "OpenVPN {reset_type} from {peer_addr}, session {client_session_id}, \
69 key_id {key_id}",
70 )
71 .with_trace("OpenVPN peer reset: {json_pretty(.)}"),
72 )
73});
74
75pub static OPENVPN_KEY_EXCHANGE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
83 EventType::new(
84 "openvpn_client_key_exchange",
85 "An OpenVPN client completed the control-channel TLS handshake and sent its key \
86 method 2 message: its options string, its username and password if it is using \
87 --auth-user-pass, and its IV_* peer info. Decide whether to answer. Reply with \
88 accept_key_exchange to send this server's own key method 2 message, or \
89 reject_key_exchange to stay silent and drop the session. If you reply with neither, \
90 the client is left unanswered. Note that even an accepted client never gets a \
91 tunnel: this server does not answer PUSH_REQUEST and derives no data channel keys.",
92 json!({
93 "type": "accept_key_exchange",
94 "reason": "Answer so the client reveals what it asks for next"
95 }),
96 )
97 .with_actions(vec![
98 accept_key_exchange_action(),
99 reject_key_exchange_action(),
100 ])
101 .with_log_template(
104 LogTemplate::new()
105 .with_info("{peer_addr} OpenVPN key exchange (user {username})")
106 .with_debug("OpenVPN key method 2 from {peer_addr}: user {username}, {options}")
107 .with_trace("OpenVPN key exchange: {json_pretty(.)}"),
108 )
109});
110
111pub fn get_openvpn_event_types() -> Vec<EventType> {
118 vec![
119 OPENVPN_PEER_RESET_EVENT.clone(),
120 OPENVPN_KEY_EXCHANGE_EVENT.clone(),
121 ]
122}
123
124pub struct OpenvpnProtocol;
126
127impl OpenvpnProtocol {
128 pub fn new() -> Self {
129 Self
130 }
131}
132
133impl Default for OpenvpnProtocol {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl Protocol for OpenvpnProtocol {
140 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
147 vec![]
148 }
149
150 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
151 vec![
152 accept_peer_action(),
153 reject_peer_action(),
154 accept_key_exchange_action(),
155 reject_key_exchange_action(),
156 ]
157 }
158
159 fn protocol_name(&self) -> &'static str {
160 "OpenVPN"
161 }
162
163 fn get_event_types(&self) -> Vec<EventType> {
164 get_openvpn_event_types()
165 }
166
167 fn stack_name(&self) -> &'static str {
168 "ETH>IP>UDP>OPENVPN"
169 }
170
171 fn keywords(&self) -> Vec<&'static str> {
172 vec!["openvpn"]
173 }
174
175 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
176 use crate::protocol::metadata::{
177 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
178 };
179
180 ProtocolMetadataV2::builder()
181 .state(DevelopmentState::Experimental)
197 .deliberately_silent()
201 .privilege_requirement(PrivilegeRequirement::None)
205 .well_known_udp_port(1194)
206 .implementation(
207 "Control channel only. Decodes the OpenVPN UDP wire format (P_CONTROL_*, \
208 P_ACK_V1, P_DATA_V1/V2); implements the reliability layer (packet ids, ACK \
209 arrays, ordered delivery, retransmission with backoff); answers \
210 P_CONTROL_HARD_RESET_CLIENT_V1/V2 with P_CONTROL_HARD_RESET_SERVER_V2; runs \
211 a real rustls TLS session whose records are fragmented across P_CONTROL_V1 \
212 packets, using a self-signed P-256 certificate generated per run whose \
213 SHA-256 fingerprint is logged for --peer-fingerprint; and reads and answers \
214 the client's key method 2 message (options string, username, password, IV_* \
215 peer info). There is NO PUSH_REPLY, NO data channel key derivation, NO data \
216 channel and NO TUN device, so no tunnel is ever established and no traffic \
217 is carried. --tls-auth, --tls-crypt and --tls-crypt-v2 clients are detected \
218 and refused rather than mis-parsed.",
219 )
220 .llm_control(
221 "Two events, one per policy decision. openvpn_peer_reset: accept_peer sends \
222 the reset reply, reject_peer stays silent. openvpn_client_key_exchange \
223 carries the client's username, password, options and IV_* peer info; \
224 accept_key_exchange sends the server's key method 2 answer, \
225 reject_key_exchange stays silent. Both decisions are enforced and both fail \
226 closed: no decision means nothing is sent.",
227 )
228 .e2e_testing(
229 "Wire format pinned to frames captured from OpenVPN 2.7.4 and decoded by a \
230 hand-written decoder in the test, not by this codec. A live openvpn 2.7 \
231 client is driven against the server with --peer-fingerprint set from the \
232 fingerprint the server logs, and must log 'Control Channel: TLSv1.x' (the \
233 TLS handshake completed over the reliability layer) and 'Peer Connection \
234 Initiated' (its key method 2 message was answered acceptably). The same test \
235 asserts the client never logs 'Initialization Sequence Completed', because \
236 the server answers no PUSH_REQUEST.",
237 )
238 .notes(
239 "NOT A VPN - it never carries traffic; a real client stalls at PUSH_REQUEST. \
240 Useful as an OpenVPN honeypot and protocol observatory: because the TLS \
241 session is real, it captures the client's OpenVPN version and platform \
242 (IV_* peer info), its expected options, and the username and password it \
243 was going to authenticate with. Use WireGuard for a working tunnel.",
244 )
245 .max_inbound_bytes(crate::server::openvpn::packet::MAX_PACKET_SIZE)
246 .build()
247 }
248
249 fn description(&self) -> &'static str {
250 "OpenVPN control-plane responder / honeypot (answers the session reset; no tunnel)"
251 }
252
253 fn example_prompt(&self) -> &'static str {
254 "Start an OpenVPN honeypot on port 1194 and log everyone who probes it"
255 }
256
257 fn group_name(&self) -> &'static str {
258 "VPN & Routing"
259 }
260
261 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
262 use crate::llm::actions::StartupExamples;
263
264 StartupExamples::new(
265 json!({
267 "type": "open_server",
268 "port": 1194,
269 "base_stack": "openvpn",
270 "instruction": "OpenVPN honeypot. Answer every handshake with accept_peer and record the peer address and session id so the probe is logged."
271 }),
272 json!({
274 "type": "open_server",
275 "port": 1194,
276 "base_stack": "openvpn",
277 "event_handlers": [{
278 "event_pattern": "openvpn_peer_reset",
279 "handler": {
280 "type": "script",
281 "language": "python",
282 "code": "return {'type': 'accept_peer', 'reason': 'honeypot'}"
283 }
284 }]
285 }),
286 json!({
288 "type": "open_server",
289 "port": 1194,
290 "base_stack": "openvpn",
291 "event_handlers": [{
292 "event_pattern": "openvpn_peer_reset",
293 "handler": {
294 "type": "static",
295 "actions": [{"type": "accept_peer"}]
296 }
297 }]
298 }),
299 )
300 }
301}
302
303impl Server for OpenvpnProtocol {
304 fn spawn(
305 &self,
306 ctx: crate::protocol::SpawnContext,
307 ) -> std::pin::Pin<
308 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
309 > {
310 Box::pin(async move {
311 use crate::server::openvpn::OpenvpnServer;
312 use std::sync::Arc;
313 OpenvpnServer::spawn_with_llm_actions(
314 ctx.legacy_listen_addr(),
315 Arc::new(ctx.llm_client),
316 ctx.state,
317 ctx.server_id,
318 ctx.status_tx,
319 )
320 .await
321 })
322 }
323
324 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
325 let action_type = action
326 .get("type")
327 .and_then(|v| v.as_str())
328 .context("Missing 'type' field in action")?;
329
330 let reason = action
331 .get("reason")
332 .and_then(|v| v.as_str())
333 .map(|s| s.to_string());
334
335 match action_type {
336 "accept_peer" => Ok(decision_result(PEER_DECISION_RESULT, true, reason)),
337 "reject_peer" => Ok(decision_result(PEER_DECISION_RESULT, false, reason)),
338 "accept_key_exchange" => {
339 Ok(decision_result(KEY_EXCHANGE_DECISION_RESULT, true, reason))
340 }
341 "reject_key_exchange" => {
342 Ok(decision_result(KEY_EXCHANGE_DECISION_RESULT, false, reason))
343 }
344 _ => Err(anyhow::anyhow!("Unknown OpenVPN action: {}", action_type)),
345 }
346 }
347}
348
349fn decision_result(name: &str, accept: bool, reason: Option<String>) -> ActionResult {
351 ActionResult::Custom {
352 name: name.to_string(),
353 data: json!({ "accept": accept, "reason": reason }),
354 }
355}
356
357fn accept_peer_action() -> ActionDefinition {
359 ActionDefinition {
360 name: "accept_peer".to_string(),
361 description: "Answer this peer's session reset with P_CONTROL_HARD_RESET_SERVER_V2 and \
362 start tracking it, acknowledging the control packets it sends next. This \
363 is enforced: without it, nothing is sent to the peer. An accepted peer \
364 goes on to complete a real TLS handshake over the control channel and \
365 send its key method 2 message, which arrives as \
366 openvpn_client_key_exchange - but it never gets a tunnel: this server \
367 answers no PUSH_REQUEST and has no data channel, so the client stalls \
368 there."
369 .to_string(),
370 parameters: vec![Parameter {
371 name: "reason".to_string(),
372 type_hint: "string".to_string(),
373 description: "Why this peer is being answered (recorded in the log)".to_string(),
374 required: false,
375 }],
376 example: json!({
377 "type": "accept_peer",
378 "reason": "Observe what the client sends after the reset"
379 }),
380 log_template: Some(
381 LogTemplate::new()
382 .with_info("-> OpenVPN answer reset ({reason})")
383 .with_debug("OpenVPN accept_peer: {reason}"),
384 ),
385 }
386}
387
388fn reject_peer_action() -> ActionDefinition {
390 ActionDefinition {
391 name: "reject_peer".to_string(),
392 description: "Refuse this peer: send nothing at all and drop it. This is enforced. \
393 OpenVPN has no reject packet at reset time, so silence is the refusal; \
394 the client will retry and then give up."
395 .to_string(),
396 parameters: vec![Parameter {
397 name: "reason".to_string(),
398 type_hint: "string".to_string(),
399 description: "Why this peer is being refused (recorded in the log)".to_string(),
400 required: false,
401 }],
402 example: json!({
403 "type": "reject_peer",
404 "reason": "Source address is not on the allow list"
405 }),
406 log_template: Some(
407 LogTemplate::new()
408 .with_info("-> OpenVPN refuse peer ({reason})")
409 .with_debug("OpenVPN reject_peer: {reason}"),
410 ),
411 }
412}
413
414fn accept_key_exchange_action() -> ActionDefinition {
416 ActionDefinition {
417 name: "accept_key_exchange".to_string(),
418 description: "Answer this client's key method 2 message with this server's own, so the \
419 handshake continues and the client reveals what it asks for next \
420 (PUSH_REQUEST and its option requirements). This is enforced: without it, \
421 nothing is sent. It still does not create a tunnel - PUSH_REQUEST is never \
422 answered and no data channel keys are derived, so the client stalls there."
423 .to_string(),
424 parameters: vec![Parameter {
425 name: "reason".to_string(),
426 type_hint: "string".to_string(),
427 description: "Why this client is being answered (recorded in the log)".to_string(),
428 required: false,
429 }],
430 example: json!({
431 "type": "accept_key_exchange",
432 "reason": "Credentials captured; let it continue so it reveals its push requirements"
433 }),
434 log_template: Some(
435 LogTemplate::new()
436 .with_info("-> OpenVPN answer key exchange ({reason})")
437 .with_debug("OpenVPN accept_key_exchange: {reason}"),
438 ),
439 }
440}
441
442fn reject_key_exchange_action() -> ActionDefinition {
444 ActionDefinition {
445 name: "reject_key_exchange".to_string(),
446 description: "Refuse this client after its key method 2 message: send nothing and drop \
447 the control session. This is enforced. OpenVPN's AUTH_FAILED is only read \
448 by a client that has already received the server's key method 2 answer, so \
449 before that point silence is the only refusal the peer can understand."
450 .to_string(),
451 parameters: vec![Parameter {
452 name: "reason".to_string(),
453 type_hint: "string".to_string(),
454 description: "Why this client is being refused (recorded in the log)".to_string(),
455 required: false,
456 }],
457 example: json!({
458 "type": "reject_key_exchange",
459 "reason": "Username is not on the allow list"
460 }),
461 log_template: Some(
462 LogTemplate::new()
463 .with_info("-> OpenVPN refuse key exchange ({reason})")
464 .with_debug("OpenVPN reject_key_exchange: {reason}"),
465 ),
466 }
467}