diff --git a/rust/gen-c-headers.py b/rust/gen-c-headers.py index 1c103ae975..3a965b0386 100755 --- a/rust/gen-c-headers.py +++ b/rust/gen-c-headers.py @@ -58,6 +58,7 @@ type_map = { "u64" :"uint64_t", "libc::c_void": "void", + "c_void": "void", "libc::c_char": "char", "libc::c_int": "int", diff --git a/rust/src/dhcp/README.txt b/rust/src/dhcp/README.txt new file mode 100644 index 0000000000..fb7fcc4d1c --- /dev/null +++ b/rust/src/dhcp/README.txt @@ -0,0 +1,4 @@ +These test pcap files are individual packets broken out of a pcap +containing 4 DHCP messages. The original source of the PCAP file is + +https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=dhcp.pcap diff --git a/rust/src/dhcp/ack.pcap b/rust/src/dhcp/ack.pcap new file mode 100644 index 0000000000..3c144dcc6a Binary files /dev/null and b/rust/src/dhcp/ack.pcap differ diff --git a/rust/src/dhcp/dhcp.rs b/rust/src/dhcp/dhcp.rs new file mode 100644 index 0000000000..e65001a4e4 --- /dev/null +++ b/rust/src/dhcp/dhcp.rs @@ -0,0 +1,420 @@ +/* Copyright (C) 2018 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 applayer; +use core; +use core::{ALPROTO_UNKNOWN, AppProto, Flow}; +use dhcp::parser::*; +use libc; +use log::*; +use nom; +use parser::*; +use std; +use std::ffi::{CStr,CString}; +use std::mem::transmute; + +static mut ALPROTO_DHCP: AppProto = ALPROTO_UNKNOWN; + +static DHCP_MIN_FRAME_LEN: u32 = 232; + +pub const BOOTP_REQUEST: u8 = 1; +pub const BOOTP_REPLY: u8 = 2; + +// DHCP option types. Names based on IANA naming: +// https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml +pub const DHCP_OPT_SUBNET_MASK: u8 = 1; +pub const DHCP_OPT_ROUTERS: u8 = 3; +pub const DHCP_OPT_DNS_SERVER: u8 = 6; +pub const DHCP_OPT_HOSTNAME: u8 = 12; +pub const DHCP_OPT_REQUESTED_IP: u8 = 50; +pub const DHCP_OPT_ADDRESS_TIME: u8 = 51; +pub const DHCP_OPT_TYPE: u8 = 53; +pub const DHCP_OPT_SERVER_ID: u8 = 54; +pub const DHCP_OPT_PARAMETER_LIST: u8 = 55; +pub const DHCP_OPT_RENEWAL_TIME: u8 = 58; +pub const DHCP_OPT_REBINDING_TIME: u8 = 59; +pub const DHCP_OPT_CLIENT_ID: u8 = 61; +pub const DHCP_OPT_END: u8 = 255; + +/// DHCP message types. +pub const DHCP_TYPE_DISCOVER: u8 = 1; +pub const DHCP_TYPE_OFFER: u8 = 2; +pub const DHCP_TYPE_REQUEST: u8 = 3; +pub const DHCP_TYPE_DECLINE: u8 = 4; +pub const DHCP_TYPE_ACK: u8 = 5; +pub const DHCP_TYPE_NAK: u8 = 6; +pub const DHCP_TYPE_RELEASE: u8 = 7; +pub const DHCP_TYPE_INFORM: u8 = 8; + +/// DHCP parameter types. +/// https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.txt +pub const DHCP_PARAM_SUBNET_MASK: u8 = 1; +pub const DHCP_PARAM_ROUTER: u8 = 3; +pub const DHCP_PARAM_DNS_SERVER: u8 = 6; +pub const DHCP_PARAM_DOMAIN: u8 = 15; +pub const DHCP_PARAM_ARP_TIMEOUT: u8 = 35; +pub const DHCP_PARAM_NTP_SERVER: u8 = 42; +pub const DHCP_PARAM_TFTP_SERVER_NAME: u8 = 66; +pub const DHCP_PARAM_TFTP_SERVER_IP: u8 = 150; + +#[repr(u32)] +pub enum DHCPEvent { + TruncatedOptions = 0, + MalformedOptions, +} + +/// The concept of a transaction is more to satisfy the Suricata +/// app-layer. This DHCP parser is actually stateless where each +/// message is its own transaction. +pub struct DHCPTransaction { + tx_id: u64, + pub message: DHCPMessage, + logged: applayer::LoggerFlags, + de_state: Option<*mut core::DetectEngineState>, + events: *mut core::AppLayerDecoderEvents, +} + +impl DHCPTransaction { + pub fn new(id: u64, message: DHCPMessage) -> DHCPTransaction { + DHCPTransaction { + tx_id: id, + message: message, + logged: applayer::LoggerFlags::new(), + de_state: None, + events: std::ptr::null_mut(), + } + } +} + +export_tx_get_detect_state!(rs_dhcp_tx_get_detect_state, DHCPTransaction); +export_tx_set_detect_state!(rs_dhcp_tx_set_detect_state, DHCPTransaction); + +pub struct DHCPState { + // Internal transaction ID. + tx_id: u64, + + // List of transactions. + transactions: Vec, + + events: u16, +} + +impl DHCPState { + pub fn new() -> DHCPState { + return DHCPState { + tx_id: 0, + transactions: Vec::new(), + events: 0, + }; + } + + pub fn parse(&mut self, input: &[u8]) -> bool { + match dhcp_parse(input) { + nom::IResult::Done(_, message) => { + let malformed_options = message.malformed_options; + let truncated_options = message.truncated_options; + self.tx_id += 1; + let transaction = DHCPTransaction::new(self.tx_id, message); + self.transactions.push(transaction); + if malformed_options { + self.set_event(DHCPEvent::MalformedOptions); + } + if truncated_options { + self.set_event(DHCPEvent::TruncatedOptions); + } + return true; + } + _ => { + return false; + } + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&DHCPTransaction> { + for tx in &mut self.transactions { + if tx.tx_id == tx_id + 1 { + return Some(tx); + } + } + return None; + } + + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + fn set_event(&mut self, event: DHCPEvent) { + if let Some(tx) = self.transactions.last_mut() { + core::sc_app_layer_decoder_events_set_event_raw( + &mut tx.events, event as u8); + self.events += 1; + } + } + + fn get_tx_iterator(&mut self, min_tx_id: u64, state: &mut u64) -> + Option<(&DHCPTransaction, u64, bool)> + { + let mut index = *state as usize; + let len = self.transactions.len(); + + while index < len { + let tx = &self.transactions[index]; + if tx.tx_id < min_tx_id + 1 { + index += 1; + continue; + } + *state = index as u64 + 1; + return Some((tx, tx.tx_id - 1, (len - index) > 1)); + } + + return None; + } +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_probing_parser(_flow: *const Flow, + input: *const libc::uint8_t, + input_len: u32, + _offset: *const u32) -> AppProto { + if input_len < DHCP_MIN_FRAME_LEN { + return ALPROTO_UNKNOWN; + } + + let slice = build_slice!(input, input_len as usize); + match parse_header(slice) { + nom::IResult::Done(_, _) => { + return unsafe { ALPROTO_DHCP }; + } + _ => { + return ALPROTO_UNKNOWN; + } + } +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_tx_get_alstate_progress(_tx: *mut libc::c_void, + _direction: libc::uint8_t) -> libc::c_int { + // As this is a stateless parser, simply use 1. + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_progress_completion_status( + _direction: libc::uint8_t) -> libc::c_int { + // The presence of a transaction means we are complete. + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_get_tx(state: *mut libc::c_void, + tx_id: libc::uint64_t) -> *mut libc::c_void { + let state = cast_pointer!(state, DHCPState); + match state.get_tx(tx_id) { + Some(tx) => { + return unsafe { transmute(tx) }; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_get_tx_count(state: *mut libc::c_void) -> libc::uint64_t { + let state = cast_pointer!(state, DHCPState); + return state.tx_id; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_parse(_flow: *const core::Flow, + state: *mut libc::c_void, + _pstate: *mut libc::c_void, + input: *const libc::uint8_t, + input_len: u32, + _data: *const libc::c_void) -> i8 { + let state = cast_pointer!(state, DHCPState); + let buf = build_slice!(input, input_len as usize); + if state.parse(buf) { + return 1; + } + return -1; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_tx_free( + state: *mut libc::c_void, + tx_id: libc::uint64_t) +{ + let state = cast_pointer!(state, DHCPState); + state.free_tx(tx_id); +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_new() -> *mut libc::c_void { + let state = DHCPState::new(); + let boxed = Box::new(state); + return unsafe { + transmute(boxed) + }; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_free(state: *mut libc::c_void) { + // Just unbox... + let _drop: Box = unsafe { transmute(state) }; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_tx_get_logged(_state: *mut libc::c_void, tx: *mut libc::c_void) -> u32 { + let tx = cast_pointer!(tx, DHCPTransaction); + return tx.logged.get(); +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_tx_set_logged(_state: *mut libc::c_void, + tx: *mut libc::c_void, + logged: libc::uint32_t) { + let tx = cast_pointer!(tx, DHCPTransaction); + tx.logged.set(logged); +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_get_events(state: *mut libc::c_void, + tx_id: libc::uint64_t) + -> *mut core::AppLayerDecoderEvents +{ + let state = cast_pointer!(state, DHCPState); + match state.get_tx(tx_id) { + Some(tx) => tx.events, + _ => std::ptr::null_mut(), + } +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_get_event_info( + event_name: *const libc::c_char, + event_id: *mut libc::c_int, + event_type: *mut core::AppLayerEventType) + -> libc::c_int +{ + if event_name == std::ptr::null() { + return -1; + } + let c_event_name: &CStr = unsafe { CStr::from_ptr(event_name) }; + let event = match c_event_name.to_str() { + Ok(s) => { + match s { + "malformed_options" => DHCPEvent::MalformedOptions as i32, + "truncated_options" => DHCPEvent::TruncatedOptions as i32, + _ => -1, // unknown event + } + }, + Err(_) => -1, // UTF-8 conversion failed + }; + unsafe{ + *event_type = core::APP_LAYER_EVENT_TYPE_TRANSACTION; + *event_id = event as libc::c_int; + }; + 0 +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_state_get_tx_iterator( + _ipproto: libc::uint8_t, + _alproto: AppProto, + state: *mut libc::c_void, + min_tx_id: libc::uint64_t, + _max_tx_id: libc::uint64_t, + istate: &mut libc::uint64_t) + -> applayer::AppLayerGetTxIterTuple +{ + let state = cast_pointer!(state, DHCPState); + match state.get_tx_iterator(min_tx_id, istate) { + Some((tx, out_tx_id, has_next)) => { + let c_tx = unsafe { transmute(tx) }; + let ires = applayer::AppLayerGetTxIterTuple::with_values( + c_tx, out_tx_id, has_next); + return ires; + } + None => { + return applayer::AppLayerGetTxIterTuple::not_found(); + } + } +} + +const PARSER_NAME: &'static [u8] = b"dhcp\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_dhcp_register_parser() { + SCLogNotice!("Registering DHCP parser."); + let ports = CString::new("[67,68]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const libc::c_char, + default_port: ports.as_ptr(), + ipproto: libc::IPPROTO_UDP, + probe_ts: rs_dhcp_probing_parser, + probe_tc: rs_dhcp_probing_parser, + min_depth: 0, + max_depth: 16, + state_new: rs_dhcp_state_new, + state_free: rs_dhcp_state_free, + tx_free: rs_dhcp_state_tx_free, + parse_ts: rs_dhcp_parse, + parse_tc: rs_dhcp_parse, + get_tx_count: rs_dhcp_state_get_tx_count, + get_tx: rs_dhcp_state_get_tx, + tx_get_comp_st: rs_dhcp_state_progress_completion_status, + tx_get_progress: rs_dhcp_tx_get_alstate_progress, + get_tx_logged: Some(rs_dhcp_tx_get_logged), + set_tx_logged: Some(rs_dhcp_tx_set_logged), + get_de_state: rs_dhcp_tx_get_detect_state, + set_de_state: rs_dhcp_tx_set_detect_state, + get_events: Some(rs_dhcp_state_get_events), + get_eventinfo: Some(rs_dhcp_state_get_event_info), + localstorage_new: None, + localstorage_free: None, + get_tx_mpm_id: None, + set_tx_mpm_id: None, + get_files: None, + get_tx_iterator: Some(rs_dhcp_state_get_tx_iterator), + }; + + let ip_proto_str = CString::new("udp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_DHCP = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + } else { + SCLogDebug!("Protocol detector and parser disabled for DHCP."); + } +} diff --git a/rust/src/dhcp/discover.pcap b/rust/src/dhcp/discover.pcap new file mode 100644 index 0000000000..f692f3f62c Binary files /dev/null and b/rust/src/dhcp/discover.pcap differ diff --git a/rust/src/dhcp/logger.rs b/rust/src/dhcp/logger.rs new file mode 100644 index 0000000000..8e3a5f7ba5 --- /dev/null +++ b/rust/src/dhcp/logger.rs @@ -0,0 +1,280 @@ +/* Copyright (C) 2018 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. + */ + +extern crate libc; + +use std; +use std::os::raw::c_void; + +use dhcp::dhcp::*; +use dhcp::parser::{DHCPOptionWrapper,DHCPOptGeneric}; +use dns::log::dns_print_addr; +use json::*; +use conf::ConfNode; + +pub struct DHCPLogger { + extended: bool, +} + +impl DHCPLogger { + + pub fn new(conf: ConfNode) -> DHCPLogger { + return DHCPLogger{ + extended: conf.get_child_bool("extended"), + }; + } + + fn get_type(&self, tx: &DHCPTransaction) -> Option { + let options = &tx.message.options; + for option in options { + let code = option.code; + match &option.option { + &DHCPOptionWrapper::Generic(ref option) => { + match code { + DHCP_OPT_TYPE => { + if option.data.len() > 0 { + return Some(option.data[0]); + } + } + _ => {} + } + } + _ => {} + } + } + return None; + } + + fn do_log(&self, tx: &DHCPTransaction) -> bool { + if !self.extended { + match self.get_type(tx) { + Some(t) => { + match t { + DHCP_TYPE_ACK => { + return true; + } + _ => {} + } + } + _ => {} + } + return false; + } + return true; + } + + pub fn log(&self, tx: &DHCPTransaction) -> Option { + if !self.do_log(tx) { + return None; + } + + let header = &tx.message.header; + let options = &tx.message.options; + let js = Json::object(); + + match header.opcode { + BOOTP_REQUEST => { + js.set_string("type", "request"); + } + BOOTP_REPLY => { + js.set_string("type", "reply"); + } + _ => { + js.set_string("type", ""); + } + } + + js.set_integer("id", header.txid as u64); + js.set_string("client_mac", + &format_addr_hex(&header.clienthw.to_vec())); + js.set_string("assigned_ip", &dns_print_addr(&header.yourip)); + + if self.extended { + js.set_string("client_ip", &dns_print_addr(&header.clientip)); + if header.opcode == BOOTP_REPLY { + js.set_string("relay_ip", + &dns_print_addr(&header.giaddr)); + js.set_string("next_server_ip", + &dns_print_addr(&header.serverip)); + } + } + + for option in options { + let code = option.code; + match &option.option { + &DHCPOptionWrapper::ClientId(ref clientid) => { + js.set_string("client_id", + &format_addr_hex(&clientid.data)); + } + &DHCPOptionWrapper::TimeValue(ref time_value) => { + match code { + DHCP_OPT_ADDRESS_TIME => { + if self.extended { + js.set_integer("lease_time", + time_value.seconds as u64); + } + } + DHCP_OPT_REBINDING_TIME => { + if self.extended { + js.set_integer("rebinding_time", + time_value.seconds as u64); + } + } + DHCP_OPT_RENEWAL_TIME => { + js.set_integer("renewal_time", + time_value.seconds as u64); + } + _ => {} + } + } + &DHCPOptionWrapper::Generic(ref option) => { + match code { + DHCP_OPT_SUBNET_MASK => { + if self.extended { + js.set_string("subnet_mask", + &dns_print_addr(&option.data)); + } + } + DHCP_OPT_HOSTNAME => { + if option.data.len() > 0 { + js.set_string_from_bytes("hostname", + &option.data); + } + } + DHCP_OPT_TYPE => { + self.log_opt_type(&js, option); + } + DHCP_OPT_REQUESTED_IP => { + if self.extended { + js.set_string("requested_ip", + &dns_print_addr(&option.data)); + } + } + DHCP_OPT_PARAMETER_LIST => { + if self.extended { + self.log_opt_parameters(&js, option); + } + } + DHCP_OPT_DNS_SERVER => { + if self.extended { + self.log_opt_dns_server(&js, option); + } + } + DHCP_OPT_ROUTERS => { + if self.extended { + self.log_opt_routers(&js, option); + } + } + _ => {} + } + } + _ => {} + } + } + + return Some(js); + } + + fn log_opt_type(&self, js: &Json, option: &DHCPOptGeneric) { + let dhcp_type = match option.data[0] { + DHCP_TYPE_DISCOVER => "discover", + DHCP_TYPE_OFFER => "offer", + DHCP_TYPE_REQUEST => "request", + DHCP_TYPE_DECLINE => "decline", + DHCP_TYPE_ACK => "ack", + DHCP_TYPE_NAK => "nak", + DHCP_TYPE_RELEASE => "release", + DHCP_TYPE_INFORM => "inform", + _ => "unknown" + }; + js.set_string("dhcp_type", dhcp_type); + } + + fn log_opt_parameters(&self, js: &Json, option: &DHCPOptGeneric) { + let params = Json::array(); + for i in &option.data { + let param = match *i { + DHCP_PARAM_SUBNET_MASK => "subnet_mask", + DHCP_PARAM_ROUTER => "router", + DHCP_PARAM_DNS_SERVER => "dns_server", + DHCP_PARAM_DOMAIN => "domain", + DHCP_PARAM_ARP_TIMEOUT => "arp_timeout", + DHCP_PARAM_NTP_SERVER => "ntp_server", + DHCP_PARAM_TFTP_SERVER_NAME => "tftp_server_name", + DHCP_PARAM_TFTP_SERVER_IP => "tftp_server_ip", + _ => "" + }; + if param.len() > 0 { + params.array_append_string(param); + } + } + js.set("params", params); + } + + fn log_opt_dns_server(&self, js: &Json, option: &DHCPOptGeneric) { + let servers = Json::array(); + for i in 0..(option.data.len() / 4) { + servers.array_append_string(&dns_print_addr( + &option.data[(i * 4)..(i * 4) + 4].to_vec())) + } + js.set("dns_servers", servers); + } + + fn log_opt_routers(&self, js: &Json, option: &DHCPOptGeneric) { + let routers = Json::array(); + for i in 0..(option.data.len() / 4) { + routers.array_append_string(&dns_print_addr( + &option.data[(i * 4)..(i * 4) + 4].to_vec())) + } + js.set("routers", routers); + } + +} + +fn format_addr_hex(input: &Vec) -> String { + let parts: Vec = input.iter() + .map(|b| format!("{:02x}", b)) + .collect(); + return parts.join(":"); +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_logger_new(conf: *const c_void) -> *mut libc::c_void { + let conf = ConfNode::wrap(conf); + let boxed = Box::new(DHCPLogger::new(conf)); + return unsafe{std::mem::transmute(boxed)}; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_logger_free(logger: *mut libc::c_void) { + let _: Box = unsafe{std::mem::transmute(logger)}; +} + +#[no_mangle] +pub extern "C" fn rs_dhcp_logger_log(logger: *mut libc::c_void, + tx: *mut libc::c_void) -> *mut JsonT { + let logger = cast_pointer!(logger, DHCPLogger); + let tx = cast_pointer!(tx, DHCPTransaction); + match logger.log(tx) { + Some(js) => { + return js.unwrap(); + } + _ => { + return std::ptr::null_mut(); + } + } +} diff --git a/rust/src/dhcp/mod.rs b/rust/src/dhcp/mod.rs new file mode 100644 index 0000000000..b34dd9859c --- /dev/null +++ b/rust/src/dhcp/mod.rs @@ -0,0 +1,20 @@ +/* Copyright (C) 2018 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. + */ + +pub mod dhcp; +pub mod parser; +pub mod logger; diff --git a/rust/src/dhcp/offer.pcap b/rust/src/dhcp/offer.pcap new file mode 100644 index 0000000000..9d23e259ce Binary files /dev/null and b/rust/src/dhcp/offer.pcap differ diff --git a/rust/src/dhcp/parser.rs b/rust/src/dhcp/parser.rs new file mode 100644 index 0000000000..411a3b445e --- /dev/null +++ b/rust/src/dhcp/parser.rs @@ -0,0 +1,288 @@ +/* Copyright (C) 2018 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 std::cmp::min; + +use dhcp::dhcp::*; +use nom::*; + +pub struct DHCPMessage { + pub header: DHCPHeader, + + pub options: Vec, + + // Set to true if the options were found to be malformed. That is + // failing to parse with enough data. + pub malformed_options: bool, + + // Set to true if the options failed to parse due to not enough + // data. + pub truncated_options: bool, +} + +pub struct DHCPHeader { + pub opcode: u8, + pub htype: u8, + pub hlen: u8, + pub hops: u8, + pub txid: u32, + pub seconds: u16, + pub flags: u16, + pub clientip: Vec, + pub yourip: Vec, + pub serverip: Vec, + pub giaddr: Vec, + pub clienthw: Vec, + pub servername: Vec, + pub bootfilename: Vec, + pub magic: Vec, +} + +pub struct DHCPOptClientId { + pub htype: u8, + pub data: Vec, +} + +/// Option type for time values. +pub struct DHCPOptTimeValue { + pub seconds: u32, +} + +pub struct DHCPOptGeneric { + pub data: Vec, +} + +pub enum DHCPOptionWrapper { + ClientId(DHCPOptClientId), + TimeValue(DHCPOptTimeValue), + Generic(DHCPOptGeneric), + End, +} + +pub struct DHCPOption { + pub code: u8, + pub data: Option>, + pub option: DHCPOptionWrapper, +} + +named!(pub parse_header, + do_parse!( + opcode: be_u8 + >> htype: be_u8 + >> hlen: be_u8 + >> hops: be_u8 + >> txid: be_u32 + >> seconds: be_u16 + >> flags: be_u16 + >> clientip: take!(4) + >> yourip: take!(4) + >> serverip: take!(4) + >> giaddr: take!(4) + >> clienthw: take!(16) + >> servername: take!(64) + >> bootfilename: take!(128) + >> magic: take!(4) + >> ( + DHCPHeader{ + opcode: opcode, + htype: htype, + hlen: hlen, + hops: hops, + txid: txid, + seconds: seconds, + flags: flags, + clientip: clientip.to_vec(), + yourip: yourip.to_vec(), + serverip: serverip.to_vec(), + giaddr: giaddr.to_vec(), + clienthw: clienthw[0..min(hlen as usize, 16)].to_vec(), + servername: servername.to_vec(), + bootfilename: bootfilename.to_vec(), + magic: magic.to_vec(), + } + ) + ) +); + +named!(pub parse_clientid_option, + do_parse!( + code: be_u8 >> + len: be_u8 >> + htype: be_u8 >> + data: take!(len - 1) >> + ( + DHCPOption{ + code: code, + data: None, + option: DHCPOptionWrapper::ClientId(DHCPOptClientId{ + htype: 1, + data: data.to_vec(), + }), + } + ) + ) +); + +named!(pub parse_address_time_option, + do_parse!( + code: be_u8 >> + len: be_u8 >> + seconds: be_u32 >> + ( + DHCPOption{ + code: code, + data: None, + option: DHCPOptionWrapper::TimeValue(DHCPOptTimeValue{ + seconds: seconds, + }), + } + ) + ) +); + +named!(pub parse_generic_option, + do_parse!( + code: be_u8 >> + len: be_u8 >> + data: take!(len) >> ( + DHCPOption{ + code: code, + data: None, + option: DHCPOptionWrapper::Generic(DHCPOptGeneric{ + data: data.to_vec(), + }), + } + )) +); + +/// Parse a single DHCP option. When option 255 (END) is parsed, the remaining +/// data will be consumed. +named!(pub parse_option, + switch!(peek!(be_u8), + // End of options case. We consume the rest of the data + // so the parse is not called again. But is there a + // better way to "break"? + DHCP_OPT_END => do_parse!( + code: be_u8 >> + data: rest >> (DHCPOption{ + code: code, + data: Some(data.to_vec()), + option: DHCPOptionWrapper::End, + })) | + DHCP_OPT_CLIENT_ID => call!(parse_clientid_option) | + DHCP_OPT_ADDRESS_TIME => call!(parse_address_time_option) | + DHCP_OPT_RENEWAL_TIME => call!(parse_address_time_option) | + DHCP_OPT_REBINDING_TIME => call!(parse_address_time_option) | + _ => call!(parse_generic_option) + )); + +/// Parse and return all the options. Upon the end of option indicator +/// all the data will be consumed. +named!(pub parse_all_options>, many0!(call!(parse_option))); + +pub fn dhcp_parse(input: &[u8]) -> IResult<&[u8], DHCPMessage> { + match parse_header(input) { + IResult::Done(rem, header) => { + let mut options = Vec::new(); + let mut next = rem; + let mut malformed_options = false; + let mut truncated_options = false; + loop { + match parse_option(next) { + IResult::Done(rem, option) => { + let done = option.code == DHCP_OPT_END; + options.push(option); + next = rem; + if done { + break; + } + } + IResult::Incomplete(_) => { + println!("incomplete parsing options"); + truncated_options = true; + break; + } + IResult::Error(_) => { + malformed_options = true; + break; + } + } + } + let message = DHCPMessage { + header: header, + options: options, + malformed_options: malformed_options, + truncated_options: truncated_options, + }; + return IResult::Done(next, message); + } + IResult::Error(err) => { + return IResult::Error(err); + } + IResult::Incomplete(incomplete) => { + return IResult::Incomplete(incomplete); + } + } +} + +#[cfg(test)] +mod tests { + use dhcp::dhcp::*; + use dhcp::parser::*; + + #[test] + fn test_parse_discover() { + let pcap = include_bytes!("discover.pcap"); + let payload = &pcap[24 + 16 + 42..]; + + match dhcp_parse(payload) { + IResult::Done(_rem, message) => { + let header = message.header; + assert_eq!(header.opcode, BOOTP_REQUEST); + assert_eq!(header.htype, 1); + assert_eq!(header.hlen, 6); + assert_eq!(header.hops, 0); + assert_eq!(header.txid, 0x00003d1d); + assert_eq!(header.seconds, 0); + assert_eq!(header.flags, 0); + assert_eq!(header.clientip, &[0, 0, 0, 0]); + assert_eq!(header.yourip, &[0, 0, 0, 0]); + assert_eq!(header.serverip, &[0, 0, 0, 0]); + assert_eq!(header.giaddr, &[0, 0, 0, 0]); + assert_eq!(&header.clienthw[..(header.hlen as usize)], + &[0x00, 0x0b, 0x82, 0x01, 0xfc, 0x42]); + assert!(header.servername.iter().all(|&x| x == 0)); + assert!(header.bootfilename.iter().all(|&x| x == 0)); + assert_eq!(header.magic, &[0x63, 0x82, 0x53, 0x63]); + + assert!(!message.malformed_options); + assert!(!message.truncated_options); + + assert_eq!(message.options.len(), 5); + assert_eq!(message.options[0].code, DHCP_OPT_TYPE); + assert_eq!(message.options[1].code, DHCP_OPT_CLIENT_ID); + assert_eq!(message.options[2].code, DHCP_OPT_REQUESTED_IP); + assert_eq!(message.options[3].code, DHCP_OPT_PARAMETER_LIST); + assert_eq!(message.options[4].code, DHCP_OPT_END); + } + _ => { + assert!(false); + } + } + } + +} diff --git a/rust/src/dhcp/request.pcap b/rust/src/dhcp/request.pcap new file mode 100644 index 0000000000..5deb8b569a Binary files /dev/null and b/rust/src/dhcp/request.pcap differ diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 00b0a5c9ec..02db7eb3d9 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -56,3 +56,4 @@ pub mod ikev2; pub mod ntp; pub mod tftp; +pub mod dhcp; diff --git a/src/Makefile.am b/src/Makefile.am index f16246502d..e323e26749 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -48,6 +48,7 @@ app-layer-register.c app-layer-register.h \ app-layer-tftp.c app-layer-tftp.h \ app-layer-ikev2.c app-layer-ikev2.h \ app-layer-krb5.c app-layer-krb5.h \ +app-layer-dhcp.c app-layer-dhcp.h \ app-layer-template.c app-layer-template.h \ app-layer-ssh.c app-layer-ssh.h \ app-layer-ssl.c app-layer-ssl.h \ @@ -328,6 +329,7 @@ output-json-tftp.c output-json-tftp.h \ output-json-smb.c output-json-smb.h \ output-json-ikev2.c output-json-ikev2.h \ output-json-krb5.c output-json-krb5.h \ +output-json-dhcp.c output-json-dhcp.h \ output-json-template.c output-json-template.h \ output-json-metadata.c output-json-metadata.h \ output-lua.c output-lua.h \ diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index cd4ad19f6c..08d340ed63 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -729,6 +729,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_IKEV2\n"); else if (pp_pe->alproto == ALPROTO_KRB5) printf(" alproto: ALPROTO_KRB5\n"); + else if (pp_pe->alproto == ALPROTO_DHCP) + printf(" alproto: ALPROTO_DHCP\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) @@ -798,6 +800,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_IKEV2\n"); else if (pp_pe->alproto == ALPROTO_KRB5) printf(" alproto: ALPROTO_KRB5\n"); + else if (pp_pe->alproto == ALPROTO_DHCP) + printf(" alproto: ALPROTO_DHCP\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) diff --git a/src/app-layer-dhcp.c b/src/app-layer-dhcp.c new file mode 100644 index 0000000000..23b8a8a02d --- /dev/null +++ b/src/app-layer-dhcp.c @@ -0,0 +1,51 @@ +/* Copyright (C) 2015 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. + */ + +/** + * \file + * + * \author Jason Ish + */ + +#include "suricata-common.h" +#include "util-unittest.h" +#include "app-layer-parser.h" +#include "app-layer-dhcp.h" + +#ifdef HAVE_RUST +#include "rust-dhcp-dhcp-gen.h" +#endif /* HAVE_RUST */ + +void RegisterDHCPParsers(void) +{ +#ifdef HAVE_RUST + rs_dhcp_register_parser(); +#endif /* HAVE_RUST */ +#ifdef UNITTESTS + AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_DHCP, + DHCPParserRegisterTests); +#endif +} + +#ifdef UNITTESTS +#endif + +void DHCPParserRegisterTests(void) +{ +#ifdef UNITTESTS +#endif +} diff --git a/src/app-layer-dhcp.h b/src/app-layer-dhcp.h new file mode 100644 index 0000000000..2981ec3b5a --- /dev/null +++ b/src/app-layer-dhcp.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2015 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. + */ + +/** + * \file + * + * \author Jason Ish + */ + +#ifndef __APP_LAYER_DHCP_H__ +#define __APP_LAYER_DHCP_H__ + +void RegisterDHCPParsers(void); +void DHCPParserRegisterTests(void); + +#endif /* __APP_LAYER_DHCP_H__ */ diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 0587a12a93..5464a0d38e 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -66,6 +66,7 @@ #include "app-layer-tftp.h" #include "app-layer-ikev2.h" #include "app-layer-krb5.h" +#include "app-layer-dhcp.h" #include "app-layer-template.h" #include "conf.h" @@ -1454,6 +1455,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterTFTPParsers(); RegisterIKEV2Parsers(); RegisterKRB5Parsers(); + RegisterDHCPParsers(); RegisterTemplateParsers(); /** IMAP */ diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index e8c199a846..7d083bcda7 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -99,6 +99,9 @@ const char *AppProtoToString(AppProto alproto) case ALPROTO_KRB5: proto_name = "krb5"; break; + case ALPROTO_DHCP: + proto_name = "dhcp"; + break; case ALPROTO_TEMPLATE: proto_name = "template"; break; @@ -140,6 +143,7 @@ AppProto StringToAppProto(const char *proto_name) if (strcmp(proto_name,"ntp")==0) return ALPROTO_NTP; if (strcmp(proto_name,"ikev2")==0) return ALPROTO_IKEV2; if (strcmp(proto_name,"krb5")==0) return ALPROTO_KRB5; + if (strcmp(proto_name,"dhcp")==0) return ALPROTO_DHCP; if (strcmp(proto_name,"template")==0) return ALPROTO_TEMPLATE; if (strcmp(proto_name,"failed")==0) return ALPROTO_FAILED; diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index 466f6ed96e..7602167c65 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -50,6 +50,7 @@ enum AppProtoEnum { ALPROTO_TFTP, ALPROTO_IKEV2, ALPROTO_KRB5, + ALPROTO_DHCP, ALPROTO_TEMPLATE, /* used by the probing parser when alproto detection fails diff --git a/src/output-json-dhcp.c b/src/output-json-dhcp.c new file mode 100644 index 0000000000..5edebfad52 --- /dev/null +++ b/src/output-json-dhcp.c @@ -0,0 +1,190 @@ +/* Copyright (C) 2015 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. + */ + +/* + * TODO: Update \author in this file and in output-json-dhcp.h. + * TODO: Remove SCLogNotice statements, or convert to debug. + * TODO: Implement your app-layers logging. + */ + +/** + * \file + * + * \author Jason Ish + */ + +#include "suricata-common.h" +#include "debug.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "app-layer-dhcp.h" +#include "output-json-dhcp.h" + +#if defined(HAVE_LIBJANSSON) && defined(HAVE_RUST) + +#include "rust-dhcp-logger-gen.h" + +typedef struct LogDHCPFileCtx_ { + LogFileCtx *file_ctx; + uint32_t flags; + void *rs_logger; +} LogDHCPFileCtx; + +typedef struct LogDHCPLogThread_ { + LogDHCPFileCtx *dhcplog_ctx; + uint32_t count; + MemBuffer *buffer; +} LogDHCPLogThread; + +static int JsonDHCPLogger(ThreadVars *tv, void *thread_data, + const Packet *p, Flow *f, void *state, void *tx, uint64_t tx_id) +{ + LogDHCPLogThread *thread = thread_data; + LogDHCPFileCtx *ctx = thread->dhcplog_ctx; + + json_t *js = CreateJSONHeader((Packet *)p, 0, "dhcp"); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + json_t *dhcp_js = rs_dhcp_logger_log(ctx->rs_logger, tx); + if (unlikely(dhcp_js == NULL)) { + goto skip; + } + json_object_set_new(js, "dhcp", dhcp_js); + + MemBufferReset(thread->buffer); + OutputJSONBuffer(js, thread->dhcplog_ctx->file_ctx, &thread->buffer); + json_decref(js); + + return TM_ECODE_OK; + +skip: + json_decref(js); + return TM_ECODE_OK; +} + +static void OutputDHCPLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogDHCPFileCtx *dhcplog_ctx = (LogDHCPFileCtx *)output_ctx->data; + rs_dhcp_logger_free(dhcplog_ctx->rs_logger); + SCFree(dhcplog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputDHCPLogInitSub(ConfNode *conf, + OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogDHCPFileCtx *dhcplog_ctx = SCCalloc(1, sizeof(*dhcplog_ctx)); + if (unlikely(dhcplog_ctx == NULL)) { + return result; + } + dhcplog_ctx->file_ctx = ajt->file_ctx; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(dhcplog_ctx); + return result; + } + output_ctx->data = dhcplog_ctx; + output_ctx->DeInit = OutputDHCPLogDeInitCtxSub; + + dhcplog_ctx->rs_logger = rs_dhcp_logger_new(conf); + + AppLayerParserRegisterLogger(IPPROTO_UDP, ALPROTO_DHCP); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +#define OUTPUT_BUFFER_SIZE 65535 + +static TmEcode JsonDHCPLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogDHCPLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogDHCP. \"initdata\" is NULL."); + SCFree(thread); + return TM_ECODE_FAILED; + } + + thread->buffer = MemBufferCreateNew(OUTPUT_BUFFER_SIZE); + if (unlikely(thread->buffer == NULL)) { + SCFree(thread); + return TM_ECODE_FAILED; + } + + thread->dhcplog_ctx = ((OutputCtx *)initdata)->data; + *data = (void *)thread; + + return TM_ECODE_OK; +} + +static TmEcode JsonDHCPLogThreadDeinit(ThreadVars *t, void *data) +{ + LogDHCPLogThread *thread = (LogDHCPLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + if (thread->buffer != NULL) { + MemBufferFree(thread->buffer); + } + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonDHCPLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_DHCP, "eve-log", "JsonDHCPLog", + "eve-log.dhcp", OutputDHCPLogInitSub, ALPROTO_DHCP, + JsonDHCPLogger, JsonDHCPLogThreadInit, + JsonDHCPLogThreadDeinit, NULL); +} + +#else /* No JSON support. */ + +void JsonDHCPLogRegister(void) +{ +} + +#endif /* HAVE_LIBJANSSON */ diff --git a/src/output-json-dhcp.h b/src/output-json-dhcp.h new file mode 100644 index 0000000000..4b8dfcec65 --- /dev/null +++ b/src/output-json-dhcp.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2015 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. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __OUTPUT_JSON_DHCP_H__ +#define __OUTPUT_JSON_DHCP_H__ + +void JsonDHCPLogRegister(void); + +#endif /* __OUTPUT_JSON_DHCP_H__ */ diff --git a/src/output.c b/src/output.c index dd4fa54ac9..e27c24fa3f 100644 --- a/src/output.c +++ b/src/output.c @@ -73,6 +73,7 @@ #include "output-json-smb.h" #include "output-json-ikev2.h" #include "output-json-krb5.h" +#include "output-json-dhcp.h" #include "output-json-template.h" #include "output-lua.h" #include "output-json-dnp3.h" @@ -1099,9 +1100,10 @@ void OutputRegisterLoggers(void) JsonSMBLogRegister(); /* IKEv2 JSON logger. */ JsonIKEv2LogRegister(); - /* KRB5 JSON logger. */ JsonKRB5LogRegister(); + /* DHCP JSON logger. */ + JsonDHCPLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); } diff --git a/src/suricata-common.h b/src/suricata-common.h index d57b3ba522..29dd172e02 100644 --- a/src/suricata-common.h +++ b/src/suricata-common.h @@ -413,6 +413,7 @@ typedef enum { LOGGER_JSON_SMB, LOGGER_JSON_IKEV2, LOGGER_JSON_KRB5, + LOGGER_JSON_DHCP, LOGGER_JSON_TEMPLATE, LOGGER_ALERT_DEBUG, diff --git a/suricata.yaml.in b/suricata.yaml.in index e82d8b0a41..60ce166e31 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -275,6 +275,14 @@ outputs: @rust_config_comment@- tftp @rust_config_comment@- ikev2 @rust_config_comment@- krb5 + - dhcp: + # DHCP logging requires Rust. + enabled: @rust_config_enabled@ + # When extended mode is on, all DHCP messages are logged + # with full detail. When extended mode is off (the + # default), just enough information to map a MAC address + # to an IP address is logged. + extended: no - ssh - stats: totals: yes # stats for all threads merged together @@ -1052,6 +1060,9 @@ app-layer: ntp: enabled: @rust_config_enabled@ + dhcp: + enabled: @rust_config_enabled@ + # Limit for the maximum number of asn1 frames to decode (default 256) asn1-max-frames: 256