Coverage Report

Created: 2026-08-14 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata7/rust/src/dhcp/parser.rs
Line
Count
Source
1
/* Copyright (C) 2018 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 std::cmp::min;
19
20
use crate::dhcp::dhcp::*;
21
use nom7::bytes::streaming::take;
22
use nom7::combinator::verify;
23
use nom7::number::streaming::{be_u16, be_u32, be_u8};
24
use nom7::IResult;
25
26
pub struct DHCPMessage {
27
    pub header: DHCPHeader,
28
29
    pub options: Vec<DHCPOption>,
30
31
    // Set to true if the options were found to be malformed. That is
32
    // failing to parse with enough data.
33
    pub malformed_options: bool,
34
35
    // Set to true if the options failed to parse due to not enough
36
    // data.
37
    pub truncated_options: bool,
38
}
39
40
pub struct DHCPHeader {
41
    pub opcode: u8,
42
    pub htype: u8,
43
    pub hlen: u8,
44
    pub hops: u8,
45
    pub txid: u32,
46
    pub seconds: u16,
47
    pub flags: u16,
48
    pub clientip: Vec<u8>,
49
    pub yourip: Vec<u8>,
50
    pub serverip: Vec<u8>,
51
    pub giaddr: Vec<u8>,
52
    pub clienthw: Vec<u8>,
53
    pub servername: Vec<u8>,
54
    pub bootfilename: Vec<u8>,
55
    pub magic: Vec<u8>,
56
}
57
58
pub struct DHCPOptClientId {
59
    pub htype: u8,
60
    pub data: Vec<u8>,
61
}
62
63
/// Option type for time values.
64
pub struct DHCPOptTimeValue {
65
    pub seconds: u32,
66
}
67
68
pub struct DHCPOptGeneric {
69
    pub data: Vec<u8>,
70
}
71
72
pub enum DHCPOptionWrapper {
73
    ClientId(DHCPOptClientId),
74
    TimeValue(DHCPOptTimeValue),
75
    Generic(DHCPOptGeneric),
76
    End,
77
}
78
79
pub struct DHCPOption {
80
    pub code: u8,
81
    pub data: Option<Vec<u8>>,
82
    pub option: DHCPOptionWrapper,
83
}
84
85
40.6k
pub fn parse_header(i: &[u8]) -> IResult<&[u8], DHCPHeader> {
86
40.6k
    let (i, opcode) = be_u8(i)?;
87
40.6k
    let (i, htype) = be_u8(i)?;
88
40.6k
    let (i, hlen) = be_u8(i)?;
89
40.6k
    let (i, hops) = be_u8(i)?;
90
40.6k
    let (i, txid) = be_u32(i)?;
91
40.5k
    let (i, seconds) = be_u16(i)?;
92
40.5k
    let (i, flags) = be_u16(i)?;
93
40.5k
    let (i, clientip) = take(4_usize)(i)?;
94
40.5k
    let (i, yourip) = take(4_usize)(i)?;
95
40.5k
    let (i, serverip) = take(4_usize)(i)?;
96
40.5k
    let (i, giaddr) = take(4_usize)(i)?;
97
40.5k
    let (i, clienthw) = take(16_usize)(i)?;
98
40.4k
    let (i, servername) = take(64_usize)(i)?;
99
40.4k
    let (i, bootfilename) = take(128_usize)(i)?;
100
40.3k
    let (i, magic) = take(4_usize)(i)?;
101
40.3k
    Ok((
102
40.3k
        i,
103
40.3k
        DHCPHeader {
104
40.3k
            opcode,
105
40.3k
            htype,
106
40.3k
            hlen,
107
40.3k
            hops,
108
40.3k
            txid,
109
40.3k
            seconds,
110
40.3k
            flags,
111
40.3k
            clientip: clientip.to_vec(),
112
40.3k
            yourip: yourip.to_vec(),
113
40.3k
            serverip: serverip.to_vec(),
114
40.3k
            giaddr: giaddr.to_vec(),
115
40.3k
            clienthw: clienthw[0..min(hlen as usize, 16)].to_vec(),
116
40.3k
            servername: servername.to_vec(),
117
40.3k
            bootfilename: bootfilename.to_vec(),
118
40.3k
            magic: magic.to_vec(),
119
40.3k
        },
120
40.3k
    ))
121
40.6k
}
122
123
90.9k
pub fn parse_clientid_option(i: &[u8]) -> IResult<&[u8], DHCPOption> {
124
90.9k
    let (i, code) = be_u8(i)?;
125
90.9k
    let (i, len) = verify(be_u8, |&v| v > 1)(i)?;
126
89.6k
    let (i, _htype) = be_u8(i)?;
127
88.0k
    let (i, data) = take(len - 1)(i)?;
128
86.1k
    Ok((
129
86.1k
        i,
130
86.1k
        DHCPOption {
131
86.1k
            code,
132
86.1k
            data: None,
133
86.1k
            option: DHCPOptionWrapper::ClientId(DHCPOptClientId {
134
86.1k
                htype: 1,
135
86.1k
                data: data.to_vec(),
136
86.1k
            }),
137
86.1k
        },
138
86.1k
    ))
139
90.9k
}
140
141
661k
pub fn parse_address_time_option(i: &[u8]) -> IResult<&[u8], DHCPOption> {
142
661k
    let (i, code) = be_u8(i)?;
143
661k
    let (i, _len) = be_u8(i)?;
144
657k
    let (i, seconds) = be_u32(i)?;
145
651k
    Ok((
146
651k
        i,
147
651k
        DHCPOption {
148
651k
            code,
149
651k
            data: None,
150
651k
            option: DHCPOptionWrapper::TimeValue(DHCPOptTimeValue { seconds }),
151
651k
        },
152
651k
    ))
153
661k
}
154
155
5.85M
pub fn parse_generic_option(i: &[u8]) -> IResult<&[u8], DHCPOption> {
156
5.85M
    let (i, code) = be_u8(i)?;
157
5.85M
    let (i, len) = be_u8(i)?;
158
5.85M
    let (i, data) = take(len)(i)?;
159
5.85M
    Ok((
160
5.85M
        i,
161
5.85M
        DHCPOption {
162
5.85M
            code,
163
5.85M
            data: None,
164
5.85M
            option: DHCPOptionWrapper::Generic(DHCPOptGeneric {
165
5.85M
                data: data.to_vec(),
166
5.85M
            }),
167
5.85M
        },
168
5.85M
    ))
169
5.85M
}
170
171
// Parse a single DHCP option. When option 255 (END) is parsed, the remaining
172
// data will be consumed.
173
6.62M
pub fn parse_option(i: &[u8]) -> IResult<&[u8], DHCPOption> {
174
6.62M
    let (_, opt) = be_u8(i)?;
175
6.61M
    match opt {
176
        DHCP_OPT_END => {
177
            // End of options case. We consume the rest of the data
178
            // so the parser is not called again. But is there a
179
            // better way to "break"?
180
7.49k
            let (data, code) = be_u8(i)?;
181
7.49k
            Ok((
182
7.49k
                &[],
183
7.49k
                DHCPOption {
184
7.49k
                    code,
185
7.49k
                    data: Some(data.to_vec()),
186
7.49k
                    option: DHCPOptionWrapper::End,
187
7.49k
                },
188
7.49k
            ))
189
        }
190
90.9k
        DHCP_OPT_CLIENT_ID => parse_clientid_option(i),
191
394k
        DHCP_OPT_ADDRESS_TIME => parse_address_time_option(i),
192
185k
        DHCP_OPT_RENEWAL_TIME => parse_address_time_option(i),
193
81.9k
        DHCP_OPT_REBINDING_TIME => parse_address_time_option(i),
194
5.85M
        _ => parse_generic_option(i),
195
    }
196
6.62M
}
197
198
39.4k
pub fn dhcp_parse(input: &[u8]) -> IResult<&[u8], DHCPMessage> {
199
39.4k
    match parse_header(input) {
200
39.1k
        Ok((rem, header)) => {
201
39.1k
            let mut options = Vec::new();
202
39.1k
            let mut next = rem;
203
39.1k
            let malformed_options = false;
204
39.1k
            let mut truncated_options = false;
205
            loop {
206
6.62M
                match parse_option(next) {
207
6.59M
                    Ok((rem, option)) => {
208
6.59M
                        let done = option.code == DHCP_OPT_END;
209
6.59M
                        options.push(option);
210
6.59M
                        next = rem;
211
6.59M
                        if done {
212
7.49k
                            break;
213
6.58M
                        }
214
                    }
215
                    Err(_) => {
216
31.6k
                        truncated_options = true;
217
31.6k
                        break;
218
                    }
219
                }
220
            }
221
39.1k
            let message = DHCPMessage {
222
39.1k
                header,
223
39.1k
                options,
224
39.1k
                malformed_options,
225
39.1k
                truncated_options,
226
39.1k
            };
227
39.1k
            return Ok((next, message));
228
        }
229
328
        Err(err) => {
230
328
            return Err(err);
231
        }
232
    }
233
39.4k
}
234
235
#[cfg(test)]
236
mod tests {
237
    use crate::dhcp::dhcp::*;
238
    use crate::dhcp::parser::*;
239
240
    #[test]
241
    fn test_parse_discover() {
242
        let pcap = include_bytes!("discover.pcap");
243
        let payload = &pcap[24 + 16 + 42..];
244
245
        let (_rem, message) = dhcp_parse(payload).unwrap();
246
        let header = message.header;
247
        assert_eq!(header.opcode, BOOTP_REQUEST);
248
        assert_eq!(header.htype, 1);
249
        assert_eq!(header.hlen, 6);
250
        assert_eq!(header.hops, 0);
251
        assert_eq!(header.txid, 0x00003d1d);
252
        assert_eq!(header.seconds, 0);
253
        assert_eq!(header.flags, 0);
254
        assert_eq!(header.clientip, &[0, 0, 0, 0]);
255
        assert_eq!(header.yourip, &[0, 0, 0, 0]);
256
        assert_eq!(header.serverip, &[0, 0, 0, 0]);
257
        assert_eq!(header.giaddr, &[0, 0, 0, 0]);
258
        assert_eq!(
259
            &header.clienthw[..(header.hlen as usize)],
260
            &[0x00, 0x0b, 0x82, 0x01, 0xfc, 0x42]
261
        );
262
        assert!(header.servername.iter().all(|&x| x == 0));
263
        assert!(header.bootfilename.iter().all(|&x| x == 0));
264
        assert_eq!(header.magic, &[0x63, 0x82, 0x53, 0x63]);
265
266
        assert!(!message.malformed_options);
267
        assert!(!message.truncated_options);
268
269
        assert_eq!(message.options.len(), 5);
270
        assert_eq!(message.options[0].code, DHCP_OPT_TYPE);
271
        assert_eq!(message.options[1].code, DHCP_OPT_CLIENT_ID);
272
        assert_eq!(message.options[2].code, DHCP_OPT_REQUESTED_IP);
273
        assert_eq!(message.options[3].code, DHCP_OPT_PARAMETER_LIST);
274
        assert_eq!(message.options[4].code, DHCP_OPT_END);
275
    }
276
277
    #[test]
278
    fn test_parse_client_id_too_short() {
279
        // Length field of 0.
280
        let buf: &[u8] = &[
281
            0x01, 0x00, // Length of 0.
282
            0x01, 0x01, // Junk data start here.
283
            0x02, 0x03,
284
        ];
285
        let r = parse_clientid_option(buf);
286
        assert!(r.is_err());
287
288
        // Length field of 1.
289
        let buf: &[u8] = &[
290
            0x01, 0x01, // Length of 1.
291
            0x01, 0x41,
292
        ];
293
        let r = parse_clientid_option(buf);
294
        assert!(r.is_err());
295
296
        // Length field of 2 -- OK.
297
        let buf: &[u8] = &[
298
            0x01, 0x02, // Length of 2.
299
            0x01, 0x41,
300
        ];
301
        let r = parse_clientid_option(buf);
302
        match r {
303
            Ok((rem, _)) => {
304
                assert_eq!(rem.len(), 0);
305
            }
306
            _ => {
307
                panic!("failed");
308
            }
309
        }
310
    }
311
}