Skip to main content

tds_protocol/
prelogin.rs

1//! TDS pre-login packet handling.
2//!
3//! The pre-login packet is the first message exchanged between client and server
4//! in TDS 7.x connections. It negotiates protocol version, encryption, and other
5//! connection parameters.
6//!
7//! Note: TDS 8.0 (strict mode) does not use pre-login negotiation; TLS is
8//! established before any TDS traffic.
9
10use bytes::{Buf, BufMut, Bytes, BytesMut};
11
12use crate::error::ProtocolError;
13use crate::prelude::*;
14use crate::version::{SqlServerVersion, TdsVersion};
15
16/// Pre-login option types.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u8)]
19#[non_exhaustive]
20pub enum PreLoginOption {
21    /// Version information.
22    Version = 0x00,
23    /// Encryption negotiation.
24    Encryption = 0x01,
25    /// Instance name (for named instances).
26    Instance = 0x02,
27    /// Thread ID.
28    ThreadId = 0x03,
29    /// MARS (Multiple Active Result Sets) support.
30    Mars = 0x04,
31    /// Trace ID for distributed tracing.
32    TraceId = 0x05,
33    /// Federated authentication required.
34    FedAuthRequired = 0x06,
35    /// Nonce for encryption.
36    Nonce = 0x07,
37    /// Terminator (end of options).
38    Terminator = 0xFF,
39}
40
41impl PreLoginOption {
42    /// Create from raw byte value.
43    pub fn from_u8(value: u8) -> Result<Self, ProtocolError> {
44        match value {
45            0x00 => Ok(Self::Version),
46            0x01 => Ok(Self::Encryption),
47            0x02 => Ok(Self::Instance),
48            0x03 => Ok(Self::ThreadId),
49            0x04 => Ok(Self::Mars),
50            0x05 => Ok(Self::TraceId),
51            0x06 => Ok(Self::FedAuthRequired),
52            0x07 => Ok(Self::Nonce),
53            0xFF => Ok(Self::Terminator),
54            _ => Err(ProtocolError::InvalidPreloginOption(value)),
55        }
56    }
57}
58
59/// Encryption level for connection.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61#[repr(u8)]
62#[non_exhaustive]
63pub enum EncryptionLevel {
64    /// Encryption is off.
65    Off = 0x00,
66    /// Encryption is on.
67    On = 0x01,
68    /// Encryption is not supported.
69    NotSupported = 0x02,
70    /// Encryption is required.
71    #[default]
72    Required = 0x03,
73    /// Client certificate authentication, base level off
74    /// (`ENCRYPT_CLIENT_CERT | ENCRYPT_OFF`, TDS 8.0+).
75    ///
76    /// `ENCRYPT_CLIENT_CERT` (0x80) is a flag OR'd onto the base encryption
77    /// level, so it appears combined with on/required as well (see
78    /// [`Self::ClientCertOn`] / [`Self::ClientCertReq`]).
79    ClientCertAuth = 0x80,
80    /// Client certificate authentication with encryption on
81    /// (`ENCRYPT_CLIENT_CERT | ENCRYPT_ON`, TDS 8.0+).
82    ClientCertOn = 0x81,
83    /// Client certificate authentication with encryption required
84    /// (`ENCRYPT_CLIENT_CERT | ENCRYPT_REQ`, TDS 8.0+).
85    ClientCertReq = 0x83,
86}
87
88impl EncryptionLevel {
89    /// Create from a raw byte value.
90    ///
91    /// Returns an error for an unrecognized byte rather than defaulting to
92    /// [`Self::Off`]: a garbage or unexpected encryption byte from the server
93    /// must not be silently read as "encryption off", which would mask a
94    /// downgrade or a malformed PRELOGIN response (#278). Mirrors the fallible
95    /// [`PreLoginOption::from_u8`].
96    pub fn from_u8(value: u8) -> Result<Self, ProtocolError> {
97        match value {
98            0x00 => Ok(Self::Off),
99            0x01 => Ok(Self::On),
100            0x02 => Ok(Self::NotSupported),
101            0x03 => Ok(Self::Required),
102            0x80 => Ok(Self::ClientCertAuth),
103            0x81 => Ok(Self::ClientCertOn),
104            0x83 => Ok(Self::ClientCertReq),
105            _ => Err(ProtocolError::InvalidEncryptionLevel(value)),
106        }
107    }
108
109    /// Check if encryption is required.
110    #[must_use]
111    pub const fn is_required(&self) -> bool {
112        // ClientCertOn/ClientCertReq carry ENCRYPT_ON/ENCRYPT_REQ in the base
113        // bits, so they imply encryption; 0x80 (ClientCertAuth) is the base-off
114        // combo retained for backwards compatibility.
115        matches!(
116            self,
117            Self::On
118                | Self::Required
119                | Self::ClientCertAuth
120                | Self::ClientCertOn
121                | Self::ClientCertReq
122        )
123    }
124}
125
126/// Pre-login message builder and parser.
127///
128/// This struct is used for both client requests and server responses:
129/// - **Client โ†’ Server**: Set `version` to the requested TDS version
130/// - **Server โ†’ Client**: `server_version` contains the SQL Server product version
131///
132/// Note: The VERSION field has different semantics in each direction:
133/// - Client sends: TDS protocol version (e.g., 7.4)
134/// - Server sends: SQL Server product version (e.g., 13.0.6300 for SQL Server 2016)
135#[derive(Debug, Clone, Default)]
136pub struct PreLogin {
137    /// TDS version (client request).
138    ///
139    /// This is the TDS protocol version the client requests. When sending a
140    /// PreLogin, set this to the desired TDS version.
141    pub version: TdsVersion,
142
143    /// SQL Server product version (server response).
144    ///
145    /// When decoding a PreLogin response from the server, this contains the
146    /// SQL Server product version (e.g., 13.0.6300 for SQL Server 2016).
147    /// This is NOT the TDS version - the actual TDS version is negotiated
148    /// in the LOGINACK token after login.
149    pub server_version: Option<SqlServerVersion>,
150
151    /// Encryption level.
152    pub encryption: EncryptionLevel,
153    /// Instance name (for named instances).
154    pub instance: Option<String>,
155    /// Thread ID.
156    pub thread_id: Option<u32>,
157    /// MARS enabled.
158    pub mars: bool,
159    /// Trace ID (Activity ID and Sequence).
160    pub trace_id: Option<TraceId>,
161    /// Federated authentication required.
162    pub fed_auth_required: bool,
163    /// Nonce for encryption.
164    pub nonce: Option<[u8; 32]>,
165}
166
167/// Distributed tracing ID.
168#[derive(Debug, Clone, Copy)]
169pub struct TraceId {
170    /// Activity ID (GUID).
171    pub activity_id: [u8; 16],
172    /// Activity sequence.
173    pub activity_sequence: u32,
174}
175
176impl PreLogin {
177    /// Create a new pre-login message with default values.
178    #[must_use]
179    pub fn new() -> Self {
180        Self {
181            version: TdsVersion::V7_4,
182            server_version: None,
183            encryption: EncryptionLevel::Required,
184            instance: None,
185            thread_id: None,
186            mars: false,
187            trace_id: None,
188            fed_auth_required: false,
189            nonce: None,
190        }
191    }
192
193    /// Set the TDS version.
194    #[must_use]
195    pub fn with_version(mut self, version: TdsVersion) -> Self {
196        self.version = version;
197        self
198    }
199
200    /// Set the encryption level.
201    #[must_use]
202    pub fn with_encryption(mut self, level: EncryptionLevel) -> Self {
203        self.encryption = level;
204        self
205    }
206
207    /// Enable MARS.
208    #[must_use]
209    pub fn with_mars(mut self, enabled: bool) -> Self {
210        self.mars = enabled;
211        self
212    }
213
214    /// Set the instance name.
215    #[must_use]
216    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
217        self.instance = Some(instance.into());
218        self
219    }
220
221    /// Advertise federated authentication support (FEDAUTHREQUIRED option).
222    ///
223    /// When set on a client PreLogin, the encoded message carries the
224    /// FEDAUTHREQUIRED option with value 0x01. The server's response echoes
225    /// its own FEDAUTHREQUIRED value in [`PreLogin::fed_auth_required`]; per
226    /// MS-TDS ยง2.2.6.4 the LOGIN7 FEDAUTH feature extension's `fFedAuthEcho`
227    /// bit MUST mirror that response value.
228    #[must_use]
229    pub fn with_fed_auth_required(mut self, required: bool) -> Self {
230        self.fed_auth_required = required;
231        self
232    }
233
234    /// Encode the pre-login message to bytes.
235    #[must_use]
236    pub fn encode(&self) -> Bytes {
237        let mut buf = BytesMut::with_capacity(256);
238
239        // Calculate option data offsets
240        // Each option entry is 5 bytes: type (1) + offset (2) + length (2)
241        // Plus 1 byte for terminator
242        let mut option_count = 3; // Version, Encryption, MARS are always present
243        if self.instance.is_some() {
244            option_count += 1;
245        }
246        if self.thread_id.is_some() {
247            option_count += 1;
248        }
249        if self.trace_id.is_some() {
250            option_count += 1;
251        }
252        if self.fed_auth_required {
253            option_count += 1;
254        }
255        if self.nonce.is_some() {
256            option_count += 1;
257        }
258
259        let header_size = option_count * 5 + 1; // +1 for terminator
260        let mut data_offset = header_size as u16;
261        let mut data_buf = BytesMut::new();
262
263        // VERSION option (6 bytes: 4 bytes version + 2 bytes sub-build)
264        buf.put_u8(PreLoginOption::Version as u8);
265        buf.put_u16(data_offset);
266        buf.put_u16(6);
267        let version_raw = self.version.raw();
268        data_buf.put_u8((version_raw >> 24) as u8);
269        data_buf.put_u8((version_raw >> 16) as u8);
270        data_buf.put_u8((version_raw >> 8) as u8);
271        data_buf.put_u8(version_raw as u8);
272        // Sub-build is always 0 for client-sent PreLogin; server sub-build
273        // lives in server_version after decode.
274        data_buf.put_u16_le(0);
275        data_offset += 6;
276
277        // ENCRYPTION option (1 byte)
278        buf.put_u8(PreLoginOption::Encryption as u8);
279        buf.put_u16(data_offset);
280        buf.put_u16(1);
281        data_buf.put_u8(self.encryption as u8);
282        data_offset += 1;
283
284        // INSTANCE option (if set)
285        if let Some(ref instance) = self.instance {
286            let instance_bytes = instance.as_bytes();
287            let len = instance_bytes.len() as u16 + 1; // +1 for null terminator
288            buf.put_u8(PreLoginOption::Instance as u8);
289            buf.put_u16(data_offset);
290            buf.put_u16(len);
291            data_buf.put_slice(instance_bytes);
292            data_buf.put_u8(0); // null terminator
293            data_offset += len;
294        }
295
296        // THREADID option (if set)
297        if let Some(thread_id) = self.thread_id {
298            buf.put_u8(PreLoginOption::ThreadId as u8);
299            buf.put_u16(data_offset);
300            buf.put_u16(4);
301            data_buf.put_u32(thread_id);
302            data_offset += 4;
303        }
304
305        // MARS option (1 byte)
306        buf.put_u8(PreLoginOption::Mars as u8);
307        buf.put_u16(data_offset);
308        buf.put_u16(1);
309        data_buf.put_u8(if self.mars { 0x01 } else { 0x00 });
310        data_offset += 1;
311
312        // TRACEID option (if set)
313        if let Some(ref trace_id) = self.trace_id {
314            buf.put_u8(PreLoginOption::TraceId as u8);
315            buf.put_u16(data_offset);
316            buf.put_u16(36);
317            data_buf.put_slice(&trace_id.activity_id);
318            data_buf.put_u32_le(trace_id.activity_sequence);
319            // Connection ID (16 bytes, typically zeros for client)
320            data_buf.put_slice(&[0u8; 16]);
321            data_offset += 36;
322        }
323
324        // FEDAUTHREQUIRED option (if set)
325        if self.fed_auth_required {
326            buf.put_u8(PreLoginOption::FedAuthRequired as u8);
327            buf.put_u16(data_offset);
328            buf.put_u16(1);
329            data_buf.put_u8(0x01);
330            data_offset += 1;
331        }
332
333        // NONCE option (if set)
334        if let Some(ref nonce) = self.nonce {
335            buf.put_u8(PreLoginOption::Nonce as u8);
336            buf.put_u16(data_offset);
337            buf.put_u16(32);
338            data_buf.put_slice(nonce);
339            let _ = data_offset; // Suppress unused warning
340        }
341
342        // Terminator
343        buf.put_u8(PreLoginOption::Terminator as u8);
344
345        // Append data section
346        buf.put_slice(&data_buf);
347
348        buf.freeze()
349    }
350
351    /// Decode a pre-login response from the server.
352    ///
353    /// Per MS-TDS spec 2.2.6.4, PreLogin message structure:
354    /// - Option headers: each 5 bytes (type:1 + offset:2 + length:2)
355    /// - Terminator: 1 byte (0xFF)
356    /// - Option data: variable length, positioned at offsets specified in headers
357    ///
358    /// Offsets in headers are absolute from the start of the PreLogin packet payload.
359    pub fn decode(mut src: impl Buf) -> Result<Self, ProtocolError> {
360        let mut prelogin = Self::default();
361
362        // Parse option headers first, collecting (option_type, offset, length)
363        let mut options = Vec::new();
364        loop {
365            if src.remaining() < 1 {
366                return Err(ProtocolError::UnexpectedEof);
367            }
368
369            let option_type = src.get_u8();
370            if option_type == PreLoginOption::Terminator as u8 {
371                break;
372            }
373
374            if src.remaining() < 4 {
375                return Err(ProtocolError::UnexpectedEof);
376            }
377
378            let offset = src.get_u16();
379            let length = src.get_u16();
380            options.push((PreLoginOption::from_u8(option_type)?, offset, length));
381        }
382
383        // Get remaining data as bytes for random access
384        let data = src.copy_to_bytes(src.remaining());
385
386        // Calculate header size: each option is 5 bytes + 1 byte terminator
387        let header_size = options.len() * 5 + 1;
388
389        for (option, packet_offset, length) in options {
390            let packet_offset = packet_offset as usize;
391            let length = length as usize;
392
393            // Convert absolute packet offset to offset within data buffer
394            // The data buffer starts after the headers, so we subtract header_size
395            if packet_offset < header_size {
396                // Invalid: offset points inside the headers
397                continue;
398            }
399            let data_offset = packet_offset - header_size;
400
401            // Bounds check
402            if data_offset + length > data.len() {
403                continue;
404            }
405
406            match option {
407                PreLoginOption::Version if length >= 4 => {
408                    // Per MS-TDS 2.2.6.4: The server sends its SQL Server product version
409                    // in the VERSION field, NOT the TDS protocol version.
410                    //
411                    // Format: UL_VERSION (4 bytes big-endian) + US_SUBBUILD (2 bytes little-endian)
412                    // UL_VERSION contains: [major][minor][build_hi][build_lo]
413                    //
414                    // For example, SQL Server 2016 sends 13.0.xxxx (major=13, minor=0)
415                    let version_bytes = &data[data_offset..data_offset + 4];
416                    let version_raw = u32::from_be_bytes([
417                        version_bytes[0],
418                        version_bytes[1],
419                        version_bytes[2],
420                        version_bytes[3],
421                    ]);
422
423                    // Extract sub_build if present
424                    let sub_build = if length >= 6 {
425                        let sub_build_bytes = &data[data_offset + 4..data_offset + 6];
426                        u16::from_le_bytes([sub_build_bytes[0], sub_build_bytes[1]])
427                    } else {
428                        0
429                    };
430
431                    // Populate the new SqlServerVersion field (correct semantics)
432                    prelogin.server_version =
433                        Some(SqlServerVersion::from_raw(version_raw, sub_build));
434
435                    // Also set version for backward compatibility
436                    prelogin.version = TdsVersion::new(version_raw);
437                }
438                PreLoginOption::Encryption if length >= 1 => {
439                    prelogin.encryption = EncryptionLevel::from_u8(data[data_offset])?;
440                }
441                PreLoginOption::Mars if length >= 1 => {
442                    prelogin.mars = data[data_offset] != 0;
443                }
444                PreLoginOption::Instance if length > 0 => {
445                    // Instance name is null-terminated string
446                    let instance_data = &data[data_offset..data_offset + length];
447                    if let Some(null_pos) = instance_data.iter().position(|&b| b == 0) {
448                        if let Ok(s) = core::str::from_utf8(&instance_data[..null_pos]) {
449                            if !s.is_empty() {
450                                prelogin.instance = Some(s.to_string());
451                            }
452                        }
453                    }
454                }
455                PreLoginOption::ThreadId if length >= 4 => {
456                    let bytes = &data[data_offset..data_offset + 4];
457                    prelogin.thread_id =
458                        Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
459                }
460                PreLoginOption::FedAuthRequired if length >= 1 => {
461                    prelogin.fed_auth_required = data[data_offset] != 0;
462                }
463                PreLoginOption::Nonce if length >= 32 => {
464                    let mut nonce = [0u8; 32];
465                    nonce.copy_from_slice(&data[data_offset..data_offset + 32]);
466                    prelogin.nonce = Some(nonce);
467                }
468                _ => {}
469            }
470        }
471
472        Ok(prelogin)
473    }
474}
475
476#[cfg(test)]
477#[allow(clippy::unwrap_used)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn test_prelogin_encode() {
483        let prelogin = PreLogin::new()
484            .with_version(TdsVersion::V7_4)
485            .with_encryption(EncryptionLevel::Required);
486
487        let encoded = prelogin.encode();
488        assert!(!encoded.is_empty());
489        // First byte should be VERSION option type
490        assert_eq!(encoded[0], PreLoginOption::Version as u8);
491    }
492
493    #[test]
494    fn test_encryption_level() {
495        assert!(EncryptionLevel::Required.is_required());
496        assert!(EncryptionLevel::On.is_required());
497        assert!(!EncryptionLevel::Off.is_required());
498        assert!(!EncryptionLevel::NotSupported.is_required());
499    }
500
501    /// #308: `ENCRYPT_CLIENT_CERT` (0x80) is a flag OR'd onto the base level,
502    /// so 0x81 (cert|on) and 0x83 (cert|req) are valid per MS-TDS and must
503    /// decode rather than erroring as `InvalidEncryptionLevel`. The combos that
504    /// carry encryption (0x81/0x83) are `is_required`.
505    #[test]
506    fn test_encryption_level_client_cert_combos_308() {
507        assert_eq!(
508            EncryptionLevel::from_u8(0x80).unwrap(),
509            EncryptionLevel::ClientCertAuth
510        );
511        assert_eq!(
512            EncryptionLevel::from_u8(0x81).unwrap(),
513            EncryptionLevel::ClientCertOn
514        );
515        assert_eq!(
516            EncryptionLevel::from_u8(0x83).unwrap(),
517            EncryptionLevel::ClientCertReq
518        );
519        assert!(EncryptionLevel::ClientCertOn.is_required());
520        assert!(EncryptionLevel::ClientCertReq.is_required());
521
522        // 0x82 has no defined meaning (ENCRYPT_CLIENT_CERT | 0x02 NotSupported)
523        // and must still fail closed.
524        assert!(matches!(
525            EncryptionLevel::from_u8(0x82),
526            Err(ProtocolError::InvalidEncryptionLevel(0x82))
527        ));
528    }
529
530    /// FEDAUTHREQUIRED (option 0x06) must be emitted with payload 0x01 when
531    /// requested and omitted otherwise, and must survive an encode/decode
532    /// round trip โ€” the login path reads the decoded flag back as the
533    /// LOGIN7 `fFedAuthEcho` source.
534    #[test]
535    fn test_prelogin_fed_auth_required_roundtrip() {
536        let without = PreLogin::new().encode();
537        let decoded = PreLogin::decode(without.as_ref()).unwrap();
538        assert!(
539            !decoded.fed_auth_required,
540            "FEDAUTHREQUIRED must default to absent/false"
541        );
542
543        let with = PreLogin::new().with_fed_auth_required(true).encode();
544        // Option header present: type 0x06 somewhere in the header section.
545        let header_end = with.iter().position(|&b| b == 0xFF).unwrap();
546        assert!(
547            with[..header_end]
548                .chunks(5)
549                .any(|opt| opt[0] == PreLoginOption::FedAuthRequired as u8),
550            "encoded PreLogin must contain a FEDAUTHREQUIRED option header"
551        );
552        let decoded = PreLogin::decode(with.as_ref()).unwrap();
553        assert!(decoded.fed_auth_required);
554    }
555
556    /// #278: an unrecognized PRELOGIN encryption byte must make decode fail,
557    /// not be silently read as ENCRYPT_OFF (which would mask a downgrade or a
558    /// malformed PRELOGIN response).
559    #[test]
560    fn test_prelogin_decode_rejects_unknown_encryption_byte() {
561        use bytes::BufMut;
562
563        let mut buf = bytes::BytesMut::new();
564        let header_size: u16 = 6; // one option header (5 bytes) + terminator (1)
565
566        // ENCRYPTION option header (type:1 + offset:2 + length:2)
567        buf.put_u8(PreLoginOption::Encryption as u8);
568        buf.put_u16(header_size); // offset to encryption data
569        buf.put_u16(1); // length
570        // Terminator
571        buf.put_u8(PreLoginOption::Terminator as u8);
572        // Data: an invalid encryption level byte
573        buf.put_u8(0x42);
574
575        let result = PreLogin::decode(buf.freeze().as_ref());
576        assert!(
577            matches!(result, Err(ProtocolError::InvalidEncryptionLevel(0x42))),
578            "an unknown encryption byte must be rejected as \
579             InvalidEncryptionLevel(0x42), not read as Off; got {result:?}"
580        );
581    }
582
583    #[test]
584    fn test_prelogin_decode_roundtrip() {
585        // Create a PreLogin with various options
586        let original = PreLogin::new()
587            .with_version(TdsVersion::V7_4)
588            .with_encryption(EncryptionLevel::On)
589            .with_mars(true);
590
591        // Encode it
592        let encoded = original.encode();
593
594        // Decode it back
595        let decoded = PreLogin::decode(encoded.as_ref()).unwrap();
596
597        // Verify the critical fields match
598        assert_eq!(decoded.version, original.version);
599        assert_eq!(decoded.encryption, original.encryption);
600        assert_eq!(decoded.mars, original.mars);
601    }
602
603    #[test]
604    fn test_prelogin_decode_encryption_offset() {
605        // Manually construct a PreLogin packet with options in non-standard order
606        // to verify offset handling works correctly
607        //
608        // Structure:
609        // - ENCRYPTION header at offset pointing to encryption data
610        // - VERSION header at offset pointing to version data
611        // - Terminator
612        // - Data section
613
614        use bytes::BufMut;
615
616        let mut buf = bytes::BytesMut::new();
617
618        // Header section: each option is 5 bytes (type:1 + offset:2 + length:2)
619        // We'll have 2 options + terminator = 11 bytes header
620        let header_size: u16 = 11;
621
622        // ENCRYPTION option header (put this first to test that we read from correct offset)
623        buf.put_u8(PreLoginOption::Encryption as u8);
624        buf.put_u16(header_size); // offset to encryption data
625        buf.put_u16(1); // length
626
627        // VERSION option header
628        buf.put_u8(PreLoginOption::Version as u8);
629        buf.put_u16(header_size + 1); // offset to version data (after encryption)
630        buf.put_u16(6); // length
631
632        // Terminator
633        buf.put_u8(PreLoginOption::Terminator as u8);
634
635        // Data section
636        // Encryption data (1 byte): ENCRYPT_ON = 0x01
637        buf.put_u8(0x01);
638
639        // Version data (6 bytes): TDS 7.4 = 0x74000004 big-endian + sub-build 0x0000 little-endian
640        buf.put_u8(0x74);
641        buf.put_u8(0x00);
642        buf.put_u8(0x00);
643        buf.put_u8(0x04);
644        buf.put_u16_le(0x0000); // sub-build
645
646        // Decode
647        let decoded = PreLogin::decode(buf.freeze().as_ref()).unwrap();
648
649        // Verify encryption was read from correct offset (not from index 0)
650        assert_eq!(decoded.encryption, EncryptionLevel::On);
651        assert_eq!(decoded.version, TdsVersion::V7_4);
652    }
653}