1pub mod actions;
33
34use crate::llm::action_helper::call_llm;
35use crate::llm::ollama_client::OllamaClient;
36use crate::logging::emit::Log;
37use crate::protocol::Event;
38use crate::state::app_state::AppState;
39use actions::{IpsecProtocol, IPSEC_HANDSHAKE_EVENT};
40use anyhow::Result;
41use std::net::SocketAddr;
42use std::sync::Arc;
43use tokio::net::UdpSocket;
44use tokio::sync::mpsc;
45use tracing::{debug, error, info, trace};
46
47pub const MAX_PACKET_SIZE: usize = 65535;
49
50const IKE_HEADER_SIZE: usize = 28;
52
53const IKEV2_VERSION: u8 = 0x20;
55
56const IKE_SA_INIT: u8 = 34;
58const IKE_AUTH: u8 = 35;
59const CREATE_CHILD_SA: u8 = 36;
60const INFORMATIONAL: u8 = 37;
61
62const IKEV1_IDENTITY_PROTECTION: u8 = 2;
64const IKEV1_AGGRESSIVE: u8 = 4;
65
66const FLAG_INITIATOR: u8 = 0x08; const FLAG_VERSION: u8 = 0x10; const FLAG_RESPONSE: u8 = 0x20; const PAYLOAD_NONE: u8 = 0;
73const PAYLOAD_SA: u8 = 33; const PAYLOAD_KE: u8 = 34; const PAYLOAD_IDI: u8 = 35; const PAYLOAD_IDR: u8 = 36; const PAYLOAD_CERT: u8 = 37; const PAYLOAD_CERTREQ: u8 = 38; const PAYLOAD_AUTH: u8 = 39; const PAYLOAD_NONCE: u8 = 40; const PAYLOAD_NOTIFY: u8 = 41; const PAYLOAD_DELETE: u8 = 42; const PAYLOAD_VENDOR: u8 = 43; const PAYLOAD_TSI: u8 = 44; const PAYLOAD_TSR: u8 = 45; const PAYLOAD_SK: u8 = 46; const PAYLOAD_CP: u8 = 47; const PAYLOAD_EAP: u8 = 48; pub struct IpsecServer;
92
93impl IpsecServer {
94 pub async fn spawn_with_llm_actions(
96 bind_addr: SocketAddr,
97 llm_client: Arc<OllamaClient>,
98 app_state: Arc<AppState>,
99 server_id: crate::state::ServerId,
100 status_tx: mpsc::UnboundedSender<String>,
101 ) -> Result<SocketAddr> {
102 info!(
103 "Starting IPSec/IKEv2 parse-and-log honeypot on {}",
104 bind_addr
105 );
106 Log::new(Some(&status_tx)).info(format!(
107 "Starting IPSec/IKEv2 honeypot on {} (parses and logs IKE, never replies)",
108 bind_addr
109 ));
110
111 let socket = UdpSocket::bind(bind_addr).await?;
113 let local_addr = socket.local_addr()?;
114 Log::new(Some(&status_tx))
115 .info(format!("IPSec/IKEv2 honeypot listening on {}", local_addr));
116
117 let socket = Arc::new(socket);
118
119 let socket_clone = socket.clone();
121 let task_registrar = app_state.clone();
122 let accept_handle = tokio::spawn(async move {
123 if let Err(e) =
124 Self::handle_packets(socket_clone, llm_client, app_state, server_id, status_tx)
125 .await
126 {
127 error!("IPSec/IKEv2 honeypot error: {}", e);
128 }
129 });
130
131 task_registrar
132 .register_server_task(server_id, accept_handle)
133 .await;
134
135 Ok(local_addr)
136 }
137
138 async fn handle_packets(
140 socket: Arc<UdpSocket>,
141 llm_client: Arc<OllamaClient>,
142 app_state: Arc<AppState>,
143 server_id: crate::state::ServerId,
144 status_tx: mpsc::UnboundedSender<String>,
145 ) -> Result<()> {
146 let mut buf = vec![0u8; MAX_PACKET_SIZE];
147
148 loop {
149 let (len, peer_addr) = match socket.recv_from(&mut buf).await {
151 Ok(result) => result,
152 Err(e) => {
153 error!("UDP recv error: {}", e);
154 continue;
155 }
156 };
157
158 let packet = &buf[..len];
159
160 if len < IKE_HEADER_SIZE {
162 trace!(
163 "Received undersized packet from {} ({} bytes)",
164 peer_addr,
165 len
166 );
167 continue;
168 }
169
170 let initiator_spi = u64::from_be_bytes([
172 packet[0], packet[1], packet[2], packet[3], packet[4], packet[5], packet[6],
173 packet[7],
174 ]);
175 let responder_spi = u64::from_be_bytes([
176 packet[8], packet[9], packet[10], packet[11], packet[12], packet[13], packet[14],
177 packet[15],
178 ]);
179 let next_payload = packet[16];
180 let version = packet[17];
181 let exchange_type = packet[18];
182 let flags = packet[19];
183 let message_id = u32::from_be_bytes([packet[20], packet[21], packet[22], packet[23]]);
184 let packet_length =
185 u32::from_be_bytes([packet[24], packet[25], packet[26], packet[27]]);
186
187 let is_initiator = (flags & FLAG_INITIATOR) != 0;
189 let is_response = (flags & FLAG_RESPONSE) != 0;
190 let version_bit = (flags & FLAG_VERSION) != 0;
191
192 let payload_types = Self::extract_payload_types(packet, next_payload);
194
195 let (ike_version, exchange_name, is_handshake) = if version == IKEV2_VERSION {
197 let (name, handshake) = match exchange_type {
198 IKE_SA_INIT => ("IKE_SA_INIT", true),
199 IKE_AUTH => ("IKE_AUTH", true),
200 CREATE_CHILD_SA => ("CREATE_CHILD_SA", false),
201 INFORMATIONAL => ("INFORMATIONAL", false),
202 _ => ("Unknown", false),
203 };
204 ("IKEv2", name, handshake)
205 } else {
206 let (name, handshake) = match exchange_type {
207 IKEV1_IDENTITY_PROTECTION => ("Identity Protection", true),
208 IKEV1_AGGRESSIVE => ("Aggressive Mode", true),
209 _ => ("Unknown", false),
210 };
211 ("IKEv1", name, handshake)
212 };
213
214 let payload_names = Self::format_payload_types(&payload_types);
216
217 trace!(
218 "IKE packet from {}: version={}, exchange={}, flags=0x{:02x} (I={}, R={}, V={}), msg_id={}, len={}, payloads=[{}]",
219 peer_addr,
220 ike_version,
221 exchange_name,
222 flags,
223 if is_initiator { "1" } else { "0" },
224 if is_response { "1" } else { "0" },
225 if version_bit { "1" } else { "0" },
226 message_id,
227 packet_length,
228 payload_names
229 );
230 Log::new(Some(&status_tx)).trace(format!(
231 "IPSec: {} {} from {} ({} bytes, payloads=[{}])",
232 ike_version, exchange_name, peer_addr, len, payload_names
233 ));
234
235 if is_handshake {
237 Self::handle_handshake_initiation(
238 peer_addr,
239 packet,
240 ike_version,
241 exchange_name,
242 initiator_spi,
243 responder_spi,
244 is_initiator,
245 is_response,
246 message_id,
247 &payload_types,
248 &llm_client,
249 &app_state,
250 server_id,
251 &status_tx,
252 )
253 .await;
254 } else {
255 debug!(
257 "IPSec {} {} from {} (honeypot: logged only, payloads=[{}])",
258 ike_version, exchange_name, peer_addr, payload_names
259 );
260 Log::new(Some(&status_tx)).debug(format!(
261 "IPSec: {} {} from {} (logged, payloads=[{}])",
262 ike_version, exchange_name, peer_addr, payload_names
263 ));
264 }
265 }
266 }
267
268 fn extract_payload_types(packet: &[u8], mut next_payload: u8) -> Vec<u8> {
270 let mut payload_types = Vec::new();
271 let mut offset = IKE_HEADER_SIZE;
272
273 while next_payload != PAYLOAD_NONE && offset + 4 <= packet.len() {
275 payload_types.push(next_payload);
276
277 if offset + 4 > packet.len() {
279 break;
280 }
281
282 let payload_length =
283 u16::from_be_bytes([packet[offset + 2], packet[offset + 3]]) as usize;
284 if payload_length < 4 || offset + payload_length > packet.len() {
285 break;
286 }
287
288 next_payload = packet[offset];
289 offset += payload_length;
290 }
291
292 payload_types
293 }
294
295 fn format_payload_types(payload_types: &[u8]) -> String {
297 payload_types
298 .iter()
299 .map(|&p| Self::payload_type_name(p))
300 .collect::<Vec<_>>()
301 .join(", ")
302 }
303
304 fn payload_type_name(payload_type: u8) -> &'static str {
306 match payload_type {
307 PAYLOAD_SA => "SA",
308 PAYLOAD_KE => "KE",
309 PAYLOAD_IDI => "IDi",
310 PAYLOAD_IDR => "IDr",
311 PAYLOAD_CERT => "CERT",
312 PAYLOAD_CERTREQ => "CERTREQ",
313 PAYLOAD_AUTH => "AUTH",
314 PAYLOAD_NONCE => "NONCE",
315 PAYLOAD_NOTIFY => "NOTIFY",
316 PAYLOAD_DELETE => "DELETE",
317 PAYLOAD_VENDOR => "VENDOR",
318 PAYLOAD_TSI => "TSi",
319 PAYLOAD_TSR => "TSr",
320 PAYLOAD_SK => "SK",
321 PAYLOAD_CP => "CP",
322 PAYLOAD_EAP => "EAP",
323 _ => "UNKNOWN",
324 }
325 }
326
327 #[allow(clippy::too_many_arguments)]
331 async fn handle_handshake_initiation(
332 peer_addr: SocketAddr,
333 packet: &[u8],
334 ike_version: &str,
335 exchange_type: &str,
336 initiator_spi: u64,
337 responder_spi: u64,
338 is_initiator: bool,
339 is_response: bool,
340 message_id: u32,
341 payload_types: &[u8],
342 llm_client: &OllamaClient,
343 app_state: &AppState,
344 server_id: crate::state::ServerId,
345 status_tx: &mpsc::UnboundedSender<String>,
346 ) {
347 let payload_names = Self::format_payload_types(payload_types);
348
349 info!(
350 "IPSec {} handshake from {} (honeypot, payloads=[{}])",
351 ike_version, peer_addr, payload_names
352 );
353 Log::new(Some(status_tx)).info(format!(
354 "IPSec: {} handshake from {} (payloads=[{}])",
355 ike_version, peer_addr, payload_names
356 ));
357
358 let event = Event::new(
360 &IPSEC_HANDSHAKE_EVENT,
361 serde_json::json!({
362 "peer_addr": peer_addr.to_string(),
363 "packet_size": packet.len(),
364 "ike_version": ike_version,
365 "exchange_type": exchange_type,
366 "initiator_spi": format!("{:016x}", initiator_spi),
367 "responder_spi": format!("{:016x}", responder_spi),
368 "is_initiator": is_initiator,
369 "is_response": is_response,
370 "message_id": message_id,
371 "payloads": payload_types.iter().map(|&p| Self::payload_type_name(p)).collect::<Vec<_>>(),
372 "honeypot_mode": true,
373 "responds_to_peer": false,
374 "analysis": {
375 "expected_payloads": if exchange_type == "IKE_SA_INIT" {
376 "SA, KE, NONCE"
377 } else if exchange_type == "IKE_AUTH" {
378 "IDi, AUTH, SA, TSi, TSr"
379 } else {
380 "varies"
381 },
382 "has_encryption": payload_types.contains(&PAYLOAD_SK),
383 "has_vendor_id": payload_types.contains(&PAYLOAD_VENDOR),
384 "has_certificate": payload_types.contains(&PAYLOAD_CERT) || payload_types.contains(&PAYLOAD_CERTREQ),
385 }
386 }),
387 );
388
389 debug!(
390 "IPSec/IKEv2 handshake analyzed from {} ({} payloads detected)",
391 peer_addr,
392 payload_types.len()
393 );
394
395 let protocol = IpsecProtocol::new();
400 match call_llm(
401 llm_client, app_state, server_id,
402 None, &event, &protocol,
404 )
405 .await
406 {
407 Ok(result) => {
408 let log = Log::new(Some(status_tx));
409 for message in &result.messages {
410 log.info(format!("{}", message));
411 }
412 let decision =
418 if result.raw_actions.iter().any(|a| {
419 a.get("type").and_then(|t| t.as_str()) == Some("reject_connection")
420 }) {
421 "model_reject"
422 } else if result.raw_actions.is_empty() {
423 "no_answer"
424 } else {
425 "model_answer"
426 };
427 info!(
428 "IPSec {} {} from {} decision={} actions={} (honeypot, nothing sent)",
429 ike_version,
430 exchange_type,
431 peer_addr,
432 decision,
433 result.raw_actions.len()
434 );
435 }
436 Err(e) => {
437 let decision = match crate::utils::WireFailure::classify(&e) {
444 crate::utils::WireFailure::Overloaded => "fail_closed_overloaded",
445 crate::utils::WireFailure::Unavailable => "fail_closed_unavailable",
446 };
447 error!(
448 "IPSec {} {} from {} decision={} (honeypot, nothing sent): {:#}",
449 ike_version, exchange_type, peer_addr, decision, e
450 );
451 Log::new(Some(status_tx)).error(format!(
452 "IPSec handshake event handling failed ({}): {}",
453 decision, e
454 ));
455 }
456 }
457 }
458}