smb_msg/
dfsc.rs

1//! Distributed File System Referral Protocol (MS-DFSC) messages.
2//!
3//! [MS-DFSC](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsc/)
4
5#[cfg(feature = "server")]
6use binrw::io::TakeSeekExt;
7use binrw::{NullWideString, prelude::*};
8use modular_bitfield::prelude::*;
9use smb_dtyp::binrw_util::prelude::*;
10use smb_msg_derive::smb_request_binrw;
11
12/// [MS-DFSC 2.2.2](<https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsc/663c9b38-41b8-4faa-b6f6-a4576b4cea62>):
13/// DFS referral requests are sent in the form of an REQ_GET_DFS_REFERRAL message, by using an appropriate transport as specified in section 2.1.
14#[smb_request_binrw]
15pub struct ReqGetDfsReferral {
16    /// An integer that indicates the highest DFS referral version understood by the client. The DFS referral versions specified by this document are 1 through 4 inclusive. A DFS client MUST support DFS referral version 1 through the version number set in this field. The referral response messages are referral version dependent and are specified in sections 2.2.5.1 through 2.2.5.4.
17    pub max_referral_level: ReferralLevel,
18    /// A null-terminated Unicode string specifying the path to be resolved. The specified path MUST NOT be case-sensitive. Its format depends on the type of referral request, as specified in section 3.1.4.2.
19    pub request_file_name: NullWideString,
20}
21
22/// The DFS referral version supported by the client.
23/// See [`ReqGetDfsReferral::max_referral_level`].
24#[smb_request_binrw]
25#[brw(repr(u16))]
26pub enum ReferralLevel {
27    /// DFS referral version 1
28    V1 = 1,
29    /// DFS referral version 2
30    V2 = 2,
31    /// DFS referral version 3
32    V3 = 3,
33    /// DFS referral version 4
34    V4 = 4,
35}
36
37#[smb_request_binrw]
38pub struct ReqGetDfsReferralEx {
39    /// An integer that indicates the highest DFS referral version understood by the client. The DFS referral versions specified by this document are 1 through 4 inclusive. A DFS client MUST support DFS referral version 1 through the version number set in this field. The referral response messages are referral version dependent and are specified in sections 2.2.5.1 through 2.2.5.4.
40    pub max_referral_level: u16,
41    pub request_flags: DfsRequestFlags,
42    request_data_length: PosMarker<u32>,
43    #[bw(write_with = PosMarker::write_size, args(request_data_length))]
44    #[br(map_stream = |s| s.take_seek(request_data_length.value as u64))]
45    pub request_data: DfsRequestData,
46}
47
48#[smb_dtyp::mbitfield]
49pub struct DfsRequestFlags {
50    /// SiteName present: The SiteName bit MUST be set to 1 if the packet contains the site name of the client.
51    pub site_name: bool,
52    #[skip]
53    __: B15,
54}
55
56/// RequestData is part of the REQ_GET_DFS_REFERRAL_EX message (section 2.2.3).
57#[smb_request_binrw]
58pub struct DfsRequestData {
59    #[bw(try_calc = request_file_name.size().try_into())]
60    request_file_name_length: u16,
61    /// A Unicode string specifying the path to be resolved. The specified path MUST be interpreted in a case-insensitive manner. Its format depends on the type of referral request, as specified in section 3.1.4.2.
62    #[br(args { size: SizedStringSize::bytes16(request_file_name_length) })]
63    request_file_name: SizedWideString,
64    #[bw(try_calc = site_name.size().try_into())]
65    site_name_length: u16,
66    /// A Unicode string specifying the name of the site to which the DFS client computer belongs. The length of this string is determined by the value of the SiteNameLength field.
67    #[br(args { size: SizedStringSize::bytes16(site_name_length) })]
68    site_name: SizedWideString,
69}
70
71impl DfsRequestData {
72    pub fn get_bin_size(&self) -> usize {
73        size_of::<u16>() * 2 // lengths
74            + self.request_file_name.len() * size_of::<u16>() // + request_file_name (wstring)
75            + self.site_name.len() * size_of::<u16>() // + site_name (wstring)
76    }
77}
78
79/// NOTE: This struct currently implements [`BinWrite`] only as a placeholder (calling it will panic).
80/// [`BinRead`] is implemented and can be used to read DFS referral responses.
81#[binrw::binread]
82#[derive(Debug, PartialEq, Eq)]
83pub struct RespGetDfsReferral {
84    pub path_consumed: u16,
85    #[bw(try_calc = referral_entries.len().try_into())]
86    #[br(temp)]
87    number_of_referrals: u16,
88    pub referral_header_flags: ReferralHeaderFlags,
89    #[br(count = number_of_referrals)]
90    pub referral_entries: Vec<ReferralEntry>,
91    // string_buffer is here, but it's use is to provide a buffer for the strings in the referral entries.
92}
93
94impl BinWrite for RespGetDfsReferral {
95    type Args<'a> = ();
96
97    fn write_options<W: binrw::io::Write + binrw::io::Seek>(
98        &self,
99        _writer: &mut W,
100        _endian: binrw::Endian,
101        _args: Self::Args<'_>,
102    ) -> binrw::BinResult<()> {
103        unimplemented!(
104            "Placeholder trait implementation for RespGetDfsReferral - writing is currently not supported"
105        );
106    }
107}
108
109#[smb_dtyp::mbitfield]
110pub struct ReferralHeaderFlags {
111    /// Whether all of the targets in the referral entries returned are DFS root targets capable of handling DFS referral requests.
112    pub referral_servers: bool,
113    /// Whether all of the targets in the referral response can be accessed without requiring further referral requests.
114    pub storage_servers: bool,
115    /// Whether DFS client target failback is enabled for all targets in this referral response. This value used only in version 4.
116    pub target_failbacl: bool,
117    #[skip]
118    __: B29,
119}
120
121#[binrw::binrw]
122#[derive(Debug, PartialEq, Eq)]
123pub struct ReferralEntry {
124    /* All entry types share the same fields in their beginnings, so we split it */
125    #[bw(calc = value.get_version())]
126    pub version: u16,
127    #[bw(calc = PosMarker::default())]
128    #[br(temp)]
129    _size: PosMarker<u16>,
130
131    #[br(args(version))]
132    // map_stream is not used here because we seek manually in the inner structs.
133    #[bw(write_with = PosMarker::write_size_plus, args(&_size, Self::COMMON_PART_SIZE as u64))]
134    pub value: ReferralEntryValue,
135}
136
137impl ReferralEntry {
138    /// The size of the common part of the referral entry - version + size.
139    pub const COMMON_PART_SIZE: usize = std::mem::size_of::<u16>() * 2;
140}
141
142macro_rules! gen_ref_entry_val {
143    (
144        $($ver:literal,)+
145    ) => {
146        pastey::paste! {
147        #[binrw::binrw]
148        #[derive(Debug, PartialEq, Eq)]
149        #[br(import(version: u16))]
150        pub enum ReferralEntryValue {
151            $(
152                #[doc = concat!("A DFS referral version", stringify!($ver), "Entry")]
153                #[br(pre_assert(version == $ver))]
154                [<V $ver>]([<ReferralEntryValueV $ver>]),
155            )+
156        }
157
158        impl ReferralEntryValue {
159            fn get_version(&self) -> u16 {
160                match self {
161                    $(
162                        Self::[<V $ver>](_) => $ver,
163                    )+
164                }
165            }
166        }
167                }
168    };
169}
170
171gen_ref_entry_val!(1, 2, 3, 4,);
172
173#[binrw::binrw]
174#[derive(Debug, PartialEq, Eq)]
175pub struct ReferralEntryValueV1 {
176    /// Type of server hosting the target
177    pub server_type: DfsServerType,
178    #[bw(calc = 0)]
179    _referral_entry_flags: u16,
180    /// The DFS target.
181    pub share_name: NullWideString,
182}
183
184/// Type of server hosting the target
185#[binrw::binrw]
186#[derive(Debug, PartialEq, Eq)]
187#[brw(repr(u16))]
188pub enum DfsServerType {
189    /// Non-root targets returned.
190    NonRoot = 0x0,
191    /// Root targets returned.
192    Root = 0x1,
193}
194
195/// DO NOT use this struct directly when bin read/writing.
196/// Use an instance of [`ReferralEntry`] instead.
197#[binrw::binrw]
198#[derive(Debug, PartialEq, Eq)]
199pub struct ReferralEntryValueV2 {
200    #[bw(calc = PosMarker::default())]
201    #[br(temp)]
202    _start: PosMarker<()>,
203    /// Type of server hosting the target
204    pub server_type: DfsServerType,
205    #[bw(calc = 0)]
206    _referral_entry_flags: u16,
207    #[bw(calc = 0)]
208    _proximity: u32,
209
210    /// The time-out value, in seconds, of the DFS root or DFS link.
211    pub time_to_live: u32,
212    #[br(assert(dfs_path_offset.value >= ReferralEntry::COMMON_PART_SIZE as u16))]
213    #[bw(calc = PosMarker::default())]
214    #[br(temp)]
215    dfs_path_offset: PosMarker<u16>,
216    #[br(assert(dfs_alternate_path_offset.value >= ReferralEntry::COMMON_PART_SIZE as u16))]
217    #[bw(calc = PosMarker::default())]
218    #[br(temp)]
219    dfs_alternate_path_offset: PosMarker<u16>,
220    #[br(assert(network_address_offset.value >= ReferralEntry::COMMON_PART_SIZE as u16))]
221    #[bw(calc = PosMarker::default())]
222    #[br(temp)]
223    network_address_offset: PosMarker<u16>,
224
225    #[bw(calc = PosMarker::default())]
226    #[br(temp)]
227    _restore_position: PosMarker<()>,
228
229    /// The DFS path that corresponds to the DFS root or the DFS link for which target information is returned.
230    #[br(seek_before = _start.seek_from((dfs_path_offset.value as usize - ReferralEntry::COMMON_PART_SIZE).try_into().unwrap()))]
231    #[bw(write_with = PosMarker::write_roff_b_plus, args(&dfs_path_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
232    pub dfs_path: NullWideString,
233    /// The DFS path that corresponds to the DFS root or the DFS link for which target information is returned.
234    #[br(seek_before = _start.seek_from((dfs_alternate_path_offset.value as usize - ReferralEntry::COMMON_PART_SIZE).try_into().unwrap()))]
235    #[bw(write_with = PosMarker::write_roff_b_plus, args(&dfs_alternate_path_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
236    pub dfs_alternate_path: NullWideString,
237    /// The DFS target that corresponds to this entry.
238    #[br(seek_before = _start.seek_from((network_address_offset.value as usize - ReferralEntry::COMMON_PART_SIZE).try_into().unwrap()))]
239    #[bw(write_with = PosMarker::write_roff_b_plus, args(&network_address_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
240    pub network_address: NullWideString,
241
242    #[br(seek_before = _restore_position.seek_from(0))]
243    #[bw(calc = ())]
244    __: (),
245}
246
247#[binrw::binrw]
248#[derive(Debug, PartialEq, Eq)]
249pub struct ReferralEntryValueV3 {
250    /// Type of server hosting the target
251    pub server_type: DfsServerType,
252    pub referral_entry_flags: ReferralEntryFlags,
253    /// The time-out value, in seconds, of the DFS root or DFS link.
254    pub time_to_live: u32,
255    #[br(args(referral_entry_flags))]
256    pub value: EntryV3Value,
257}
258
259impl ReferralEntryValueV3 {
260    /// The size of the common part of the referral entry - version + size.
261    pub const COMMON_PART_SIZE: usize = std::mem::size_of::<u16>() * 2 + std::mem::size_of::<u32>();
262}
263
264#[smb_dtyp::mbitfield]
265pub struct ReferralEntryFlags {
266    #[skip]
267    __: bool,
268    pub name_list_referral: bool,
269    #[skip]
270    __: B14,
271}
272
273#[binrw::binrw]
274#[derive(Debug, PartialEq, Eq)]
275#[br(import(flags: ReferralEntryFlags))]
276pub enum EntryV3Value {
277    /// The DFS path that corresponds to the DFS root or the DFS link for which target information is returned.
278    #[br(pre_assert(flags.name_list_referral()))]
279    DfsPath(EntryV3V4DfsPaths),
280    /// The DFS target that corresponds to this entry.
281    #[br(pre_assert(!flags.name_list_referral()))]
282    NetworkAddress(EntryV3DCRefs),
283}
284
285impl EntryV3Value {
286    /// (Internal)
287    ///
288    /// The offset of EntryV3Value from the beginning of the [`ReferralEntry`] structure.
289    /// This is used to calculate the offsets of the fields in the structure.
290    const OFFSET_FROM_ENTRY_START: u16 =
291        (ReferralEntry::COMMON_PART_SIZE + ReferralEntryValueV3::COMMON_PART_SIZE) as u16;
292}
293
294/// 2.2.5.3.1 NameListReferral Flag Set to 0
295#[binrw::binrw]
296#[derive(Debug, PartialEq, Eq)]
297pub struct EntryV3V4DfsPaths {
298    #[bw(calc = PosMarker::default())]
299    #[br(temp)]
300    _start: PosMarker<()>,
301    #[br(assert(dfs_path_offset.value >= EntryV3Value::OFFSET_FROM_ENTRY_START))]
302    #[bw(calc = PosMarker::default())]
303    #[br(temp)]
304    dfs_path_offset: PosMarker<u16>,
305    #[br(assert(dfs_alternate_path_offset.value >= EntryV3Value::OFFSET_FROM_ENTRY_START))]
306    #[bw(calc = PosMarker::default())]
307    #[br(temp)]
308    dfs_alternate_path_offset: PosMarker<u16>,
309    #[br(assert(network_address_offset.value >= EntryV3Value::OFFSET_FROM_ENTRY_START))]
310    #[bw(calc = PosMarker::default())]
311    #[br(temp)]
312    network_address_offset: PosMarker<u16>,
313    #[bw(calc = 0)]
314    _service_site_guid: u128,
315
316    #[bw(calc = PosMarker::default())]
317    #[br(temp)]
318    _restore_position: PosMarker<()>,
319
320    /// The DFS path that corresponds to the DFS root or the DFS link for which target information is returned.
321    #[br(seek_before = _start.seek_from((dfs_path_offset.value - EntryV3Value::OFFSET_FROM_ENTRY_START).into()))]
322    #[bw(write_with = PosMarker::write_roff_b_plus, args(&dfs_path_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
323    pub dfs_path: NullWideString,
324    /// The DFS path that corresponds to the DFS root or the DFS link for which target information is returned.
325    #[br(seek_before = _start.seek_from((dfs_alternate_path_offset.value - EntryV3Value::OFFSET_FROM_ENTRY_START).into()))]
326    #[bw(write_with = PosMarker::write_roff_b_plus, args(&dfs_alternate_path_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
327    pub dfs_alternate_path: NullWideString,
328    /// The DFS target that corresponds to this entry.
329    #[br(seek_before = _start.seek_from((network_address_offset.value - EntryV3Value::OFFSET_FROM_ENTRY_START).into()))]
330    #[bw(write_with = PosMarker::write_roff_b_plus, args(&network_address_offset, &_start, ReferralEntry::COMMON_PART_SIZE as u64))]
331    pub network_address: NullWideString,
332
333    #[br(seek_before = _restore_position.seek_from(0))]
334    #[bw(calc = ())]
335    __: (),
336}
337
338/// 2.2.5.3.2 NameListReferral Flag Set to 1
339#[binrw::binrw]
340#[derive(Debug, PartialEq, Eq)]
341pub struct EntryV3DCRefs {
342    #[bw(calc = PosMarker::default())]
343    #[br(temp)]
344    _start: PosMarker<()>,
345    #[br(assert(special_name_offset.value >= EntryV3Value::OFFSET_FROM_ENTRY_START))]
346    #[bw(calc = PosMarker::default())]
347    #[br(temp)]
348    special_name_offset: PosMarker<u16>,
349    number_of_expanded_names: u16,
350    #[br(assert(expanded_name_offset.value >= EntryV3Value::OFFSET_FROM_ENTRY_START))]
351    #[bw(calc = PosMarker::default())]
352    #[br(temp)]
353    expanded_name_offset: PosMarker<u16>,
354
355    #[bw(calc = PosMarker::default())]
356    #[br(temp)]
357    _restore_position: PosMarker<()>,
358
359    #[br(seek_before = _start.seek_from((special_name_offset.value - EntryV3Value::OFFSET_FROM_ENTRY_START).into()))]
360    pub special_name: NullWideString,
361    #[br(seek_before = _start.seek_from((expanded_name_offset.value - EntryV3Value::OFFSET_FROM_ENTRY_START).into()))]
362    #[br(count = number_of_expanded_names)]
363    pub expanded_names: Vec<NullWideString>,
364
365    #[br(seek_before = _restore_position.seek_from(0))]
366    #[bw(calc = ())]
367    __: (),
368}
369
370#[binrw::binrw]
371#[derive(Debug, PartialEq, Eq)]
372pub struct ReferralEntryValueV4 {
373    /// Type of server hosting the target
374    pub server_type: DfsServerType,
375    // The ONLY valid flag is TargetSetBoundary.
376    #[br(assert((referral_entry_flags & !u16::from_le_bytes(ReferralEntryFlagsV4::new().with_target_set_boundary(true).into_bytes())) == 0))]
377    pub referral_entry_flags: u16,
378    /// The time-out value, in seconds, of the DFS root or DFS link.
379    pub time_to_live: u32,
380    // name_list_referral: bool is ALWAYS 0, so we know the type of the value.
381    pub refs: EntryV3V4DfsPaths,
382}
383
384/// Internal.
385#[smb_dtyp::mbitfield]
386struct ReferralEntryFlagsV4 {
387    #[skip]
388    __: B2,
389    #[skip(getters)]
390    target_set_boundary: bool,
391    #[skip]
392    __: B13,
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::*;
399
400    test_binrw_request! {
401        struct ReqGetDfsReferral {
402            max_referral_level: ReferralLevel::V4,
403            request_file_name: r"\ADC.aviv.local\dfs\Docs".into(),
404        } => "04005c004100440043002e0061007600690076002e006c006f00630061006c005c006400660073005c0044006f00630073000000"
405    }
406
407    #[cfg(feature = "client")]
408    smb_tests::test_binrw_read! {
409        struct RespGetDfsReferral {
410            path_consumed: 48,
411            referral_header_flags: ReferralHeaderFlags::new().with_storage_servers(true),
412            referral_entries: vec![
413                ReferralEntry {
414                    value: ReferralEntryValue::V4(ReferralEntryValueV4 {
415                        server_type: DfsServerType::NonRoot,
416                        referral_entry_flags: u16::from_le_bytes(
417                            ReferralEntryFlagsV4::new()
418                                .with_target_set_boundary(true)
419                                .into_bytes()
420                        ),
421                        time_to_live: 1800,
422                        refs: EntryV3V4DfsPaths {
423                            dfs_path: r"\ADC.aviv.local\dfs\Docs".into(),
424                            dfs_alternate_path: r"\ADC.aviv.local\dfs\Docs".into(),
425                            network_address: r"\ADC\Shares\Docs".into()
426                        }
427                    })
428                },
429                ReferralEntry {
430                    value: ReferralEntryValue::V4(ReferralEntryValueV4 {
431                        server_type: DfsServerType::NonRoot,
432                        referral_entry_flags: 0,
433                        time_to_live: 1800,
434                        refs: EntryV3V4DfsPaths {
435                            dfs_path: r"\ADC.aviv.local\dfs\Docs".into(),
436                            dfs_alternate_path: r"\ADC.aviv.local\dfs\Docs".into(),
437                            network_address: r"\FSRV\Shares\MyShare".into()
438                        }
439                    })
440                }
441            ],
442        } => "300002000200000004002200000004000807000044007600a8000000000000000000000000000000000004002200000000000807000022005400a
443        800000000000000000000000000000000005c004100440043002e0061007600690076002e006c006f00630061006c005c006400660073005c0044006f00
444        6300730000005c004100440043002e0061007600690076002e006c006f00630061006c005c006400660073005c0044006f006300730000005c004100440
445        043005c005300680061007200650073005c0044006f006300730000005c0046005300520056005c005300680061007200650073005c004d007900530068
446        006100720065000000"
447    }
448}