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}