Skip to main content

netget/server/ipsec/
mod.rs

1//! IPSec/IKEv2 parse-and-log honeypot
2//!
3//! # What this actually is
4//!
5//! A **read-only IKE honeypot**. It binds a UDP socket, parses the 28-byte IKE
6//! header and walks the payload chain of every datagram it receives, then reports
7//! what it saw. It performs **no cryptography, establishes no Security
8//! Associations, creates no tunnel interface, and never transmits a single byte
9//! back to the peer**. Nothing here is a VPN; use WireGuard
10//! (`src/server/wireguard/`) when a working tunnel is required.
11//!
12//! Staying silent is deliberate: it avoids accidentally negotiating anything and
13//! avoids fingerprinting the honeypot by its responses.
14//!
15//! # What the LLM controls
16//!
17//! Each IKE_SA_INIT / IKE_AUTH (or IKEv1 Identity Protection / Aggressive Mode)
18//! datagram raises an `ipsec_handshake` event through
19//! [`crate::llm::action_helper::call_llm`], which runs any configured
20//! script/static event handler first and falls back to a real LLM call only when
21//! none is configured. The resulting actions are **classification and logging
22//! decisions only** - `accept_connection`, `reject_connection`, `log_handshake`
23//! and `send_notify` all record intent without changing what goes on the wire.
24//!
25//! # Status
26//!
27//! `DevelopmentState::Experimental`. Turning this into a real IKEv2 responder
28//! means implementing SA negotiation, Diffie-Hellman, authentication, ESP and
29//! kernel XFRM/SAD/SPD programming - a multi-month project, not a patch. It is
30//! deliberately out of scope.
31
32pub 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
47/// Maximum IKE packet size
48pub const MAX_PACKET_SIZE: usize = 65535;
49
50/// IKEv2 header minimum size (28 bytes)
51const IKE_HEADER_SIZE: usize = 28;
52
53/// IKEv2 version (major=2, minor=0)
54const IKEV2_VERSION: u8 = 0x20;
55
56/// IKEv2 Exchange Types (from RFC 7296)
57const IKE_SA_INIT: u8 = 34;
58const IKE_AUTH: u8 = 35;
59const CREATE_CHILD_SA: u8 = 36;
60const INFORMATIONAL: u8 = 37;
61
62/// IKEv1 Exchange Types (for detection)
63const IKEV1_IDENTITY_PROTECTION: u8 = 2;
64const IKEV1_AGGRESSIVE: u8 = 4;
65
66/// IKE Header Flags (RFC 7296 Section 3.1)
67const FLAG_INITIATOR: u8 = 0x08; // Initiator bit
68const FLAG_VERSION: u8 = 0x10; // Version bit (must be 0 for IKEv2)
69const FLAG_RESPONSE: u8 = 0x20; // Response bit
70
71/// IKE Payload Types (RFC 7296 Section 3.2)
72const PAYLOAD_NONE: u8 = 0;
73const PAYLOAD_SA: u8 = 33; // Security Association
74const PAYLOAD_KE: u8 = 34; // Key Exchange
75const PAYLOAD_IDI: u8 = 35; // Identification - Initiator
76const PAYLOAD_IDR: u8 = 36; // Identification - Responder
77const PAYLOAD_CERT: u8 = 37; // Certificate
78const PAYLOAD_CERTREQ: u8 = 38; // Certificate Request
79const PAYLOAD_AUTH: u8 = 39; // Authentication
80const PAYLOAD_NONCE: u8 = 40; // Nonce
81const PAYLOAD_NOTIFY: u8 = 41; // Notify
82const PAYLOAD_DELETE: u8 = 42; // Delete
83const PAYLOAD_VENDOR: u8 = 43; // Vendor ID
84const PAYLOAD_TSI: u8 = 44; // Traffic Selector - Initiator
85const PAYLOAD_TSR: u8 = 45; // Traffic Selector - Responder
86const PAYLOAD_SK: u8 = 46; // Encrypted and Authenticated
87const PAYLOAD_CP: u8 = 47; // Configuration
88const PAYLOAD_EAP: u8 = 48; // Extensible Authentication
89
90/// IPSec/IKEv2 enhanced honeypot server
91pub struct IpsecServer;
92
93impl IpsecServer {
94    /// Spawn IPSec/IKEv2 honeypot with integrated LLM actions
95    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        // Bind UDP socket (IKE uses UDP port 500, NAT-T uses 4500)
112        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        // Spawn packet handler
120        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    /// Handle incoming IKE packets
139    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            // Receive packet
150            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            // Parse IKE header
161            if len < IKE_HEADER_SIZE {
162                trace!(
163                    "Received undersized packet from {} ({} bytes)",
164                    peer_addr,
165                    len
166                );
167                continue;
168            }
169
170            // Extract IKE header fields (28 bytes - RFC 7296 Section 3.1)
171            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            // Analyze flags
188            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            // Extract payload chain
193            let payload_types = Self::extract_payload_types(packet, next_payload);
194
195            // Determine IKE version and exchange type
196            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            // Format payload chain for logging
215            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            // For handshake initiation, provide detailed analysis
236            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                // Log other packet types for reconnaissance detection
256                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    /// Extract payload types from IKE message
269    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        // Walk the payload chain
274        while next_payload != PAYLOAD_NONE && offset + 4 <= packet.len() {
275            payload_types.push(next_payload);
276
277            // Each payload has: next_payload(1) + reserved(1) + length(2)
278            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    /// Format payload types as human-readable names
296    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    /// Get payload type name
305    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    /// Handle handshake initiation - parse, report, and raise the LLM event.
328    ///
329    /// No IKE response is ever transmitted: this honeypot is receive-only.
330    #[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        // Build event for the script/static handler or the LLM
359        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        // Route through the event handler / LLM. Any configured script or static
396        // handler runs in-process with no model call; otherwise this falls back to
397        // the LLM. The resulting actions are classification decisions only - the
398        // honeypot still transmits nothing.
399        let protocol = IpsecProtocol::new();
400        match call_llm(
401            llm_client, app_state, server_id,
402            None, // IKE is connectionless; no per-connection state is tracked
403            &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                // Three outcomes have to stay distinguishable in the log, because the
413                // wire cannot distinguish them: this honeypot is receive-only and every
414                // one of them looks identical to the peer (nothing arrives). An operator
415                // greps `decision=` to tell "the model refused" from "the model said
416                // nothing" from "the backend broke".
417                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                // Deliberately no wire response. There is nothing correct to send: the
438                // honeypot never transmits (see the module docs), and an IKE NOTIFY here
439                // would both fingerprint it and answer for a negotiation it cannot run.
440                // Classify anyway so an overloaded backend is distinguishable from a
441                // broken one in the log; the error itself goes to the log and the status
442                // stream, never to the peer.
443                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}