Coverage Report

Created: 2026-09-06 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/rust/src/nfs/nfs4.rs
Line
Count
Source
1
/* Copyright (C) 2018-2020 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
// written by Victor Julien
19
20
use nom7::bytes::streaming::take;
21
use nom7::number::streaming::be_u32;
22
use nom7::{Err, IResult};
23
24
use crate::direction::Direction;
25
use crate::nfs::nfs::*;
26
use crate::flow::Flow;
27
use crate::nfs::nfs4_records::*;
28
use crate::nfs::nfs_records::*;
29
use crate::nfs::rpc_records::*;
30
use crate::nfs::types::*;
31
32
use crate::kerberos::{parse_kerberos5_request, Kerberos5Ticket, SecBlobError};
33
34
0
fn parse_req_gssapi(i: &[u8]) -> IResult<&[u8], Kerberos5Ticket, SecBlobError> {
35
0
    let (i, len) = be_u32(i)?;
36
0
    let (i, buf) = take(len as usize)(i)?;
37
0
    let (_, ap) = parse_kerberos5_request(buf)?;
38
0
    Ok((i, ap))
39
0
}
40
41
impl NFSState {
42
    /* normal write: PUTFH (file handle), WRITE (write opts/data). File handle
43
     * is not part of the write record itself so we pass it in here. */
44
0
    fn write_v4<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs4RequestWrite<'b>, fh: &'b [u8]) {
45
        // for now assume that stable FILE_SYNC flags means a single chunk
46
0
        let is_last = w.stable == 2;
47
        SCLogDebug!("is_last {}", is_last);
48
49
0
        let mut fill_bytes = 0;
50
0
        let pad = w.write_len % 4;
51
0
        if pad != 0 {
52
0
            fill_bytes = 4 - pad;
53
0
        }
54
55
        // linux defines a max of 1mb. Allow several multiples.
56
0
        if w.write_len == 0 || w.write_len > 16777216 {
57
0
            return;
58
0
        }
59
60
0
        let file_handle = fh.to_vec();
61
0
        let file_name = if let Some(name) = self.namemap.get(fh) {
62
            SCLogDebug!("WRITE name {:?}", name);
63
0
            name.to_vec()
64
        } else {
65
            SCLogDebug!("WRITE object {:?} not found", w.stateid.data);
66
0
            Vec::new()
67
        };
68
69
0
        let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
70
0
            Some(tx) => {
71
0
                if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
72
0
                    filetracker_newchunk(
73
0
                        &mut tdf.file_tracker,
74
0
                        &file_name,
75
0
                        w.data,
76
0
                        w.offset,
77
0
                        w.write_len,
78
0
                        fill_bytes as u8,
79
0
                        is_last,
80
0
                        &r.hdr.xid,
81
                    );
82
0
                    tdf.chunk_count += 1;
83
0
                    if is_last {
84
0
                        tdf.file_last_xid = r.hdr.xid;
85
0
                        tx.is_last = true;
86
0
                        tx.response_done = true;
87
0
                    }
88
0
                }
89
0
                true
90
            }
91
0
            None => false,
92
        };
93
0
        if !found {
94
0
            let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
95
0
            if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
96
0
                filetracker_newchunk(
97
0
                    &mut tdf.file_tracker,
98
0
                    &file_name,
99
0
                    w.data,
100
0
                    w.offset,
101
0
                    w.write_len,
102
0
                    fill_bytes as u8,
103
0
                    is_last,
104
0
                    &r.hdr.xid,
105
                );
106
0
                tx.procedure = NFSPROC4_WRITE;
107
0
                tx.xid = r.hdr.xid;
108
0
                tx.is_first = true;
109
0
                tx.nfs_version = r.progver as u16;
110
0
                if is_last {
111
0
                    tdf.file_last_xid = r.hdr.xid;
112
0
                    tx.is_last = true;
113
0
                    tx.request_done = true;
114
0
                    tx.is_file_closed = true;
115
0
                }
116
0
            }
117
0
        }
118
0
        self.ts_chunk_xid = r.hdr.xid;
119
0
        debug_validate_bug_on!(w.data.len() as u32 > w.write_len);
120
0
        self.ts_chunk_left = w.write_len - w.data.len() as u32;
121
0
    }
122
123
0
    fn close_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
124
0
        self.commit_v4(r, fh)
125
0
    }
126
127
0
    fn commit_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
128
        SCLogDebug!("COMMIT, closing shop");
129
130
0
        let file_handle = fh.to_vec();
131
0
        if let Some(tx) = self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
132
0
            if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
133
0
                filetracker_close(&mut tdf.file_tracker);
134
0
                tdf.file_last_xid = r.hdr.xid;
135
0
                tx.is_last = true;
136
0
                tx.request_done = true;
137
0
                tx.is_file_closed = true;
138
0
            }
139
0
        }
140
0
    }
141
142
340
    fn new_tx_v4(
143
340
        &mut self, r: &RpcPacket, xidmap: &NFSRequestXidMap, procedure: u32, _aux_opcodes: &[u32],
144
340
    ) {
145
340
        let mut tx = self.new_tx();
146
340
        tx.xid = r.hdr.xid;
147
340
        tx.procedure = procedure;
148
340
        tx.request_done = true;
149
340
        tx.file_name = xidmap.file_name.to_vec();
150
340
        tx.nfs_version = r.progver as u16;
151
340
        tx.file_handle = xidmap.file_handle.to_vec();
152
153
340
        tx.auth_type = r.creds_flavor;
154
        #[allow(clippy::single_match)]
155
340
        match r.creds {
156
0
            RpcRequestCreds::Unix(ref u) => {
157
0
                tx.request_machine_name = u.machine_name_buf.to_vec();
158
0
                tx.request_uid = u.uid;
159
0
                tx.request_gid = u.gid;
160
0
            }
161
340
            _ => {}
162
        }
163
        SCLogDebug!(
164
            "NFSv4: TX created: ID {} XID {} PROCEDURE {}",
165
            tx.id,
166
            tx.xid,
167
            tx.procedure
168
        );
169
340
        self.transactions.push(tx);
170
340
    }
171
172
    /* A normal READ request looks like: PUTFH (file handle) READ (read opts).
173
     * We need the file handle for the READ.
174
     */
175
691
    fn compound_request<'b>(
176
691
        &mut self, r: &RpcPacket<'b>, cr: &Nfs4RequestCompoundRecord<'b>,
177
691
        xidmap: &mut NFSRequestXidMap,
178
691
    ) {
179
691
        let mut last_putfh: Option<&'b [u8]> = None;
180
691
        let mut main_opcode: u32 = 0;
181
691
        let mut aux_opcodes: Vec<u32> = Vec::new();
182
183
1.26k
        for c in &cr.commands {
184
            SCLogDebug!("c {:?}", c);
185
571
            match *c {
186
0
                Nfs4RequestContent::PutFH(ref rd) => {
187
0
                    last_putfh = Some(rd.value);
188
0
                    aux_opcodes.push(NFSPROC4_PUTFH);
189
0
                }
190
0
                Nfs4RequestContent::Read(ref rd) => {
191
                    SCLogDebug!("READv4: {:?}", rd);
192
0
                    if let Some(fh) = last_putfh {
193
0
                        xidmap.chunk_offset = rd.offset;
194
0
                        xidmap.file_handle = fh.to_vec();
195
0
                        self.xidmap_handle2name(xidmap);
196
0
                    }
197
                }
198
0
                Nfs4RequestContent::Open(ref rd) => {
199
0
                    SCLogDebug!("OPENv4: {}", String::from_utf8_lossy(rd.filename));
200
0
                    xidmap.file_name = rd.filename.to_vec();
201
0
                }
202
1
                Nfs4RequestContent::Lookup(ref rd) => {
203
1
                    SCLogDebug!("LOOKUPv4: {}", String::from_utf8_lossy(rd.filename));
204
1
                    xidmap.file_name = rd.filename.to_vec();
205
1
                }
206
20
                Nfs4RequestContent::Write(ref rd) => {
207
                    SCLogDebug!("WRITEv4: {:?}", rd);
208
20
                    if let Some(fh) = last_putfh {
209
0
                        self.write_v4(r, rd, fh);
210
20
                    }
211
                }
212
                Nfs4RequestContent::Commit => {
213
                    SCLogDebug!("COMMITv4");
214
3
                    if let Some(fh) = last_putfh {
215
0
                        self.commit_v4(r, fh);
216
3
                    }
217
                }
218
93
                Nfs4RequestContent::Close(ref _rd) => {
219
                    SCLogDebug!("CLOSEv4: {:?}", _rd);
220
93
                    if let Some(fh) = last_putfh {
221
0
                        self.close_v4(r, fh);
222
93
                    }
223
                }
224
64
                Nfs4RequestContent::Create(ref rd) => {
225
                    SCLogDebug!("CREATEv4: {:?}", rd);
226
64
                    if let Some(fh) = last_putfh {
227
0
                        xidmap.file_handle = fh.to_vec();
228
64
                    }
229
64
                    xidmap.file_name = rd.filename.to_vec();
230
64
                    main_opcode = NFSPROC4_CREATE;
231
                }
232
276
                Nfs4RequestContent::Remove(rd) => {
233
276
                    SCLogDebug!("REMOVEv4: {:?}", rd);
234
276
                    xidmap.file_name = rd.to_vec();
235
276
                    main_opcode = NFSPROC4_REMOVE;
236
276
                }
237
10
                Nfs4RequestContent::SetClientId(ref _rd) => {
238
10
                    SCLogDebug!(
239
10
                        "SETCLIENTIDv4: client id {} r_netid {} r_addr {}",
240
10
                        String::from_utf8_lossy(_rd.client_id),
241
10
                        String::from_utf8_lossy(_rd.r_netid),
242
10
                        String::from_utf8_lossy(_rd.r_addr)
243
10
                    );
244
10
                }
245
104
                _ => {}
246
            }
247
        }
248
249
691
        if main_opcode != 0 {
250
340
            self.new_tx_v4(r, xidmap, main_opcode, &aux_opcodes);
251
351
        }
252
691
    }
253
254
    /// complete request record
255
18.6k
    pub fn process_request_record_v4(&mut self, r: &RpcPacket) {
256
        SCLogDebug!(
257
            "NFSv4 REQUEST {} procedure {} ({}) blob size {}",
258
            r.hdr.xid,
259
            r.procedure,
260
            self.requestmap.len(),
261
            r.prog_data.len()
262
        );
263
264
18.6k
        let mut xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0);
265
266
18.6k
        if r.procedure == NFSPROC4_NULL {
267
183
            if let RpcRequestCreds::GssApi(ref creds) = r.creds {
268
0
                if creds.procedure == 1 {
269
0
                    let _x = parse_req_gssapi(r.prog_data);
270
0
                    SCLogDebug!("RPCSEC_GSS_INIT {:?}", _x);
271
0
                }
272
183
            }
273
18.4k
        } else if r.procedure == NFSPROC4_COMPOUND {
274
17.8k
            let mut data = r.prog_data;
275
276
17.8k
            if let RpcRequestCreds::GssApi(ref creds) = r.creds {
277
0
                if creds.procedure == 0 && creds.service == 2 {
278
                    SCLogDebug!("GSS INTEGRITY: {:?}", creds);
279
0
                    match parse_rpc_gssapi_integrity(r.prog_data) {
280
0
                        Ok((_rem, rec)) => {
281
0
                            SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
282
0
                            data = rec.data;
283
0
                            // store proc and serv for the reply
284
0
                            xidmap.gssapi_proc = creds.procedure;
285
0
                            xidmap.gssapi_service = creds.service;
286
0
                        }
287
0
                        Err(Err::Incomplete(_n)) => {
288
                            SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
289
0
                            self.set_event(NFSEvent::MalformedData);
290
0
                            return;
291
                        }
292
0
                        Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
293
                            SCLogDebug!(
294
                                "NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}",
295
                                _e
296
                            );
297
0
                            self.set_event(NFSEvent::MalformedData);
298
0
                            return;
299
                        }
300
                    }
301
0
                }
302
17.8k
            }
303
304
17.8k
            match parse_nfs4_request_compound(data) {
305
691
                Ok((_, rd)) => {
306
691
                    SCLogDebug!("NFSPROC4_COMPOUND: {:?}", rd);
307
691
                    self.compound_request(r, &rd, &mut xidmap);
308
691
                }
309
15.7k
                Err(Err::Incomplete(_n)) => {
310
15.7k
                    SCLogDebug!("NFSPROC4_COMPOUND: INCOMPLETE {:?}", _n);
311
15.7k
                    self.set_event(NFSEvent::MalformedData);
312
15.7k
                }
313
1.40k
                Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
314
1.40k
                    SCLogDebug!("NFSPROC4_COMPOUND: Parsing failed: {:?}", _e);
315
1.40k
                    self.set_event(NFSEvent::MalformedData);
316
1.40k
                }
317
            };
318
587
        }
319
320
18.6k
        self.requestmap.put(r.hdr.xid, xidmap);
321
18.6k
    }
322
323
1.93k
    fn compound_response<'b>(
324
1.93k
        &mut self, flow: *mut Flow, r: &RpcReplyPacket<'b>, cr: &Nfs4ResponseCompoundRecord<'b>,
325
1.93k
        xidmap: &mut NFSRequestXidMap,
326
1.93k
    ) {
327
1.93k
        let mut insert_filename_with_getfh = false;
328
1.93k
        let mut main_opcode_status: u32 = 0;
329
1.93k
        let mut main_opcode_status_set: bool = false;
330
331
2.02k
        for c in &cr.commands {
332
            SCLogDebug!("c {:?}", c);
333
3
            match *c {
334
0
                Nfs4ResponseContent::ReadDir(_s, Some(ref rd)) => {
335
                    SCLogDebug!("READDIRv4: status {} eof {}", _s, rd.eof);
336
337
                    #[allow(clippy::manual_flatten)]
338
0
                    for d in &rd.listing {
339
0
                        if let Some(_d) = d {
340
0
                            SCLogDebug!("READDIRv4: dir {}", String::from_utf8_lossy(_d.name));
341
0
                        }
342
                    }
343
                }
344
0
                Nfs4ResponseContent::Remove(s) => {
345
0
                    SCLogDebug!("REMOVE4: status {}", s);
346
0
                    main_opcode_status = s;
347
0
                    main_opcode_status_set = true;
348
0
                }
349
39
                Nfs4ResponseContent::Create(s) => {
350
39
                    SCLogDebug!("CREATE4: status {}", s);
351
39
                    main_opcode_status = s;
352
39
                    main_opcode_status_set = true;
353
39
                }
354
0
                Nfs4ResponseContent::Read(s, Some(ref rd)) => {
355
0
                    SCLogDebug!(
356
0
                        "READ4: xidmap {:?} status {} data {}",
357
0
                        xidmap,
358
0
                        s,
359
0
                        rd.data.len()
360
0
                    );
361
0
                    // convert record to generic read reply
362
0
                    let reply = NfsReplyRead {
363
0
                        status: s,
364
0
                        attr_follows: 0,
365
0
                        attr_blob: &[],
366
0
                        count: rd.count,
367
0
                        eof: rd.eof,
368
0
                        data_len: rd.data.len() as u32,
369
0
                        data: rd.data,
370
0
                    };
371
0
                    self.process_read_record(flow, r, &reply, Some(xidmap));
372
0
                }
373
0
                Nfs4ResponseContent::Open(_s, Some(ref _rd)) => {
374
0
                    SCLogDebug!("OPENv4: status {} opendata {:?}", _s, _rd);
375
0
                    insert_filename_with_getfh = true;
376
0
                }
377
0
                Nfs4ResponseContent::GetFH(_s, Some(ref rd)) => {
378
0
                    if insert_filename_with_getfh {
379
0
                        self.namemap
380
0
                            .put(rd.value.to_vec(), xidmap.file_name.to_vec());
381
0
                    }
382
                }
383
0
                Nfs4ResponseContent::PutRootFH(s)
384
3
                    if s == NFS4_OK && xidmap.file_name.is_empty() =>
385
0
                {
386
0
                    xidmap.file_name = b"<mount_root>".to_vec();
387
0
                    SCLogDebug!("filename {:?}", xidmap.file_name);
388
0
                }
389
45
                _ => {}
390
            }
391
        }
392
393
1.93k
        if main_opcode_status_set {
394
39
            let resp_handle = Vec::new();
395
39
            self.mark_response_tx_done(flow, r.hdr.xid, r.reply_state, main_opcode_status, &resp_handle);
396
1.90k
        }
397
1.93k
    }
398
399
8.26k
    pub fn process_reply_record_v4(&mut self, flow: *mut Flow, r: &RpcReplyPacket, xidmap: &mut NFSRequestXidMap) {
400
8.26k
        if xidmap.procedure == NFSPROC4_COMPOUND {
401
7.87k
            let mut data = r.prog_data;
402
403
7.87k
            if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 2 {
404
                SCLogDebug!("GSS INTEGRITY as set by call: {:?}", xidmap);
405
0
                match parse_rpc_gssapi_integrity(r.prog_data) {
406
0
                    Ok((_rem, rec)) => {
407
0
                        SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
408
0
                        data = rec.data;
409
0
                    }
410
0
                    Err(Err::Incomplete(_n)) => {
411
                        SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
412
0
                        self.set_event(NFSEvent::MalformedData);
413
0
                        return;
414
                    }
415
0
                    Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
416
                        SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}", _e);
417
0
                        self.set_event(NFSEvent::MalformedData);
418
0
                        return;
419
                    }
420
                }
421
7.87k
            }
422
7.87k
            match parse_nfs4_response_compound(data) {
423
1.93k
                Ok((_, rd)) => {
424
1.93k
                    SCLogDebug!("COMPOUNDv4: {:?}", rd);
425
1.93k
                    self.compound_response(flow, r, &rd, xidmap);
426
1.93k
                }
427
3.82k
                Err(Err::Incomplete(_)) => {
428
3.82k
                    self.set_event(NFSEvent::MalformedData);
429
3.82k
                }
430
2.10k
                Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
431
2.10k
                    SCLogDebug!("Parsing failed: {:?}", _e);
432
2.10k
                    self.set_event(NFSEvent::MalformedData);
433
2.10k
                }
434
            };
435
390
        }
436
8.26k
    }
437
}