diff options
Diffstat (limited to '')
-rw-r--r-- | rust/src/ike/detect.rs | 237 | ||||
-rw-r--r-- | rust/src/ike/ike.rs | 449 | ||||
-rw-r--r-- | rust/src/ike/ikev1.rs | 169 | ||||
-rw-r--r-- | rust/src/ike/ikev2.rs | 316 | ||||
-rw-r--r-- | rust/src/ike/logger.rs | 234 | ||||
-rw-r--r-- | rust/src/ike/mod.rs | 29 | ||||
-rw-r--r-- | rust/src/ike/parser.rs | 712 |
7 files changed, 2146 insertions, 0 deletions
diff --git a/rust/src/ike/detect.rs b/rust/src/ike/detect.rs new file mode 100644 index 0000000..64d27a4 --- /dev/null +++ b/rust/src/ike/detect.rs @@ -0,0 +1,237 @@ +/* Copyright (C) 2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Frank Honza <frank.honza@dcso.de> + +use super::ipsec_parser::IkeV2Transform; +use crate::ike::ike::*; +use std::ffi::CStr; +use std::ptr; + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_exch_type(tx: &mut IKETransaction, exch_type: *mut u8) -> u8 { + debug_validate_bug_on!(exch_type.is_null()); + + if tx.ike_version == 1 { + if let Some(r) = tx.hdr.ikev1_header.exchange_type { + unsafe { + *exch_type = r; + } + return 1; + } + } else if tx.ike_version == 2 { + unsafe { + *exch_type = tx.hdr.ikev2_header.exch_type.0; + } + return 1; + } + + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_spi_initiator( + tx: &mut IKETransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + debug_validate_bug_on!(buffer.is_null() || buffer_len.is_null()); + + unsafe { + *buffer = tx.hdr.spi_initiator.as_ptr(); + *buffer_len = tx.hdr.spi_initiator.len() as u32; + } + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_spi_responder( + tx: &mut IKETransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + debug_validate_bug_on!(buffer.is_null() || buffer_len.is_null()); + + unsafe { + *buffer = tx.hdr.spi_responder.as_ptr(); + *buffer_len = tx.hdr.spi_responder.len() as u32; + } + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_nonce( + tx: &mut IKETransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + debug_validate_bug_on!(buffer.is_null() || buffer_len.is_null()); + + if tx.ike_version == 1 && !tx.hdr.ikev1_header.nonce.is_empty() { + let p = &tx.hdr.ikev1_header.nonce; + unsafe { + *buffer = p.as_ptr(); + *buffer_len = p.len() as u32; + } + return 1; + } + + unsafe { + *buffer = ptr::null(); + *buffer_len = 0; + } + + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_key_exchange( + tx: &mut IKETransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + debug_validate_bug_on!(buffer.is_null() || buffer_len.is_null()); + + if tx.ike_version == 1 && !tx.hdr.ikev1_header.key_exchange.is_empty() { + let p = &tx.hdr.ikev1_header.key_exchange; + unsafe { + *buffer = p.as_ptr(); + *buffer_len = p.len() as u32; + } + return 1; + } + + unsafe { + *buffer = ptr::null(); + *buffer_len = 0; + } + + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_ike_tx_get_vendor( + tx: &IKETransaction, i: u32, buf: *mut *const u8, len: *mut u32, +) -> u8 { + if tx.ike_version == 1 && i < tx.hdr.ikev1_header.vendor_ids.len() as u32 { + unsafe { + *len = tx.hdr.ikev1_header.vendor_ids[i as usize].len() as u32; + *buf = tx.hdr.ikev1_header.vendor_ids[i as usize].as_ptr(); + } + return 1; + } + + unsafe { + *buf = ptr::null(); + *len = 0; + } + + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_get_sa_attribute( + tx: &mut IKETransaction, sa_type: *const std::os::raw::c_char, value: *mut u32, +) -> u8 { + debug_validate_bug_on!(value.is_null()); + let mut ret_val = 0; + let mut ret_code = 0; + let sa_type_s: Result<_, _>; + + unsafe { sa_type_s = CStr::from_ptr(sa_type).to_str() } + SCLogInfo!("{:#?}", sa_type_s); + + if let Ok(sa) = sa_type_s { + if tx.ike_version == 1 { + if !tx.hdr.ikev1_transforms.is_empty() { + // there should be only one chosen server_transform, check event + if let Some(server_transform) = tx.hdr.ikev1_transforms.first() { + for attr in server_transform { + if attr.attribute_type.to_string() == sa { + if let Some(numeric_value) = attr.numeric_value { + ret_val = numeric_value; + ret_code = 1; + break; + } + } + } + } + } + } else if tx.ike_version == 2 { + for attr in tx.hdr.ikev2_transforms.iter() { + match attr { + IkeV2Transform::Encryption(e) => { + if sa == "alg_enc" { + ret_val = e.0 as u32; + ret_code = 1; + break; + } + } + IkeV2Transform::Auth(e) => { + if sa == "alg_auth" { + ret_val = e.0 as u32; + ret_code = 1; + break; + } + } + IkeV2Transform::PRF(ref e) => { + if sa == "alg_prf" { + ret_val = e.0 as u32; + ret_code = 1; + break; + } + } + IkeV2Transform::DH(ref e) => { + if sa == "alg_dh" { + ret_val = e.0 as u32; + ret_code = 1; + break; + } + } + _ => (), + } + } + } + } + + unsafe { + *value = ret_val; + } + return ret_code; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_get_key_exchange_payload_length( + tx: &mut IKETransaction, value: *mut u32, +) -> u8 { + debug_validate_bug_on!(value.is_null()); + + if tx.ike_version == 1 && !tx.hdr.ikev1_header.key_exchange.is_empty() { + *value = tx.hdr.ikev1_header.key_exchange.len() as u32; + return 1; + } + + *value = 0; + return 0; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_get_nonce_payload_length( + tx: &mut IKETransaction, value: *mut u32, +) -> u8 { + debug_validate_bug_on!(value.is_null()); + + if tx.ike_version == 1 && !tx.hdr.ikev1_header.nonce.is_empty() { + *value = tx.hdr.ikev1_header.nonce.len() as u32; + return 1; + } + + *value = 0; + return 0; +} diff --git a/rust/src/ike/ike.rs b/rust/src/ike/ike.rs new file mode 100644 index 0000000..2a5448a --- /dev/null +++ b/rust/src/ike/ike.rs @@ -0,0 +1,449 @@ +/* Copyright (C) 2020-2021 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Frank Honza <frank.honza@dcso.de> + +extern crate ipsec_parser; +use self::ipsec_parser::*; + +use crate::applayer; +use crate::applayer::*; +use crate::core::{self, *}; +use crate::ike::ikev1::{handle_ikev1, IkeV1Header, Ikev1Container}; +use crate::ike::ikev2::{handle_ikev2, Ikev2Container}; +use crate::ike::parser::*; +use nom7::Err; +use std; +use std::collections::HashSet; +use std::ffi::CString; + +#[derive(AppLayerEvent)] +pub enum IkeEvent { + MalformedData, + NoEncryption, + WeakCryptoEnc, + WeakCryptoPrf, + WeakCryptoDh, + WeakCryptoAuth, + WeakCryptoNoDh, + WeakCryptoNoAuth, + InvalidProposal, + UnknownProposal, + PayloadExtraData, + MultipleServerProposal, +} + +pub struct IkeHeaderWrapper { + pub spi_initiator: String, + pub spi_responder: String, + pub maj_ver: u8, + pub min_ver: u8, + pub msg_id: u32, + pub flags: u8, + pub ikev1_transforms: Vec<Vec<SaAttribute>>, + pub ikev2_transforms: Vec<IkeV2Transform>, + pub ikev1_header: IkeV1Header, + pub ikev2_header: IkeV2Header, +} + +impl Default for IkeHeaderWrapper { + fn default() -> Self { + Self::new() + } +} + +impl IkeHeaderWrapper { + pub fn new() -> Self { + Self { + spi_initiator: String::new(), + spi_responder: String::new(), + maj_ver: 0, + min_ver: 0, + msg_id: 0, + flags: 0, + ikev1_transforms: Vec::new(), + ikev2_transforms: Vec::new(), + ikev1_header: IkeV1Header::default(), + ikev2_header: IkeV2Header { + init_spi: 0, + resp_spi: 0, + next_payload: IkePayloadType::NoNextPayload, + maj_ver: 0, + min_ver: 0, + exch_type: IkeExchangeType(0), + flags: 0, + msg_id: 0, + length: 0, + }, + } + } +} + +#[derive(Default)] +pub struct IkePayloadWrapper { + pub ikev1_payload_types: Option<HashSet<u8>>, + pub ikev2_payload_types: Vec<IkePayloadType>, +} + +#[derive(Default)] +pub struct IKETransaction { + tx_id: u64, + + pub ike_version: u8, + pub direction: Direction, + pub hdr: IkeHeaderWrapper, + pub payload_types: IkePayloadWrapper, + pub notify_types: Vec<NotifyType>, + + /// errors seen during exchange + pub errors: u32, + + logged: LoggerFlags, + pub tx_data: applayer::AppLayerTxData, +} + +impl Transaction for IKETransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +impl IKETransaction { + pub fn new(direction: Direction) -> Self { + Self { + direction, + tx_data: applayer::AppLayerTxData::for_direction(direction), + ..Default::default() + } + } + + /// Set an event. + pub fn set_event(&mut self, event: IkeEvent) { + self.tx_data.set_event(event as u8); + } +} + +#[derive(Default)] +pub struct IKEState { + state_data: AppLayerStateData, + tx_id: u64, + pub transactions: Vec<IKETransaction>, + + pub ikev1_container: Ikev1Container, + pub ikev2_container: Ikev2Container, +} + +impl State<IKETransaction> for IKEState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&IKETransaction> { + self.transactions.get(index) + } +} + +impl IKEState { + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let tx = self + .transactions + .iter() + .position(|tx| tx.tx_id == tx_id + 1); + debug_assert!(tx.is_some()); + if let Some(idx) = tx { + let _ = self.transactions.remove(idx); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&mut IKETransaction> { + self.transactions.iter_mut().find(|tx| tx.tx_id == tx_id + 1) + } + + pub fn new_tx(&mut self, direction: Direction) -> IKETransaction { + let mut tx = IKETransaction::new(direction); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + /// Set an event. The event is set on the most recent transaction. + pub fn set_event(&mut self, event: IkeEvent) { + if let Some(tx) = self.transactions.last_mut() { + tx.set_event(event); + } else { + SCLogDebug!( + "IKE: trying to set event {} on non-existing transaction", + event as u32 + ); + } + } + + fn handle_input(&mut self, input: &[u8], direction: Direction) -> AppLayerResult { + // We're not interested in empty requests. + if input.is_empty() { + return AppLayerResult::ok(); + } + + let mut current = input; + match parse_isakmp_header(current) { + Ok((rem, isakmp_header)) => { + current = rem; + + if isakmp_header.maj_ver != 1 && isakmp_header.maj_ver != 2 { + SCLogDebug!("Unsupported ISAKMP major_version"); + return AppLayerResult::err(); + } + + if isakmp_header.maj_ver == 1 { + handle_ikev1(self, current, isakmp_header, direction); + } else if isakmp_header.maj_ver == 2 { + handle_ikev2(self, current, isakmp_header, direction); + } else { + return AppLayerResult::err(); + } + return AppLayerResult::ok(); // todo either remove outer loop or check header length-field if we have completely read everything + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Insufficient data while parsing IKE"); + return AppLayerResult::err(); + } + Err(_) => { + SCLogDebug!("Error while parsing IKE packet"); + return AppLayerResult::err(); + } + } + } +} + +/// Probe to see if this input looks like a request or response. +fn probe(input: &[u8], direction: Direction, rdir: *mut u8) -> bool { + match parse_isakmp_header(input) { + Ok((_, isakmp_header)) => { + if isakmp_header.maj_ver == 1 { + if isakmp_header.resp_spi == 0 && direction != Direction::ToServer { + unsafe { + *rdir = Direction::ToServer.into(); + } + } + return true; + } else if isakmp_header.maj_ver == 2 { + if isakmp_header.min_ver != 0 { + SCLogDebug!( + "ipsec_probe: could be ipsec, but with unsupported/invalid version {}.{}", + isakmp_header.maj_ver, + isakmp_header.min_ver + ); + return false; + } + if isakmp_header.exch_type < 34 || isakmp_header.exch_type > 37 { + SCLogDebug!("ipsec_probe: could be ipsec, but with unsupported/invalid exchange type {}", + isakmp_header.exch_type); + return false; + } + if isakmp_header.length as usize != input.len() { + SCLogDebug!("ipsec_probe: could be ipsec, but length does not match"); + return false; + } + + if isakmp_header.resp_spi == 0 && direction != Direction::ToServer { + unsafe { + *rdir = Direction::ToServer.into(); + } + } + return true; + } + + return false; + } + Err(_) => return false, + } +} + +// C exports. + +/// C entry point for a probing parser. +#[no_mangle] +pub unsafe extern "C" fn rs_ike_probing_parser( + _flow: *const Flow, direction: u8, input: *const u8, input_len: u32, rdir: *mut u8, +) -> AppProto { + if input_len < 28 { + // at least the ISAKMP_HEADER must be there, not ALPROTO_UNKNOWN because over UDP + return ALPROTO_FAILED; + } + + if !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if probe(slice, direction.into(), rdir) { + return ALPROTO_IKE; + } + } + return ALPROTO_FAILED; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_new( + _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { + let state = IKEState::default(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut _; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_free(state: *mut std::os::raw::c_void) { + // Just unbox... + std::mem::drop(Box::from_raw(state as *mut IKEState)); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { + let state = cast_pointer!(state, IKEState); + state.free_tx(tx_id); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_parse_request( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, IKEState); + return state.handle_input(stream_slice.as_slice(), Direction::ToServer); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_parse_response( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, IKEState); + return state.handle_input(stream_slice.as_slice(), Direction::ToClient); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_get_tx( + state: *mut std::os::raw::c_void, tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, IKEState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { + let state = cast_pointer!(state, IKEState); + return state.tx_id; +} + +#[no_mangle] +pub extern "C" fn rs_ike_state_progress_completion_status(_direction: u8) -> std::os::raw::c_int { + // This parser uses 1 to signal transaction completion status. + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_ike_tx_get_alstate_progress( + _tx: *mut std::os::raw::c_void, _direction: u8, +) -> std::os::raw::c_int { + return 1; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_tx_get_logged( + _state: *mut std::os::raw::c_void, tx: *mut std::os::raw::c_void, +) -> u32 { + let tx = cast_pointer!(tx, IKETransaction); + return tx.logged.get(); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_tx_set_logged( + _state: *mut std::os::raw::c_void, tx: *mut std::os::raw::c_void, logged: u32, +) { + let tx = cast_pointer!(tx, IKETransaction); + tx.logged.set(logged); +} + +static mut ALPROTO_IKE: AppProto = ALPROTO_UNKNOWN; + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"ike\0"; +const PARSER_ALIAS: &[u8] = b"ikev2\0"; + +export_tx_data_get!(rs_ike_get_tx_data, IKETransaction); +export_state_data_get!(rs_ike_get_state_data, IKEState); + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_register_parser() { + let default_port = CString::new("500").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: core::IPPROTO_UDP, + probe_ts: Some(rs_ike_probing_parser), + probe_tc: Some(rs_ike_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_ike_state_new, + state_free: rs_ike_state_free, + tx_free: rs_ike_state_tx_free, + parse_ts: rs_ike_parse_request, + parse_tc: rs_ike_parse_response, + get_tx_count: rs_ike_state_get_tx_count, + get_tx: rs_ike_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_ike_tx_get_alstate_progress, + get_eventinfo: Some(IkeEvent::get_event_info), + get_eventinfo_byid: Some(IkeEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(applayer::state_get_tx_iterator::<IKEState, IKETransaction>), + get_tx_data: rs_ike_get_tx_data, + get_state_data: rs_ike_get_state_data, + apply_tx_config: None, + flags: 0, + truncate: None, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("udp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_IKE = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + + AppLayerRegisterParserAlias( + PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + PARSER_ALIAS.as_ptr() as *const std::os::raw::c_char, + ); + SCLogDebug!("Rust IKE parser registered."); + } else { + SCLogDebug!("Protocol detector and parser disabled for IKE."); + } +} diff --git a/rust/src/ike/ikev1.rs b/rust/src/ike/ikev1.rs new file mode 100644 index 0000000..1e79c29 --- /dev/null +++ b/rust/src/ike/ikev1.rs @@ -0,0 +1,169 @@ +/* Copyright (C) 2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Frank Honza <frank.honza@dcso.de> + +use crate::applayer::*; +use crate::common::to_hex; +use crate::core::Direction; +use crate::ike::ike::{IKEState, IkeEvent}; +use crate::ike::parser::*; +use nom7::Err; +use std; +use std::collections::HashSet; + +#[derive(Default)] +pub struct IkeV1Header { + pub exchange_type: Option<u8>, + pub encrypted_payloads: bool, + + pub key_exchange: Vec<u8>, + pub nonce: Vec<u8>, + pub vendor_ids: Vec<String>, +} + +#[derive(Default)] +pub struct Ikev1ParticipantData { + pub key_exchange: String, + pub nonce: String, + pub nb_transforms: u64, + pub transform: Vec<SaAttribute>, +} + +impl Ikev1ParticipantData { + pub fn reset(&mut self) { + self.key_exchange.clear(); + self.nonce.clear(); + self.nb_transforms = 0; + self.transform.clear(); + } + + pub fn update( + &mut self, key_exchange: &str, nonce: &str, transforms: &Vec<Vec<SaAttribute>>, + ) { + self.key_exchange = key_exchange.to_string(); + self.nonce = nonce.to_string(); + if self.nb_transforms == 0 && !transforms.is_empty() { + self.transform.extend(transforms[0].iter().cloned()); + } + self.nb_transforms += transforms.len() as u64; + } +} + +#[derive(Default)] +pub struct Ikev1Container { + pub domain_of_interpretation: Option<u32>, + pub client: Ikev1ParticipantData, + pub server: Ikev1ParticipantData, +} + +pub fn handle_ikev1( + state: &mut IKEState, current: &[u8], isakmp_header: IsakmpHeader, direction: Direction, +) -> AppLayerResult { + let mut tx = state.new_tx(direction); + + tx.ike_version = 1; + tx.hdr.spi_initiator = format!("{:016x}", isakmp_header.init_spi); + tx.hdr.spi_responder = format!("{:016x}", isakmp_header.resp_spi); + tx.hdr.maj_ver = isakmp_header.maj_ver; + tx.hdr.min_ver = isakmp_header.min_ver; + tx.hdr.ikev1_header.exchange_type = Some(isakmp_header.exch_type); + tx.hdr.msg_id = isakmp_header.msg_id; + tx.hdr.flags = isakmp_header.flags; + + let mut cur_payload_type = isakmp_header.next_payload; + let mut payload_types: HashSet<u8> = HashSet::new(); + payload_types.insert(cur_payload_type); + + if isakmp_header.flags & 0x01 != 0x01 { + match parse_ikev1_payload_list(current) { + Ok((rem, payload_list)) => { + for isakmp_payload in payload_list { + if parse_payload( + cur_payload_type, + isakmp_payload.data, + isakmp_payload.data.len() as u16, + &mut state.ikev1_container.domain_of_interpretation, + &mut tx.hdr.ikev1_header.key_exchange, + &mut tx.hdr.ikev1_header.nonce, + &mut tx.hdr.ikev1_transforms, + &mut tx.hdr.ikev1_header.vendor_ids, + &mut payload_types, + ).is_err() { + SCLogDebug!("Error while parsing IKEV1 payloads"); + return AppLayerResult::err(); + } + + cur_payload_type = isakmp_payload.payload_header.next_payload; + } + + if payload_types.contains(&(IsakmpPayloadType::SecurityAssociation as u8)) { + // clear transforms on a new SA in case there is happening a new key exchange + // on the same flow, elsewise properties would be added to the old/other SA + if direction == Direction::ToServer { + state.ikev1_container.client.reset(); + } else { + state.ikev1_container.server.reset(); + } + } + + // add transaction values to state values + if direction == Direction::ToServer { + state.ikev1_container.client.update( + &to_hex(tx.hdr.ikev1_header.key_exchange.as_ref()), + &to_hex(tx.hdr.ikev1_header.nonce.as_ref()), + &tx.hdr.ikev1_transforms, + ); + } else { + if state.ikev1_container.server.nb_transforms <= 1 + && state.ikev1_container.server.nb_transforms + + tx.hdr.ikev1_transforms.len() as u64 + > 1 + { + SCLogDebug!("More than one chosen server proposal"); + state.set_event(IkeEvent::MultipleServerProposal); + } + + state.ikev1_container.server.update( + &to_hex(tx.hdr.ikev1_header.key_exchange.as_ref()), + &to_hex(tx.hdr.ikev1_header.nonce.as_ref()), + &tx.hdr.ikev1_transforms, + ); + } + + if !rem.is_empty() { + // more data left unread than should be + SCLogDebug!("Unread Payload Data"); + state.set_event(IkeEvent::PayloadExtraData); + } + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Insufficient data while parsing IKEV1"); + return AppLayerResult::err(); + } + Err(_) => { + SCLogDebug!("Error while parsing payloads and adding to the state"); + return AppLayerResult::err(); + } + } + } + + tx.payload_types.ikev1_payload_types = Some(payload_types); + tx.hdr.ikev1_header.encrypted_payloads = isakmp_header.flags & 0x01 == 0x01; + state.transactions.push(tx); + return AppLayerResult::ok(); +} diff --git a/rust/src/ike/ikev2.rs b/rust/src/ike/ikev2.rs new file mode 100644 index 0000000..a1be25f --- /dev/null +++ b/rust/src/ike/ikev2.rs @@ -0,0 +1,316 @@ +/* Copyright (C) 2017-2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// written by Pierre Chifflier <chifflier@wzdftpd.net> + +use crate::applayer::*; +use crate::core::Direction; +use crate::ike::ipsec_parser::*; + +use super::ipsec_parser::IkeV2Transform; +use crate::ike::ike::{IKEState, IKETransaction, IkeEvent}; +use crate::ike::parser::IsakmpHeader; +use ipsec_parser::{IkeExchangeType, IkePayloadType, IkeV2Header}; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum IKEV2ConnectionState { + Init, + InitSASent, + InitKESent, + InitNonceSent, + RespSASent, + RespKESent, + + ParsingDone, + + Invalid, +} + +impl IKEV2ConnectionState { + pub fn advance(&self, payload: &IkeV2Payload) -> IKEV2ConnectionState { + use self::IKEV2ConnectionState::*; + match (self, &payload.content) { + (&Init, &IkeV2PayloadContent::SA(_)) => InitSASent, + (&InitSASent, &IkeV2PayloadContent::KE(_)) => InitKESent, + (&InitKESent, &IkeV2PayloadContent::Nonce(_)) => InitNonceSent, + (&InitNonceSent, &IkeV2PayloadContent::SA(_)) => RespSASent, + (&RespSASent, &IkeV2PayloadContent::KE(_)) => RespKESent, + (&RespKESent, &IkeV2PayloadContent::Nonce(_)) => ParsingDone, // should go to RespNonceSent, + (&ParsingDone, _) => self.clone(), + (_, &IkeV2PayloadContent::Notify(_)) => self.clone(), + (_, &IkeV2PayloadContent::Dummy) => self.clone(), + (_, _) => Invalid, + } + } +} + +pub struct Ikev2Container { + /// The connection state + pub connection_state: IKEV2ConnectionState, + + /// The transforms proposed by the initiator + pub client_transforms: Vec<Vec<IkeV2Transform>>, + + /// The encryption algorithm selected by the responder + pub alg_enc: IkeTransformEncType, + /// The authentication algorithm selected by the responder + pub alg_auth: IkeTransformAuthType, + /// The PRF algorithm selected by the responder + pub alg_prf: IkeTransformPRFType, + /// The Diffie-Hellman algorithm selected by the responder + pub alg_dh: IkeTransformDHType, + /// The extended sequence numbers parameter selected by the responder + pub alg_esn: IkeTransformESNType, + /// The Diffie-Hellman group from the server KE message, if present. + pub dh_group: IkeTransformDHType, +} + +impl Default for Ikev2Container { + fn default() -> Ikev2Container { + Ikev2Container { + connection_state: IKEV2ConnectionState::Init, + dh_group: IkeTransformDHType::None, + client_transforms: Vec::new(), + alg_enc: IkeTransformEncType::ENCR_NULL, + alg_auth: IkeTransformAuthType::NONE, + alg_prf: IkeTransformPRFType::PRF_NULL, + alg_dh: IkeTransformDHType::None, + alg_esn: IkeTransformESNType::NoESN, + } + } +} + +pub fn handle_ikev2( + state: &mut IKEState, current: &[u8], isakmp_header: IsakmpHeader, direction: Direction, +) -> AppLayerResult { + let hdr = IkeV2Header { + init_spi: isakmp_header.init_spi, + resp_spi: isakmp_header.resp_spi, + next_payload: IkePayloadType(isakmp_header.next_payload), + maj_ver: isakmp_header.maj_ver, + min_ver: isakmp_header.min_ver, + exch_type: IkeExchangeType(isakmp_header.exch_type), + flags: isakmp_header.flags, + msg_id: isakmp_header.msg_id, + length: isakmp_header.length, + }; + + let mut tx = state.new_tx(direction); + tx.ike_version = 2; + // use init_spi as transaction identifier + // tx.xid = hdr.init_spi; todo is this used somewhere? + tx.hdr.ikev2_header = hdr.clone(); + tx.hdr.spi_initiator = format!("{:016x}", isakmp_header.init_spi); + tx.hdr.spi_responder = format!("{:016x}", isakmp_header.resp_spi); + tx.hdr.maj_ver = isakmp_header.maj_ver; + tx.hdr.min_ver = isakmp_header.min_ver; + tx.hdr.msg_id = isakmp_header.msg_id; + tx.hdr.flags = isakmp_header.flags; + let mut payload_types = Vec::new(); + let mut errors = 0; + let mut notify_types = Vec::new(); + match parse_ikev2_payload_list(current, hdr.next_payload) { + Ok((_, Ok(ref p))) => { + for payload in p { + payload_types.push(payload.hdr.next_payload_type); + match payload.content { + IkeV2PayloadContent::Dummy => (), + IkeV2PayloadContent::SA(ref prop) => { + // if hdr.flags & IKEV2_FLAG_INITIATOR != 0 { + add_proposals(state, &mut tx, prop, direction); + // } + } + IkeV2PayloadContent::KE(ref kex) => { + SCLogDebug!("KEX {:?}", kex.dh_group); + if direction == Direction::ToClient { + state.ikev2_container.dh_group = kex.dh_group; + } + } + IkeV2PayloadContent::Nonce(ref _n) => { + SCLogDebug!("Nonce: {:?}", _n); + } + IkeV2PayloadContent::Notify(ref n) => { + SCLogDebug!("Notify: {:?}", n); + if n.notify_type.is_error() { + errors += 1; + } + notify_types.push(n.notify_type); + } + // XXX CertificateRequest + // XXX Certificate + // XXX Authentication + // XXX TSi + // XXX TSr + // XXX IDr + _ => { + SCLogDebug!("Unknown payload content {:?}", payload.content); + } + } + state.ikev2_container.connection_state = + state.ikev2_container.connection_state.advance(payload); + tx.payload_types + .ikev2_payload_types + .append(&mut payload_types); + tx.errors = errors; + tx.notify_types.append(&mut notify_types); + } + } + _e => { + SCLogDebug!("parse_ikev2_payload_with_type: {:?}", _e); + } + } + state.transactions.push(tx); + return AppLayerResult::ok(); +} + +fn add_proposals( + state: &mut IKEState, tx: &mut IKETransaction, prop: &Vec<IkeV2Proposal>, direction: Direction, +) { + for p in prop { + let transforms: Vec<IkeV2Transform> = p.transforms.iter().map(|x| x.into()).collect(); + // Rule 1: warn on weak or unknown transforms + for xform in &transforms { + match *xform { + IkeV2Transform::Encryption(ref enc) => { + match *enc { + IkeTransformEncType::ENCR_DES_IV64 + | IkeTransformEncType::ENCR_DES + | IkeTransformEncType::ENCR_3DES + | IkeTransformEncType::ENCR_RC5 + | IkeTransformEncType::ENCR_IDEA + | IkeTransformEncType::ENCR_CAST + | IkeTransformEncType::ENCR_BLOWFISH + | IkeTransformEncType::ENCR_3IDEA + | IkeTransformEncType::ENCR_DES_IV32 + | IkeTransformEncType::ENCR_NULL => { + SCLogDebug!("Weak Encryption: {:?}", enc); + // XXX send event only if direction == Direction::ToClient ? + tx.set_event(IkeEvent::WeakCryptoEnc); + } + _ => (), + } + } + IkeV2Transform::PRF(ref prf) => match *prf { + IkeTransformPRFType::PRF_NULL => { + SCLogDebug!("'Null' PRF transform proposed"); + tx.set_event(IkeEvent::InvalidProposal); + } + IkeTransformPRFType::PRF_HMAC_MD5 | IkeTransformPRFType::PRF_HMAC_SHA1 => { + SCLogDebug!("Weak PRF: {:?}", prf); + tx.set_event(IkeEvent::WeakCryptoPrf); + } + _ => (), + }, + IkeV2Transform::Auth(ref auth) => { + match *auth { + IkeTransformAuthType::NONE => { + // Note: this could be expected with an AEAD encryption alg. + // See rule 4 + } + IkeTransformAuthType::AUTH_HMAC_MD5_96 + | IkeTransformAuthType::AUTH_HMAC_SHA1_96 + | IkeTransformAuthType::AUTH_DES_MAC + | IkeTransformAuthType::AUTH_KPDK_MD5 + | IkeTransformAuthType::AUTH_AES_XCBC_96 + | IkeTransformAuthType::AUTH_HMAC_MD5_128 + | IkeTransformAuthType::AUTH_HMAC_SHA1_160 => { + SCLogDebug!("Weak auth: {:?}", auth); + tx.set_event(IkeEvent::WeakCryptoAuth); + } + _ => (), + } + } + IkeV2Transform::DH(ref dh) => match *dh { + IkeTransformDHType::None => { + SCLogDebug!("'None' DH transform proposed"); + tx.set_event(IkeEvent::InvalidProposal); + } + IkeTransformDHType::Modp768 + | IkeTransformDHType::Modp1024 + | IkeTransformDHType::Modp1024s160 + | IkeTransformDHType::Modp1536 => { + SCLogDebug!("Weak DH: {:?}", dh); + tx.set_event(IkeEvent::WeakCryptoDh); + } + _ => (), + }, + IkeV2Transform::Unknown(_tx_type, _tx_id) => { + SCLogDebug!("Unknown proposal: type={:?}, id={}", _tx_type, _tx_id); + tx.set_event(IkeEvent::UnknownProposal); + } + _ => (), + } + } + // Rule 2: check if no DH was proposed + if !transforms.iter().any(|x| match *x { + IkeV2Transform::DH(_) => true, + _ => false, + }) { + SCLogDebug!("No DH transform found"); + tx.set_event(IkeEvent::WeakCryptoNoDh); + } + // Rule 3: check if proposing AH ([RFC7296] section 3.3.1) + if p.protocol_id == ProtocolID::AH { + SCLogDebug!("Proposal uses protocol AH - no confidentiality"); + tx.set_event(IkeEvent::NoEncryption); + } + // Rule 4: lack of integrity is accepted only if using an AEAD proposal + // Look if no auth was proposed, including if proposal is Auth::None + if !transforms.iter().any(|x| match *x { + IkeV2Transform::Auth(IkeTransformAuthType::NONE) => false, + IkeV2Transform::Auth(_) => true, + _ => false, + }) && !transforms.iter().any(|x| match *x { + IkeV2Transform::Encryption(ref enc) => enc.is_aead(), + _ => false, + }) { + SCLogDebug!("No integrity transform found"); + tx.set_event(IkeEvent::WeakCryptoNoAuth); + } + // Finally + if direction == Direction::ToClient { + transforms.iter().for_each(|t| match *t { + IkeV2Transform::Encryption(ref e) => { + state.ikev2_container.alg_enc = *e; + tx.hdr.ikev2_transforms.push(IkeV2Transform::Encryption(*e)); + } + IkeV2Transform::Auth(ref a) => { + state.ikev2_container.alg_auth = *a; + tx.hdr.ikev2_transforms.push(IkeV2Transform::Auth(*a)); + } + IkeV2Transform::PRF(ref p) => { + state.ikev2_container.alg_prf = *p; + tx.hdr.ikev2_transforms.push(IkeV2Transform::PRF(*p)); + } + IkeV2Transform::DH(ref dh) => { + state.ikev2_container.alg_dh = *dh; + tx.hdr.ikev2_transforms.push(IkeV2Transform::DH(*dh)); + } + IkeV2Transform::ESN(ref e) => { + state.ikev2_container.alg_esn = *e; + tx.hdr.ikev2_transforms.push(IkeV2Transform::ESN(*e)); + } + _ => {} + }); + SCLogDebug!("Selected transforms: {:?}", transforms); + } else { + SCLogDebug!("Proposed transforms: {:?}", transforms); + state.ikev2_container.client_transforms.push(transforms); + } + } +} diff --git a/rust/src/ike/logger.rs b/rust/src/ike/logger.rs new file mode 100644 index 0000000..cc5705f --- /dev/null +++ b/rust/src/ike/logger.rs @@ -0,0 +1,234 @@ +/* Copyright (C) 2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use super::ike::{IKEState, IKETransaction}; +use super::ipsec_parser::IKEV2_FLAG_INITIATOR; +use crate::core::Direction; +use crate::ike::parser::{ExchangeType, IsakmpPayloadType, SaAttribute}; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use num_traits::FromPrimitive; +use std; +use std::convert::TryFrom; + +const LOG_EXTENDED: u32 = 0x01; + +fn add_attributes(transform: &Vec<SaAttribute>, js: &mut JsonBuilder) -> Result<(), JsonError> { + for attribute in transform { + js.set_string( + attribute.attribute_type.to_string().as_str(), + attribute.attribute_value.to_string().as_str(), + )?; + + if let Some(numeric_value) = attribute.numeric_value { + js.set_uint( + format!("{}_raw", attribute.attribute_type).as_str(), + numeric_value as u64, + )?; + } else if let Some(hex_value) = &attribute.hex_value { + js.set_string( + format!("{}_raw", attribute.attribute_type).as_str(), + hex_value, + )?; + } + } + + return Ok(()); +} + +fn log_ike( + state: &IKEState, tx: &IKETransaction, flags: u32, jb: &mut JsonBuilder, +) -> Result<(), JsonError> { + jb.open_object("ike")?; + + jb.set_uint("version_major", tx.hdr.maj_ver as u64)?; + jb.set_uint("version_minor", tx.hdr.min_ver as u64)?; + jb.set_string("init_spi", &tx.hdr.spi_initiator)?; + jb.set_string("resp_spi", &tx.hdr.spi_responder)?; + jb.set_uint("message_id", tx.hdr.msg_id as u64)?; + + if tx.ike_version == 1 { + if let Some(exchange_type) = tx.hdr.ikev1_header.exchange_type { + jb.set_uint("exchange_type", exchange_type as u64)?; + if (flags & LOG_EXTENDED) == LOG_EXTENDED { + if let Some(etype) = ExchangeType::from_u8(exchange_type) { + jb.set_string("exchange_type_verbose", etype.to_string().as_str())?; + }; + } + } + } else if tx.ike_version == 2 { + jb.set_uint("exchange_type", tx.hdr.ikev2_header.exch_type.0 as u64)?; + } + + if tx.ike_version == 1 { + if state.ikev1_container.server.nb_transforms > 0 { + // log the first transform as the chosen one + add_attributes(&state.ikev1_container.server.transform, jb)?; + } + if tx.direction == Direction::ToClient && tx.hdr.ikev1_transforms.len() > 1 { + // in case we have multiple server transforms log them in a list + jb.open_array("server_proposals")?; + for server_transform in &tx.hdr.ikev1_transforms { + jb.start_object()?; + add_attributes(server_transform, jb)?; + jb.close()?; + } + jb.close()?; + } + } else if tx.ike_version == 2 { + if tx.hdr.flags & IKEV2_FLAG_INITIATOR != 0 { + jb.set_string("role", "initiator")?; + } else { + jb.set_string("role", "responder")?; + jb.set_string("alg_enc", &format!("{:?}", state.ikev2_container.alg_enc))?; + jb.set_string("alg_auth", &format!("{:?}", state.ikev2_container.alg_auth))?; + jb.set_string("alg_prf", &format!("{:?}", state.ikev2_container.alg_prf))?; + jb.set_string("alg_dh", &format!("{:?}", state.ikev2_container.alg_dh))?; + jb.set_string("alg_esn", &format!("{:?}", state.ikev2_container.alg_esn))?; + } + } + + // payloads in packet + jb.open_array("payload")?; + if tx.ike_version == 1 { + if let Some(payload_types) = &tx.payload_types.ikev1_payload_types { + for pt in payload_types { + append_payload_type_extended(jb, pt)?; + } + } + } else if tx.ike_version == 2 { + for payload in tx.payload_types.ikev2_payload_types.iter() { + jb.append_string(&format!("{:?}", payload))?; + } + } + jb.close()?; + + if tx.ike_version == 1 { + log_ikev1(state, tx, jb)?; + } else if tx.ike_version == 2 { + log_ikev2(tx, jb)?; + } + jb.close()?; + return Ok(()); +} + +fn log_ikev1(state: &IKEState, tx: &IKETransaction, jb: &mut JsonBuilder) -> Result<(), JsonError> { + jb.open_object("ikev1")?; + + if let Some(doi) = state.ikev1_container.domain_of_interpretation { + jb.set_uint("doi", doi as u64)?; + } + jb.set_bool("encrypted_payloads", tx.hdr.ikev1_header.encrypted_payloads)?; + + if !tx.hdr.ikev1_header.encrypted_payloads { + // enable logging of collected state if not-encrypted payloads + + // client data + jb.open_object("client")?; + if !state.ikev1_container.client.key_exchange.is_empty() { + jb.set_string( + "key_exchange_payload", + &state.ikev1_container.client.key_exchange, + )?; + if let Ok(client_key_length) = + u64::try_from(state.ikev1_container.client.key_exchange.len()) + { + jb.set_uint("key_exchange_payload_length", client_key_length / 2)?; + } + } + if !state.ikev1_container.client.nonce.is_empty() { + jb.set_string("nonce_payload", &state.ikev1_container.client.nonce)?; + if let Ok(client_nonce_length) = u64::try_from(state.ikev1_container.client.nonce.len()) + { + jb.set_uint("nonce_payload_length", client_nonce_length / 2)?; + } + } + + if tx.direction == Direction::ToServer && !tx.hdr.ikev1_transforms.is_empty() { + jb.open_array("proposals")?; + for client_transform in &tx.hdr.ikev1_transforms { + jb.start_object()?; + add_attributes(client_transform, jb)?; + jb.close()?; + } + jb.close()?; // proposals + } + jb.close()?; // client + + // server data + jb.open_object("server")?; + if !state.ikev1_container.server.key_exchange.is_empty() { + jb.set_string( + "key_exchange_payload", + &state.ikev1_container.server.key_exchange, + )?; + if let Ok(server_key_length) = + u64::try_from(state.ikev1_container.server.key_exchange.len()) + { + jb.set_uint("key_exchange_payload_length", server_key_length / 2)?; + } + } + if !state.ikev1_container.server.nonce.is_empty() { + jb.set_string("nonce_payload", &state.ikev1_container.server.nonce)?; + if let Ok(server_nonce_length) = u64::try_from(state.ikev1_container.server.nonce.len()) + { + jb.set_uint("nonce_payload_length", server_nonce_length / 2)?; + } + } + jb.close()?; // server + + if !tx.hdr.ikev1_header.vendor_ids.is_empty() { + jb.open_array("vendor_ids")?; + for vendor in &tx.hdr.ikev1_header.vendor_ids { + jb.append_string(vendor)?; + } + jb.close()?; // vendor_ids + } + } + jb.close()?; + + return Ok(()); +} + +fn append_payload_type_extended(js: &mut JsonBuilder, pt: &u8) -> Result<(), JsonError> { + if let Some(v) = IsakmpPayloadType::from_u8(*pt) { + js.append_string(&format!("{:?}", v))?; + } + Ok(()) +} + +fn log_ikev2(tx: &IKETransaction, jb: &mut JsonBuilder) -> Result<(), JsonError> { + jb.open_object("ikev2")?; + + jb.set_uint("errors", tx.errors as u64)?; + if !tx.notify_types.is_empty() { + jb.open_array("notify")?; + for notify in tx.notify_types.iter() { + jb.append_string(&format!("{:?}", notify))?; + } + jb.close()?; + } + jb.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_ike_logger_log( + state: &mut IKEState, tx: *mut std::os::raw::c_void, flags: u32, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, IKETransaction); + log_ike(state, tx, flags, js).is_ok() +} diff --git a/rust/src/ike/mod.rs b/rust/src/ike/mod.rs new file mode 100644 index 0000000..366688e --- /dev/null +++ b/rust/src/ike/mod.rs @@ -0,0 +1,29 @@ +/* Copyright (C) 2017-2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +//! IKE parser, detection, logger and application layer module. + +// written by Pierre Chifflier <chifflier@wzdftpd.net> + +extern crate ipsec_parser; + +mod detect; +pub mod ike; +mod ikev1; +mod ikev2; +pub mod logger; +mod parser; diff --git a/rust/src/ike/parser.rs b/rust/src/ike/parser.rs new file mode 100644 index 0000000..dcc5745 --- /dev/null +++ b/rust/src/ike/parser.rs @@ -0,0 +1,712 @@ +/* Copyright (C) 2020 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use crate::common::to_hex; +use core::fmt; +use nom7::bytes::streaming::take; +use nom7::combinator::{complete, cond, map}; +use nom7::multi::many0; +use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8}; +use nom7::{Err, IResult}; +use std::collections::HashSet; + +// Generic ISAKMP "Container" structs +#[repr(u8)] +#[derive(Copy, Clone, FromPrimitive)] +pub enum ExchangeType { + None = 0, + Base = 1, + IdentityProtection = 2, + AuthenticationOnly = 3, + Aggressive = 4, + Informational = 5, + Transaction = 6, + QuickMode = 32, + NewGroupMode = 33, +} + +impl fmt::Display for ExchangeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ExchangeType::Base => write!(f, "Base"), + ExchangeType::IdentityProtection => write!(f, "Identity Protection"), + ExchangeType::AuthenticationOnly => write!(f, "Authentication Only"), + ExchangeType::Aggressive => write!(f, "Aggressive"), + ExchangeType::Informational => write!(f, "Informational"), + ExchangeType::Transaction => write!(f, "Transaction (Config Mode)"), + ExchangeType::QuickMode => write!(f, "Quick Mode"), + ExchangeType::NewGroupMode => write!(f, "New Group Mode"), + _ => write!(f, "Unknown Exchange Type"), + } + } +} + +pub struct IsakmpHeader { + pub init_spi: u64, + pub resp_spi: u64, + pub next_payload: u8, + pub maj_ver: u8, + pub min_ver: u8, + pub exch_type: u8, + pub flags: u8, + pub msg_id: u32, + pub length: u32, +} + +pub struct IsakmpPayloadHeader { + pub next_payload: u8, + pub reserved: u8, + pub payload_length: u16, +} + +pub struct IsakmpPayload<'a> { + pub payload_header: IsakmpPayloadHeader, + pub data: &'a [u8], +} + +// IKEV1 specific payloads + +// 1 -> Security Association +pub struct SecurityAssociationPayload<'a> { + pub domain_of_interpretation: u32, + pub situation: Option<&'a [u8]>, + pub data: Option<&'a [u8]>, +} + +// 2 -> Proposal +pub struct ProposalPayload<'a> { + pub proposal_number: u8, + pub proposal_type: u8, + pub spi_size: u8, + pub number_transforms: u8, + pub spi: &'a [u8], + pub data: &'a [u8], +} + +// 3 -> Transform +pub struct TransformPayload<'a> { + pub transform_number: u8, + pub transform_type: u8, + pub sa_attributes: &'a [u8], +} + +// 4 -> Key Exchange +pub struct KeyExchangePayload<'a> { + pub key_exchange_data: &'a [u8], +} + +// 5 -> Identification +// 6 -> Certificate +// 7 -> Certificate Request +// 8 -> Hash +// 9 -> Signature + +// 10 -> Nonce +pub struct NoncePayload<'a> { + pub nonce_data: &'a [u8], +} + +// 11 -> Notification +// 12 -> Delete + +// 13 -> Vendor ID +pub struct VendorPayload<'a> { + pub vendor_id: &'a [u8], +} + +// Attributes inside Transform +#[derive(Debug, Clone)] +pub enum AttributeType { + Unknown = 0, + EncryptionAlgorithm = 1, + HashAlgorithm = 2, + AuthenticationMethod = 3, + GroupDescription = 4, + GroupType = 5, + GroupPrime = 6, + GroupGeneratorOne = 7, + GroupGeneratorTwo = 8, + GroupCurveA = 9, + GroupCurveB = 10, + LifeType = 11, + LifeDuration = 12, + Prf = 13, + KeyLength = 14, + FieldSize = 15, + GroupOrder = 16, +} + +impl fmt::Display for AttributeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AttributeType::EncryptionAlgorithm => write!(f, "alg_enc"), + AttributeType::HashAlgorithm => write!(f, "alg_hash"), + AttributeType::AuthenticationMethod => write!(f, "alg_auth"), + AttributeType::GroupDescription => write!(f, "alg_dh"), + AttributeType::GroupType => write!(f, "sa_group_type"), + AttributeType::GroupPrime => write!(f, "sa_group_prime"), + AttributeType::GroupGeneratorOne => write!(f, "sa_group_generator_one"), + AttributeType::GroupGeneratorTwo => write!(f, "sa_group_generator_two"), + AttributeType::GroupCurveA => write!(f, "sa_group_curve_a"), + AttributeType::GroupCurveB => write!(f, "sa_group_curve_b"), + AttributeType::LifeType => write!(f, "sa_life_type"), + AttributeType::LifeDuration => write!(f, "sa_life_duration"), + AttributeType::Prf => write!(f, "alg_prf"), + AttributeType::KeyLength => write!(f, "sa_key_length"), + AttributeType::FieldSize => write!(f, "sa_field_size"), + AttributeType::GroupOrder => write!(f, "sa_group_order"), + _ => write!(f, "unknown"), + } + } +} + +#[derive(Debug, Clone)] +pub enum AttributeValue { + // https://www.iana.org/assignments/ipsec-registry/ipsec-registry.xhtml + Unknown, + // Encryption Algorithm + EncDesCbc, + EncIdeaCbc, + EncBlowfishCbc, + EncRc5R16B64Cbc, + EncTripleDesCbc, + EncCastCbc, + EncAesCbc, + EncCamelliaCbc, + // Hash Algorithm + HashMd5, + HashSha, + HashTiger, + HashSha2_256, + HashSha2_384, + HashSha2_512, + // Authentication Method + AuthPreSharedKey, + AuthDssSignatures, + AuthRsaSignatures, + AuthEncryptionWithRsa, + AuthRevisedEncryptionWithRsa, + AuthReserved, + AuthEcdsaSha256, + AuthEcdsaSha384, + AuthEcdsaSha512, + // Group Description + GroupDefault768BitModp, + GroupAlternate1024BitModpGroup, + GroupEc2nOnGp2p155, + GroupEc2nOnGp2p185, + GroupModp1536Bit, + GroupEc2nOverGf2p163, + GroupEc2nOverGf2p283, + GroupEc2nOverGf2p409, + GroupEc2nOverGf2p571, + GroupModp2048Bit, + GroupModp3072Bit, + GroupModp4096Bit, + GroupModp6144Bit, + GroupModp8192Bit, + GroupRandomEcp256, + GroupRandomEcp384, + GroupRandomEcp521, + GroupModp1024With160BitPrime, + GroupModp2048With224BitPrime, + GroupModp2048With256BitPrime, + GroupRandomEcp192, + GroupRandomEcp224, + GroupBrainpoolEcp224, + GroupBrainpoolEcp256, + GroupBrainpoolEcp384, + GroupBrainpoolEcp512, + // Life Type + LifeTypeSeconds, + LifeTypeKilobytes, +} + +impl fmt::Display for AttributeValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Clone)] +pub struct SaAttribute { + pub attribute_format: u8, + pub attribute_type: AttributeType, + pub attribute_value: AttributeValue, + pub numeric_value: Option<u32>, + pub hex_value: Option<String>, +} + +pub fn parse_isakmp_header(i: &[u8]) -> IResult<&[u8], IsakmpHeader> { + let (i, init_spi) = be_u64(i)?; + let (i, resp_spi) = be_u64(i)?; + let (i, next_payload) = be_u8(i)?; + let (i, vers_byte) = be_u8(i)?; + let vers = (vers_byte >> 4, vers_byte & 0b1111); + let (i, exch_type) = be_u8(i)?; + let (i, flags) = be_u8(i)?; + let (i, msg_id) = be_u32(i)?; + let (i, length) = be_u32(i)?; + let hdr = IsakmpHeader { + init_spi, + resp_spi, + next_payload, + maj_ver: vers.0, + min_ver: vers.1, + exch_type, + flags, + msg_id, + length, + }; + Ok((i, hdr)) +} + +pub fn parse_security_association(i: &[u8]) -> IResult<&[u8], SecurityAssociationPayload> { + let start_i = i; + let (i, domain_of_interpretation) = be_u32(i)?; + let (i, situation) = cond(domain_of_interpretation == 1, take(4_usize))(i)?; + let (i, data) = cond(domain_of_interpretation == 1 && start_i.len() >= 8, |b| { + take(start_i.len() - 8)(b) + })(i)?; + Ok(( + i, + SecurityAssociationPayload { + domain_of_interpretation, + situation, + data, + }, + )) +} + +pub fn parse_key_exchange(i: &[u8], length: u16) -> IResult<&[u8], KeyExchangePayload> { + let (i, key_exchange_data) = take(length as usize)(i)?; + Ok((i, KeyExchangePayload { key_exchange_data })) +} + +pub fn parse_proposal(i: &[u8]) -> IResult<&[u8], ProposalPayload> { + let start_i = i; + let (i, proposal_number) = be_u8(i)?; + let (i, proposal_type) = be_u8(i)?; + let (i, spi_size) = be_u8(i)?; + let (i, number_transforms) = be_u8(i)?; + let (i, spi) = take(spi_size as usize)(i)?; + let (i, payload_data) = cond((start_i.len() - 4) >= spi_size.into(), |b| { + take((start_i.len() - 4) - spi_size as usize)(b) + })(i)?; + let payload = ProposalPayload { + proposal_number, + proposal_type, + spi_size, + number_transforms, + spi, + data: payload_data.unwrap_or_default(), + }; + Ok((i, payload)) +} + +pub fn parse_transform(i: &[u8], length: u16) -> IResult<&[u8], TransformPayload> { + let (i, transform_number) = be_u8(i)?; + let (i, transform_type) = be_u8(i)?; + let (i, _) = be_u16(i)?; + let (i, payload_data) = cond(length >= 4, |b| take(length - 4)(b))(i)?; + Ok(( + i, + TransformPayload { + transform_number, + transform_type, + sa_attributes: payload_data.unwrap_or_default(), + }, + )) +} + +pub fn parse_vendor_id(i: &[u8], length: u16) -> IResult<&[u8], VendorPayload> { + map(take(length), |v| VendorPayload { vendor_id: v })(i) +} + +fn get_attribute_type(v: u16) -> AttributeType { + match v { + 1 => AttributeType::EncryptionAlgorithm, + 2 => AttributeType::HashAlgorithm, + 3 => AttributeType::AuthenticationMethod, + 4 => AttributeType::GroupDescription, + 5 => AttributeType::GroupType, + 6 => AttributeType::GroupPrime, + 7 => AttributeType::GroupGeneratorOne, + 8 => AttributeType::GroupGeneratorTwo, + 9 => AttributeType::GroupCurveA, + 10 => AttributeType::GroupCurveB, + 11 => AttributeType::LifeType, + 12 => AttributeType::LifeDuration, + 13 => AttributeType::Prf, + 14 => AttributeType::KeyLength, + 15 => AttributeType::FieldSize, + 16 => AttributeType::GroupOrder, + _ => AttributeType::Unknown, + } +} + +fn get_encryption_algorithm(v: u16) -> AttributeValue { + match v { + 1 => AttributeValue::EncDesCbc, + 2 => AttributeValue::EncIdeaCbc, + 3 => AttributeValue::EncBlowfishCbc, + 4 => AttributeValue::EncRc5R16B64Cbc, + 5 => AttributeValue::EncTripleDesCbc, + 6 => AttributeValue::EncCastCbc, + 7 => AttributeValue::EncAesCbc, + 8 => AttributeValue::EncCamelliaCbc, + _ => AttributeValue::Unknown, + } +} + +fn get_hash_algorithm(v: u16) -> AttributeValue { + match v { + 1 => AttributeValue::HashMd5, + 2 => AttributeValue::HashSha, + 3 => AttributeValue::HashTiger, + 4 => AttributeValue::HashSha2_256, + 5 => AttributeValue::HashSha2_384, + 6 => AttributeValue::HashSha2_512, + _ => AttributeValue::Unknown, + } +} + +fn get_authentication_method(v: u16) -> AttributeValue { + match v { + 1 => AttributeValue::AuthPreSharedKey, + 2 => AttributeValue::AuthDssSignatures, + 3 => AttributeValue::AuthRsaSignatures, + 4 => AttributeValue::AuthEncryptionWithRsa, + 5 => AttributeValue::AuthRevisedEncryptionWithRsa, + 6 => AttributeValue::AuthReserved, + 7 => AttributeValue::AuthReserved, + 8 => AttributeValue::AuthReserved, + 9 => AttributeValue::AuthEcdsaSha256, + 10 => AttributeValue::AuthEcdsaSha384, + 11 => AttributeValue::AuthEcdsaSha512, + _ => AttributeValue::Unknown, + } +} + +fn get_group_description(v: u16) -> AttributeValue { + match v { + 1 => AttributeValue::GroupDefault768BitModp, + 2 => AttributeValue::GroupAlternate1024BitModpGroup, + 3 => AttributeValue::GroupEc2nOnGp2p155, + 4 => AttributeValue::GroupEc2nOnGp2p185, + 5 => AttributeValue::GroupModp1536Bit, + 6 => AttributeValue::GroupEc2nOverGf2p163, + 7 => AttributeValue::GroupEc2nOverGf2p163, + 8 => AttributeValue::GroupEc2nOverGf2p283, + 9 => AttributeValue::GroupEc2nOverGf2p283, + 10 => AttributeValue::GroupEc2nOverGf2p409, + 11 => AttributeValue::GroupEc2nOverGf2p409, + 12 => AttributeValue::GroupEc2nOverGf2p571, + 13 => AttributeValue::GroupEc2nOverGf2p571, + 14 => AttributeValue::GroupModp2048Bit, + 15 => AttributeValue::GroupModp3072Bit, + 16 => AttributeValue::GroupModp4096Bit, + 17 => AttributeValue::GroupModp6144Bit, + 18 => AttributeValue::GroupModp8192Bit, + 19 => AttributeValue::GroupRandomEcp256, + 20 => AttributeValue::GroupRandomEcp384, + 21 => AttributeValue::GroupRandomEcp521, + 22 => AttributeValue::GroupModp1024With160BitPrime, + 23 => AttributeValue::GroupModp2048With224BitPrime, + 24 => AttributeValue::GroupModp2048With256BitPrime, + 25 => AttributeValue::GroupRandomEcp192, + 26 => AttributeValue::GroupRandomEcp224, + 27 => AttributeValue::GroupBrainpoolEcp224, + 28 => AttributeValue::GroupBrainpoolEcp256, + 29 => AttributeValue::GroupBrainpoolEcp384, + 30 => AttributeValue::GroupBrainpoolEcp512, + _ => AttributeValue::Unknown, + } +} + +pub fn parse_sa_attribute(i: &[u8]) -> IResult<&[u8], Vec<SaAttribute>> { + fn parse_attribute(i: &[u8]) -> IResult<&[u8], SaAttribute> { + let (i, b) = be_u16(i)?; + let format = ((b >> 15) as u8, b & 0x7f_ff); + let (i, attribute_length_or_value) = be_u16(i)?; // depends on format bit) = 1 -> value | 0 -> number of following bytes + let (i, numeric_variable_value) = + cond(format.0 == 0 && attribute_length_or_value == 4, be_u32)(i)?; // interpret as number + let (i, variable_attribute_value) = cond( + format.0 == 0 && attribute_length_or_value != 4, + take(attribute_length_or_value), + )(i)?; + let attr = SaAttribute { + attribute_format: format.0, + attribute_type: get_attribute_type(format.1), + attribute_value: match format.1 { + 1 => get_encryption_algorithm(attribute_length_or_value), + 2 => get_hash_algorithm(attribute_length_or_value), + 3 => get_authentication_method(attribute_length_or_value), + 4 => get_group_description(attribute_length_or_value), + 11 => match attribute_length_or_value { + 1 => AttributeValue::LifeTypeSeconds, + 2 => AttributeValue::LifeTypeKilobytes, + _ => AttributeValue::Unknown, + }, + _ => AttributeValue::Unknown, + }, + numeric_value: match format.0 { + 1 => Some(attribute_length_or_value as u32), + 0 => numeric_variable_value, + _ => None, + }, + hex_value: match format.0 { + 0 => variable_attribute_value + .map(to_hex), + _ => None, + }, + }; + Ok((i, attr)) + } + many0(complete(parse_attribute))(i) +} + +pub fn parse_nonce(i: &[u8], length: u16) -> IResult<&[u8], NoncePayload> { + map(take(length), |v| NoncePayload { nonce_data: v })(i) +} + +pub fn parse_ikev1_payload_list(i: &[u8]) -> IResult<&[u8], Vec<IsakmpPayload>> { + fn parse_payload(i: &[u8]) -> IResult<&[u8], IsakmpPayload> { + let (i, next_payload) = be_u8(i)?; + let (i, reserved) = be_u8(i)?; + let (i, payload_length) = be_u16(i)?; + let (i, payload_data) = cond(payload_length >= 4, |b| take(payload_length - 4)(b))(i)?; + Ok(( + i, + IsakmpPayload { + payload_header: IsakmpPayloadHeader { + next_payload, + reserved, + payload_length, + }, + data: payload_data.unwrap_or_default(), + }, + )) + } + many0(complete(parse_payload))(i) +} + +#[derive(FromPrimitive, Debug)] +pub enum IsakmpPayloadType { + None = 0, + SecurityAssociation = 1, + Proposal = 2, + Transform = 3, + KeyExchange = 4, + Identification = 5, + Certificate = 6, + CertificateRequest = 7, + Hash = 8, + Signature = 9, + Nonce = 10, + Notification = 11, + Delete = 12, + VendorID = 13, + SaKekPayload = 15, + SaTekPayload = 16, + KeyDownload = 17, + SequenceNumber = 18, + ProofOfPossession = 19, + NatDiscovery = 20, + NatOriginalAddress = 21, + GroupAssociatedPolicy = 22, +} + +impl fmt::Display for IsakmpPayloadType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub fn parse_payload( + payload_type: u8, data: &[u8], data_length: u16, domain_of_interpretation: &mut Option<u32>, + key_exchange: &mut Vec<u8>, nonce: &mut Vec<u8>, transforms: &mut Vec<Vec<SaAttribute>>, + vendor_ids: &mut Vec<String>, payload_types: &mut HashSet<u8>, +) -> Result<(), ()> { + payload_types.insert(payload_type); + + let element = num::FromPrimitive::from_u8(payload_type); + match element { + Some(IsakmpPayloadType::SecurityAssociation) => { + if parse_security_association_payload( + data, + data_length, + domain_of_interpretation, + key_exchange, + nonce, + transforms, + vendor_ids, + payload_types, + ).is_err() { + SCLogDebug!("Error parsing SecurityAssociation"); + return Err(()); + } + Ok(()) + } + Some(IsakmpPayloadType::Proposal) => { + if parse_proposal_payload( + data, + data_length, + domain_of_interpretation, + key_exchange, + nonce, + transforms, + vendor_ids, + payload_types, + ).is_err() { + SCLogDebug!("Error parsing Proposal"); + return Err(()); + } + Ok(()) + } + Some(IsakmpPayloadType::Transform) => { + if let Ok((_rem, payload)) = parse_transform(data, data_length) { + if let Ok((_, attribute_list)) = parse_sa_attribute(payload.sa_attributes) { + transforms.push(attribute_list); + } + } + Ok(()) + } + Some(IsakmpPayloadType::KeyExchange) => { + let res = parse_key_exchange(data, data_length); + if let Ok((_rem, payload)) = res { + *key_exchange = Vec::from(payload.key_exchange_data); + } + Ok(()) + } + Some(IsakmpPayloadType::Nonce) => { + let res = parse_nonce(data, data_length); + if let Ok((_rem, payload)) = res { + *nonce = Vec::from(payload.nonce_data); + } + Ok(()) + } + Some(IsakmpPayloadType::VendorID) => { + let res = parse_vendor_id(data, data_length); + if let Ok((_rem, payload)) = res { + vendor_ids.push(to_hex(payload.vendor_id)); + } + Ok(()) + } + _ => Ok(()), + } +} + +fn parse_proposal_payload( + data: &[u8], data_length: u16, domain_of_interpretation: &mut Option<u32>, + key_exchange: &mut Vec<u8>, nonce: &mut Vec<u8>, transforms: &mut Vec<Vec<SaAttribute>>, + vendor_ids: &mut Vec<String>, payload_types: &mut HashSet<u8>, +) -> Result<(), ()> { + match parse_proposal(&data[0..data_length as usize]) { + Ok((_rem, payload)) => { + let mut cur_payload_type = IsakmpPayloadType::Transform as u8; + match parse_ikev1_payload_list(payload.data) { + Ok((_, payload_list)) => { + for isakmp_payload in payload_list { + if parse_payload( + cur_payload_type, + isakmp_payload.data, + isakmp_payload.data.len() as u16, + domain_of_interpretation, + key_exchange, + nonce, + transforms, + vendor_ids, + payload_types, + ).is_err() { + SCLogDebug!("Error parsing transform payload"); + return Err(()); + } + cur_payload_type = isakmp_payload.payload_header.next_payload; + } + Ok(()) + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Incomplete data parsing payload list"); + Err(()) + } + Err(_) => { + SCLogDebug!("Error parsing payload list"); + Err(()) + } + } + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Incomplete data"); + Err(()) + } + Err(_) => Err(()), + } +} + +fn parse_security_association_payload( + data: &[u8], data_length: u16, domain_of_interpretation: &mut Option<u32>, + key_exchange: &mut Vec<u8>, nonce: &mut Vec<u8>, transforms: &mut Vec<Vec<SaAttribute>>, + vendor_ids: &mut Vec<String>, payload_types: &mut HashSet<u8>, +) -> Result<(), ()> { + match parse_security_association(&data[0..data_length as usize]) { + Ok((_rem, payload)) => { + *domain_of_interpretation = Some(payload.domain_of_interpretation); + if payload.domain_of_interpretation == 1 { + // 1 is assigned to IPsec DOI + let mut cur_payload_type = IsakmpPayloadType::Proposal as u8; + if let Some(p_data) = payload.data { + match parse_ikev1_payload_list(p_data) { + Ok((_, payload_list)) => { + for isakmp_payload in payload_list { + if parse_payload( + cur_payload_type, + isakmp_payload.data, + isakmp_payload.data.len() as u16, + domain_of_interpretation, + key_exchange, + nonce, + transforms, + vendor_ids, + payload_types, + ).is_err() { + SCLogDebug!("Error parsing proposal payload"); + return Err(()); + } + cur_payload_type = isakmp_payload.payload_header.next_payload; + } + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Incomplete data parsing payload list"); + return Err(()); + } + Err(_) => { + SCLogDebug!("Error parsing payload list"); + return Err(()); + } + } + } + } + Ok(()) + } + Err(Err::Incomplete(_)) => { + SCLogDebug!("Incomplete data"); + Err(()) + } + Err(_) => Err(()), + } +} |