pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::state::app_state::AppState;
use actions::{IpsecProtocol, IPSEC_HANDSHAKE_EVENT};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tracing::{debug, error, info, trace};
pub const MAX_PACKET_SIZE: usize = 65535;
const IKE_HEADER_SIZE: usize = 28;
const IKEV2_VERSION: u8 = 0x20;
const IKE_SA_INIT: u8 = 34;
const IKE_AUTH: u8 = 35;
const CREATE_CHILD_SA: u8 = 36;
const INFORMATIONAL: u8 = 37;
const IKEV1_IDENTITY_PROTECTION: u8 = 2;
const IKEV1_AGGRESSIVE: u8 = 4;
const FLAG_INITIATOR: u8 = 0x08; const FLAG_VERSION: u8 = 0x10; const FLAG_RESPONSE: u8 = 0x20;
const PAYLOAD_NONE: u8 = 0;
const 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;
impl IpsecServer {
pub async fn spawn_with_llm_actions(
bind_addr: SocketAddr,
llm_client: Arc<OllamaClient>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<SocketAddr> {
info!(
"Starting IPSec/IKEv2 parse-and-log honeypot on {}",
bind_addr
);
Log::new(Some(&status_tx)).info(format!(
"Starting IPSec/IKEv2 honeypot on {} (parses and logs IKE, never replies)",
bind_addr
));
let socket = UdpSocket::bind(bind_addr).await?;
let local_addr = socket.local_addr()?;
Log::new(Some(&status_tx))
.info(format!("IPSec/IKEv2 honeypot listening on {}", local_addr));
let socket = Arc::new(socket);
let socket_clone = socket.clone();
let task_registrar = app_state.clone();
let accept_handle = tokio::spawn(async move {
if let Err(e) =
Self::handle_packets(socket_clone, llm_client, app_state, server_id, status_tx)
.await
{
error!("IPSec/IKEv2 honeypot error: {}", e);
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
async fn handle_packets(
socket: Arc<UdpSocket>,
llm_client: Arc<OllamaClient>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut buf = vec![0u8; MAX_PACKET_SIZE];
loop {
let (len, peer_addr) = match socket.recv_from(&mut buf).await {
Ok(result) => result,
Err(e) => {
error!("UDP recv error: {}", e);
continue;
}
};
let packet = &buf[..len];
if len < IKE_HEADER_SIZE {
trace!(
"Received undersized packet from {} ({} bytes)",
peer_addr,
len
);
continue;
}
let initiator_spi = u64::from_be_bytes([
packet[0], packet[1], packet[2], packet[3], packet[4], packet[5], packet[6],
packet[7],
]);
let responder_spi = u64::from_be_bytes([
packet[8], packet[9], packet[10], packet[11], packet[12], packet[13], packet[14],
packet[15],
]);
let next_payload = packet[16];
let version = packet[17];
let exchange_type = packet[18];
let flags = packet[19];
let message_id = u32::from_be_bytes([packet[20], packet[21], packet[22], packet[23]]);
let packet_length =
u32::from_be_bytes([packet[24], packet[25], packet[26], packet[27]]);
let is_initiator = (flags & FLAG_INITIATOR) != 0;
let is_response = (flags & FLAG_RESPONSE) != 0;
let version_bit = (flags & FLAG_VERSION) != 0;
let payload_types = Self::extract_payload_types(packet, next_payload);
let (ike_version, exchange_name, is_handshake) = if version == IKEV2_VERSION {
let (name, handshake) = match exchange_type {
IKE_SA_INIT => ("IKE_SA_INIT", true),
IKE_AUTH => ("IKE_AUTH", true),
CREATE_CHILD_SA => ("CREATE_CHILD_SA", false),
INFORMATIONAL => ("INFORMATIONAL", false),
_ => ("Unknown", false),
};
("IKEv2", name, handshake)
} else {
let (name, handshake) = match exchange_type {
IKEV1_IDENTITY_PROTECTION => ("Identity Protection", true),
IKEV1_AGGRESSIVE => ("Aggressive Mode", true),
_ => ("Unknown", false),
};
("IKEv1", name, handshake)
};
let payload_names = Self::format_payload_types(&payload_types);
trace!(
"IKE packet from {}: version={}, exchange={}, flags=0x{:02x} (I={}, R={}, V={}), msg_id={}, len={}, payloads=[{}]",
peer_addr,
ike_version,
exchange_name,
flags,
if is_initiator { "1" } else { "0" },
if is_response { "1" } else { "0" },
if version_bit { "1" } else { "0" },
message_id,
packet_length,
payload_names
);
Log::new(Some(&status_tx)).trace(format!(
"IPSec: {} {} from {} ({} bytes, payloads=[{}])",
ike_version, exchange_name, peer_addr, len, payload_names
));
if is_handshake {
Self::handle_handshake_initiation(
peer_addr,
packet,
ike_version,
exchange_name,
initiator_spi,
responder_spi,
is_initiator,
is_response,
message_id,
&payload_types,
&llm_client,
&app_state,
server_id,
&status_tx,
)
.await;
} else {
debug!(
"IPSec {} {} from {} (honeypot: logged only, payloads=[{}])",
ike_version, exchange_name, peer_addr, payload_names
);
Log::new(Some(&status_tx)).debug(format!(
"IPSec: {} {} from {} (logged, payloads=[{}])",
ike_version, exchange_name, peer_addr, payload_names
));
}
}
}
fn extract_payload_types(packet: &[u8], mut next_payload: u8) -> Vec<u8> {
let mut payload_types = Vec::new();
let mut offset = IKE_HEADER_SIZE;
while next_payload != PAYLOAD_NONE && offset + 4 <= packet.len() {
payload_types.push(next_payload);
if offset + 4 > packet.len() {
break;
}
let payload_length =
u16::from_be_bytes([packet[offset + 2], packet[offset + 3]]) as usize;
if payload_length < 4 || offset + payload_length > packet.len() {
break;
}
next_payload = packet[offset];
offset += payload_length;
}
payload_types
}
fn format_payload_types(payload_types: &[u8]) -> String {
payload_types
.iter()
.map(|&p| Self::payload_type_name(p))
.collect::<Vec<_>>()
.join(", ")
}
fn payload_type_name(payload_type: u8) -> &'static str {
match payload_type {
PAYLOAD_SA => "SA",
PAYLOAD_KE => "KE",
PAYLOAD_IDI => "IDi",
PAYLOAD_IDR => "IDr",
PAYLOAD_CERT => "CERT",
PAYLOAD_CERTREQ => "CERTREQ",
PAYLOAD_AUTH => "AUTH",
PAYLOAD_NONCE => "NONCE",
PAYLOAD_NOTIFY => "NOTIFY",
PAYLOAD_DELETE => "DELETE",
PAYLOAD_VENDOR => "VENDOR",
PAYLOAD_TSI => "TSi",
PAYLOAD_TSR => "TSr",
PAYLOAD_SK => "SK",
PAYLOAD_CP => "CP",
PAYLOAD_EAP => "EAP",
_ => "UNKNOWN",
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_handshake_initiation(
peer_addr: SocketAddr,
packet: &[u8],
ike_version: &str,
exchange_type: &str,
initiator_spi: u64,
responder_spi: u64,
is_initiator: bool,
is_response: bool,
message_id: u32,
payload_types: &[u8],
llm_client: &OllamaClient,
app_state: &AppState,
server_id: crate::state::ServerId,
status_tx: &mpsc::UnboundedSender<String>,
) {
let payload_names = Self::format_payload_types(payload_types);
info!(
"IPSec {} handshake from {} (honeypot, payloads=[{}])",
ike_version, peer_addr, payload_names
);
Log::new(Some(status_tx)).info(format!(
"IPSec: {} handshake from {} (payloads=[{}])",
ike_version, peer_addr, payload_names
));
let event = Event::new(
&IPSEC_HANDSHAKE_EVENT,
serde_json::json!({
"peer_addr": peer_addr.to_string(),
"packet_size": packet.len(),
"ike_version": ike_version,
"exchange_type": exchange_type,
"initiator_spi": format!("{:016x}", initiator_spi),
"responder_spi": format!("{:016x}", responder_spi),
"is_initiator": is_initiator,
"is_response": is_response,
"message_id": message_id,
"payloads": payload_types.iter().map(|&p| Self::payload_type_name(p)).collect::<Vec<_>>(),
"honeypot_mode": true,
"responds_to_peer": false,
"analysis": {
"expected_payloads": if exchange_type == "IKE_SA_INIT" {
"SA, KE, NONCE"
} else if exchange_type == "IKE_AUTH" {
"IDi, AUTH, SA, TSi, TSr"
} else {
"varies"
},
"has_encryption": payload_types.contains(&PAYLOAD_SK),
"has_vendor_id": payload_types.contains(&PAYLOAD_VENDOR),
"has_certificate": payload_types.contains(&PAYLOAD_CERT) || payload_types.contains(&PAYLOAD_CERTREQ),
}
}),
);
debug!(
"IPSec/IKEv2 handshake analyzed from {} ({} payloads detected)",
peer_addr,
payload_types.len()
);
let protocol = IpsecProtocol::new();
match call_llm(
llm_client, app_state, server_id,
None, &event, &protocol,
)
.await
{
Ok(result) => {
let log = Log::new(Some(status_tx));
for message in &result.messages {
log.info(format!("{}", message));
}
let decision =
if result.raw_actions.iter().any(|a| {
a.get("type").and_then(|t| t.as_str()) == Some("reject_connection")
}) {
"model_reject"
} else if result.raw_actions.is_empty() {
"no_answer"
} else {
"model_answer"
};
info!(
"IPSec {} {} from {} decision={} actions={} (honeypot, nothing sent)",
ike_version,
exchange_type,
peer_addr,
decision,
result.raw_actions.len()
);
}
Err(e) => {
let decision = match crate::utils::WireFailure::classify(&e) {
crate::utils::WireFailure::Overloaded => "fail_closed_overloaded",
crate::utils::WireFailure::Unavailable => "fail_closed_unavailable",
};
error!(
"IPSec {} {} from {} decision={} (honeypot, nothing sent): {:#}",
ike_version, exchange_type, peer_addr, decision, e
);
Log::new(Some(status_tx)).error(format!(
"IPSec handshake event handling failed ({}): {}",
decision, e
));
}
}
}
}