1use crate::llm::actions::{
12 protocol_trait::{ActionResult, Protocol, Server},
13 ActionDefinition, Parameter,
14};
15use crate::protocol::log_template::LogTemplate;
16use crate::protocol::EventType;
17use crate::state::app_state::AppState;
18use anyhow::{Context, Result};
19use serde_json::json;
20use std::sync::LazyLock;
21
22pub static IPSEC_HANDSHAKE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
24 EventType::new(
25 "ipsec_handshake",
26 "An IKE handshake datagram (IKEv2 IKE_SA_INIT/IKE_AUTH or IKEv1 Identity \
27 Protection/Aggressive Mode) was received and parsed. The honeypot sends no \
28 reply; respond with log_handshake, accept_connection or reject_connection to \
29 record how this attempt should be classified.",
30 json!({"type": "log_handshake", "details": "IKE handshake observed"}),
31 )
32 .with_actions(vec![
39 log_handshake_action(),
40 accept_connection_action(),
41 reject_connection_action(),
42 ])
43 .with_log_template(
47 LogTemplate::new()
48 .with_info("{peer_addr} IKE {exchange_type} ({duration_ms}ms)")
49 .with_debug("IKE handshake from {peer_addr}: {ike_version} {exchange_type}")
50 .with_trace("IKE handshake: {json_pretty(.)}"),
51 )
52});
53
54pub fn get_ipsec_event_types() -> Vec<EventType> {
61 vec![IPSEC_HANDSHAKE_EVENT.clone()]
62}
63
64pub struct IpsecProtocol;
66
67impl IpsecProtocol {
68 pub fn new() -> Self {
69 Self
70 }
71}
72
73impl Protocol for IpsecProtocol {
75 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
82 vec![]
83 }
84 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
91 vec![
92 accept_connection_action(),
93 reject_connection_action(),
94 log_handshake_action(),
95 ]
96 }
97 fn protocol_name(&self) -> &'static str {
98 "IPSec/IKEv2"
99 }
100 fn get_event_types(&self) -> Vec<EventType> {
101 get_ipsec_event_types()
102 }
103 fn stack_name(&self) -> &'static str {
104 "ETH>IP>UDP>IPSEC"
105 }
106 fn keywords(&self) -> Vec<&'static str> {
107 vec!["ipsec", "ikev2", "ike"]
108 }
109 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
110 use crate::protocol::metadata::{
111 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
112 };
113
114 ProtocolMetadataV2::builder()
115 .connectionless()
116 .state(DevelopmentState::Experimental)
117 .deliberately_silent()
120 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(500))
121 .well_known_udp_port(500)
122 .implementation(
123 "Receive-only IKE honeypot: manual 28-byte header parsing plus payload-chain \
124 walk. No cryptography, no Security Associations, no tunnel interface, and \
125 no packets are ever transmitted back to the peer.",
126 )
127 .llm_control(
128 "Classification of observed IKE attempts only (accept/reject/log labels \
129 recorded in the log). The LLM cannot influence what is sent on the wire, \
130 because nothing is sent.",
131 )
132 .e2e_testing("Crafted IKEv1/IKEv2 datagrams over UDP (detection only)")
133 .notes(
134 "NOT A VPN. Detects and analyses IKE reconnaissance; use WireGuard for a \
135 working tunnel. A real IKEv2 responder would need SA negotiation, DH, \
136 auth, ESP and kernel XFRM programming - deliberately out of scope.",
137 )
138 .max_inbound_bytes(crate::server::ipsec::MAX_PACKET_SIZE)
139 .build()
140 }
141 fn description(&self) -> &'static str {
142 "IPSec/IKEv2 honeypot - parses and logs IKE handshakes, never replies (not a VPN)"
143 }
144 fn example_prompt(&self) -> &'static str {
145 "Start an IPSec/IKEv2 honeypot on port 500 to analyze VPN reconnaissance attempts"
146 }
147 fn group_name(&self) -> &'static str {
148 "VPN & Routing"
149 }
150
151 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
152 use crate::llm::actions::StartupExamples;
153
154 let script = r#"import json, sys
156data = json.load(sys.stdin)
157event = data["event"]
158if data["event_type_id"] == "ipsec_handshake":
159 actions = [{"type": "log_handshake", "details": "IKE handshake observed"}]
160else:
161 actions = []
162print(json.dumps({"actions": actions}))"#;
163
164 StartupExamples::new(
165 json!({
167 "type": "open_server",
168 "port": 500,
169 "base_stack": "ipsec",
170 "instruction": "IPSec/IKEv2 honeypot. Log all IKE handshake attempts with detailed protocol analysis. Extract SPIs, exchange types, and payload information."
171 }),
172 json!({
174 "type": "open_server",
175 "port": 500,
176 "base_stack": "ipsec",
177 "event_handlers": [{
178 "event_pattern": "ipsec_handshake",
179 "handler": {
180 "type": "script",
181 "language": "python",
182 "code": script
183 }
184 }]
185 }),
186 json!({
188 "type": "open_server",
189 "port": 500,
190 "base_stack": "ipsec",
191 "event_handlers": [{
192 "event_pattern": "ipsec_handshake",
193 "handler": {
194 "type": "static",
195 "actions": [{"type": "log_handshake", "details": "IKE handshake detected"}]
196 }
197 }]
198 }),
199 )
200 }
201}
202
203impl Server for IpsecProtocol {
205 fn spawn(
206 &self,
207 ctx: crate::protocol::SpawnContext,
208 ) -> std::pin::Pin<
209 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
210 > {
211 Box::pin(async move {
212 use crate::server::ipsec::IpsecServer;
213 use std::sync::Arc;
214 IpsecServer::spawn_with_llm_actions(
215 ctx.legacy_listen_addr(),
216 Arc::new(ctx.llm_client),
217 ctx.state,
218 ctx.server_id,
219 ctx.status_tx,
220 )
221 .await
222 })
223 }
224 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
225 let action_type = action
226 .get("type")
227 .and_then(|v| v.as_str())
228 .context("Missing 'type' field in action")?;
229
230 match action_type {
231 "accept_connection" => self.execute_accept_connection(action),
232 "reject_connection" => self.execute_reject_connection(action),
233 "log_handshake" => self.execute_log_handshake(action),
234 "send_notify" => self.execute_send_notify(action),
235 "inspect_traffic" => self.execute_inspect_traffic(action),
236 "list_connections" => Ok(ActionResult::NoAction), "close_connection" => Ok(ActionResult::NoAction), _ => Err(anyhow::anyhow!("Unknown IPSec action: {}", action_type)),
239 }
240 }
241}
242
243impl IpsecProtocol {
244 fn execute_accept_connection(&self, action: serde_json::Value) -> Result<ActionResult> {
246 let _message = action
247 .get("message")
248 .and_then(|v| v.as_str())
249 .unwrap_or("Connection accepted");
250
251 Ok(ActionResult::NoAction)
254 }
255
256 fn execute_reject_connection(&self, action: serde_json::Value) -> Result<ActionResult> {
258 let _reason = action
259 .get("reason")
260 .and_then(|v| v.as_str())
261 .unwrap_or("Unauthorized");
262
263 Ok(ActionResult::NoAction)
266 }
267
268 fn execute_log_handshake(&self, action: serde_json::Value) -> Result<ActionResult> {
270 let _details = action
271 .get("details")
272 .and_then(|v| v.as_str())
273 .unwrap_or("Handshake logged");
274
275 Ok(ActionResult::NoAction)
277 }
278
279 fn execute_send_notify(&self, action: serde_json::Value) -> Result<ActionResult> {
281 let _notify_type = action
282 .get("notify_type")
283 .and_then(|v| v.as_str())
284 .unwrap_or("NO_PROPOSAL_CHOSEN");
285
286 Ok(ActionResult::NoAction)
288 }
289
290 fn execute_inspect_traffic(&self, action: serde_json::Value) -> Result<ActionResult> {
292 let _inspect = action
293 .get("inspect")
294 .and_then(|v| v.as_bool())
295 .unwrap_or(true);
296
297 Ok(ActionResult::NoAction)
299 }
300}
301
302fn accept_connection_action() -> ActionDefinition {
304 ActionDefinition {
305 name: "accept_connection".to_string(),
306 description: "Classify this IKE attempt as legitimate and record it in the log. No SA \
307 is established and no IKE response is sent - the honeypot is \
308 receive-only. Use this to label benign/expected VPN clients."
309 .to_string(),
310 parameters: vec![Parameter {
311 name: "message".to_string(),
312 type_hint: "string".to_string(),
313 description: "Optional message to log".to_string(),
314 required: false,
315 }],
316 example: json!({
317 "type": "accept_connection",
318 "message": "Legitimate VPN connection accepted"
319 }),
320 log_template: Some(
321 LogTemplate::new()
322 .with_info("-> IKE accept")
323 .with_debug("IKE connection accepted: {message}"),
324 ),
325 }
326}
327
328fn reject_connection_action() -> ActionDefinition {
330 ActionDefinition {
331 name: "reject_connection".to_string(),
332 description: "Classify this IKE attempt as unwanted (scan, probe, misconfigured client) \
333 and record the reason. The packet is silently dropped either way - no \
334 rejection is transmitted, because the honeypot never replies."
335 .to_string(),
336 parameters: vec![Parameter {
337 name: "reason".to_string(),
338 type_hint: "string".to_string(),
339 description: "Reason for rejection".to_string(),
340 required: false,
341 }],
342 example: json!({
343 "type": "reject_connection",
344 "reason": "Suspicious reconnaissance attempt"
345 }),
346 log_template: Some(
347 LogTemplate::new()
348 .with_info("-> IKE reject: {reason}")
349 .with_debug("IKE connection rejected: {reason}"),
350 ),
351 }
352}
353
354fn log_handshake_action() -> ActionDefinition {
356 ActionDefinition {
357 name: "log_handshake".to_string(),
358 description: "Record an analyst note about this IKE handshake (what it looks like, why \
359 it matters). This is the primary action for this protocol."
360 .to_string(),
361 parameters: vec![Parameter {
362 name: "details".to_string(),
363 type_hint: "string".to_string(),
364 description: "Additional details to log".to_string(),
365 required: false,
366 }],
367 example: json!({
368 "type": "log_handshake",
369 "details": "VPN scan attempt detected"
370 }),
371 log_template: Some(
372 LogTemplate::new()
373 .with_info("-> IKE logged: {details}")
374 .with_debug("IKE handshake logged: {details}"),
375 ),
376 }
377}