diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index 43b669582..950ff2050 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -18,8 +18,14 @@ pub type SqlServerClient = Client>; pub const SQLSERVER_DRIVER_PANIC_ERROR_PREFIX: &str = "SQL Server driver panic:"; const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"]; // Match JDBC/tiberius `encrypt=false`: encrypt only login, then drop back to raw TDS. -// `NotSupported` sends ENCRYPT_NOT_SUP and is rejected by some legacy SQL Server setups. const SQLSERVER_LEGACY_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::Off; +// Some very old SQL Server setups only accepted DBX <= 0.5.48 because the fallback +// advertised no encryption support at all. Keep it as the last-resort compatibility path. +const SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::NotSupported; +const SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS: [(&str, tiberius::EncryptionLevel); 2] = [ + ("login-only encryption", SQLSERVER_LEGACY_ENCRYPTION_LEVEL), + ("no-encryption compatibility fallback", SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL), +]; #[derive(Debug, PartialEq, Eq)] struct SqlServerEndpoint<'a> { @@ -51,17 +57,17 @@ pub async fn connect( timeout: Duration, ) -> Result { if sqlserver_legacy_encryption_disabled(url_params) { - return try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout).await; + return try_connect_legacy_sqlserver_encryption(host, port, user, pass, database, timeout).await; } match try_connect(host, port, user, pass, database, tiberius::EncryptionLevel::Required, timeout).await { Ok(client) => Ok(client), - Err(encrypted_error) => { - try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout).await.map_err( - |plain_error| { - if is_sqlserver_tls_handshake_error(&encrypted_error) { - format!( - "{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \ + Err(encrypted_error) => try_connect_legacy_sqlserver_encryption(host, port, user, pass, database, timeout) + .await + .map_err(|plain_error| { + if is_sqlserver_tls_handshake_error(&encrypted_error) { + format!( + "{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \ If you are connecting to SQL Server 2008/2008 R2/2012 or another legacy instance, \ try SQL Server legacy unencrypted mode. It behaves like encrypt=false and only helps \ when the server allows unencrypted transport or login-only encryption. It will still fail \ @@ -69,14 +75,31 @@ pub async fn connect( Only use this mode on trusted networks, VPNs, \ or SSH tunnels.\n\n\ Automatic legacy unencrypted fallback also failed: {plain_error}" - ) - } else { - plain_error - } - }, - ) + ) + } else { + plain_error + } + }), + } +} + +async fn try_connect_legacy_sqlserver_encryption( + host: &str, + port: u16, + user: &str, + pass: &str, + database: Option<&str>, + timeout: Duration, +) -> Result { + let mut errors = Vec::new(); + for (label, encryption) in SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS { + match try_connect(host, port, user, pass, database, encryption, timeout).await { + Ok(client) => return Ok(client), + Err(error) => errors.push(format!("{label} failed: {error}")), } } + + Err(errors.join("\n")) } fn sqlserver_legacy_encryption_disabled(url_params: Option<&str>) -> bool { @@ -1876,17 +1899,15 @@ mod tests { } #[test] - fn sqlserver_legacy_encryption_mode_matches_jdbc_encrypt_false_semantics() { + fn sqlserver_legacy_encryption_modes_cover_jdbc_and_no_encryption_fallback() { assert_eq!(super::SQLSERVER_LEGACY_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::Off); + assert_eq!(super::SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::NotSupported); } #[test] - fn sqlserver_automatic_fallback_uses_legacy_encryption_mode() { - let source = include_str!("sqlserver.rs"); - let connect = source.split("pub async fn connect").nth(1).unwrap(); - let connect = connect.split("fn sqlserver_legacy_encryption_disabled").next().unwrap(); - assert!(connect.contains("database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout")); - assert!(!connect.contains("EncryptionLevel::NotSupported, timeout")); + fn sqlserver_automatic_fallback_preserves_v48_no_encryption_compatibility() { + let levels = super::SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS.map(|(_, encryption)| encryption); + assert_eq!(levels, [tiberius::EncryptionLevel::Off, tiberius::EncryptionLevel::NotSupported]); } #[test]