Skip to main content

ad_time/protocols/
smb.rs

1/// SMB2 NEGOTIATE time source — fallback on TCP/445.
2///
3/// Protocol Specifications:
4/// - **MS-SMB2 §2.2.3**: SMB2 NEGOTIATE Request
5/// - **MS-SMB2 §2.2.4**: SMB2 NEGOTIATE Response
6///
7/// Sends SMB2 NEGOTIATE request and reads SystemTime from the response.
8use std::io::{Read, Write};
9use std::net::{SocketAddr, TcpStream};
10use std::time::{Duration, Instant, SystemTime};
11
12use super::common::{filetime_to_system_time, map_io_err, system_time_to_us};
13use super::smb_common::build_negotiate_request;
14use crate::time_src::{OffsetMicros, TimeSource, TimeSourceError};
15
16pub struct SmbSource;
17
18/// Sequential field reader for little-endian binary structs.
19struct FieldReader<'a> {
20    buf: &'a [u8],
21    pos: usize,
22}
23
24impl<'a> FieldReader<'a> {
25    fn new(buf: &'a [u8]) -> Self {
26        Self { buf, pos: 0 }
27    }
28
29    fn read_u16_le(&mut self) -> Result<u16, TimeSourceError> {
30        let b = self.next_bytes(2)?;
31        Ok(u16::from_le_bytes([b[0], b[1]]))
32    }
33
34    fn read_u32_le(&mut self) -> Result<u32, TimeSourceError> {
35        let b = self.next_bytes(4)?;
36        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
37    }
38
39    fn read_u64_le(&mut self) -> Result<u64, TimeSourceError> {
40        let b = self.next_bytes(8)?;
41        Ok(u64::from_le_bytes([
42            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
43        ]))
44    }
45
46    fn skip(&mut self, n: usize) -> Result<(), TimeSourceError> {
47        self.next_bytes(n)?;
48        Ok(())
49    }
50
51    fn next_bytes(&mut self, n: usize) -> Result<&'a [u8], TimeSourceError> {
52        let end = self
53            .pos
54            .checked_add(n)
55            .ok_or_else(|| TimeSourceError::Parse("FieldReader overflow".into()))?;
56        if end > self.buf.len() {
57            return Err(TimeSourceError::Parse("SMB body overruns buffer".into()));
58        }
59        let b = &self.buf[self.pos..end];
60        self.pos = end;
61        Ok(b)
62    }
63}
64
65impl TimeSource for SmbSource {
66    fn name(&self) -> &'static str {
67        "smb"
68    }
69
70    fn fetch(
71        &self,
72        target: SocketAddr,
73        timeout: Duration,
74    ) -> Result<OffsetMicros, TimeSourceError> {
75        let smb_addr: SocketAddr = (target.ip(), 445).into();
76        fetch_smb(smb_addr, timeout)
77    }
78}
79
80fn fetch_smb(addr: SocketAddr, timeout: Duration) -> Result<OffsetMicros, TimeSourceError> {
81    let mut stream =
82        TcpStream::connect_timeout(&addr, timeout).map_err(|e| map_io_err(e, "connect"))?;
83    stream
84        .set_read_timeout(Some(timeout))
85        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
86    stream
87        .set_write_timeout(Some(timeout))
88        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
89
90    let t_send = Instant::now();
91    let t_send_sys = SystemTime::now();
92
93    let request = build_negotiate_request();
94    stream
95        .write_all(&request)
96        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
97
98    // Read NetBIOS header (4 bytes) to know response length.
99    let mut nb_header = [0u8; 4];
100    stream
101        .read_exact(&mut nb_header)
102        .map_err(|e| map_io_err(e, "read_header"))?;
103    // NetBIOS session message: byte 0 = 0x00, bytes 1..4 = 24-bit big-endian length.
104    let msg_len = u32::from_be_bytes(nb_header) & 0x00FF_FFFF;
105    if msg_len > 65536 {
106        return Err(TimeSourceError::Protocol(format!(
107            "implausibly large SMB2 response: {} bytes",
108            msg_len
109        )));
110    }
111    if msg_len < 64 + 65 {
112        return Err(TimeSourceError::Parse(format!(
113            "SMB2 response too short: {} bytes",
114            msg_len
115        )));
116    }
117
118    let mut body = vec![0u8; msg_len as usize];
119    stream
120        .read_exact(&mut body)
121        .map_err(|e| map_io_err(e, "read_body"))?;
122
123    let rtt = t_send.elapsed();
124
125    // body[0..64] is SMB2 header; body[64..] is NEGOTIATE_RESPONSE.
126    let negotiate = &body[64..];
127    let server_time = parse_negotiate_response(negotiate)?;
128
129    // Single-point approximation: server timestamp ≈ midpoint of our send/recv window.
130    // Precision: ±RTT/2 — sufficient for Kerberos 5-minute skew window.
131    let t_mid_us = system_time_to_us(t_send_sys)? + (rtt.as_micros() as i64) / 2;
132    let server_us = system_time_to_us(server_time)?;
133
134    Ok(server_us - t_mid_us)
135}
136
137/// Parse SMB2 NEGOTIATE_RESPONSE (MS-SMB2 §2.2.4) and extract SystemTime.
138fn parse_negotiate_response(b: &[u8]) -> Result<SystemTime, TimeSourceError> {
139    let mut r = FieldReader::new(b);
140    // Fields are little-endian; read sequentially per MS-SMB2 §2.2.4.
141    let structure_size = r.read_u16_le()?; //  0: StructureSize (must be 65)
142    if structure_size != 65 {
143        return Err(TimeSourceError::Protocol(format!(
144            "unexpected SMB2 NEGOTIATE_RESPONSE StructureSize: {}",
145            structure_size
146        )));
147    }
148    let _security_mode = r.read_u16_le()?; //  2: SecurityMode
149    let _dialect_revision = r.read_u16_le()?; //  4: DialectRevision
150    let _negotiate_ctx_cnt = r.read_u16_le()?; //  6: NegotiateContextCount/Reserved
151    r.skip(16)?; //  8: ServerGuid ([u8; 16])
152    let _capabilities = r.read_u32_le()?; // 24: Capabilities
153    let _max_transact = r.read_u32_le()?; // 28: MaxTransactSize
154    let _max_read = r.read_u32_le()?; // 32: MaxReadSize
155    let _max_write = r.read_u32_le()?; // 36: MaxWriteSize
156    let system_time = r.read_u64_le()?; // 40: SystemTime (FILETIME)
157
158    filetime_to_system_time(system_time)
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use std::time::UNIX_EPOCH;
165
166    #[test]
167    fn filetime_unix_epoch() {
168        // FILETIME of Unix epoch = 116444736000000000
169        let ft: u64 = 116_444_736_000_000_000;
170        let st = filetime_to_system_time(ft).unwrap();
171        assert_eq!(st, UNIX_EPOCH);
172    }
173
174    #[test]
175    fn filetime_2024_01_01() {
176        // 2024-01-01 00:00:00 UTC as FILETIME
177        // Unix timestamp = 1704067200
178        // FILETIME = (1704067200 + 11644473600) * 10_000_000 = 133485408000000000
179        let ft: u64 = 133_485_408_000_000_000;
180        let st = filetime_to_system_time(ft).unwrap();
181        let unix_secs = st.duration_since(UNIX_EPOCH).unwrap().as_secs();
182        assert_eq!(unix_secs, 1_704_067_200);
183    }
184
185    #[test]
186    fn filetime_before_unix_epoch_errors() {
187        assert!(filetime_to_system_time(0).is_err());
188        assert!(filetime_to_system_time(100).is_err());
189    }
190
191    #[test]
192    fn negotiate_response_too_short() {
193        assert!(parse_negotiate_response(&[0u8; 10]).is_err());
194    }
195
196    #[test]
197    fn negotiate_response_bad_structure_size() {
198        let mut b = vec![0u8; 50];
199        // StructureSize = 99 (wrong)
200        b[0..2].copy_from_slice(&99u16.to_le_bytes());
201        assert!(parse_negotiate_response(&b).is_err());
202    }
203
204    #[test]
205    fn build_negotiate_request_has_random_guid() {
206        // ClientGuid is at offset 4 (NetBIOS) + 64 (SMB2 header) + 12 (body offset) = 80..96
207        use crate::protocols::smb_common::build_negotiate_request;
208        let r1 = build_negotiate_request();
209        let r2 = build_negotiate_request();
210        assert_ne!(
211            &r1[80..96],
212            &r2[80..96],
213            "ClientGuid must differ between calls"
214        );
215        // Sanity: neither is all-zero (overwhelmingly probable)
216        assert_ne!(&r1[80..96], &[0u8; 16]);
217    }
218
219    #[test]
220    fn build_negotiate_request_advertises_smb311() {
221        use crate::protocols::smb_common::build_negotiate_request;
222        let req = build_negotiate_request();
223        // Packet layout: NetBIOS(4) + SMB2 header(64) + body fixed(36) = dialects start at pkt[104]
224        assert_eq!(
225            u16::from_le_bytes([req[104], req[105]]),
226            0x0311,
227            "first dialect must be SMB 3.1.1"
228        );
229        // NegotiateContextOffset at body offset 28 → pkt[4 + 64 + 28] = pkt[96]
230        let neg_ctx_off = u32::from_le_bytes([req[96], req[97], req[98], req[99]]);
231        assert_eq!(
232            neg_ctx_off, 112,
233            "NegotiateContextOffset must be 112 (8-byte aligned from SMB2 header start)"
234        );
235        // PREAUTH_INTEGRITY_CAPABILITIES context type at pkt[4 + 112] = pkt[116]
236        assert_eq!(
237            u16::from_le_bytes([req[116], req[117]]),
238            0x0001,
239            "negotiate context must be PREAUTH_INTEGRITY_CAPABILITIES"
240        );
241    }
242
243    #[test]
244    fn fetch_smb_rejects_large_msg_len() {
245        // Simulate a NetBIOS header claiming a 128 KB response body (> 65536 limit).
246        // We cannot call fetch_smb (needs a real socket), but we can verify the
247        // guard arithmetic: msg_len field is 24-bit from bytes [1..4].
248        let large: u32 = 0x0002_0000; // 131072 bytes
249        assert!(large > 65536);
250        // Confirm the mask used in production: u32::from_be_bytes([0, 2, 0, 0]) & 0x00FF_FFFF = 131072
251        let nb = [0x00u8, 0x02, 0x00, 0x00];
252        let msg_len = u32::from_be_bytes(nb) & 0x00FF_FFFF;
253        assert_eq!(msg_len, 131072);
254        assert!(msg_len > 65536);
255    }
256
257    use proptest::prelude::*;
258
259    proptest! {
260        #[test]
261        fn parse_negotiate_response_never_panics(data in proptest::collection::vec(any::<u8>(), 0..256)) {
262            let _ = parse_negotiate_response(&data);
263        }
264    }
265}
266
267#[cfg(feature = "fuzzing")]
268pub fn fuzz_parse_negotiate_response(
269    data: &[u8],
270) -> Result<std::time::SystemTime, crate::time_src::TimeSourceError> {
271    parse_negotiate_response(data)
272}