Coverage Report

Created: 2026-09-06 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/rust/src/dhcp/dhcp.rs
Line
Count
Source
1
/* Copyright (C) 2018-2026 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
use suricata_sys::sys::{
19
    AppLayerParserState, AppProto, SCAppLayerParserConfParserEnabled,
20
    SCAppLayerProtoDetectConfProtoDetectionEnabled,
21
};
22
23
use crate::applayer::{self, *};
24
use crate::core::{ALPROTO_UNKNOWN, IPPROTO_UDP};
25
use crate::dhcp::parser::*;
26
use crate::direction::Direction;
27
use crate::flow::Flow;
28
use std;
29
use std::ffi::CString;
30
31
pub(super) static mut ALPROTO_DHCP: AppProto = ALPROTO_UNKNOWN;
32
33
static DHCP_MIN_FRAME_LEN: u32 = 232;
34
35
pub const BOOTP_REQUEST: u8 = 1;
36
pub const BOOTP_REPLY: u8 = 2;
37
38
// DHCP option types. Names based on IANA naming:
39
// https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
40
pub const DHCP_OPT_SUBNET_MASK: u8 = 1;
41
pub const DHCP_OPT_ROUTERS: u8 = 3;
42
pub const DHCP_OPT_DNS_SERVER: u8 = 6;
43
pub const DHCP_OPT_HOSTNAME: u8 = 12;
44
pub const DHCP_OPT_REQUESTED_IP: u8 = 50;
45
pub const DHCP_OPT_ADDRESS_TIME: u8 = 51;
46
pub const DHCP_OPT_TYPE: u8 = 53;
47
//unused pub const DHCP_OPT_SERVER_ID: u8 = 54;
48
pub const DHCP_OPT_PARAMETER_LIST: u8 = 55;
49
pub const DHCP_OPT_RENEWAL_TIME: u8 = 58;
50
pub const DHCP_OPT_REBINDING_TIME: u8 = 59;
51
pub const DHCP_OPT_VENDOR_CLASS_ID: u8 = 60;
52
pub const DHCP_OPT_CLIENT_ID: u8 = 61;
53
pub const DHCP_OPT_END: u8 = 255;
54
55
/// DHCP message types.
56
pub const DHCP_TYPE_DISCOVER: u8 = 1;
57
pub const DHCP_TYPE_OFFER: u8 = 2;
58
pub const DHCP_TYPE_REQUEST: u8 = 3;
59
pub const DHCP_TYPE_DECLINE: u8 = 4;
60
pub const DHCP_TYPE_ACK: u8 = 5;
61
pub const DHCP_TYPE_NAK: u8 = 6;
62
pub const DHCP_TYPE_RELEASE: u8 = 7;
63
pub const DHCP_TYPE_INFORM: u8 = 8;
64
65
// DHCP parameter types.
66
// https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.txt
67
pub const DHCP_PARAM_SUBNET_MASK: u8 = 1;
68
pub const DHCP_PARAM_ROUTER: u8 = 3;
69
pub const DHCP_PARAM_DNS_SERVER: u8 = 6;
70
pub const DHCP_PARAM_DOMAIN: u8 = 15;
71
pub const DHCP_PARAM_ARP_TIMEOUT: u8 = 35;
72
pub const DHCP_PARAM_NTP_SERVER: u8 = 42;
73
pub const DHCP_PARAM_TFTP_SERVER_NAME: u8 = 66;
74
pub const DHCP_PARAM_TFTP_SERVER_IP: u8 = 150;
75
76
#[derive(AppLayerEvent)]
77
pub enum DHCPEvent {
78
    TruncatedOptions,
79
    MalformedOptions,
80
}
81
82
/// The concept of a transaction is more to satisfy the Suricata
83
/// app-layer. This DHCP parser is actually stateless where each
84
/// message is its own transaction.
85
pub struct DHCPTransaction {
86
    tx_id: u64,
87
    pub message: DHCPMessage,
88
    tx_data: applayer::AppLayerTxData,
89
}
90
91
impl DHCPTransaction {
92
17.3k
    pub fn new(id: u64, message: DHCPMessage, direction: Direction) -> DHCPTransaction {
93
17.3k
        DHCPTransaction {
94
17.3k
            tx_id: id,
95
17.3k
            message,
96
17.3k
            tx_data: applayer::AppLayerTxData::for_direction(direction),
97
17.3k
        }
98
17.3k
    }
99
}
100
101
impl Transaction for DHCPTransaction {
102
33.5k
    fn id(&self) -> u64 {
103
33.5k
        self.tx_id
104
33.5k
    }
105
}
106
107
#[derive(Default)]
108
pub struct DHCPState {
109
    state_data: AppLayerStateData,
110
111
    // Internal transaction ID.
112
    tx_id: u64,
113
114
    // List of transactions.
115
    transactions: Vec<DHCPTransaction>,
116
117
    events: u16,
118
}
119
120
impl State<DHCPTransaction> for DHCPState {
121
16.7k
    fn get_transaction_count(&self) -> usize {
122
16.7k
        self.transactions.len()
123
16.7k
    }
124
125
16.7k
    fn get_transaction_by_index(&self, index: usize) -> Option<&DHCPTransaction> {
126
16.7k
        self.transactions.get(index)
127
16.7k
    }
128
}
129
130
impl DHCPState {
131
866
    pub fn new() -> Self {
132
866
        Default::default()
133
866
    }
134
135
17.5k
    pub fn parse(&mut self, input: &[u8], direction: Direction) -> bool {
136
17.5k
        match parse_dhcp(input) {
137
17.3k
            Ok((_, message)) => {
138
17.3k
                let malformed_options = message.malformed_options;
139
17.3k
                let truncated_options = message.truncated_options;
140
17.3k
                self.tx_id += 1;
141
17.3k
                let transaction = DHCPTransaction::new(self.tx_id, message, direction);
142
17.3k
                self.transactions.push(transaction);
143
17.3k
                if malformed_options {
144
0
                    self.set_event(DHCPEvent::MalformedOptions);
145
17.3k
                }
146
17.3k
                if truncated_options {
147
16.0k
                    self.set_event(DHCPEvent::TruncatedOptions);
148
16.0k
                }
149
17.3k
                return true;
150
            }
151
            _ => {
152
267
                return false;
153
            }
154
        }
155
17.5k
    }
156
157
0
    pub fn get_tx(&mut self, tx_id: u64) -> Option<&DHCPTransaction> {
158
0
        self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1)
159
0
    }
160
161
16.7k
    fn free_tx(&mut self, tx_id: u64) {
162
16.7k
        let len = self.transactions.len();
163
16.7k
        let mut found = false;
164
16.7k
        let mut index = 0;
165
16.7k
        for i in 0..len {
166
16.7k
            let tx = &self.transactions[i];
167
16.7k
            if tx.tx_id == tx_id + 1 {
168
16.7k
                found = true;
169
16.7k
                index = i;
170
16.7k
                break;
171
0
            }
172
        }
173
16.7k
        if found {
174
16.7k
            self.transactions.remove(index);
175
16.7k
        }
176
16.7k
    }
177
178
16.0k
    fn set_event(&mut self, event: DHCPEvent) {
179
16.0k
        if let Some(tx) = self.transactions.last_mut() {
180
16.0k
            tx.tx_data.set_event(event as u8);
181
16.0k
            self.events += 1;
182
16.0k
        }
183
16.0k
    }
184
}
185
186
39
unsafe extern "C" fn dhcp_probing_parser(
187
39
    _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8,
188
39
) -> AppProto {
189
39
    if input_len < DHCP_MIN_FRAME_LEN || input.is_null() {
190
17
        return ALPROTO_UNKNOWN;
191
22
    }
192
193
22
    let slice = build_slice!(input, input_len as usize);
194
22
    match parse_header(slice) {
195
        Ok((_, _)) => {
196
18
            return ALPROTO_DHCP;
197
        }
198
        _ => {
199
4
            return ALPROTO_UNKNOWN;
200
        }
201
    }
202
39
}
203
204
33.5k
extern "C" fn dhcp_tx_get_alstate_progress(
205
33.5k
    _tx: *mut std::os::raw::c_void, _direction: u8,
206
33.5k
) -> std::os::raw::c_int {
207
    // As this is a stateless parser, simply use 1.
208
33.5k
    return 1;
209
33.5k
}
210
211
0
unsafe extern "C" fn dhcp_state_get_tx(
212
0
    state: *mut std::os::raw::c_void, tx_id: u64,
213
0
) -> *mut std::os::raw::c_void {
214
0
    let state = cast_pointer!(state, DHCPState);
215
0
    match state.get_tx(tx_id) {
216
0
        Some(tx) => {
217
0
            return tx as *const _ as *mut _;
218
        }
219
        None => {
220
0
            return std::ptr::null_mut();
221
        }
222
    }
223
0
}
224
225
51.6k
unsafe extern "C" fn dhcp_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 {
226
51.6k
    let state = cast_pointer!(state, DHCPState);
227
51.6k
    return state.tx_id;
228
51.6k
}
229
230
9.42k
unsafe extern "C" fn dhcp_parse_request(
231
9.42k
    _flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState,
232
9.42k
    stream_slice: StreamSlice, _data: *const std::os::raw::c_void,
233
9.42k
) -> AppLayerResult {
234
9.42k
    let state = cast_pointer!(state, DHCPState);
235
9.42k
    if state.parse(stream_slice.as_slice(), Direction::ToServer) {
236
9.27k
        return AppLayerResult::ok();
237
154
    }
238
154
    return AppLayerResult::err();
239
9.42k
}
240
241
8.17k
unsafe extern "C" fn dhcp_parse_response(
242
8.17k
    _flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState,
243
8.17k
    stream_slice: StreamSlice, _data: *const std::os::raw::c_void,
244
8.17k
) -> AppLayerResult {
245
8.17k
    let state = cast_pointer!(state, DHCPState);
246
8.17k
    if state.parse(stream_slice.as_slice(), Direction::ToClient) {
247
8.05k
        return AppLayerResult::ok();
248
113
    }
249
113
    return AppLayerResult::err();
250
8.17k
}
251
252
16.7k
pub unsafe extern "C" fn dhcp_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) {
253
16.7k
    let state = cast_pointer!(state, DHCPState);
254
16.7k
    state.free_tx(tx_id);
255
16.7k
}
256
257
866
extern "C" fn dhcp_state_new(
258
866
    _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto,
259
866
) -> *mut std::os::raw::c_void {
260
866
    let state = DHCPState::new();
261
866
    let boxed = Box::new(state);
262
866
    return Box::into_raw(boxed) as *mut _;
263
866
}
264
265
866
unsafe extern "C" fn dhcp_state_free(state: *mut std::os::raw::c_void) {
266
866
    std::mem::drop(Box::from_raw(state as *mut DHCPState));
267
866
}
268
269
export_tx_data_get!(dhcp_get_tx_data, DHCPTransaction);
270
export_state_data_get!(dhcp_get_state_data, DHCPState);
271
272
const PARSER_NAME: &[u8] = b"dhcp\0";
273
274
#[no_mangle]
275
40
pub unsafe extern "C" fn SCRegisterDhcpParser() {
276
    SCLogDebug!("Registering DHCP parser.");
277
40
    let ports = CString::new("[67,68]").unwrap();
278
40
    let parser = RustParser {
279
40
        name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char,
280
40
        default_port: ports.as_ptr(),
281
40
        ipproto: IPPROTO_UDP,
282
40
        probe_ts: Some(dhcp_probing_parser),
283
40
        probe_tc: Some(dhcp_probing_parser),
284
40
        min_depth: 0,
285
40
        max_depth: 16,
286
40
        state_new: dhcp_state_new,
287
40
        state_free: dhcp_state_free,
288
40
        tx_free: dhcp_state_tx_free,
289
40
        parse_ts: dhcp_parse_request,
290
40
        parse_tc: dhcp_parse_response,
291
40
        get_tx_count: dhcp_state_get_tx_count,
292
40
        get_tx: dhcp_state_get_tx,
293
40
        tx_comp_st_ts: 1,
294
40
        tx_comp_st_tc: 1,
295
40
        tx_get_progress: dhcp_tx_get_alstate_progress,
296
40
        get_eventinfo: Some(DHCPEvent::get_event_info),
297
40
        get_eventinfo_byid: Some(DHCPEvent::get_event_info_by_id),
298
40
        localstorage_new: None,
299
40
        localstorage_free: None,
300
40
        get_tx_files: None,
301
40
        get_tx_iterator: Some(applayer::state_get_tx_iterator::<DHCPState, DHCPTransaction>),
302
40
        get_tx_data: dhcp_get_tx_data,
303
40
        get_state_data: dhcp_get_state_data,
304
40
        apply_tx_config: None,
305
40
        flags: 0,
306
40
        get_frame_id_by_name: None,
307
40
        get_frame_name_by_id: None,
308
40
        get_state_id_by_name: None,
309
40
        get_state_name_by_id: None,
310
40
    };
311
312
40
    let ip_proto_str = CString::new("udp").unwrap();
313
314
40
    if SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
315
40
        let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
316
40
        ALPROTO_DHCP = alproto;
317
40
        if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
318
40
            let _ = AppLayerRegisterParser(&parser, alproto);
319
40
        }
320
0
    } else {
321
0
        SCLogDebug!("Protocol detector and parser disabled for DHCP.");
322
0
    }
323
40
}
324
325
#[cfg(test)]
326
mod tests {
327
    use super::*;
328
329
    #[test]
330
    fn test_tx_skip_inspect_direction() {
331
        let pcap = include_bytes!("discover.pcap");
332
        let payload = &pcap[24 + 16 + 42..];
333
334
        // a to-server tx should be marked to skip the to-client inspection it
335
        // will never be observed in, and a to-client tx the reverse
336
        let (_rem, message) = parse_dhcp(payload).unwrap();
337
        let ts_tx = DHCPTransaction::new(1, message, Direction::ToServer);
338
        assert_eq!(
339
            APP_LAYER_TX_SKIP_INSPECT_TC,
340
            ts_tx.tx_data.flags & APP_LAYER_TX_SKIP_INSPECT_TC
341
        );
342
        assert_eq!(0, ts_tx.tx_data.flags & APP_LAYER_TX_SKIP_INSPECT_TS);
343
344
        let (_rem, message) = parse_dhcp(payload).unwrap();
345
        let tc_tx = DHCPTransaction::new(2, message, Direction::ToClient);
346
        assert_eq!(
347
            APP_LAYER_TX_SKIP_INSPECT_TS,
348
            tc_tx.tx_data.flags & APP_LAYER_TX_SKIP_INSPECT_TS
349
        );
350
        assert_eq!(0, tc_tx.tx_data.flags & APP_LAYER_TX_SKIP_INSPECT_TC);
351
    }
352
}