diff --git a/rust/src/nfs/nfs.rs b/rust/src/nfs/nfs.rs index c859eee0da..03a91be3b3 100644 --- a/rust/src/nfs/nfs.rs +++ b/rust/src/nfs/nfs.rs @@ -2433,22 +2433,34 @@ impl NFSState { match parse_rpc(cur_i, false) { Ok((_rem, ref hdr)) => { // RPCSEC_GSS integrity compounds arrive wrapped (length, seqnum, data): - // unwrap like the full record path, or the envelope length is read as the - // compound tag and the scan stays incomplete (buffering toward the claim). - let (gss_proc, gss_service) = match &hdr.creds { - RpcRequestCreds::GssApi(ref g) => (g.procedure, g.service), - _ => (0, 0), + // unwrap like the full record path before scanning - the raw + // envelope's length would be read as the compound tag, buffering toward the claim + let gss = match &hdr.creds { + RpcRequestCreds::GssApi(ref g) => Some((g.procedure, g.service)), + _ => None, }; - let scanres = match gss_compound_scan_data( - gss_proc, - gss_service, - hdr.prog_data, - ) { + let scanres = match gss_compound_scan_data(gss, hdr.prog_data) { GssCompoundScan::Compound(data) => { scan_nfs4_request_compound_write_len(data, max_write) } GssCompoundScan::EnvelopeIncomplete => Nfs4CompoundScan::Incomplete, GssCompoundScan::EnvelopeMalformed => Nfs4CompoundScan::Malformed, + // privacy: encrypted; keep the call for its + // reply and skip the record, no tx + GssCompoundScan::Opaque => { + let mut xidmap = + NFSRequestXidMap::new(hdr.progver, hdr.procedure, 0); + if let Some((procedure, service)) = gss { + xidmap.gssapi_proc = procedure; + xidmap.gssapi_service = service; + } + self.requestmap.put(hdr.hdr.xid, xidmap); + return self.skip_partial_record( + Direction::ToServer, + cur_i, + rec_size, + ); + } }; match scanres { Nfs4CompoundScan::Oversized(_write_len) => { @@ -2737,29 +2749,33 @@ impl NFSState { let max_read = unsafe { NFS_CFG_MAX_READ_SIZE }; // a zero limit disables the check if max_read != 0 { - // the request recorded its RPCSEC_GSS credential combination in the - // xidmap; integrity-wrapped compounds (proc 0, service 2) are unwrapped first. - let gss_integrity = self - .requestmap - .get(&phdr.xid) - .map(|xm| xm.gssapi_proc == 0 && xm.gssapi_service == 2) - .unwrap_or(false); + // (0, 0) is the non-GSS default; GSS requests record + // their combination (only (0, 2) is unwrapped) + let gss = self.requestmap.get(&phdr.xid).and_then(|xm| { + if xm.gssapi_proc == 0 && xm.gssapi_service == 0 { + None + } else { + Some((xm.gssapi_proc, xm.gssapi_service)) + } + }); match parse_rpc_reply(cur_i, false) { Ok((_rem, ref hdr)) => { - let scanres = if gss_integrity { - match gss_compound_scan_data(0, 2, hdr.prog_data) { - GssCompoundScan::Compound(data) => { - scan_nfs4_response_compound_read_len(data, max_read) - } - GssCompoundScan::EnvelopeIncomplete => { - Nfs4CompoundScan::Incomplete - } - GssCompoundScan::EnvelopeMalformed => { - Nfs4CompoundScan::Malformed - } + let scanres = match gss_compound_scan_data(gss, hdr.prog_data) { + GssCompoundScan::Compound(data) => { + scan_nfs4_response_compound_read_len(data, max_read) + } + GssCompoundScan::EnvelopeIncomplete => Nfs4CompoundScan::Incomplete, + GssCompoundScan::EnvelopeMalformed => Nfs4CompoundScan::Malformed, + // privacy: encrypted reply; the call is done, + // pop its xid and skip the record + GssCompoundScan::Opaque => { + self.requestmap.pop(&hdr.hdr.xid); + return self.skip_partial_record( + Direction::ToClient, + cur_i, + rec_size, + ); } - } else { - scan_nfs4_response_compound_read_len(hdr.prog_data, max_read) }; match scanres { Nfs4CompoundScan::Oversized(read_len) => { diff --git a/rust/src/nfs/nfs4.rs b/rust/src/nfs/nfs4.rs index 824dac55d1..ae3245ef69 100644 --- a/rust/src/nfs/nfs4.rs +++ b/rust/src/nfs/nfs4.rs @@ -344,15 +344,16 @@ impl NFSState { let mut data = r.prog_data; if let RpcRequestCreds::GssApi(ref creds) = r.creds { + // store proc and serv for the reply; only (0, 2) is + // unwrapped below + xidmap.gssapi_proc = creds.procedure; + xidmap.gssapi_service = creds.service; if creds.procedure == 0 && creds.service == 2 { SCLogDebug!("GSS INTEGRITY: {:?}", creds); match parse_rpc_gssapi_integrity(r.prog_data) { Ok((_rem, rec)) => { SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec); data = rec.data; - // store proc and serv for the reply - xidmap.gssapi_proc = creds.procedure; - xidmap.gssapi_service = creds.service; } Err(Err::Incomplete(_n)) => { SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n); diff --git a/rust/src/nfs/rpc_records.rs b/rust/src/nfs/rpc_records.rs index 31acb9bf2b..fa1630a0c4 100644 --- a/rust/src/nfs/rpc_records.rs +++ b/rust/src/nfs/rpc_records.rs @@ -129,22 +129,28 @@ pub enum GssCompoundScan<'a> { /// The claimed envelope length fails the bounds check: definitive malformed /// (the full record path rejects it the same way). EnvelopeMalformed, + /// RPCSEC_GSS privacy (0, 3): the compound is encrypted, so there is + /// nothing to scan either way; it is opaque, not malformed. + Opaque, } -/// `gss_proc`/`gss_service` carry the request's RPCSEC_GSS credential combination -/// (header on the request side, xidmap on the response). Only the (0, 2) combination -/// the full record path unwraps is unwrapped here, so both always see the same compound. -pub fn gss_compound_scan_data( - gss_proc: u32, gss_service: u32, prog_data: &[u8], -) -> GssCompoundScan<'_> { - if gss_proc == 0 && gss_service == 2 { - match parse_rpc_gssapi_integrity(prog_data) { +/// `gss` is the request's RPCSEC_GSS credential combination (header on the +/// request side, xidmap on the response; None: non-GSS). Only (0, 2) is +/// unwrapped, (0, 3) is opaque; another GSS combination is definitive +/// malformed, never Incomplete +pub fn gss_compound_scan_data(gss: Option<(u32, u32)>, prog_data: &[u8]) -> GssCompoundScan<'_> { + match gss { + None => GssCompoundScan::Compound(prog_data), + // rpc_gss_svc_none (RFC 2203): no integrity/privacy wrap + Some((0, 1)) => GssCompoundScan::Compound(prog_data), + Some((0, 2)) => match parse_rpc_gssapi_integrity(prog_data) { Ok((_rem, ref rec)) => GssCompoundScan::Compound(rec.data), Err(Err::Incomplete(_)) => GssCompoundScan::EnvelopeIncomplete, Err(_) => GssCompoundScan::EnvelopeMalformed, - } - } else { - GssCompoundScan::Compound(prog_data) + }, + // rpc_gss_svc_privacy: the payload is encrypted, in both directions + Some((0, 3)) => GssCompoundScan::Opaque, + Some(_) => GssCompoundScan::EnvelopeMalformed, } } @@ -504,26 +510,47 @@ mod tests { envelope.extend_from_slice(&((compound.len()) as u32).to_be_bytes()); // len envelope.extend_from_slice(&1u32.to_be_bytes()); // seqnum envelope.extend_from_slice(compound); // data - let r = gss_compound_scan_data(0, 2, &envelope[..]); + let r = gss_compound_scan_data(Some((0, 2)), &envelope[..]); match r { GssCompoundScan::Compound(d) => assert_eq!(d, compound), _ => panic!("unexpected {:?}", r), } } + #[test] + fn test_gss_compound_scan_data_service_none() { + // (0, 1): rpc_gss_svc_none (RFC 2203) -- the data is not + // wrapped, the raw compound is scanned as-is + let prog: &[u8] = &[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02]; + assert_eq!( + gss_compound_scan_data(Some((0, 1)), prog), + GssCompoundScan::Compound(prog) + ); + } + + #[test] + fn test_gss_compound_scan_data_privacy() { + // rpc_gss_svc_privacy: encrypted, opaque to the scan + let prog: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0xde, 0xad, 0xbe, 0xef]; + assert_eq!( + gss_compound_scan_data(Some((0, 3)), prog), + GssCompoundScan::Opaque + ); + } + #[test] fn test_gss_compound_scan_data_no_wrapper() { // non-GSS requests: prog_data is scanned as-is let prog: &[u8] = &[0x00, 0x00, 0x00, 0x02]; assert_eq!( - gss_compound_scan_data(0, 0, prog), + gss_compound_scan_data(None, prog), GssCompoundScan::Compound(prog) ); - // other GSS combinations are not unwrapped (the full record path - // does not unwrap them either) + // a GSS combination the full record path does not unwrap: definitive + // malformed (the full path rejects it the same way), never Incomplete assert_eq!( - gss_compound_scan_data(1, 0, prog), - GssCompoundScan::Compound(prog) + gss_compound_scan_data(Some((1, 0)), prog), + GssCompoundScan::EnvelopeMalformed ); } @@ -535,7 +562,7 @@ mod tests { envelope.extend_from_slice(&100u32.to_be_bytes()); envelope.extend_from_slice(&1u32.to_be_bytes()); assert_eq!( - gss_compound_scan_data(0, 2, &envelope[..]), + gss_compound_scan_data(Some((0, 2)), &envelope[..]), GssCompoundScan::EnvelopeIncomplete ); } @@ -549,7 +576,7 @@ mod tests { envelope.extend_from_slice(&1u32.to_be_bytes()); envelope.extend_from_slice(&[0u8; 16]); assert_eq!( - gss_compound_scan_data(0, 2, &envelope[..]), + gss_compound_scan_data(Some((0, 2)), &envelope[..]), GssCompoundScan::EnvelopeMalformed ); }