//! DCE/RPC TCP transport — async port of `DceRpcTcpClient.cs`. //! //! Direct port of `src/MxNativeClient/DceRpcTcpClient.cs` over tokio. //! Provides: //! //! - [`DceRpcTcpClient::connect`] — open a TCP connection //! - [`DceRpcTcpClient::bind`] — unauthenticated bind (`cs:33-53`) //! - [`DceRpcTcpClient::bind_with_managed_ntlm_packet_integrity`] — //! NTLMv2 packet-integrity bind using [`crate::ntlm::NtlmClientContext`] //! (`cs:65-106`) //! - [`DceRpcTcpClient::call`] / [`DceRpcTcpClient::call_bound`] / //! [`DceRpcTcpClient::call_bound_object`] — request dispatch //! (`cs:151-182,252-282`) //! //! The `BindWithNtlmConnect` / `BindWithNtlmPacketIntegrity` flavours from //! the .NET reference (`cs:55-63,108-149`) wrap `System.Net.Security.SspiClientContext`, //! which is .NET-specific. They're explicitly out of scope for the Rust //! port — the managed-NTLM path is the only one we need (cite //! `design/00-overview.md` principle 3 and `design/40-protocol-invariants.md`). //! //! ## Packet integrity //! //! When [`DceRpcTcpClient::bind_with_managed_ntlm_packet_integrity`] is used, //! every subsequent `call*` PDU is wrapped per `cs:201-250`: //! //! ```text //! pdu_layout = unauthenticated_request //! || pad to 4-byte align (filled with 0xBB, cs:215) //! || DceRpcAuthTrailer (8 bytes) //! || 16-byte NTLM signature //! ``` //! //! The header's `frag_length` is rewritten to the new full length and //! `auth_length` is set to 16 (the signature size). `NtlmClientContext::sign` //! is called over `pdu[0..length-16]` and the result is written into the //! trailing 16 bytes. Mirrors `cs:201-250` exactly. #![allow(clippy::indexing_slicing)] use std::net::SocketAddr; use thiserror::Error; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use crate::error::RpcError; use crate::guid::Guid; use crate::ntlm::{NtlmClientContext, NtlmError, SIGNATURE_LEN}; use crate::pdu::{ AuthLevel, AuthTrailer, AuthType, BindPdu, FaultPdu, PacketType, PduHeader, PresentationContext, ResponsePdu, SyntaxId, }; /// Errors raised by the TCP transport. Mirrors the wrap of /// `IOException` / `InvalidOperationException` / `DceRpcFaultException` /// at `DceRpcTcpClient.cs:170-174,205,245,393`. #[derive(Debug, Error)] #[non_exhaustive] pub enum TransportError { /// I/O failure on the underlying TCP stream. #[error("transport I/O: {0}")] Io(#[from] std::io::Error), /// PDU codec failure — wraps [`RpcError`]. #[error("PDU codec: {0}")] Codec(#[from] RpcError), /// NTLM signing failed (signing called before authenticate completed /// or buffer length issues). Mirrors `cs:245`. #[error("NTLM signing: {0}")] Ntlm(#[from] NtlmError), /// `Connect()` was not called or the socket was closed /// (`cs:402-407`). #[error("DCE/RPC TCP client is not connected")] NotConnected, /// A `Call*` arrived with `auth_level == PacketIntegrity` but no /// auth trailer was set up by a prior bind (`cs:203-206,245`). #[error("packet-integrity auth requested without an auth trailer")] AuthContextMissing, /// Server returned a `Fault` PDU. Mirrors `DceRpcFaultException` /// (`cs:411-420`). #[error("DCE/RPC fault 0x{status:08x}")] Fault { status: u32 }, /// Server replied with a packet type the transport doesn't expect at /// that point in the conversation (e.g. a `Request` where a Response /// was expected). #[error("unexpected response packet type: {actual:?}")] UnexpectedResponsePacketType { actual: PacketType }, } /// Fixed `auth_context_id` used by the .NET reference for every /// authenticated PDU (`cs:90,133`). The same value is reused across the /// connection so the server can correlate the trailer back to the /// negotiated NTLM context. pub const NTLM_AUTH_CONTEXT_ID: u32 = 79232; /// Fixed PDU header constants used for outbound frames (`cs:336-347`). const FRAME_VERSION: u8 = 5; const FRAME_VERSION_MINOR: u8 = 0; const FRAME_PACKET_FLAGS: u8 = 0x03; const FRAME_DATA_REPRESENTATION: u32 = 0x10; /// `DceRpcTcpClient` — a single-connection async DCE/RPC client. /// /// Construct with [`connect`](Self::connect), then call one of the /// `bind*` methods, then dispatch one or more `call*` requests, then drop /// to close the socket. /// /// Not Clone (the underlying `TcpStream` is single-owner) and not Sync /// (mutable internal state — call id counter, NTLM context, auth trailer). pub struct DceRpcTcpClient { stream: TcpStream, next_call_id: u32, bound_context_id: u16, ntlm: Option, auth_trailer: Option, auth_level: AuthLevel, } impl DceRpcTcpClient { /// Open a TCP connection to `addr`. Mirrors `Connect()` /// (`DceRpcTcpClient.cs:26-31`). /// /// # Errors /// Propagates [`std::io::Error`] from [`TcpStream::connect`]. pub async fn connect(addr: SocketAddr) -> std::io::Result { let stream = TcpStream::connect(addr).await?; Ok(Self { stream, next_call_id: 1, bound_context_id: 0, ntlm: None, auth_trailer: None, auth_level: AuthLevel::None, }) } /// Local socket address (for tests / diagnostics). /// /// # Errors /// Propagates [`std::io::Error`] if the socket is invalid. pub fn local_addr(&self) -> std::io::Result { self.stream.local_addr() } /// Currently negotiated `bound_context_id`. Set by the bind methods. #[must_use] pub fn bound_context_id(&self) -> u16 { self.bound_context_id } /// Currently negotiated `auth_level`. Tracks `_authLevel` from the /// .NET reference (`cs:18,104,147`). #[must_use] pub fn auth_level(&self) -> AuthLevel { self.auth_level } /// Unauthenticated bind. Mirrors `Bind` /// (`DceRpcTcpClient.cs:33-53`). /// /// On success returns the response PDU header (typically `BindAck` /// per `[C706]` §12.6.4.4); the bound presentation context id is /// always 0 for this transport (the .NET reference only ever /// presents one context at a time). /// /// # Errors /// I/O, codec, or unexpected packet type. pub async fn bind( &mut self, interface_id: Guid, version_major: u16, version_minor: u16, ) -> Result { let call_id = self.next_call_id; self.next_call_id = self.next_call_id.wrapping_add(1); let pdu = make_bind_pdu(interface_id, version_major, version_minor, call_id); let bytes = pdu.encode(); self.stream.write_all(&bytes).await?; let response = read_pdu(&mut self.stream).await?; let header = PduHeader::decode(&response)?; Ok(header) } /// Bind + Auth3 round-trip using the managed NTLMv2 packet-integrity /// path. Mirrors `BindWithManagedNtlmPacketIntegrity` /// (`cs:65-106`). /// /// Takes ownership of the [`NtlmClientContext`] for the lifetime of /// the connection; subsequent `call*` requests are signed with it. /// The .NET reference creates the context via /// `ManagedNtlmClientContext.FromEnvironment()` (cs:70) — that /// helper is open follow-up F1 in the Rust port; for now the caller /// constructs `NtlmClientContext::new(user, password, domain, workstation)` /// explicitly. /// /// # Errors /// I/O, codec, or NTLM (Type1/Type3 building). pub async fn bind_with_managed_ntlm_packet_integrity( &mut self, interface_id: Guid, version_major: u16, version_minor: u16, mut ntlm: NtlmClientContext, ) -> Result { let call_id = self.next_call_id; self.next_call_id = self.next_call_id.wrapping_add(1); let type1 = ntlm.create_type1(); let pdu = make_bind_pdu(interface_id, version_major, version_minor, call_id); let trailer = AuthTrailer { auth_type: AuthType::WinNt, auth_level: AuthLevel::PacketIntegrity, auth_pad_length: 0, auth_reserved: 0, auth_context_id: NTLM_AUTH_CONTEXT_ID, }; let bind_with_auth = pdu.encode_with_auth(trailer, &type1); self.stream.write_all(&bind_with_auth).await?; let response = read_pdu(&mut self.stream).await?; let response_header = PduHeader::decode(&response)?; let challenge = BindPdu::read_auth_value(&response)?; let mut inputs = crate::ntlm::OsInputs; let type3 = ntlm.create_type3(&challenge.token, &mut inputs)?; let auth3_header = make_request_header(PacketType::Auth3, response_header.call_id); let auth3 = BindPdu::encode_auth3(auth3_header, trailer, &type3); self.stream.write_all(&auth3).await?; self.bound_context_id = 0; self.ntlm = Some(ntlm); self.auth_trailer = Some(trailer); self.auth_level = AuthLevel::PacketIntegrity; Ok(response_header) } /// Dispatch a Request on the explicit context id. Mirrors `Call` /// (`DceRpcTcpClient.cs:151-154`). /// /// # Errors /// I/O, codec, NTLM signing, or `TransportError::Fault` if the server /// returned a Fault PDU. pub async fn call( &mut self, context_id: u16, opnum: u16, stub_data: &[u8], ) -> Result { self.call_core(context_id, opnum, stub_data, None).await } /// Dispatch a Request on the bound context with no object UUID. /// Mirrors `CallBound` (`cs:179-182`). /// /// # Errors /// As for [`call`](Self::call). pub async fn call_bound( &mut self, opnum: u16, stub_data: &[u8], ) -> Result { let cid = self.bound_context_id; self.call_core(cid, opnum, stub_data, None).await } /// Dispatch a Request on the bound context with an object UUID /// (sets `PFC_OBJECT_UUID = 0x80` in the packet flags). Mirrors /// `CallBoundObject` (`cs:156-159`). /// /// # Errors /// As for [`call`](Self::call). pub async fn call_bound_object( &mut self, object_uuid: Guid, opnum: u16, stub_data: &[u8], ) -> Result { let cid = self.bound_context_id; self.call_core(cid, opnum, stub_data, Some(object_uuid)) .await } async fn call_core( &mut self, context_id: u16, opnum: u16, stub_data: &[u8], object_uuid: Option, ) -> Result { let call_id = self.next_call_id; self.next_call_id = self.next_call_id.wrapping_add(1); let header = make_request_header(PacketType::Request, call_id); let request = encode_request_bytes(header, context_id, opnum, stub_data, object_uuid); let pdu = if self.auth_level == AuthLevel::PacketIntegrity { let trailer = self .auth_trailer .ok_or(TransportError::AuthContextMissing)?; let ntlm = self .ntlm .as_mut() .ok_or(TransportError::AuthContextMissing)?; encode_packet_integrity_request(&request, trailer, ntlm)? } else { request }; self.stream.write_all(&pdu).await?; let response = read_pdu(&mut self.stream).await?; let response_header = PduHeader::decode(&response)?; match response_header.packet_type { PacketType::Response => Ok(ResponsePdu::decode(&response)?), PacketType::Fault => { let fault = FaultPdu::decode(&response)?; Err(TransportError::Fault { status: fault.status, }) } other => Err(TransportError::UnexpectedResponsePacketType { actual: other }), } } } /// Build the standard outbound bind PDU (`cs:33-48,73-84,116-127`). fn make_bind_pdu( interface_id: Guid, version_major: u16, version_minor: u16, call_id: u32, ) -> BindPdu { BindPdu { header: make_request_header(PacketType::Bind, call_id), max_transmit_fragment: 4280, max_receive_fragment: 4280, association_group_id: 0, presentation_contexts: vec![PresentationContext { context_id: 0, abstract_syntax: SyntaxId { uuid_bytes: *interface_id.as_bytes(), version_major, version_minor, }, transfer_syntaxes: vec![SyntaxId::NDR20], }], reserved25_28: [0; 3], } } /// Build a fresh outbound PDU header. Mirrors `CreateHeader` /// (`cs:336-347`). `fragment_length` and `auth_length` are 0; the PDU /// encoder fills `fragment_length` later. `packet_flags = 0x03` matches /// `cs:342`. fn make_request_header(packet_type: PacketType, call_id: u32) -> PduHeader { PduHeader { version: FRAME_VERSION, version_minor: FRAME_VERSION_MINOR, packet_type, packet_flags: FRAME_PACKET_FLAGS, data_representation: FRAME_DATA_REPRESENTATION, fragment_length: 0, auth_length: 0, call_id, } } /// Build the unauthenticated `Request` PDU bytes. Mirrors /// `EncodeRequestBytes` (`DceRpcTcpClient.cs:252-282`). /// /// Layout: /// /// ```text /// offset size field /// 0 16 PduHeader /// 16 4 allocation_hint u32 LE = stub.len() /// 20 2 context_id u16 LE /// 22 2 opnum u16 LE /// 24..(24+16 if object) 16 object_uuid (only when PFC_OBJECT_UUID) /// stub_offset.. var stub_data /// ``` /// /// Sets `packet_flags |= 0x80` (`PFC_OBJECT_UUID`) when `object_uuid` is /// `Some`, mirroring `cs:269`. pub(crate) fn encode_request_bytes( header: PduHeader, context_id: u16, opnum: u16, stub_data: &[u8], object_uuid: Option, ) -> Vec { let object_length = if object_uuid.is_some() { 16 } else { 0 }; let fixed_offset = PduHeader::LENGTH; let stub_offset = fixed_offset + 8 + object_length; let length = stub_offset + stub_data.len(); let mut pdu = vec![0u8; length]; let request_header = PduHeader { packet_type: PacketType::Request, fragment_length: u16::try_from(length).unwrap_or(u16::MAX), auth_length: 0, packet_flags: { let base = if header.packet_flags == 0 { 0x03 } else { header.packet_flags }; if object_uuid.is_some() { base | 0x80 } else { base } }, ..header }; let _ = request_header.encode(&mut pdu); pdu[fixed_offset..fixed_offset + 4].copy_from_slice( &u32::try_from(stub_data.len()) .unwrap_or(u32::MAX) .to_le_bytes(), ); pdu[fixed_offset + 4..fixed_offset + 6].copy_from_slice(&context_id.to_le_bytes()); pdu[fixed_offset + 6..fixed_offset + 8].copy_from_slice(&opnum.to_le_bytes()); if let Some(uuid) = object_uuid { pdu[fixed_offset + 8..fixed_offset + 24].copy_from_slice(uuid.as_bytes()); } pdu[stub_offset..stub_offset + stub_data.len()].copy_from_slice(stub_data); pdu } /// Wrap an unauthenticated Request PDU with packet-integrity padding, /// auth trailer, and 16-byte NTLM signature. Mirrors /// `EncodePacketIntegrityRequest` (`DceRpcTcpClient.cs:201-250`). /// /// Layout: /// /// ```text /// 0..N unauthenticated PDU (header + body) /// N..N+pad 0xBB pad bytes to 4-byte boundary (cs:215) /// N+pad.. AuthTrailer (8 bytes; auth_pad_length set to pad) /// last 16 bytes NTLM signature over [0..length-16] /// ``` /// /// The PDU header inside is rewritten to set `fragment_length = length` /// and `auth_length = 16`. pub(crate) fn encode_packet_integrity_request( unauthenticated: &[u8], trailer: AuthTrailer, ntlm: &mut NtlmClientContext, ) -> Result, TransportError> { let pad_length = align_up(unauthenticated.len(), 4) - unauthenticated.len(); let length = unauthenticated.len() + pad_length + AuthTrailer::LENGTH + SIGNATURE_LEN; let mut pdu = vec![0u8; length]; pdu[..unauthenticated.len()].copy_from_slice(unauthenticated); if pad_length > 0 { pdu[unauthenticated.len()..unauthenticated.len() + pad_length].fill(0xBB); } // Rewrite the embedded PDU header. let parsed_header = PduHeader::decode(unauthenticated)?; let header = PduHeader { packet_type: PacketType::Request, packet_flags: if parsed_header.packet_flags == 0 { 0x03 } else { parsed_header.packet_flags }, fragment_length: u16::try_from(length).unwrap_or(u16::MAX), auth_length: u16::try_from(SIGNATURE_LEN).unwrap_or(u16::MAX), ..parsed_header }; let _ = header.encode(&mut pdu); // Write the auth trailer (with auth_pad_length reflecting our pad). let trailer = AuthTrailer { auth_pad_length: u8::try_from(pad_length).unwrap_or(u8::MAX), ..trailer }; let trailer_offset = unauthenticated.len() + pad_length; let mut trailer_buf = [0u8; AuthTrailer::LENGTH]; trailer.encode(&mut trailer_buf)?; pdu[trailer_offset..trailer_offset + AuthTrailer::LENGTH].copy_from_slice(&trailer_buf); // Sign over [0..length - SIGNATURE_LEN] and write the signature into the // trailing 16 bytes. The .NET reference fills the placeholder with 0x20 // before signing (`cs:231`); since `Sign` reads only [0..length-16], // the placeholder doesn't affect the MAC, but we keep the same // initial bytes so any future test that compares full PDUs has a // consistent shape. pdu[length - SIGNATURE_LEN..].fill(0x20); let signature = ntlm.sign(&pdu[..length - SIGNATURE_LEN])?; pdu[length - SIGNATURE_LEN..].copy_from_slice(&signature); Ok(pdu) } const fn align_up(value: usize, alignment: usize) -> usize { let r = value % alignment; if r == 0 { value } else { value + alignment - r } } /// Read one full PDU from `stream`. Mirrors `ReadPdu` + `ReadExact` /// (`DceRpcTcpClient.cs:372-400`). Returns the full bytes including the /// 16-byte header. async fn read_pdu(stream: &mut TcpStream) -> Result, TransportError> { let mut header_bytes = [0u8; PduHeader::LENGTH]; stream.read_exact(&mut header_bytes).await?; let header = PduHeader::decode(&header_bytes)?; let frag = header.fragment_length as usize; if frag < PduHeader::LENGTH { return Err(TransportError::Codec(RpcError::InvalidFragmentLength { frag_length: frag, buffer_len: header_bytes.len(), auth_length: header.auth_length as usize, })); } let mut pdu = vec![0u8; frag]; pdu[..PduHeader::LENGTH].copy_from_slice(&header_bytes); if frag > PduHeader::LENGTH { stream.read_exact(&mut pdu[PduHeader::LENGTH..]).await?; } Ok(pdu) } #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; use tokio::net::TcpListener; #[test] fn align_up_matches_dotnet_align() { assert_eq!(align_up(0, 4), 0); assert_eq!(align_up(1, 4), 4); assert_eq!(align_up(28, 4), 28); assert_eq!(align_up(29, 4), 32); } #[test] fn ntlm_auth_context_id_matches_dotnet() { // .NET hard-codes 79232 = 0x13580 at cs:90,133. assert_eq!(NTLM_AUTH_CONTEXT_ID, 79232); assert_eq!(NTLM_AUTH_CONTEXT_ID, 0x0001_3580); } #[test] fn make_request_header_uses_v5_drep_0x10_flags_03() { let h = make_request_header(PacketType::Bind, 7); assert_eq!(h.version, 5); assert_eq!(h.version_minor, 0); assert_eq!(h.packet_type, PacketType::Bind); assert_eq!(h.packet_flags, 0x03); assert_eq!(h.data_representation, 0x10); assert_eq!(h.fragment_length, 0); assert_eq!(h.auth_length, 0); assert_eq!(h.call_id, 7); } #[test] fn encode_request_bytes_no_object_uuid_layout() { let header = make_request_header(PacketType::Request, 42); let stub = [0xAAu8, 0xBB, 0xCC, 0xDD]; let bytes = encode_request_bytes(header, 0, 6, &stub, None); // Total = header(16) + 8 fixed + stub(4) = 28. assert_eq!(bytes.len(), 28); // PFC_OBJECT_UUID bit must NOT be set (cs:269). assert_eq!(bytes[3] & 0x80, 0); // allocation_hint = 4 assert_eq!( u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), 4 ); // context_id = 0 assert_eq!(u16::from_le_bytes([bytes[20], bytes[21]]), 0); // opnum = 6 assert_eq!(u16::from_le_bytes([bytes[22], bytes[23]]), 6); // stub bytes follow at offset 24. assert_eq!(&bytes[24..28], &stub); } #[test] fn encode_request_bytes_with_object_uuid_sets_pfc_bit_and_inserts_uuid() { let header = make_request_header(PacketType::Request, 99); let stub = [0x01u8, 0x02]; let uuid = Guid::new([0x11; 16]); let bytes = encode_request_bytes(header, 0, 0, &stub, Some(uuid)); // Total = header(16) + 8 fixed + 16 object UUID + 2 stub = 42. assert_eq!(bytes.len(), 42); // PFC_OBJECT_UUID bit must be set (cs:269). assert_eq!(bytes[3] & 0x80, 0x80); // Object UUID at offset 24..40. assert_eq!(&bytes[24..40], uuid.as_bytes()); // Stub at offset 40. assert_eq!(&bytes[40..42], &stub); } #[test] fn encode_packet_integrity_request_pad_and_signature_layout() { // Build an unauthenticated request that's NOT 4-byte aligned to // exercise the pad branch (cs:208,213-216). let header = make_request_header(PacketType::Request, 1); let stub = [0xDE, 0xAD, 0xBE]; // 3 bytes -> total 27 (header 16 + 8 fixed + 3 stub) let unauth = encode_request_bytes(header, 0, 0, &stub, None); assert_eq!(unauth.len(), 27); // Build an NTLM context that's authenticated enough for sign() to // succeed (Type1 + Type3 with fixed inputs). let mut ntlm = NtlmClientContext::new("U", "P", "D", Some("")); ntlm.create_type1(); let challenge = make_dummy_challenge(); ntlm.create_type3( &challenge, &mut crate::ntlm::FixedInputs { client_challenge: [0u8; 8], exported_session_key: [0u8; 16], filetime: 0, }, ) .unwrap(); let trailer = AuthTrailer { auth_type: AuthType::WinNt, auth_level: AuthLevel::PacketIntegrity, auth_pad_length: 0, auth_reserved: 0, auth_context_id: NTLM_AUTH_CONTEXT_ID, }; let pdu = encode_packet_integrity_request(&unauth, trailer, &mut ntlm).unwrap(); // pad = 1 byte (27 -> 28); total = 27 + 1 + 8 + 16 = 52. assert_eq!(pdu.len(), 52); // Pad byte at offset 27 must be 0xBB. assert_eq!(pdu[27], 0xBB); // Trailer auth_pad_length at offset 28+2 = 30 (per AuthTrailer // encode: auth_type, auth_level, auth_pad_length, ...). assert_eq!(pdu[28], AuthType::WinNt.as_byte()); assert_eq!(pdu[29], AuthLevel::PacketIntegrity.as_byte()); assert_eq!(pdu[30], 1); // Embedded header: fragment_length=52, auth_length=16. let h = PduHeader::decode(&pdu).unwrap(); assert_eq!(h.fragment_length as usize, 52); assert_eq!(h.auth_length as usize, SIGNATURE_LEN); // The trailing 16 bytes are the NTLM signature; they MUST not be // the 0x20 placeholder fill. assert_ne!(&pdu[36..52], &[0x20u8; 16]); } #[test] fn encode_packet_integrity_request_no_pad_when_already_aligned() { // 28-byte unauth (header 16 + 8 fixed + 4 stub) is already aligned. let header = make_request_header(PacketType::Request, 1); let stub = [0xDEu8, 0xAD, 0xBE, 0xEF]; let unauth = encode_request_bytes(header, 0, 0, &stub, None); assert_eq!(unauth.len(), 28); let mut ntlm = NtlmClientContext::new("U", "P", "D", Some("")); ntlm.create_type1(); let challenge = make_dummy_challenge(); ntlm.create_type3( &challenge, &mut crate::ntlm::FixedInputs { client_challenge: [0u8; 8], exported_session_key: [0u8; 16], filetime: 0, }, ) .unwrap(); let trailer = AuthTrailer { auth_type: AuthType::WinNt, auth_level: AuthLevel::PacketIntegrity, auth_pad_length: 0, auth_reserved: 0, auth_context_id: NTLM_AUTH_CONTEXT_ID, }; let pdu = encode_packet_integrity_request(&unauth, trailer, &mut ntlm).unwrap(); // Total = 28 + 0 (no pad) + 8 trailer + 16 sig = 52. assert_eq!(pdu.len(), 52); // auth_pad_length at offset 30 is 0. assert_eq!(pdu[30], 0); } /// Build a minimum-viable Type2 challenge so create_type3 succeeds in /// tests. Mirrors what a real server would send. fn make_dummy_challenge() -> Vec { // 48-byte minimum Type2: signature(8) + msg_type(4) + target_name_fields(8) // + flags(4) + server_challenge(8) + reserved(8) + target_info_fields(8) // = 56 bytes when including target_info fields. Use the same scaffold // ntlm::tests uses internally. let mut buf = vec![0u8; 56]; buf[..8].copy_from_slice(b"NTLMSSP\0"); // message_type = 2 buf[8..12].copy_from_slice(&2u32.to_le_bytes()); // target_name fields zero (no target name) // flags buf[20..24].copy_from_slice( &(crate::ntlm::NEGOTIATE_UNICODE | crate::ntlm::NEGOTIATE_NTLM | crate::ntlm::NEGOTIATE_EXTENDED_SESSION_SECURITY | crate::ntlm::NEGOTIATE_TARGET_INFO | crate::ntlm::NEGOTIATE_KEY_EXCHANGE | crate::ntlm::NEGOTIATE_128 | crate::ntlm::NEGOTIATE_56) .to_le_bytes(), ); // server challenge bytes 24..32 left zero is fine // reserved 32..40 zero // target_info fields 40..48: length(2) + max_length(2) + offset(4) // We'll point target_info at offset 48 with length 8 (one EOL pair). buf[40..42].copy_from_slice(&8u16.to_le_bytes()); buf[42..44].copy_from_slice(&8u16.to_le_bytes()); buf[44..48].copy_from_slice(&48u32.to_le_bytes()); // target_info: 4 bytes EOL marker (id=0, len=0) repeated to pad buf } /// Round-trip test using a hand-rolled tokio echo-bind server. /// Verifies the client can `connect` -> `bind` -> read a BindAck. #[tokio::test] async fn bind_round_trip_with_local_listener() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); // Spawn a one-shot server that accepts one connection, reads a // Bind PDU, and writes back a minimal BindAck-shaped PDU. let server = tokio::spawn(async move { let (mut sock, _) = listener.accept().await.unwrap(); // Read the 16-byte header. let mut hdr = [0u8; 16]; sock.read_exact(&mut hdr).await.unwrap(); let parsed = PduHeader::decode(&hdr).unwrap(); // Drain the rest. let mut body = vec![0u8; parsed.fragment_length as usize - 16]; sock.read_exact(&mut body).await.unwrap(); // Send a fake BindAck — header only, length=16, no body. let resp = PduHeader { version: 5, version_minor: 0, packet_type: PacketType::BindAck, packet_flags: 0x03, data_representation: 0x10, fragment_length: 16, auth_length: 0, call_id: parsed.call_id, }; let mut out = [0u8; 16]; resp.encode(&mut out).unwrap(); sock.write_all(&out).await.unwrap(); }); let mut client = DceRpcTcpClient::connect(addr).await.unwrap(); let header = client.bind(Guid::new([0x99; 16]), 1, 0).await.unwrap(); assert_eq!(header.packet_type, PacketType::BindAck); server.await.unwrap(); } /// `call` over a server that echoes back a Fault must surface as /// `TransportError::Fault`. #[tokio::test] async fn call_returns_fault_when_server_responds_with_fault() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { let (mut sock, _) = listener.accept().await.unwrap(); // 1. Drain the Bind, send a minimal BindAck. let mut hdr = [0u8; 16]; sock.read_exact(&mut hdr).await.unwrap(); let bind = PduHeader::decode(&hdr).unwrap(); let mut body = vec![0u8; bind.fragment_length as usize - 16]; sock.read_exact(&mut body).await.unwrap(); let resp = PduHeader { version: 5, version_minor: 0, packet_type: PacketType::BindAck, packet_flags: 0x03, data_representation: 0x10, fragment_length: 16, auth_length: 0, call_id: bind.call_id, }; let mut out = [0u8; 16]; resp.encode(&mut out).unwrap(); sock.write_all(&out).await.unwrap(); // 2. Drain the Request, reply with a Fault carrying status=0xDEADBEEF. sock.read_exact(&mut hdr).await.unwrap(); let req = PduHeader::decode(&hdr).unwrap(); let mut body = vec![0u8; req.fragment_length as usize - 16]; sock.read_exact(&mut body).await.unwrap(); let fault = FaultPdu { header: PduHeader { version: 5, version_minor: 0, packet_type: PacketType::Fault, packet_flags: 0x03, data_representation: 0x10, fragment_length: 0, // overwritten by encode auth_length: 0, call_id: req.call_id, }, allocation_hint: 0, context_id: 0, cancel_count: 0, reserved23: 0, status: 0xDEAD_BEEF, stub_data: Vec::new(), }; let bytes = fault.encode(); sock.write_all(&bytes).await.unwrap(); }); let mut client = DceRpcTcpClient::connect(addr).await.unwrap(); let _ = client.bind(Guid::new([0x99; 16]), 1, 0).await.unwrap(); let err = client.call(0, 0, &[]).await.unwrap_err(); match err { TransportError::Fault { status } => assert_eq!(status, 0xDEAD_BEEF), other => panic!("expected Fault, got {other:?}"), } server.await.unwrap(); } #[test] fn auth_context_missing_when_packet_integrity_set_without_trailer() { // Direct unit test: forcing auth_level high without setting up the // trailer/ntlm pair should yield `AuthContextMissing` from // call_core via the runtime gate. We simulate that gate inline. // (The full async path would need a server; this test just // confirms the variant exists and matches the documented error.) let err = TransportError::AuthContextMissing; let msg = format!("{err}"); assert!(msg.contains("auth trailer")); } }