use crate::llm::actions::{
protocol_trait::{ActionResult, Protocol, Server},
ActionDefinition, Parameter,
};
use crate::protocol::log_template::LogTemplate;
use crate::protocol::EventType;
use crate::state::app_state::AppState;
use anyhow::{Context, Result};
use serde_json::json;
use std::sync::LazyLock;
pub static IPSEC_HANDSHAKE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
EventType::new(
"ipsec_handshake",
"An IKE handshake datagram (IKEv2 IKE_SA_INIT/IKE_AUTH or IKEv1 Identity \
Protection/Aggressive Mode) was received and parsed. The honeypot sends no \
reply; respond with log_handshake, accept_connection or reject_connection to \
record how this attempt should be classified.",
json!({"type": "log_handshake", "details": "IKE handshake observed"}),
)
.with_actions(vec![
log_handshake_action(),
accept_connection_action(),
reject_connection_action(),
])
.with_log_template(
LogTemplate::new()
.with_info("{peer_addr} IKE {exchange_type} ({duration_ms}ms)")
.with_debug("IKE handshake from {peer_addr}: {ike_version} {exchange_type}")
.with_trace("IKE handshake: {json_pretty(.)}"),
)
});
pub fn get_ipsec_event_types() -> Vec<EventType> {
vec![IPSEC_HANDSHAKE_EVENT.clone()]
}
pub struct IpsecProtocol;
impl IpsecProtocol {
pub fn new() -> Self {
Self
}
}
impl Protocol for IpsecProtocol {
fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
vec![]
}
fn get_sync_actions(&self) -> Vec<ActionDefinition> {
vec![
accept_connection_action(),
reject_connection_action(),
log_handshake_action(),
]
}
fn protocol_name(&self) -> &'static str {
"IPSec/IKEv2"
}
fn get_event_types(&self) -> Vec<EventType> {
get_ipsec_event_types()
}
fn stack_name(&self) -> &'static str {
"ETH>IP>UDP>IPSEC"
}
fn keywords(&self) -> Vec<&'static str> {
vec!["ipsec", "ikev2", "ike"]
}
fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
use crate::protocol::metadata::{
DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
};
ProtocolMetadataV2::builder()
.connectionless()
.state(DevelopmentState::Experimental)
.deliberately_silent()
.privilege_requirement(PrivilegeRequirement::PrivilegedPort(500))
.well_known_udp_port(500)
.implementation(
"Receive-only IKE honeypot: manual 28-byte header parsing plus payload-chain \
walk. No cryptography, no Security Associations, no tunnel interface, and \
no packets are ever transmitted back to the peer.",
)
.llm_control(
"Classification of observed IKE attempts only (accept/reject/log labels \
recorded in the log). The LLM cannot influence what is sent on the wire, \
because nothing is sent.",
)
.e2e_testing("Crafted IKEv1/IKEv2 datagrams over UDP (detection only)")
.notes(
"NOT A VPN. Detects and analyses IKE reconnaissance; use WireGuard for a \
working tunnel. A real IKEv2 responder would need SA negotiation, DH, \
auth, ESP and kernel XFRM programming - deliberately out of scope.",
)
.max_inbound_bytes(crate::server::ipsec::MAX_PACKET_SIZE)
.build()
}
fn description(&self) -> &'static str {
"IPSec/IKEv2 honeypot - parses and logs IKE handshakes, never replies (not a VPN)"
}
fn example_prompt(&self) -> &'static str {
"Start an IPSec/IKEv2 honeypot on port 500 to analyze VPN reconnaissance attempts"
}
fn group_name(&self) -> &'static str {
"VPN & Routing"
}
fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
use crate::llm::actions::StartupExamples;
let script = r#"import json, sys
data = json.load(sys.stdin)
event = data["event"]
if data["event_type_id"] == "ipsec_handshake":
actions = [{"type": "log_handshake", "details": "IKE handshake observed"}]
else:
actions = []
print(json.dumps({"actions": actions}))"#;
StartupExamples::new(
json!({
"type": "open_server",
"port": 500,
"base_stack": "ipsec",
"instruction": "IPSec/IKEv2 honeypot. Log all IKE handshake attempts with detailed protocol analysis. Extract SPIs, exchange types, and payload information."
}),
json!({
"type": "open_server",
"port": 500,
"base_stack": "ipsec",
"event_handlers": [{
"event_pattern": "ipsec_handshake",
"handler": {
"type": "script",
"language": "python",
"code": script
}
}]
}),
json!({
"type": "open_server",
"port": 500,
"base_stack": "ipsec",
"event_handlers": [{
"event_pattern": "ipsec_handshake",
"handler": {
"type": "static",
"actions": [{"type": "log_handshake", "details": "IKE handshake detected"}]
}
}]
}),
)
}
}
impl Server for IpsecProtocol {
fn spawn(
&self,
ctx: crate::protocol::SpawnContext,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
> {
Box::pin(async move {
use crate::server::ipsec::IpsecServer;
use std::sync::Arc;
IpsecServer::spawn_with_llm_actions(
ctx.legacy_listen_addr(),
Arc::new(ctx.llm_client),
ctx.state,
ctx.server_id,
ctx.status_tx,
)
.await
})
}
fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
let action_type = action
.get("type")
.and_then(|v| v.as_str())
.context("Missing 'type' field in action")?;
match action_type {
"accept_connection" => self.execute_accept_connection(action),
"reject_connection" => self.execute_reject_connection(action),
"log_handshake" => self.execute_log_handshake(action),
"send_notify" => self.execute_send_notify(action),
"inspect_traffic" => self.execute_inspect_traffic(action),
"list_connections" => Ok(ActionResult::NoAction), "close_connection" => Ok(ActionResult::NoAction), _ => Err(anyhow::anyhow!("Unknown IPSec action: {}", action_type)),
}
}
}
impl IpsecProtocol {
fn execute_accept_connection(&self, action: serde_json::Value) -> Result<ActionResult> {
let _message = action
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Connection accepted");
Ok(ActionResult::NoAction)
}
fn execute_reject_connection(&self, action: serde_json::Value) -> Result<ActionResult> {
let _reason = action
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("Unauthorized");
Ok(ActionResult::NoAction)
}
fn execute_log_handshake(&self, action: serde_json::Value) -> Result<ActionResult> {
let _details = action
.get("details")
.and_then(|v| v.as_str())
.unwrap_or("Handshake logged");
Ok(ActionResult::NoAction)
}
fn execute_send_notify(&self, action: serde_json::Value) -> Result<ActionResult> {
let _notify_type = action
.get("notify_type")
.and_then(|v| v.as_str())
.unwrap_or("NO_PROPOSAL_CHOSEN");
Ok(ActionResult::NoAction)
}
fn execute_inspect_traffic(&self, action: serde_json::Value) -> Result<ActionResult> {
let _inspect = action
.get("inspect")
.and_then(|v| v.as_bool())
.unwrap_or(true);
Ok(ActionResult::NoAction)
}
}
fn accept_connection_action() -> ActionDefinition {
ActionDefinition {
name: "accept_connection".to_string(),
description: "Classify this IKE attempt as legitimate and record it in the log. No SA \
is established and no IKE response is sent - the honeypot is \
receive-only. Use this to label benign/expected VPN clients."
.to_string(),
parameters: vec![Parameter {
name: "message".to_string(),
type_hint: "string".to_string(),
description: "Optional message to log".to_string(),
required: false,
}],
example: json!({
"type": "accept_connection",
"message": "Legitimate VPN connection accepted"
}),
log_template: Some(
LogTemplate::new()
.with_info("-> IKE accept")
.with_debug("IKE connection accepted: {message}"),
),
}
}
fn reject_connection_action() -> ActionDefinition {
ActionDefinition {
name: "reject_connection".to_string(),
description: "Classify this IKE attempt as unwanted (scan, probe, misconfigured client) \
and record the reason. The packet is silently dropped either way - no \
rejection is transmitted, because the honeypot never replies."
.to_string(),
parameters: vec![Parameter {
name: "reason".to_string(),
type_hint: "string".to_string(),
description: "Reason for rejection".to_string(),
required: false,
}],
example: json!({
"type": "reject_connection",
"reason": "Suspicious reconnaissance attempt"
}),
log_template: Some(
LogTemplate::new()
.with_info("-> IKE reject: {reason}")
.with_debug("IKE connection rejected: {reason}"),
),
}
}
fn log_handshake_action() -> ActionDefinition {
ActionDefinition {
name: "log_handshake".to_string(),
description: "Record an analyst note about this IKE handshake (what it looks like, why \
it matters). This is the primary action for this protocol."
.to_string(),
parameters: vec![Parameter {
name: "details".to_string(),
type_hint: "string".to_string(),
description: "Additional details to log".to_string(),
required: false,
}],
example: json!({
"type": "log_handshake",
"details": "VPN scan attempt detected"
}),
log_template: Some(
LogTemplate::new()
.with_info("-> IKE logged: {details}")
.with_debug("IKE handshake logged: {details}"),
),
}
}