summaryrefslogtreecommitdiffstats
path: root/rust/src/nfs/nfs.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/nfs/nfs.rs')
-rw-r--r--rust/src/nfs/nfs.rs2121
1 files changed, 2121 insertions, 0 deletions
diff --git a/rust/src/nfs/nfs.rs b/rust/src/nfs/nfs.rs
new file mode 100644
index 0000000..dfb5e0e
--- /dev/null
+++ b/rust/src/nfs/nfs.rs
@@ -0,0 +1,2121 @@
+/* Copyright (C) 2017-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.
+ */
+
+// written by Victor Julien
+
+use std;
+use std::cmp;
+use std::collections::HashMap;
+use std::ffi::CString;
+
+use nom7::{Err, Needed};
+
+use crate::applayer;
+use crate::applayer::*;
+use crate::frames::*;
+use crate::core::*;
+use crate::conf::*;
+use crate::filetracker::*;
+use crate::filecontainer::*;
+
+use crate::nfs::types::*;
+use crate::nfs::rpc_records::*;
+use crate::nfs::nfs_records::*;
+use crate::nfs::nfs2_records::*;
+use crate::nfs::nfs3_records::*;
+
+pub static mut SURICATA_NFS_FILE_CONFIG: Option<&'static SuricataFileContext> = None;
+
+pub const NFS_MIN_FRAME_LEN: u16 = 32;
+
+static mut NFS_MAX_TX: usize = 1024;
+
+pub const RPC_TCP_PRE_CREDS: usize = 28;
+pub const RPC_UDP_PRE_CREDS: usize = 24;
+
+static mut ALPROTO_NFS: AppProto = ALPROTO_UNKNOWN;
+/*
+ * Record parsing.
+ *
+ * Incomplete records come in due to TCP splicing. For all record types
+ * except READ and WRITE, processing only begins when the full record
+ * is available. For READ/WRITE partial records are processed as well to
+ * avoid queuing too much data.
+ *
+ * Getting file names.
+ *
+ * NFS makes heavy use of 'file handles' for operations. In many cases it
+ * uses a file name just once and after that just the handle. For example,
+ * if a client did a file listing (e.g. READDIRPLUS) and would READ the
+ * file afterwards, the name will only appear in the READDIRPLUS answer.
+ * To be able to log the names we store a mapping between file handles
+ * and file names in NFSState::namemap.
+ *
+ * Mapping NFS to Suricata's transaction model.
+ *
+ * The easiest way to do transactions would be to map each command/reply with
+ * the same XID to a transaction. This would allow for per XID logging, detect
+ * etc. However this model doesn't fit well with file tracking. The file
+ * tracking in Suricata is really expecting to be one or more files to live
+ * inside a single transaction. Would XID pairs be a transaction however,
+ * there would be many transactions forming a single file. This will be very
+ * inefficient.
+ *
+ * The model implemented here is as follows: each file transfer is a single
+ * transaction. All XID pairs unrelated to those file transfers create
+ * transactions per pair.
+ *
+ * A complicating factor is that the procedure matching is per tx, and a
+ * file transfer may have multiple procedures involved. Currently now only
+ * a COMMIT after WRITEs. A vector of additional procedures is kept to
+ * match on this.
+ *
+ * File tracking
+ *
+ * Files are tracked per 'FileTransferTracker' and are stored in the
+ * NFSTransaction where they can be looked up per handle as part of the
+ * Transaction lookup.
+ */
+
+#[derive(AppLayerFrameType)]
+pub enum NFSFrameType {
+ RPCPdu,
+ RPCHdr,
+ RPCData,
+ RPCCreds, // for rpc calls | rpc.creds [creds_flavor + creds_len + creds]
+
+ NFSPdu,
+ NFSStatus,
+
+ NFS4Pdu,
+ NFS4Hdr,
+ NFS4Ops,
+ NFS4Status,
+}
+
+#[derive(FromPrimitive, Debug, AppLayerEvent)]
+pub enum NFSEvent {
+ MalformedData = 0,
+ NonExistingVersion = 1,
+ UnsupportedVersion = 2,
+ TooManyTransactions = 3,
+}
+
+#[derive(Debug)]
+pub enum NFSTransactionTypeData {
+ RENAME(Vec<u8>),
+ FILE(NFSTransactionFile),
+}
+
+#[derive(Default, Debug)]
+pub struct NFSTransactionFile {
+ /// file transactions are unidirectional in the sense that they track
+ /// a single file on one direction
+ pub direction: Direction, // Direction::ToClient or Direction::ToServer
+
+ /// additional procedures part of a single file transfer. Currently
+ /// only COMMIT on WRITEs.
+ pub file_additional_procs: Vec<u32>,
+
+ pub chunk_count: u32,
+
+ /// last xid of this file transfer. Last READ or COMMIT normally.
+ pub file_last_xid: u32,
+
+ /// after a gap, this will be set to a time in the future. If the file
+ /// receives no updates before that, it will be considered complete.
+ pub post_gap_ts: u64,
+
+ /// file tracker for a single file. Boxed so that we don't use
+ /// as much space if we're not a file tx.
+ pub file_tracker: FileTransferTracker,
+}
+
+impl NFSTransactionFile {
+ pub fn new() -> Self {
+ return Self {
+ file_tracker: FileTransferTracker::new(),
+ ..Default::default()
+ }
+ }
+ pub fn update_file_flags(&mut self, flow_file_flags: u16) {
+ let dir_flag = if self.direction == Direction::ToServer { STREAM_TOSERVER } else { STREAM_TOCLIENT };
+ self.file_tracker.file_flags = unsafe { FileFlowFlagsToFlags(flow_file_flags, dir_flag) };
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_gettxfiles(_state: *mut std::ffi::c_void, tx: *mut std::ffi::c_void, direction: u8) -> AppLayerGetFileState {
+ let tx = cast_pointer!(tx, NFSTransaction);
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ let tx_dir : u8 = tdf.direction.into();
+ if direction & tx_dir != 0 {
+ if let Some(sfcm) = { SURICATA_NFS_FILE_CONFIG } {
+ return AppLayerGetFileState { fc: &mut tdf.file_tracker.file, cfg: sfcm.files_sbcfg }
+ }
+ }
+ }
+ AppLayerGetFileState::err()
+}
+
+#[derive(Debug)]
+pub struct NFSTransaction {
+ pub id: u64, /// internal id
+ pub xid: u32, /// nfs req/reply pair id
+ pub procedure: u32,
+ /// file name of the object we're dealing with. In case of RENAME
+ /// this is the 'from' or original name.
+ pub file_name: Vec<u8>,
+
+ pub auth_type: u32,
+ pub request_machine_name: Vec<u8>,
+ pub request_uid: u32,
+ pub request_gid: u32,
+
+ pub rpc_response_status: u32,
+ pub nfs_response_status: u32,
+
+ pub is_first: bool,
+ pub is_last: bool,
+
+ /// for state tracking. false means this side is in progress, true
+ /// that it's complete.
+ pub request_done: bool,
+ pub response_done: bool,
+
+ pub nfs_version: u16,
+
+ /// is a special file tx that we look up by file_handle instead of XID
+ pub is_file_tx: bool,
+ pub is_file_closed: bool,
+ pub file_handle: Vec<u8>,
+
+ /// Procedure type specific data
+ /// TODO see if this can be an `Option<Box<NFSTransactionTypeData>>`. Initial
+ /// attempt failed.
+ pub type_data: Option<NFSTransactionTypeData>,
+
+ pub tx_data: AppLayerTxData,
+}
+
+impl Default for NFSTransaction {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl NFSTransaction {
+ pub fn new() -> Self {
+ return Self {
+ id: 0,
+ xid: 0,
+ procedure: 0,
+ file_name:Vec::new(),
+ request_machine_name:Vec::new(),
+ request_uid:0,
+ request_gid:0,
+ rpc_response_status:0,
+ nfs_response_status:0,
+ auth_type: 0,
+ is_first: false,
+ is_last: false,
+ request_done: false,
+ response_done: false,
+ nfs_version:0,
+ is_file_tx: false,
+ is_file_closed: false,
+ file_handle:Vec::new(),
+ type_data: None,
+ tx_data: AppLayerTxData::new(),
+ }
+ }
+
+ pub fn free(&mut self) {
+ debug_validate_bug_on!(self.tx_data.files_opened > 1);
+ debug_validate_bug_on!(self.tx_data.files_logged > 1);
+ }
+}
+
+impl Transaction for NFSTransaction {
+ fn id(&self) -> u64 {
+ self.id
+ }
+}
+
+impl Drop for NFSTransaction {
+ fn drop(&mut self) {
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = self.type_data {
+ if let Some(sfcm) = unsafe { SURICATA_NFS_FILE_CONFIG } {
+ tdf.file_tracker.file.free(sfcm);
+ }
+ }
+ self.free();
+ }
+}
+
+#[derive(Debug)]
+pub struct NFSRequestXidMap {
+ pub progver: u32,
+ pub procedure: u32,
+ pub chunk_offset: u64,
+ pub file_name:Vec<u8>,
+
+ /// READ replies can use this to get to the handle the request used
+ pub file_handle:Vec<u8>,
+
+ pub gssapi_proc: u32,
+ pub gssapi_service: u32,
+}
+
+impl NFSRequestXidMap {
+ pub fn new(progver: u32, procedure: u32, chunk_offset: u64) -> NFSRequestXidMap {
+ NFSRequestXidMap {
+ progver,
+ procedure,
+ chunk_offset,
+ file_name:Vec::new(),
+ file_handle:Vec::new(),
+ gssapi_proc: 0,
+ gssapi_service: 0,
+ }
+ }
+}
+
+/// little wrapper around the FileTransferTracker::new_chunk method
+pub fn filetracker_newchunk(ft: &mut FileTransferTracker, name: &[u8], data: &[u8],
+ chunk_offset: u64, chunk_size: u32, fill_bytes: u8, is_last: bool, xid: &u32)
+{
+ if let Some(sfcm) = unsafe { SURICATA_NFS_FILE_CONFIG } {
+ ft.new_chunk(sfcm, name, data, chunk_offset,
+ chunk_size, fill_bytes, is_last, xid);
+ }
+}
+
+fn filetracker_trunc(ft: &mut FileTransferTracker)
+{
+ if let Some(sfcm) = unsafe { SURICATA_NFS_FILE_CONFIG } {
+ ft.trunc(sfcm);
+ }
+}
+
+pub fn filetracker_close(ft: &mut FileTransferTracker)
+{
+ if let Some(sfcm) = unsafe { SURICATA_NFS_FILE_CONFIG } {
+ ft.close(sfcm);
+ }
+}
+
+fn filetracker_update(ft: &mut FileTransferTracker, data: &[u8], gap_size: u32) -> u32
+{
+ if let Some(sfcm) = unsafe { SURICATA_NFS_FILE_CONFIG } {
+ ft.update(sfcm, data, gap_size)
+ } else {
+ 0
+ }
+}
+
+
+#[derive(Debug)]
+pub struct NFSState {
+ state_data: AppLayerStateData,
+
+ /// map xid to procedure so replies can lookup the procedure
+ pub requestmap: HashMap<u32, NFSRequestXidMap>,
+
+ /// map file handle (1) to name (2)
+ pub namemap: HashMap<Vec<u8>, Vec<u8>>,
+
+ /// transactions list
+ pub transactions: Vec<NFSTransaction>,
+
+ /// partial record tracking
+ pub ts_chunk_xid: u32,
+ pub tc_chunk_xid: u32,
+ /// size of the current chunk that we still need to receive
+ pub ts_chunk_left: u32,
+ pub tc_chunk_left: u32,
+ /// file handle of in progress toserver WRITE file chunk
+ ts_chunk_fh: Vec<u8>,
+
+ ts_ssn_gap: bool,
+ tc_ssn_gap: bool,
+
+ ts_gap: bool, // last TS update was gap
+ tc_gap: bool, // last TC update was gap
+
+ is_udp: bool,
+
+ /// true as long as we have file txs that are in a post-gap
+ /// state. It means we'll do extra house keeping for those.
+ check_post_gap_file_txs: bool,
+ post_gap_files_checked: bool,
+
+ pub nfs_version: u16,
+
+ /// tx counter for assigning incrementing id's to tx's
+ tx_id: u64,
+
+ /// Timestamp in seconds of last update. This is packet time,
+ /// potentially coming from pcaps.
+ ts: u64,
+}
+
+impl Default for NFSState {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl State<NFSTransaction> for NFSState {
+ fn get_transaction_count(&self) -> usize {
+ self.transactions.len()
+ }
+
+ fn get_transaction_by_index(&self, index: usize) -> Option<&NFSTransaction> {
+ self.transactions.get(index)
+ }
+}
+
+impl NFSState {
+ /// Allocation function for a new TLS parser instance
+ pub fn new() -> NFSState {
+ NFSState {
+ state_data: AppLayerStateData::new(),
+ requestmap:HashMap::new(),
+ namemap:HashMap::new(),
+ transactions: Vec::new(),
+ ts_chunk_xid:0,
+ tc_chunk_xid:0,
+ ts_chunk_left:0,
+ tc_chunk_left:0,
+ ts_chunk_fh:Vec::new(),
+ ts_ssn_gap:false,
+ tc_ssn_gap:false,
+ ts_gap:false,
+ tc_gap:false,
+ is_udp:false,
+ check_post_gap_file_txs:false,
+ post_gap_files_checked:false,
+ nfs_version:0,
+ tx_id:0,
+ ts: 0,
+ }
+ }
+
+ fn update_ts(&mut self, ts: u64) {
+ if ts != self.ts {
+ self.ts = ts;
+ self.post_gap_files_checked = false;
+ }
+ }
+
+ pub fn new_tx(&mut self) -> NFSTransaction {
+ let mut tx = NFSTransaction::new();
+ self.tx_id += 1;
+ tx.id = self.tx_id;
+ if self.transactions.len() > unsafe { NFS_MAX_TX } {
+ // set at least one another transaction to the drop state
+ for tx_old in &mut self.transactions {
+ if !tx_old.request_done || !tx_old.response_done {
+ tx_old.request_done = true;
+ tx_old.response_done = true;
+ tx_old.is_file_closed = true;
+ tx_old.tx_data.set_event(NFSEvent::TooManyTransactions as u8);
+ break;
+ }
+ }
+ }
+ return tx;
+ }
+
+ pub fn free_tx(&mut self, tx_id: u64) {
+ //SCLogNotice!("Freeing TX with ID {}", tx_id);
+ 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.id == tx_id + 1 {
+ found = true;
+ index = i;
+ break;
+ }
+ }
+ if found {
+ SCLogDebug!("freeing TX with ID {} at index {}", tx_id, index);
+ self.transactions.remove(index);
+ }
+ }
+
+ pub fn get_tx_by_id(&mut self, tx_id: u64) -> Option<&NFSTransaction> {
+ SCLogDebug!("get_tx_by_id: tx_id={}", tx_id);
+ for tx in &mut self.transactions {
+ if tx.id == tx_id + 1 {
+ SCLogDebug!("Found NFS TX with ID {}", tx_id);
+ return Some(tx);
+ }
+ }
+ SCLogDebug!("Failed to find NFS TX with ID {}", tx_id);
+ return None;
+ }
+
+ pub fn get_tx_by_xid(&mut self, tx_xid: u32) -> Option<&mut NFSTransaction> {
+ SCLogDebug!("get_tx_by_xid: tx_xid={}", tx_xid);
+ for tx in &mut self.transactions {
+ if !tx.is_file_tx && tx.xid == tx_xid {
+ SCLogDebug!("Found NFS TX with ID {} XID {:04X}", tx.id, tx.xid);
+ return Some(tx);
+ }
+ }
+ SCLogDebug!("Failed to find NFS TX with XID {:04X}", tx_xid);
+ return None;
+ }
+
+ /// Set an event. The event is set on the most recent transaction.
+ pub fn set_event(&mut self, event: NFSEvent) {
+ let len = self.transactions.len();
+ if len == 0 {
+ return;
+ }
+
+ let tx = &mut self.transactions[len - 1];
+ tx.tx_data.set_event(event as u8);
+ }
+
+ // TODO maybe not enough users to justify a func
+ pub fn mark_response_tx_done(&mut self, xid: u32, rpc_status: u32, nfs_status: u32, resp_handle: &Vec<u8>)
+ {
+ if let Some(mytx) = self.get_tx_by_xid(xid) {
+ mytx.response_done = true;
+ mytx.rpc_response_status = rpc_status;
+ mytx.nfs_response_status = nfs_status;
+ if mytx.file_handle.is_empty() && !resp_handle.is_empty() {
+ mytx.file_handle = resp_handle.to_vec();
+ }
+
+ SCLogDebug!("process_reply_record: tx ID {} XID {:04X} REQUEST {} RESPONSE {}",
+ mytx.id, mytx.xid, mytx.request_done, mytx.response_done);
+ } else {
+ //SCLogNotice!("process_reply_record: not TX found for XID {}", r.hdr.xid);
+ }
+ }
+
+ fn add_rpc_udp_ts_pdu(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_len: i64) -> Option<Frame> {
+ let rpc_udp_ts_pdu = Frame::new(flow, stream_slice, input, rpc_len, NFSFrameType::RPCPdu as u8);
+ SCLogDebug!("rpc_udp_pdu ts frame {:?}", rpc_udp_ts_pdu);
+ rpc_udp_ts_pdu
+ }
+
+ fn add_rpc_udp_ts_creds(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], creds_len: i64) {
+ let _rpc_udp_ts_creds = Frame::new(flow, stream_slice, input, creds_len, NFSFrameType::RPCCreds as u8);
+ SCLogDebug!("rpc_creds ts frame {:?}", _rpc_udp_ts_creds);
+ }
+
+ fn add_rpc_tcp_ts_pdu(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_len: i64) -> Option<Frame> {
+ let rpc_tcp_ts_pdu = Frame::new(flow, stream_slice, input, rpc_len, NFSFrameType::RPCPdu as u8);
+ SCLogDebug!("rpc_tcp_pdu ts frame {:?}", rpc_tcp_ts_pdu);
+ rpc_tcp_ts_pdu
+ }
+
+ fn add_rpc_tcp_ts_creds(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], creds_len: i64) {
+ let _rpc_tcp_ts_creds = Frame::new(flow, stream_slice, input, creds_len, NFSFrameType::RPCCreds as u8);
+ SCLogDebug!("rpc_tcp_ts_creds {:?}", _rpc_tcp_ts_creds);
+ }
+
+ fn add_nfs_ts_frame(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], nfs_len: i64) {
+ let _nfs_req_pdu = Frame::new(flow, stream_slice, input, nfs_len, NFSFrameType::NFSPdu as u8);
+ SCLogDebug!("nfs_ts_pdu Frame {:?}", _nfs_req_pdu);
+ }
+
+ fn add_nfs4_ts_frames(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], nfs4_len: i64) {
+ let _nfs4_ts_pdu = Frame::new(flow, stream_slice, input, nfs4_len, NFSFrameType::NFS4Pdu as u8);
+ SCLogDebug!("nfs4_ts_pdu Frame: {:?}", _nfs4_ts_pdu);
+ if nfs4_len > 8 {
+ let _nfs4_ts_hdr = Frame::new(flow, stream_slice, input, 8, NFSFrameType::NFS4Hdr as u8);
+ SCLogDebug!("nfs4_ts_hdr Frame {:?}", _nfs4_ts_hdr);
+ let _nfs4_ts_ops = Frame::new(flow, stream_slice, &input[8..], nfs4_len - 8, NFSFrameType::NFS4Ops as u8);
+ SCLogDebug!("nfs4_ts_ops Frame {:?}", _nfs4_ts_ops);
+ }
+ }
+
+ fn add_rpc_udp_tc_pdu(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_len: i64) -> Option<Frame> {
+ let rpc_udp_tc_pdu = Frame::new(flow, stream_slice, input, rpc_len, NFSFrameType::RPCPdu as u8);
+ SCLogDebug!("rpc_tc_pdu frame {:?}", rpc_udp_tc_pdu);
+ rpc_udp_tc_pdu
+ }
+
+ fn add_rpc_udp_tc_frames(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_len: i64) {
+ if rpc_len > 8 {
+ let _rpc_udp_tc_hdr = Frame::new(flow, stream_slice, input, 8, NFSFrameType::RPCHdr as u8);
+ let _rpc_udp_tc_data = Frame::new(flow, stream_slice, &input[8..], rpc_len - 8, NFSFrameType::RPCData as u8);
+ SCLogDebug!("rpc_udp_tc_hdr frame {:?}", _rpc_udp_tc_hdr);
+ SCLogDebug!("rpc_udp_tc_data frame {:?}", _rpc_udp_tc_data);
+ }
+ }
+
+ fn add_rpc_tcp_tc_pdu(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_tcp_len: i64) -> Option<Frame> {
+ let rpc_tcp_tc_pdu = Frame::new(flow, stream_slice, input, rpc_tcp_len, NFSFrameType::RPCPdu as u8);
+ SCLogDebug!("rpc_tcp_pdu tc frame {:?}", rpc_tcp_tc_pdu);
+ rpc_tcp_tc_pdu
+ }
+
+ fn add_rpc_tcp_tc_frames(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], rpc_tcp_len: i64) {
+ if rpc_tcp_len > 12 {
+ let _rpc_tcp_tc_hdr = Frame::new(flow, stream_slice, input, 12, NFSFrameType::RPCHdr as u8);
+ let _rpc_tcp_tc_data = Frame::new(flow, stream_slice, &input[12..], rpc_tcp_len - 12, NFSFrameType::RPCData as u8);
+ SCLogDebug!("rpc_tcp_tc_hdr frame {:?}", _rpc_tcp_tc_hdr);
+ SCLogDebug!("rpc_tcp_tc_data frame {:?}", _rpc_tcp_tc_data);
+ }
+ }
+
+ fn add_nfs_tc_frames(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], nfs_len: i64) {
+ if nfs_len > 0 {
+ let _nfs_tc_pdu = Frame::new(flow, stream_slice, input, nfs_len, NFSFrameType::NFSPdu as u8);
+ SCLogDebug!("nfs_tc_pdu frame {:?}", _nfs_tc_pdu);
+ let _nfs_res_status = Frame::new(flow, stream_slice, input, 4, NFSFrameType::NFSStatus as u8);
+ SCLogDebug!("nfs_tc_status frame {:?}", _nfs_res_status);
+ }
+ }
+
+ fn add_nfs4_tc_frames(&mut self, flow: *const Flow, stream_slice: &StreamSlice, input: &[u8], nfs4_len: i64) {
+ if nfs4_len > 0 {
+ let _nfs4_tc_pdu = Frame::new(flow, stream_slice, input, nfs4_len, NFSFrameType::NFS4Pdu as u8);
+ SCLogDebug!("nfs4_tc_pdu frame {:?}", _nfs4_tc_pdu);
+ let _nfs4_tc_status = Frame::new(flow, stream_slice, input, 4, NFSFrameType::NFS4Status as u8);
+ SCLogDebug!("nfs4_tc_status frame {:?}", _nfs4_tc_status);
+ }
+ if nfs4_len > 8 {
+ let _nfs4_tc_hdr = Frame::new(flow, stream_slice, input, 8, NFSFrameType::NFS4Hdr as u8);
+ SCLogDebug!("nfs4_tc_hdr frame {:?}", _nfs4_tc_hdr);
+ let _nfs4_tc_ops = Frame::new(flow, stream_slice, &input[8..], nfs4_len - 8, NFSFrameType::NFS4Ops as u8);
+ SCLogDebug!("nfs4_tc_ops frame {:?}", _nfs4_tc_ops);
+ }
+ }
+
+ fn post_gap_housekeeping_for_files(&mut self)
+ {
+ let mut post_gap_txs = false;
+ for tx in &mut self.transactions {
+ if let Some(NFSTransactionTypeData::FILE(ref mut f)) = tx.type_data {
+ if f.post_gap_ts > 0 {
+ if self.ts > f.post_gap_ts {
+ tx.request_done = true;
+ tx.response_done = true;
+ filetracker_trunc(&mut f.file_tracker);
+ } else {
+ post_gap_txs = true;
+ }
+ }
+ }
+ }
+ self.check_post_gap_file_txs = post_gap_txs;
+ }
+
+ /* after a gap we will consider all transactions complete for our
+ * direction. File transfer transactions are an exception. Those
+ * can handle gaps. For the file transactions we set the current
+ * (flow) time and prune them in 60 seconds if no update for them
+ * was received. */
+ fn post_gap_housekeeping(&mut self, dir: Direction)
+ {
+ if self.ts_ssn_gap && dir == Direction::ToServer {
+ for tx in &mut self.transactions {
+ if tx.id >= self.tx_id {
+ SCLogDebug!("post_gap_housekeeping: done");
+ break;
+ }
+ if let Some(NFSTransactionTypeData::FILE(ref mut f)) = tx.type_data {
+ // leaving FILE txs open as they can deal with gaps. We
+ // remove them after 60 seconds of no activity though.
+ if f.post_gap_ts == 0 {
+ f.post_gap_ts = self.ts + 60;
+ self.check_post_gap_file_txs = true;
+ }
+ } else {
+ SCLogDebug!("post_gap_housekeeping: tx {} marked as done TS", tx.id);
+ tx.request_done = true;
+ }
+ }
+ } else if self.tc_ssn_gap && dir == Direction::ToClient {
+ for tx in &mut self.transactions {
+ if tx.id >= self.tx_id {
+ SCLogDebug!("post_gap_housekeeping: done");
+ break;
+ }
+ if let Some(NFSTransactionTypeData::FILE(ref mut f)) = tx.type_data {
+ // leaving FILE txs open as they can deal with gaps. We
+ // remove them after 60 seconds of no activity though.
+ if f.post_gap_ts == 0 {
+ f.post_gap_ts = self.ts + 60;
+ self.check_post_gap_file_txs = true;
+ }
+ } else {
+ SCLogDebug!("post_gap_housekeeping: tx {} marked as done TC", tx.id);
+ tx.request_done = true;
+ tx.response_done = true;
+ }
+ }
+ }
+ }
+
+ pub fn process_request_record_lookup(&mut self, r: &RpcPacket, xidmap: &mut NFSRequestXidMap) {
+ match parse_nfs3_request_lookup(r.prog_data) {
+ Ok((_, lookup)) => {
+ SCLogDebug!("LOOKUP {:?}", lookup);
+ xidmap.file_name = lookup.name_vec;
+ },
+ _ => {
+ self.set_event(NFSEvent::MalformedData);
+ },
+ };
+ }
+
+ pub fn xidmap_handle2name(&mut self, xidmap: &mut NFSRequestXidMap) {
+ match self.namemap.get(&xidmap.file_handle) {
+ Some(n) => {
+ SCLogDebug!("xidmap_handle2name: name {:?}", n);
+ xidmap.file_name = n.to_vec();
+ },
+ _ => {
+ SCLogDebug!("xidmap_handle2name: object {:?} not found",
+ xidmap.file_handle);
+ },
+ }
+ }
+
+ /// complete request record
+ fn process_request_record(&mut self, flow: *const Flow, stream_slice: &StreamSlice, r: &RpcPacket) {
+ SCLogDebug!("REQUEST {} procedure {} ({}) blob size {}",
+ r.hdr.xid, r.procedure, self.requestmap.len(), r.prog_data.len());
+
+ match r.progver {
+ 4 => {
+ self.add_nfs4_ts_frames(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_request_record_v4(r)
+ },
+ 3 => {
+ self.add_nfs_ts_frame(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_request_record_v3(r)
+ },
+ 2 => {
+ self.add_nfs_ts_frame(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_request_record_v2(r)
+ },
+ _ => { },
+ }
+ }
+
+ pub fn new_file_tx(&mut self, file_handle: &[u8], file_name: &[u8], direction: Direction)
+ -> &mut NFSTransaction
+ {
+ let mut tx = self.new_tx();
+ tx.file_name = file_name.to_vec();
+ tx.file_handle = file_handle.to_vec();
+ tx.is_file_tx = true;
+
+ tx.type_data = Some(NFSTransactionTypeData::FILE(NFSTransactionFile::new()));
+ if let Some(NFSTransactionTypeData::FILE(ref mut d)) = tx.type_data {
+ d.direction = direction;
+ d.file_tracker.tx_id = tx.id - 1;
+ tx.tx_data.update_file_flags(self.state_data.file_flags);
+ d.update_file_flags(tx.tx_data.file_flags);
+ }
+ tx.tx_data.init_files_opened();
+ tx.tx_data.file_tx = if direction == Direction::ToServer { STREAM_TOSERVER } else { STREAM_TOCLIENT }; // TODO direction to flag func?
+ SCLogDebug!("new_file_tx: TX FILE created: ID {} NAME {}",
+ tx.id, String::from_utf8_lossy(file_name));
+ self.transactions.push(tx);
+ let tx_ref = self.transactions.last_mut();
+ return tx_ref.unwrap();
+ }
+
+ pub fn get_file_tx_by_handle(&mut self, file_handle: &[u8], direction: Direction)
+ -> Option<&mut NFSTransaction>
+ {
+ let fh = file_handle.to_vec();
+ for tx in &mut self.transactions {
+ if let Some(NFSTransactionTypeData::FILE(ref mut d)) = tx.type_data {
+ if tx.is_file_tx && !tx.is_file_closed &&
+ direction == d.direction &&
+ tx.file_handle == fh
+ {
+ tx.tx_data.update_file_flags(self.state_data.file_flags);
+ d.update_file_flags(tx.tx_data.file_flags);
+ SCLogDebug!("Found NFS file TX with ID {} XID {:04X}", tx.id, tx.xid);
+ return Some(tx);
+ }
+ }
+ }
+ SCLogDebug!("Failed to find NFS TX with handle {:?}", file_handle);
+ return None;
+ }
+
+ pub fn process_write_record<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs3RequestWrite<'b>) -> u32 {
+ let mut fill_bytes = 0;
+ let pad = w.count % 4;
+ if pad != 0 {
+ fill_bytes = 4 - pad;
+ }
+
+ // linux defines a max of 1mb. Allow several multiples.
+ if w.count == 0 || w.count > 16777216 {
+ return 0;
+ }
+
+ // for now assume that stable FILE_SYNC flags means a single chunk
+ let is_last = w.stable == 2;
+ let file_handle = w.handle.value.to_vec();
+ let file_name = if let Some(name) = self.namemap.get(w.handle.value) {
+ SCLogDebug!("WRITE name {:?}", name);
+ name.to_vec()
+ } else {
+ SCLogDebug!("WRITE object {:?} not found", w.handle.value);
+ Vec::new()
+ };
+
+ let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
+ Some(tx) => {
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ filetracker_newchunk(&mut tdf.file_tracker,
+ &file_name, w.file_data, w.offset,
+ w.file_len, fill_bytes as u8, is_last, &r.hdr.xid);
+ tdf.chunk_count += 1;
+ if is_last {
+ tdf.file_last_xid = r.hdr.xid;
+ tx.is_last = true;
+ tx.response_done = true;
+ tx.is_file_closed = true;
+ }
+ true
+ } else {
+ false
+ }
+ },
+ None => { false },
+ };
+ if !found {
+ let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ filetracker_newchunk(&mut tdf.file_tracker,
+ &file_name, w.file_data, w.offset,
+ w.file_len, fill_bytes as u8, is_last, &r.hdr.xid);
+ tx.procedure = NFSPROC3_WRITE;
+ tx.xid = r.hdr.xid;
+ tx.is_first = true;
+ tx.nfs_version = r.progver as u16;
+ if is_last {
+ tdf.file_last_xid = r.hdr.xid;
+ tx.is_last = true;
+ tx.request_done = true;
+ tx.is_file_closed = true;
+ }
+ }
+ }
+ if !self.is_udp {
+ self.ts_chunk_xid = r.hdr.xid;
+ debug_validate_bug_on!(w.file_data.len() as u32 > w.count);
+ self.ts_chunk_left = w.count - w.file_data.len() as u32;
+ self.ts_chunk_fh = file_handle;
+ SCLogDebug!("REQUEST chunk_xid {:04X} chunk_left {}", self.ts_chunk_xid, self.ts_chunk_left);
+ }
+ 0
+ }
+
+ fn process_partial_write_request_record<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs3RequestWrite<'b>) -> u32 {
+ SCLogDebug!("REQUEST {} procedure {} blob size {}", r.hdr.xid, r.procedure, r.prog_data.len());
+
+ let mut xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0);
+ xidmap.file_handle = w.handle.value.to_vec();
+ self.requestmap.insert(r.hdr.xid, xidmap);
+
+ return self.process_write_record(r, w);
+ }
+
+ fn process_reply_record(&mut self, flow: *const Flow, stream_slice: &StreamSlice, r: &RpcReplyPacket) -> u32 {
+ let mut xidmap;
+ match self.requestmap.remove(&r.hdr.xid) {
+ Some(p) => { xidmap = p; },
+ _ => {
+ SCLogDebug!("REPLY: xid {:04X} NOT FOUND. GAPS? TS:{} TC:{}",
+ r.hdr.xid, self.ts_ssn_gap, self.tc_ssn_gap);
+
+ // TODO we might be able to try to infer from the size + data
+ // that this is a READ reply and pass the data to the file API anyway?
+ return 0;
+ },
+ }
+ SCLogDebug!("process_reply_record: removed xid {:04X} from requestmap",
+ r.hdr.xid);
+
+ if self.nfs_version == 0 {
+ self.nfs_version = xidmap.progver as u16;
+ }
+
+ match xidmap.progver {
+ 2 => {
+ SCLogDebug!("NFSv2 reply record");
+ self.add_nfs_tc_frames(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_reply_record_v2(r, &xidmap);
+ return 0;
+ },
+ 3 => {
+ SCLogDebug!("NFSv3 reply record");
+ self.add_nfs_tc_frames(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_reply_record_v3(r, &mut xidmap);
+ return 0;
+ },
+ 4 => {
+ SCLogDebug!("NFSv4 reply record");
+ self.add_nfs4_tc_frames(flow, stream_slice, r.prog_data, r.prog_data_size as i64);
+ self.process_reply_record_v4(r, &mut xidmap);
+ return 0;
+ },
+ _ => {
+ SCLogDebug!("Invalid NFS version");
+ self.set_event(NFSEvent::NonExistingVersion);
+ return 0;
+ },
+ }
+ }
+
+ // update in progress chunks for file transfers
+ // return how much data we consumed
+ fn filetracker_update(&mut self, direction: Direction, data: &[u8], gap_size: u32) -> u32 {
+ let mut chunk_left = if direction == Direction::ToServer {
+ self.ts_chunk_left
+ } else {
+ self.tc_chunk_left
+ };
+ if chunk_left == 0 {
+ return 0
+ }
+ let xid = if direction == Direction::ToServer {
+ self.ts_chunk_xid
+ } else {
+ self.tc_chunk_xid
+ };
+ SCLogDebug!("filetracker_update: chunk left {}, input {} chunk_xid {:04X}", chunk_left, data.len(), xid);
+
+ let file_handle;
+ // we have the data that we expect
+ if chunk_left <= data.len() as u32 {
+ chunk_left = 0;
+
+ if direction == Direction::ToServer {
+ self.ts_chunk_xid = 0;
+ file_handle = self.ts_chunk_fh.to_vec();
+ self.ts_chunk_fh.clear();
+ } else {
+ self.tc_chunk_xid = 0;
+
+ // chunk done, remove requestmap entry
+ match self.requestmap.remove(&xid) {
+ None => {
+ SCLogDebug!("no file handle found for XID {:04X}", xid);
+ return 0
+ },
+ Some(xidmap) => {
+ file_handle = xidmap.file_handle.to_vec();
+ },
+ }
+ }
+ } else {
+ chunk_left -= data.len() as u32;
+
+ if direction == Direction::ToServer {
+ file_handle = self.ts_chunk_fh.to_vec();
+ } else {
+ // see if we have a file handle to work on
+ match self.requestmap.get(&xid) {
+ None => {
+ SCLogDebug!("no file handle found for XID {:04X}", xid);
+ return 0
+ },
+ Some(xidmap) => {
+ file_handle = xidmap.file_handle.to_vec();
+ },
+ }
+ }
+ }
+
+ if direction == Direction::ToServer {
+ self.ts_chunk_left = chunk_left;
+ } else {
+ self.tc_chunk_left = chunk_left;
+ }
+
+ let ssn_gap = self.ts_ssn_gap | self.tc_ssn_gap;
+ // get the tx and update it
+ let consumed = match self.get_file_tx_by_handle(&file_handle, direction) {
+ Some(tx) => {
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ if ssn_gap {
+ let queued_data = tdf.file_tracker.get_queued_size();
+ if queued_data > 2000000 { // TODO should probably be configurable
+ SCLogDebug!("QUEUED size {} while we've seen GAPs. Truncating file.", queued_data);
+ filetracker_trunc(&mut tdf.file_tracker);
+ }
+ }
+
+ // reset timestamp if we get called after a gap
+ if tdf.post_gap_ts > 0 {
+ tdf.post_gap_ts = 0;
+ }
+
+ tdf.chunk_count += 1;
+ let cs = filetracker_update(&mut tdf.file_tracker, data, gap_size);
+ /* see if we need to close the tx */
+ if tdf.file_tracker.is_done() {
+ if direction == Direction::ToClient {
+ tx.response_done = true;
+ tx.is_file_closed = true;
+ SCLogDebug!("TX {} response is done now that the file track is ready", tx.id);
+ } else {
+ tx.request_done = true;
+ tx.is_file_closed = true;
+ SCLogDebug!("TX {} request is done now that the file track is ready", tx.id);
+ }
+ }
+ cs
+ } else {
+ 0
+ }
+ },
+ None => { 0 },
+ };
+ return consumed;
+ }
+
+ /// xidmapr is an Option as it's already removed from the map if we
+ /// have a complete record. Otherwise we do a lookup ourselves.
+ pub fn process_read_record<'b>(&mut self, r: &RpcReplyPacket<'b>,
+ reply: &NfsReplyRead<'b>, xidmapr: Option<&NFSRequestXidMap>) -> u32
+ {
+ let file_name;
+ let file_handle;
+ let chunk_offset;
+ let nfs_version;
+
+ let mut fill_bytes = 0;
+ let pad = reply.count % 4;
+ if pad != 0 {
+ fill_bytes = 4 - pad;
+ }
+
+ // linux defines a max of 1mb. Allow several multiples.
+ if reply.count == 0 || reply.count > 16777216 {
+ return 0;
+ }
+
+ match xidmapr {
+ Some(xidmap) => {
+ file_name = xidmap.file_name.to_vec();
+ file_handle = xidmap.file_handle.to_vec();
+ chunk_offset = xidmap.chunk_offset;
+ nfs_version = xidmap.progver;
+ },
+ None => {
+ if let Some(xidmap) = self.requestmap.get(&r.hdr.xid) {
+ file_name = xidmap.file_name.to_vec();
+ file_handle = xidmap.file_handle.to_vec();
+ chunk_offset = xidmap.chunk_offset;
+ nfs_version = xidmap.progver;
+ } else {
+ return 0;
+ }
+ },
+ }
+ SCLogDebug!("chunk_offset {}", chunk_offset);
+
+ let mut is_last = reply.eof;
+ SCLogDebug!("XID {} is_last {} fill_bytes {} reply.count {} reply.data_len {} reply.data.len() {}",
+ r.hdr.xid, is_last, fill_bytes, reply.count, reply.data_len, reply.data.len());
+
+ if nfs_version == 2 {
+ let size = match parse_nfs2_attribs(reply.attr_blob) {
+ Ok((_, ref attr)) => attr.asize,
+ _ => 0,
+ };
+ SCLogDebug!("NFSv2 READ reply record: File size {}. Offset {} data len {}: total {}",
+ size, chunk_offset, reply.data_len, chunk_offset + reply.data_len as u64);
+
+ if size as u64 == chunk_offset + reply.data_len as u64 {
+ is_last = true;
+ }
+
+ }
+
+ let is_partial = reply.data.len() < reply.count as usize;
+ SCLogDebug!("partial data? {}", is_partial);
+
+ let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToClient) {
+ Some(tx) => {
+ SCLogDebug!("updated TX {:?}", tx);
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ filetracker_newchunk(&mut tdf.file_tracker,
+ &file_name, reply.data, chunk_offset,
+ reply.count, fill_bytes as u8, is_last, &r.hdr.xid);
+ tdf.chunk_count += 1;
+ if is_last {
+ tdf.file_last_xid = r.hdr.xid;
+ tx.rpc_response_status = r.reply_state;
+ tx.nfs_response_status = reply.status;
+ tx.is_last = true;
+ tx.request_done = true;
+
+ /* if this is a partial record we will close the tx
+ * when we've received the final data */
+ if !is_partial {
+ tx.response_done = true;
+ SCLogDebug!("TX {} is DONE", tx.id);
+ }
+ }
+ true
+ } else {
+ false
+ }
+ },
+ None => { false },
+ };
+ if !found {
+ let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToClient);
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ filetracker_newchunk(&mut tdf.file_tracker,
+ &file_name, reply.data, chunk_offset,
+ reply.count, fill_bytes as u8, is_last, &r.hdr.xid);
+ tx.procedure = if nfs_version < 4 { NFSPROC3_READ } else { NFSPROC4_READ };
+ tx.xid = r.hdr.xid;
+ tx.is_first = true;
+ if is_last {
+ tdf.file_last_xid = r.hdr.xid;
+ tx.rpc_response_status = r.reply_state;
+ tx.nfs_response_status = reply.status;
+ tx.is_last = true;
+ tx.request_done = true;
+
+ /* if this is a partial record we will close the tx
+ * when we've received the final data */
+ if !is_partial {
+ tx.response_done = true;
+ SCLogDebug!("TX {} is DONE", tx.id);
+ }
+ }
+ }
+ }
+
+ if !self.is_udp {
+ self.tc_chunk_xid = r.hdr.xid;
+ debug_validate_bug_on!(reply.data.len() as u32 > reply.count);
+ self.tc_chunk_left = reply.count - reply.data.len() as u32;
+ }
+
+ SCLogDebug!("REPLY {} to procedure {} blob size {} / {}: chunk_left {} chunk_xid {:04X}",
+ r.hdr.xid, NFSPROC3_READ, r.prog_data.len(), reply.count, self.tc_chunk_left,
+ self.tc_chunk_xid);
+ 0
+ }
+
+ fn process_partial_read_reply_record<'b>(&mut self, r: &RpcReplyPacket<'b>, reply: &NfsReplyRead<'b>) -> u32 {
+ SCLogDebug!("REPLY {} to procedure READ blob size {} / {}",
+ r.hdr.xid, r.prog_data.len(), reply.count);
+
+ return self.process_read_record(r, reply, None);
+ }
+
+ fn peek_reply_record(&mut self, r: &RpcPacketHeader) -> u32 {
+ if let Some(xidmap) = self.requestmap.get(&r.xid) {
+ return xidmap.procedure;
+ } else {
+ SCLogDebug!("REPLY: xid {} NOT FOUND", r.xid);
+ return 0;
+ }
+ }
+
+ pub fn parse_tcp_data_ts_gap(&mut self, gap_size: u32) -> AppLayerResult {
+ SCLogDebug!("parse_tcp_data_ts_gap ({})", gap_size);
+ let gap = vec![0; gap_size as usize];
+ let consumed = self.filetracker_update(Direction::ToServer, &gap, gap_size);
+ if consumed > gap_size {
+ SCLogDebug!("consumed more than GAP size: {} > {}", consumed, gap_size);
+ return AppLayerResult::ok();
+ }
+ self.ts_ssn_gap = true;
+ self.ts_gap = true;
+ SCLogDebug!("parse_tcp_data_ts_gap ({}) done", gap_size);
+ return AppLayerResult::ok();
+ }
+
+ pub fn parse_tcp_data_tc_gap(&mut self, gap_size: u32) -> AppLayerResult {
+ SCLogDebug!("parse_tcp_data_tc_gap ({})", gap_size);
+ let gap = vec![0; gap_size as usize];
+ let consumed = self.filetracker_update(Direction::ToClient, &gap, gap_size);
+ if consumed > gap_size {
+ SCLogDebug!("consumed more than GAP size: {} > {}", consumed, gap_size);
+ return AppLayerResult::ok();
+ }
+ self.tc_ssn_gap = true;
+ self.tc_gap = true;
+ SCLogDebug!("parse_tcp_data_tc_gap ({}) done", gap_size);
+ return AppLayerResult::ok();
+ }
+
+ /// Handle partial records
+ fn parse_tcp_partial_data_ts<'b>(&mut self, base_input: &'b[u8], cur_i: &'b[u8],
+ phdr: &RpcRequestPacketPartial, rec_size: usize) -> AppLayerResult {
+ // special case: avoid buffering file write blobs
+ // as these can be large.
+ if rec_size >= 512 && cur_i.len() >= 44 {
+ // large record, likely file xfer
+ SCLogDebug!("large record {}, likely file xfer", rec_size);
+
+ // quick peek, are we in WRITE mode?
+ if phdr.procedure == NFSPROC3_WRITE {
+ SCLogDebug!("CONFIRMED WRITE: large record {}, file chunk xfer", rec_size);
+
+ // lets try to parse the RPC record. Might fail with Incomplete.
+ match parse_rpc(cur_i, false) {
+ Ok((_rem, ref hdr)) => {
+ // we got here because rec_size > input, so we should never have
+ // remaining data
+ debug_validate_bug_on!(!_rem.is_empty());
+
+ match parse_nfs3_request_write(hdr.prog_data, false) {
+ Ok((_, ref w)) => {
+ // deal with the partial nfs write data
+ self.process_partial_write_request_record(hdr, w);
+ return AppLayerResult::ok();
+ }
+ Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ Err(Err::Incomplete(_)) => {
+ // this is normal, fall through to incomplete handling
+ }
+ }
+ }
+ Err(Err::Incomplete(_)) => {
+ // size check was done for a minimal RPC record size,
+ // so Incomplete is normal.
+ SCLogDebug!("TS data incomplete");
+ }
+ Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ }
+ }
+ }
+ // make sure we pass a value higher than current input
+ // but lower than the record size
+ let n1 = cmp::max(cur_i.len(), 1024);
+ let n2 = cmp::min(n1, rec_size);
+ return AppLayerResult::incomplete((base_input.len() - cur_i.len()) as u32, n2 as u32);
+ }
+
+ /// Parsing function, handling TCP chunks fragmentation
+ pub fn parse_tcp_data_ts(&mut self, flow: *const Flow, stream_slice: &StreamSlice) -> AppLayerResult {
+ let mut cur_i = stream_slice.as_slice();
+ // take care of in progress file chunk transfers
+ // and skip buffer beyond it
+ let consumed = self.filetracker_update(Direction::ToServer, cur_i, 0);
+ if consumed > 0 {
+ if consumed > cur_i.len() as u32 {
+ return AppLayerResult::err();
+ }
+ cur_i = &cur_i[consumed as usize..];
+ }
+ if cur_i.is_empty() {
+ return AppLayerResult::ok();
+ }
+ if self.ts_gap {
+ SCLogDebug!("TS trying to catch up after GAP (input {})", cur_i.len());
+
+ let mut _cnt = 0;
+ while !cur_i.is_empty() {
+ _cnt += 1;
+ match nfs_probe(cur_i, Direction::ToServer) {
+ 1 => {
+ SCLogDebug!("expected data found");
+ self.ts_gap = false;
+ break;
+ },
+ 0 => {
+ SCLogDebug!("incomplete, queue and retry with the next block (input {}). Looped {} times.",
+ cur_i.len(), _cnt);
+ return AppLayerResult::incomplete(stream_slice.len() - cur_i.len() as u32, (cur_i.len() + 1) as u32);
+ },
+ -1 => {
+ cur_i = &cur_i[1..];
+ if cur_i.is_empty() {
+ SCLogDebug!("all post-GAP data in this chunk was bad. Looped {} times.", _cnt);
+ }
+ },
+ _ => {
+ return AppLayerResult::err();
+ },
+ }
+ }
+ SCLogDebug!("TS GAP handling done (input {})", cur_i.len());
+ }
+
+ while !cur_i.is_empty() { // min record size
+ self.add_rpc_tcp_ts_pdu(flow, stream_slice, cur_i, cur_i.len() as i64);
+ match parse_rpc_request_partial(cur_i) {
+ Ok((_, ref rpc_phdr)) => {
+ let rec_size = (rpc_phdr.hdr.frag_len + 4) as usize;
+
+ // Handle partial records
+ if rec_size > cur_i.len() {
+ return self.parse_tcp_partial_data_ts(stream_slice.as_slice(), cur_i, rpc_phdr, rec_size);
+ }
+
+ // we have the full records size worth of data,
+ // let's parse it. Errors lead to event, but are
+ // not fatal as we already have enough info to
+ // go to the next record.
+ match parse_rpc(cur_i, true) {
+ Ok((_, ref rpc_record)) => {
+ self.add_rpc_tcp_ts_creds(flow, stream_slice, &cur_i[RPC_TCP_PRE_CREDS..], (rpc_record.creds_len + 8) as i64);
+ self.process_request_record(flow, stream_slice, rpc_record);
+ }
+ Err(Err::Incomplete(_)) => {
+ self.set_event(NFSEvent::MalformedData);
+ }
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ }
+ }
+ cur_i = &cur_i[rec_size..];
+ }
+ Err(Err::Incomplete(needed)) => {
+ if let Needed::Size(n) = needed {
+ SCLogDebug!("Not enough data for partial RPC header {:?}", needed);
+ // 28 is the partial RPC header size parse_rpc_request_partial
+ // looks for.
+ let n = usize::from(n);
+ let need = if n > 28 { n } else { 28 };
+ return AppLayerResult::incomplete(stream_slice.len() - cur_i.len() as u32, need as u32);
+ }
+ return AppLayerResult::err();
+ }
+ /* This error is fatal. If we failed to parse the RPC hdr we don't
+ * have a length and we don't know where the next record starts. */
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ }
+ };
+
+ self.post_gap_housekeeping(Direction::ToServer);
+ if self.check_post_gap_file_txs && !self.post_gap_files_checked {
+ self.post_gap_housekeeping_for_files();
+ self.post_gap_files_checked = true;
+ }
+
+ AppLayerResult::ok()
+ }
+
+ /// Handle partial records
+ fn parse_tcp_partial_data_tc<'b>(&mut self, base_input: &'b[u8], cur_i: &'b[u8],
+ phdr: &RpcPacketHeader, rec_size: usize) -> AppLayerResult {
+ // special case: avoid buffering file read blobs
+ // as these can be large.
+ if rec_size >= 512 && cur_i.len() >= 128 {//36 {
+ // large record, likely file xfer
+ SCLogDebug!("large record {}, likely file xfer", rec_size);
+
+ // quick peek, are in READ mode?
+ if self.peek_reply_record(phdr) == NFSPROC3_READ {
+ SCLogDebug!("CONFIRMED large READ record {}, likely file chunk xfer", rec_size);
+
+ // we should have enough data to parse the RPC record
+ match parse_rpc_reply(cur_i, false) {
+ Ok((_rem, ref hdr)) => {
+ // we got here because rec_size > input, so we should never have
+ // remaining data
+ debug_validate_bug_on!(!_rem.is_empty());
+
+ match parse_nfs3_reply_read(hdr.prog_data, false) {
+ Ok((_, ref r)) => {
+ // deal with the partial nfs read data
+ self.process_partial_read_reply_record(hdr, r);
+ return AppLayerResult::ok();
+ }
+ Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ Err(Err::Incomplete(_)) => {
+ // this is normal, fall through to incomplete handling
+ }
+ }
+ }
+ Err(Err::Incomplete(_)) => {
+ // size check was done for a minimal RPC record size,
+ // so Incomplete is normal.
+ SCLogDebug!("TC data incomplete");
+ }
+ Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ }
+ }
+ }
+ // make sure we pass a value higher than current input
+ // but lower than the record size
+ let n1 = cmp::max(cur_i.len(), 1024);
+ let n2 = cmp::min(n1, rec_size);
+ return AppLayerResult::incomplete((base_input.len() - cur_i.len()) as u32, n2 as u32);
+ }
+
+ /// Parsing function, handling TCP chunks fragmentation
+ pub fn parse_tcp_data_tc(&mut self, flow: *const Flow, stream_slice: &StreamSlice) -> AppLayerResult {
+ let mut cur_i = stream_slice.as_slice();
+ // take care of in progress file chunk transfers
+ // and skip buffer beyond it
+ let consumed = self.filetracker_update(Direction::ToClient, cur_i, 0);
+ if consumed > 0 {
+ if consumed > cur_i.len() as u32 {
+ return AppLayerResult::err();
+ }
+ cur_i = &cur_i[consumed as usize..];
+ }
+ if cur_i.is_empty() {
+ return AppLayerResult::ok();
+ }
+ if self.tc_gap {
+ SCLogDebug!("TC trying to catch up after GAP (input {})", cur_i.len());
+
+ let mut _cnt = 0;
+ while !cur_i.is_empty() {
+ _cnt += 1;
+ match nfs_probe(cur_i, Direction::ToClient) {
+ 1 => {
+ SCLogDebug!("expected data found");
+ self.tc_gap = false;
+ break;
+ },
+ 0 => {
+ SCLogDebug!("incomplete, queue and retry with the next block (input {}). Looped {} times.",
+ cur_i.len(), _cnt);
+ return AppLayerResult::incomplete(stream_slice.len() - cur_i.len() as u32, (cur_i.len() + 1) as u32);
+ },
+ -1 => {
+ cur_i = &cur_i[1..];
+ if cur_i.is_empty() {
+ SCLogDebug!("all post-GAP data in this chunk was bad. Looped {} times.", _cnt);
+ }
+ },
+ _ => {
+ return AppLayerResult::err();
+ }
+ }
+ }
+ SCLogDebug!("TC GAP handling done (input {})", cur_i.len());
+ }
+
+ while !cur_i.is_empty() {
+ self.add_rpc_tcp_tc_pdu(flow, stream_slice, cur_i, cur_i.len() as i64);
+ match parse_rpc_packet_header(cur_i) {
+ Ok((_, ref rpc_phdr)) => {
+ let rec_size = (rpc_phdr.frag_len + 4) as usize;
+ // see if we have all data available
+ if rec_size > cur_i.len() {
+ return self.parse_tcp_partial_data_tc(stream_slice.as_slice(), cur_i, rpc_phdr, rec_size);
+ }
+
+ // we have the full data of the record, lets parse
+ match parse_rpc_reply(cur_i, true) {
+ Ok((_, ref rpc_record)) => {
+ self.add_rpc_tcp_tc_frames(flow, stream_slice, cur_i, cur_i.len() as i64);
+ self.process_reply_record(flow, stream_slice, rpc_record);
+ }
+ Err(Err::Incomplete(_)) => {
+ // we shouldn't get incomplete as we have the full data
+ // so if we got incomplete anyway it's the data that is
+ // bad.
+ self.set_event(NFSEvent::MalformedData);
+ }
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ }
+ }
+ cur_i = &cur_i[rec_size..]; // progress input past parsed record
+ }
+ Err(Err::Incomplete(needed)) => {
+ if let Needed::Size(n) = needed {
+ SCLogDebug!("Not enough data for partial RPC header {:?}", needed);
+ // 12 is the partial RPC header size parse_rpc_packet_header
+ // looks for.
+ let n = usize::from(n);
+ let need = if n > 12 { n } else { 12 };
+ return AppLayerResult::incomplete(stream_slice.len() - cur_i.len() as u32, need as u32);
+ }
+ return AppLayerResult::err();
+ }
+ /* This error is fatal. If we failed to parse the RPC hdr we don't
+ * have a length and we don't know where the next record starts. */
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ self.set_event(NFSEvent::MalformedData);
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ return AppLayerResult::err();
+ }
+ }
+ };
+ self.post_gap_housekeeping(Direction::ToClient);
+ if self.check_post_gap_file_txs && !self.post_gap_files_checked {
+ self.post_gap_housekeeping_for_files();
+ self.post_gap_files_checked = true;
+ }
+ AppLayerResult::ok()
+ }
+ /// Parsing function
+ pub fn parse_udp_ts(&mut self, flow: *const Flow, stream_slice: &StreamSlice) -> AppLayerResult {
+ let input = stream_slice.as_slice();
+ SCLogDebug!("parse_udp_ts ({})", input.len());
+ self.add_rpc_udp_ts_pdu(flow, stream_slice, input, input.len() as i64);
+ if !input.is_empty() {
+ match parse_rpc_udp_request(input) {
+ Ok((_, ref rpc_record)) => {
+ self.is_udp = true;
+ self.add_rpc_udp_ts_creds(flow, stream_slice, &input[RPC_UDP_PRE_CREDS..], (rpc_record.creds_len + 8) as i64);
+ match rpc_record.progver {
+ 3 => {
+ self.process_request_record(flow, stream_slice, rpc_record);
+ },
+ 2 => {
+ self.add_nfs_ts_frame(flow, stream_slice, rpc_record.prog_data, rpc_record.prog_data_size as i64);
+ self.process_request_record_v2(rpc_record);
+ },
+ _ => { },
+ }
+ },
+ Err(Err::Incomplete(_)) => {
+ },
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ }
+ }
+ }
+ AppLayerResult::ok()
+ }
+
+ /// Parsing function
+ pub fn parse_udp_tc(&mut self, flow: *const Flow, stream_slice: &StreamSlice) -> AppLayerResult {
+ let input = stream_slice.as_slice();
+ SCLogDebug!("parse_udp_tc ({})", input.len());
+ self.add_rpc_udp_tc_pdu(flow, stream_slice, input, input.len() as i64);
+ if !input.is_empty() {
+ match parse_rpc_udp_reply(input) {
+ Ok((_, ref rpc_record)) => {
+ self.is_udp = true;
+ self.add_rpc_udp_tc_frames(flow, stream_slice, input, input.len() as i64);
+ self.process_reply_record(flow, stream_slice, rpc_record);
+ },
+ Err(Err::Incomplete(_)) => {
+ },
+ Err(Err::Error(_e)) |
+ Err(Err::Failure(_e)) => {
+ SCLogDebug!("Parsing failed: {:?}", _e);
+ }
+ }
+ }
+ AppLayerResult::ok()
+ }
+}
+
+/// Returns *mut NFSState
+#[no_mangle]
+pub extern "C" fn rs_nfs_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto) -> *mut std::os::raw::c_void {
+ let state = NFSState::new();
+ let boxed = Box::new(state);
+ SCLogDebug!("allocating state");
+ return Box::into_raw(boxed) as *mut _;
+}
+
+/// Params:
+/// - state: *mut NFSState as void pointer
+#[no_mangle]
+pub extern "C" fn rs_nfs_state_free(state: *mut std::os::raw::c_void) {
+ // Just unbox...
+ SCLogDebug!("freeing state");
+ std::mem::drop(unsafe { Box::from_raw(state as *mut NFSState) });
+}
+
+/// C binding parse a NFS TCP request. Returns 1 on success, -1 on failure.
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_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, NFSState);
+ let flow = cast_pointer!(flow, Flow);
+
+ if stream_slice.is_gap() {
+ return rs_nfs_parse_request_tcp_gap(state, stream_slice.gap_size());
+ }
+ SCLogDebug!("parsing {} bytes of request data", stream_slice.len());
+
+ state.update_ts(flow.get_last_time().as_secs());
+ state.parse_tcp_data_ts(flow, &stream_slice)
+}
+
+#[no_mangle]
+pub extern "C" fn rs_nfs_parse_request_tcp_gap(
+ state: &mut NFSState,
+ input_len: u32)
+ -> AppLayerResult
+{
+ state.parse_tcp_data_ts_gap(input_len)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_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, NFSState);
+ let flow = cast_pointer!(flow, Flow);
+
+ if stream_slice.is_gap() {
+ return rs_nfs_parse_response_tcp_gap(state, stream_slice.gap_size());
+ }
+ SCLogDebug!("parsing {} bytes of response data", stream_slice.len());
+
+ state.update_ts(flow.get_last_time().as_secs());
+ state.parse_tcp_data_tc(flow, &stream_slice)
+}
+
+#[no_mangle]
+pub extern "C" fn rs_nfs_parse_response_tcp_gap(
+ state: &mut NFSState,
+ input_len: u32)
+ -> AppLayerResult
+{
+ state.parse_tcp_data_tc_gap(input_len)
+}
+
+/// C binding to parse an NFS/UDP request. Returns 1 on success, -1 on failure.
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_parse_request_udp(f: *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, NFSState);
+
+ SCLogDebug!("parsing {} bytes of request data", stream_slice.len());
+ state.parse_udp_ts(f, &stream_slice)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_parse_response_udp(f: *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, NFSState);
+ SCLogDebug!("parsing {} bytes of response data", stream_slice.len());
+ state.parse_udp_tc(f, &stream_slice)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_state_get_tx_count(state: *mut std::os::raw::c_void)
+ -> u64
+{
+ let state = cast_pointer!(state, NFSState);
+ SCLogDebug!("rs_nfs_state_get_tx_count: returning {}", state.tx_id);
+ return state.tx_id;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_state_get_tx(state: *mut std::os::raw::c_void,
+ tx_id: u64)
+ -> *mut std::os::raw::c_void
+{
+ let state = cast_pointer!(state, NFSState);
+ match state.get_tx_by_id(tx_id) {
+ Some(tx) => {
+ return tx as *const _ as *mut _;
+ }
+ None => {
+ return std::ptr::null_mut();
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_state_tx_free(state: *mut std::os::raw::c_void,
+ tx_id: u64)
+{
+ let state = cast_pointer!(state, NFSState);
+ state.free_tx(tx_id);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_tx_get_alstate_progress(tx: *mut std::os::raw::c_void,
+ direction: u8)
+ -> std::os::raw::c_int
+{
+ let tx = cast_pointer!(tx, NFSTransaction);
+ if direction == Direction::ToServer.into() && tx.request_done {
+ SCLogDebug!("TOSERVER progress 1");
+ return 1;
+ } else if direction == Direction::ToClient.into() && tx.response_done {
+ SCLogDebug!("TOCLIENT progress 1");
+ return 1;
+ } else {
+ SCLogDebug!("{} progress 0", direction);
+ return 0;
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_get_tx_data(
+ tx: *mut std::os::raw::c_void)
+ -> *mut AppLayerTxData
+{
+ let tx = cast_pointer!(tx, NFSTransaction);
+ return &mut tx.tx_data;
+}
+
+export_state_data_get!(rs_nfs_get_state_data, NFSState);
+
+/// return procedure(s) in the tx. At 0 return the main proc,
+/// otherwise get procs from the 'file_additional_procs'.
+/// Keep calling until 0 is returned.
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_tx_get_procedures(tx: &mut NFSTransaction,
+ i: u16,
+ procedure: *mut u32)
+ -> u8
+{
+ if i == 0 {
+ *procedure = tx.procedure;
+ return 1;
+ }
+
+ if !tx.is_file_tx {
+ return 0;
+ }
+
+ /* file tx handling follows */
+
+ if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
+ let idx = i as usize - 1;
+ if idx < tdf.file_additional_procs.len() {
+ let p = tdf.file_additional_procs[idx];
+ *procedure = p;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_tx_get_version(tx: &mut NFSTransaction,
+ version: *mut u32)
+{
+ *version = tx.nfs_version as u32;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_init(context: &'static mut SuricataFileContext)
+{
+ SURICATA_NFS_FILE_CONFIG = Some(context);
+}
+
+fn nfs_probe_dir(i: &[u8], rdir: *mut u8) -> i8 {
+ match parse_rpc_packet_header(i) {
+ Ok((_, ref hdr)) => {
+ let dir = if hdr.msgtype == 0 {
+ Direction::ToServer
+ } else {
+ Direction::ToClient
+ };
+ unsafe { *rdir = dir as u8 };
+ return 1;
+ },
+ Err(Err::Incomplete(_)) => {
+ return 0;
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+}
+
+pub fn nfs_probe(i: &[u8], direction: Direction) -> i32 {
+ if direction == Direction::ToClient {
+ match parse_rpc_reply(i, false) {
+ Ok((_, ref rpc)) => {
+ if rpc.hdr.frag_len >= 24 && rpc.hdr.frag_len <= 35000 && rpc.hdr.msgtype == 1 && rpc.reply_state == 0 && rpc.accept_state == 0 {
+ SCLogDebug!("TC PROBE LEN {} XID {} TYPE {}", rpc.hdr.frag_len, rpc.hdr.xid, rpc.hdr.msgtype);
+ return 1;
+ } else {
+ return -1;
+ }
+ },
+ Err(Err::Incomplete(_)) => {
+ match parse_rpc_packet_header (i) {
+ Ok((_, ref rpc_hdr)) => {
+ if rpc_hdr.frag_len >= 24 && rpc_hdr.frag_len <= 35000 && rpc_hdr.xid != 0 && rpc_hdr.msgtype == 1 {
+ SCLogDebug!("TC PROBE LEN {} XID {} TYPE {}", rpc_hdr.frag_len, rpc_hdr.xid, rpc_hdr.msgtype);
+ return 1;
+ } else {
+ return -1;
+ }
+ },
+ Err(Err::Incomplete(_)) => {
+ return 0;
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+ } else {
+ match parse_rpc(i, false) {
+ Ok((_, ref rpc)) => {
+ if rpc.hdr.frag_len >= 40 && rpc.hdr.msgtype == 0 &&
+ rpc.rpcver == 2 && (rpc.progver == 3 || rpc.progver == 4) &&
+ rpc.program == 100003 &&
+ rpc.procedure <= NFSPROC3_COMMIT
+ {
+ return rpc_auth_type_known(rpc.creds_flavor) as i32;
+ } else {
+ return -1;
+ }
+ },
+ Err(Err::Incomplete(_)) => {
+ return 0;
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+ }
+}
+
+pub fn nfs_probe_udp(i: &[u8], direction: Direction) -> i32 {
+ if direction == Direction::ToClient {
+ match parse_rpc_udp_reply(i) {
+ Ok((_, ref rpc)) => {
+ if i.len() >= 32 && rpc.hdr.msgtype == 1 && rpc.reply_state == 0 && rpc.accept_state == 0 {
+ SCLogDebug!("TC PROBE LEN {} XID {} TYPE {}", rpc.hdr.frag_len, rpc.hdr.xid, rpc.hdr.msgtype);
+ return 1;
+ } else {
+ return -1;
+ }
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+ } else {
+ match parse_rpc_udp_request(i) {
+ Ok((_, ref rpc)) => {
+ if i.len() >= 48 && rpc.hdr.msgtype == 0 && rpc.progver == 3 && rpc.program == 100003 {
+ return 1;
+ } else if i.len() >= 48 && rpc.hdr.msgtype == 0 && rpc.progver == 2 && rpc.program == 100003 {
+ SCLogDebug!("NFSv2!");
+ return 1;
+ } else {
+ return -1;
+ }
+ },
+ Err(_) => {
+ return -1;
+ },
+ }
+ }
+}
+
+/// MIDSTREAM
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_probe_ms(
+ _flow: *const Flow,
+ direction: u8, input: *const u8,
+ len: u32, rdir: *mut u8) -> AppProto
+{
+ let slice: &[u8] = build_slice!(input, len as usize);
+ SCLogDebug!("rs_nfs_probe_ms: probing direction {:02x}", direction);
+ let mut adirection : u8 = 0;
+ match nfs_probe_dir(slice, &mut adirection) {
+ 1 => {
+ if adirection == Direction::ToServer.into() {
+ SCLogDebug!("nfs_probe_dir said Direction::ToServer");
+ } else {
+ SCLogDebug!("nfs_probe_dir said Direction::ToClient");
+ }
+ match nfs_probe(slice, adirection.into()) {
+ 1 => {
+ SCLogDebug!("nfs_probe success: dir {:02x} adir {:02x}", direction, adirection);
+ if (direction & DIR_BOTH) != adirection {
+ *rdir = adirection;
+ }
+ ALPROTO_NFS
+ },
+ 0 => { ALPROTO_UNKNOWN },
+ _ => { ALPROTO_FAILED },
+ }
+ },
+ 0 => {
+ ALPROTO_UNKNOWN
+ },
+ _ => {
+ ALPROTO_FAILED
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_probe(_f: *const Flow,
+ direction: u8,
+ input: *const u8,
+ len: u32,
+ _rdir: *mut u8)
+ -> AppProto
+{
+ let slice: &[u8] = build_slice!(input, len as usize);
+ SCLogDebug!("rs_nfs_probe: running probe");
+ match nfs_probe(slice, direction.into()) {
+ 1 => { ALPROTO_NFS },
+ -1 => { ALPROTO_FAILED },
+ _ => { ALPROTO_UNKNOWN },
+ }
+}
+
+/// TOSERVER probe function
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_probe_udp_ts(_f: *const Flow,
+ _direction: u8,
+ input: *const u8,
+ len: u32,
+ _rdir: *mut u8)
+ -> AppProto
+{
+ let slice: &[u8] = build_slice!(input, len as usize);
+ match nfs_probe_udp(slice, Direction::ToServer) {
+ 1 => { ALPROTO_NFS },
+ -1 => { ALPROTO_FAILED },
+ _ => { ALPROTO_UNKNOWN },
+ }
+}
+
+/// TOCLIENT probe function
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_probe_udp_tc(_f: *const Flow,
+ _direction: u8,
+ input: *const u8,
+ len: u32,
+ _rdir: *mut u8)
+ -> AppProto
+{
+ let slice: &[u8] = build_slice!(input, len as usize);
+ match nfs_probe_udp(slice, Direction::ToClient) {
+ 1 => { ALPROTO_NFS },
+ -1 => { ALPROTO_FAILED },
+ _ => { ALPROTO_UNKNOWN },
+ }
+}
+
+// Parser name as a C style string.
+const PARSER_NAME: &[u8] = b"nfs\0";
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_register_parser() {
+ let default_port = CString::new("[2049]").unwrap();
+ let parser = RustParser {
+ name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char,
+ default_port: std::ptr::null(),
+ ipproto: IPPROTO_TCP,
+ probe_ts: None,
+ probe_tc: None,
+ min_depth: 0,
+ max_depth: 16,
+ state_new: rs_nfs_state_new,
+ state_free: rs_nfs_state_free,
+ tx_free: rs_nfs_state_tx_free,
+ parse_ts: rs_nfs_parse_request,
+ parse_tc: rs_nfs_parse_response,
+ get_tx_count: rs_nfs_state_get_tx_count,
+ get_tx: rs_nfs_state_get_tx,
+ tx_comp_st_ts: 1,
+ tx_comp_st_tc: 1,
+ tx_get_progress: rs_nfs_tx_get_alstate_progress,
+ get_eventinfo: Some(NFSEvent::get_event_info),
+ get_eventinfo_byid : Some(NFSEvent::get_event_info_by_id),
+ localstorage_new: None,
+ localstorage_free: None,
+ get_tx_files: Some(rs_nfs_gettxfiles),
+ get_tx_iterator: Some(applayer::state_get_tx_iterator::<NFSState, NFSTransaction>),
+ get_tx_data: rs_nfs_get_tx_data,
+ get_state_data: rs_nfs_get_state_data,
+ apply_tx_config: None,
+ flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS,
+ truncate: None,
+ get_frame_id_by_name: Some(NFSFrameType::ffi_id_from_name),
+ get_frame_name_by_id: Some(NFSFrameType::ffi_name_from_id),
+ };
+
+ let ip_proto_str = CString::new("tcp").unwrap();
+
+ if AppLayerProtoDetectConfProtoDetectionEnabled(
+ ip_proto_str.as_ptr(),
+ parser.name,
+ ) != 0
+ {
+ let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
+ ALPROTO_NFS = alproto;
+
+ let midstream = conf_get_bool("stream.midstream");
+ if midstream {
+ if AppLayerProtoDetectPPParseConfPorts(ip_proto_str.as_ptr(), IPPROTO_TCP,
+ parser.name, ALPROTO_NFS, 0, NFS_MIN_FRAME_LEN,
+ rs_nfs_probe_ms, rs_nfs_probe_ms) == 0 {
+ SCLogDebug!("No NFSTCP app-layer configuration, enabling NFSTCP detection TCP detection on port {:?}.",
+ default_port);
+ /* register 'midstream' probing parsers if midstream is enabled. */
+ AppLayerProtoDetectPPRegister(IPPROTO_TCP,
+ default_port.as_ptr(), ALPROTO_NFS, 0,
+ NFS_MIN_FRAME_LEN, Direction::ToServer.into(),
+ rs_nfs_probe_ms, rs_nfs_probe_ms);
+ }
+ } else {
+ AppLayerProtoDetectPPRegister(IPPROTO_TCP,
+ default_port.as_ptr(), ALPROTO_NFS, 0,
+ NFS_MIN_FRAME_LEN, Direction::ToServer.into(),
+ rs_nfs_probe, rs_nfs_probe);
+ }
+ if AppLayerParserConfParserEnabled(
+ ip_proto_str.as_ptr(),
+ parser.name,
+ ) != 0
+ {
+ let _ = AppLayerRegisterParser(&parser, alproto);
+ }
+ SCLogDebug!("Rust nfs parser registered.");
+ } else {
+ SCLogDebug!("Protocol detector and parser disabled for nfs.");
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_nfs_udp_register_parser() {
+ let default_port = CString::new("[2049]").unwrap();
+ let parser = RustParser {
+ name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char,
+ default_port: std::ptr::null(),
+ ipproto: IPPROTO_UDP,
+ probe_ts: None,
+ probe_tc: None,
+ min_depth: 0,
+ max_depth: 16,
+ state_new: rs_nfs_state_new,
+ state_free: rs_nfs_state_free,
+ tx_free: rs_nfs_state_tx_free,
+ parse_ts: rs_nfs_parse_request_udp,
+ parse_tc: rs_nfs_parse_response_udp,
+ get_tx_count: rs_nfs_state_get_tx_count,
+ get_tx: rs_nfs_state_get_tx,
+ tx_comp_st_ts: 1,
+ tx_comp_st_tc: 1,
+ tx_get_progress: rs_nfs_tx_get_alstate_progress,
+ get_eventinfo: Some(NFSEvent::get_event_info),
+ get_eventinfo_byid : Some(NFSEvent::get_event_info_by_id),
+ localstorage_new: None,
+ localstorage_free: None,
+ get_tx_files: Some(rs_nfs_gettxfiles),
+ get_tx_iterator: Some(applayer::state_get_tx_iterator::<NFSState, NFSTransaction>),
+ get_tx_data: rs_nfs_get_tx_data,
+ get_state_data: rs_nfs_get_state_data,
+ apply_tx_config: None,
+ flags: 0,
+ truncate: None,
+ get_frame_id_by_name: Some(NFSFrameType::ffi_id_from_name),
+ get_frame_name_by_id: Some(NFSFrameType::ffi_name_from_id),
+ };
+
+ let ip_proto_str = CString::new("udp").unwrap();
+
+ if AppLayerProtoDetectConfProtoDetectionEnabled(
+ ip_proto_str.as_ptr(),
+ parser.name,
+ ) != 0
+ {
+ let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
+ ALPROTO_NFS = alproto;
+
+ if AppLayerProtoDetectPPParseConfPorts(ip_proto_str.as_ptr(), IPPROTO_UDP,
+ parser.name, ALPROTO_NFS, 0, NFS_MIN_FRAME_LEN,
+ rs_nfs_probe_udp_ts, rs_nfs_probe_udp_tc) == 0 {
+ SCLogDebug!("No NFSUDP app-layer configuration, enabling NFSUDP detection UDP detection on port {:?}.",
+ default_port);
+ AppLayerProtoDetectPPRegister(IPPROTO_UDP,
+ default_port.as_ptr(), ALPROTO_NFS, 0,
+ NFS_MIN_FRAME_LEN, Direction::ToServer.into(),
+ rs_nfs_probe_udp_ts, rs_nfs_probe_udp_tc);
+ }
+ if AppLayerParserConfParserEnabled(
+ ip_proto_str.as_ptr(),
+ parser.name,
+ ) != 0
+ {
+ let _ = AppLayerRegisterParser(&parser, alproto);
+ }
+ if let Some(val) = conf_get("app-layer.protocols.nfs.max-tx") {
+ if let Ok(v) = val.parse::<usize>() {
+ NFS_MAX_TX = v;
+ } else {
+ SCLogError!("Invalid value for nfs.max-tx");
+ }
+ }
+ SCLogDebug!("Rust nfs parser registered.");
+ } else {
+ SCLogDebug!("Protocol detector and parser disabled for nfs.");
+ }
+}