Skip to content

Strict encryption option #412, hostname_in_certificate option #340, t… - #413

Open
olback wants to merge 9 commits into
tiberius-rs:mainfrom
olback:tds80
Open

Strict encryption option #412, hostname_in_certificate option #340, t…#413
olback wants to merge 9 commits into
tiberius-rs:mainfrom
olback:tds80

Conversation

@olback

@olback olback commented Apr 8, 2026

Copy link
Copy Markdown

Fixes

New

  • Client hostname should be provided in LoginMessage #414 - Provide client hostname in LoginMessage
  • TDS 8.0 and "Strict" encryption option #412 - Add TDS 8.0 and Strict encryption option
    • New default feature flag: tds80, extends the existing feature tds73
    • Added compile-time check to disallow TDS 8.0 without any TLS implementation.
    • Strict option added to EncryptionLevel
      • Connection string parser also updated.
    • Added tds/8.0 ALPN to TLS implementations that support it. ALPN is only requested when EncryptionLevel = Strict. See note below.
    • Client hostname automatically set in login message
      • Added config option to overwrite hostname

New/updated tests

  • Tests
    • Add test cases for encrypt=strict in connection string
    • Add test cases for hostname in certificate in connection string
    • Add test cases for Workstation ID connection string

Qs

  • Should max TLS version be configured when TDS 8.0 is not enabled? Does it matter?
  • Should a user be able to specify min/max TLS versions via the configuration?

NOTE: While not required, ALPN should be set when negotiating TLS. SQL Server will assume tds/8.0 if unset.
image

image

As ffmpeg twitter once said: talk is cheap, send patches

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • TDS 8.0 protocol is now the default encryption standard
    • Strict encryption mode with ALPN support for enhanced security
    • Certificate hostname validation configuration option
    • Client/workstation name support in connection strings
    • Improved certificate handling with better file extension recognition

Walkthrough

Adds TDS 8.0 / Strict encryption support and wiring: new tds80 feature, EncryptionLevel::Strict, strict-mode early TLS negotiation and ALPN, hostname-in-certificate and client-name configuration, platform hostname detection for login, CA-path parsing tweaks, TLS verifier updates, and Cargo feature/dependency adjustments.

Changes

Cohort / File(s) Summary
Manifest & feature config
Cargo.toml, src/lib.rs
Added Unix-only libc dep, bumped async-native-tls to 0.5, changed default features to ["tds80","winauth","native-tls"], added tds80 = ["tds73"] feature, and added a compile-time guard requiring a TLS feature when tds80 is enabled.
Encryption enum & wire encoding
src/tds.rs
Added EncryptionLevel::Strict = 4 and as_wire_value() mapping (Strict wire-encodes as 3); unit tests added for as_wire_value.
Client config & ADO.NET parsing
src/client/config.rs, src/client/config/ado_net.rs
Added hostname_in_certificate: Option<String> and client_name: Option<String> to Config; new setters and crate getter; trust_cert_ca now accepts impl Into<PathBuf>; ADO.NET parsing accepts encrypt=strict and new key aliases; tests added.
Connection flow & login code
src/client/connection.rs, src/tds/codec/pre_login.rs
Connection::connect pre-wraps transport with TLS for Strict; login refactored to take Config; tls_handshake reworked to match on EncryptionLevel (explicit Strict path); PreloginMessage uses as_wire_value() and negotiates Strict explicitly.
TLS stream constant & ALPN
src/client/tls_stream.rs
Added pub(crate) const TDS_ALPN_PROTOCOL_NAME: &str = "tds/8.0";.
native-tls adjustments
src/client/tls_stream/native_tls_stream.rs
When encryption == Strict request ALPN for tds/8.0; use case-insensitive CA file-extension checks; use get_hostname_in_certificate() for SNI/verification.
OpenTLS adjustments
src/client/tls_stream/opentls_tls_stream.rs
Log warning that OpenTLS lacks ALPN for Strict (SQL Server will assume TDS 8.0); use get_hostname_in_certificate() for verification hostname.
rustls verifier & TLS1.3 handling
src/client/tls_stream/rustls_tls_stream.rs
Implemented NoCertVerifier::verify_tls13_signature to accept signatures; use get_hostname_in_certificate() for ServerName; case-insensitive CA extension checks; add ALPN when Strict.
Login message hostname detection
src/tds/codec/login.rs
LoginMessage::new() now initializes hostname via new get_hostname() with platform-specific implementations (Windows: GetComputerNameW; Unix: libc::gethostname); added hostname() setter and error logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title references three distinct issues (#412, #340, and a truncated reference) and is incomplete, making it vague about the full scope of changes despite being related to the changeset. Complete the title and make it more specific, e.g., 'Add TDS 8.0 strict encryption, hostname in certificate, and client name support' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering fixes, new features, test cases, and open questions about the implementation.
Linked Issues check ✅ Passed The PR fully implements the requirement from issue #336 by changing trust_cert_ca to accept impl Into and storing the path directly via path.into().
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives: TDS 8.0 support, strict encryption, hostname in certificate, client name support, and addressing issue #336.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/config.rs (1)

380-390: ⚠️ Potential issue | 🟠 Major

Reject encrypt=strict when tds80 is not compiled in.

This branch is only gated on a TLS backend. A default-features = false build with native-tls or rustls but without tds80 will still accept strict from the connection string and hand an unsupported mode to the connection layer.

🔧 Suggested fix
-                Err(_) if val.eq_ignore_ascii_case("strict") => Ok(EncryptionLevel::Strict),
+                Err(_) if val.eq_ignore_ascii_case("strict") => {
+                    #[cfg(feature = "tds80")]
+                    {
+                        Ok(EncryptionLevel::Strict)
+                    }
+                    #[cfg(not(feature = "tds80"))]
+                    {
+                        Err(crate::Error::Conversion(
+                            "Connection string: `encrypt=strict` requires the `tds80` feature"
+                                .into(),
+                        ))
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/config.rs` around lines 380 - 390, The encrypt() parsing currently
accepts "strict" unconditionally; change the branch handling Err(_) if
val.eq_ignore_ascii_case("strict") inside encrypt() so that it only returns
Ok(EncryptionLevel::Strict) when the crate feature "tds80" is enabled (e.g.
using cfg!(feature = "tds80") or #[cfg] gating), and otherwise return an Err
parsed error (reject the value) so non-tds80 builds (default-features = false)
do not accept "strict"; update the match in encrypt() that references
Self::parse_bool and EncryptionLevel to perform this conditional check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/connection.rs`:
- Line 82: Remove the leftover commented-out dead code line that defines a
transport ("// let transport = Framed::new(MaybeTlsStream::Raw(tcp_stream),
PacketCodec);"); it is redundant because transport initialization is handled by
the subsequent match expression—delete that commented line so only the active
match-based transport creation remains (search for the transport variable
initialization and references to MaybeTlsStream and PacketCodec in connection.rs
to locate it).
- Around line 480-485: The code currently matches transport.into_inner() and
uses MaybeTlsStream::Raw(tcp) to call create_tls_stream(...,
TlsPreloginWrapper::new(tcp)).await? with an unreachable!() fallback; replace
that panic-prone fallback with a defensive error return (or at minimum a
debug_assert! plus a clear Err variant) so unexpected variants produce a
Result::Err instead of panicking. Locate the match around transport.into_inner()
in connection.rs and change the unreachable!() arm to return a meaningful error
(or debug_assert! then return a conversion error) so callers of this code
receive a graceful error when MaybeTlsStream is not Raw.

In `@src/client/tls_stream/native_tls_stream.rs`:
- Around line 17-20: The rustls backend currently uses
ClientConfig::builder().with_safe_defaults() without limiting TLS versions,
causing a mismatch with native-tls which caps to TLS 1.2 when the tds80 feature
is disabled; update the rustls client config (the ClientConfig constructed in
rustls_tls_stream.rs) to enforce a maximum of TLS1.2 in the non-tds80 case
(mirror the conditional used in native_tls_stream.rs) by setting the config's
supported protocol versions to only TLS1_2 (e.g., replace or override the
default versions on the created ClientConfig when cfg(not(feature = "tds80"))).

In `@src/tds.rs`:
- Around line 28-29: Prelogin ENCRYPTION currently writes the raw
EncryptionLevel discriminant (used in PreloginMessage::encode) so
EncryptionLevel::Strict ends up as 0x04 on the wire; update prelogin() to remap
Strict into a wire-compatible value (e.g., map EncryptionLevel::Strict to
EncryptionLevel::Required or the 0x01–0x03 equivalent) before
constructing/encoding the PreloginMessage so encode (which does
write_u8(self.encryption as u8)) never emits 0x04; locate the prelogin() method
and apply the mapping there (leave the internal config.encryption unchanged if
needed, only modify the value passed into PreloginMessage).

---

Outside diff comments:
In `@src/client/config.rs`:
- Around line 380-390: The encrypt() parsing currently accepts "strict"
unconditionally; change the branch handling Err(_) if
val.eq_ignore_ascii_case("strict") inside encrypt() so that it only returns
Ok(EncryptionLevel::Strict) when the crate feature "tds80" is enabled (e.g.
using cfg!(feature = "tds80") or #[cfg] gating), and otherwise return an Err
parsed error (reject the value) so non-tds80 builds (default-features = false)
do not accept "strict"; update the match in encrypt() that references
Self::parse_bool and EncryptionLevel to perform this conditional check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4d3682a-7245-4c64-b7a2-0e566d4ad112

📥 Commits

Reviewing files that changed from the base of the PR and between a6b4fcd and 2634e8c.

📒 Files selected for processing (10)
  • Cargo.toml
  • src/client/config.rs
  • src/client/connection.rs
  • src/client/tls_stream/native_tls_stream.rs
  • src/client/tls_stream/opentls_tls_stream.rs
  • src/client/tls_stream/rustls_tls_stream.rs
  • src/lib.rs
  • src/tds.rs
  • src/tds/codec/login.rs
  • src/tds/codec/pre_login.rs

Comment thread src/client/connection.rs Outdated
Comment thread src/client/connection.rs
Comment thread src/client/tls_stream/native_tls_stream.rs Outdated
Comment thread src/tds.rs
Comment thread src/tds/codec/login.rs Outdated
@olback
olback marked this pull request as draft April 9, 2026 11:15
@olback
olback marked this pull request as ready for review April 9, 2026 18:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/tds/codec/login.rs (1)

199-200: 🧹 Nitpick | 🔵 Trivial

Minor: Comment mentions "byte" but buffer is u16 array.

The comment says "1 byte for the null terminator" but the buffer is [0u16; 15 + 1], so it should say "1 element" or "1 WCHAR".

📝 Suggested fix
-            // MAX_COMPUTERNAME_LENGTH is 15 and we need 1 byte for the null terminator
+            // MAX_COMPUTERNAME_LENGTH is 15 and we need 1 WCHAR for the null terminator
             let mut buffer = [0u16; 15 + 1];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tds/codec/login.rs` around lines 199 - 200, The comment above the buffer
allocation incorrectly says "1 byte for the null terminator" even though the
buffer is a u16 array; update the comment to reflect a 1-element/1 WCHAR null
terminator instead. Locate the comment near the declaration let mut buffer =
[0u16; 15 + 1] (related to MAX_COMPUTERNAME_LENGTH) in src/tds/codec/login.rs
and replace "1 byte" with "1 element" or "1 WCHAR" so the comment accurately
matches the u16 buffer type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 195-196: The default TLS backend was changed in Cargo.toml
(default = ["tds80","winauth","rustls"] and tds80 = ["tds73"]) which is a
breaking change for downstream users relying on native-tls; update the release
notes / CHANGELOG to call out that the default feature set now enables rustls
instead of native-tls, describe the consequences (different TLS behavior and
potential need to compile ring), and add clear migration guidance showing how
consumers can restore the old behavior by explicitly enabling the previous
feature (e.g., selecting native-tls and/or disabling rustls) in their Cargo.toml
or dependency feature list, plus any platform-specific build notes for ring.

In `@src/client/config.rs`:
- Around line 408-410: The parse path silently falls through to parse_bool and
yields a confusing "Not a valid boolean" when the user supplies "strict" but the
crate was compiled without the tds80 feature; update the parsing logic (the
FromStr/parse function that branches to EncryptionLevel::Strict and calls
parse_bool) to explicitly check for val.eq_ignore_ascii_case("strict") when
cfg!(not(feature = "tds80")) and return a clear Err variant/message indicating
that "strict" requires the tds80 feature (or suggest enabling that feature), so
users see a meaningful error instead of the generic parse_bool failure.

In `@src/client/tls_stream/native_tls_stream.rs`:
- Around line 17-19: The async-native-tls dependency is missing the "alpn"
feature required for builder.request_alpns to be available; update the
Cargo.toml dependency entry for async-native-tls to include the "alpn" feature
in its features list (e.g., add "alpn" alongside "runtime-async-std") so calls
like builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]) compile
successfully.

---

Duplicate comments:
In `@src/tds/codec/login.rs`:
- Around line 199-200: The comment above the buffer allocation incorrectly says
"1 byte for the null terminator" even though the buffer is a u16 array; update
the comment to reflect a 1-element/1 WCHAR null terminator instead. Locate the
comment near the declaration let mut buffer = [0u16; 15 + 1] (related to
MAX_COMPUTERNAME_LENGTH) in src/tds/codec/login.rs and replace "1 byte" with "1
element" or "1 WCHAR" so the comment accurately matches the u16 buffer type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 501ec9e4-d7c6-4477-b96a-0ec12f4b8299

📥 Commits

Reviewing files that changed from the base of the PR and between 2634e8c and e839e4e.

📒 Files selected for processing (11)
  • Cargo.toml
  • src/client/config.rs
  • src/client/config/ado_net.rs
  • src/client/connection.rs
  • src/client/tls_stream.rs
  • src/client/tls_stream/native_tls_stream.rs
  • src/client/tls_stream/opentls_tls_stream.rs
  • src/client/tls_stream/rustls_tls_stream.rs
  • src/tds.rs
  • src/tds/codec/login.rs
  • src/tds/codec/pre_login.rs

Comment thread Cargo.toml Outdated
Comment thread src/client/config.rs
Comment on lines +408 to +410
Err(_) if val.eq_ignore_ascii_case("strict") && cfg!(feature = "tds80") => {
Ok(EncryptionLevel::Strict)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider improving error message when tds80 feature is disabled.

When encrypt=strict is specified but the tds80 feature is not enabled, the code falls through to return a generic "Not a valid boolean" error from parse_bool. This may confuse users who expect strict to work.

♻️ Proposed improvement for clearer error handling
                 Err(_) if val == "DANGER_PLAINTEXT" => Ok(EncryptionLevel::NotSupported),
-                Err(_) if val.eq_ignore_ascii_case("strict") && cfg!(feature = "tds80") => {
-                    Ok(EncryptionLevel::Strict)
-                }
+                Err(_) if val.eq_ignore_ascii_case("strict") => {
+                    #[cfg(feature = "tds80")]
+                    {
+                        Ok(EncryptionLevel::Strict)
+                    }
+                    #[cfg(not(feature = "tds80"))]
+                    {
+                        Err(crate::Error::Conversion(
+                            "encrypt=strict requires the 'tds80' feature to be enabled".into(),
+                        ))
+                    }
+                }
                 Err(e) => Err(e),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Err(_) if val.eq_ignore_ascii_case("strict") && cfg!(feature = "tds80") => {
Ok(EncryptionLevel::Strict)
}
Err(_) if val.eq_ignore_ascii_case("strict") => {
#[cfg(feature = "tds80")]
{
Ok(EncryptionLevel::Strict)
}
#[cfg(not(feature = "tds80"))]
{
Err(crate::Error::Conversion(
"encrypt=strict requires the 'tds80' feature to be enabled".into(),
))
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/config.rs` around lines 408 - 410, The parse path silently falls
through to parse_bool and yields a confusing "Not a valid boolean" when the user
supplies "strict" but the crate was compiled without the tds80 feature; update
the parsing logic (the FromStr/parse function that branches to
EncryptionLevel::Strict and calls parse_bool) to explicitly check for
val.eq_ignore_ascii_case("strict") when cfg!(not(feature = "tds80")) and return
a clear Err variant/message indicating that "strict" requires the tds80 feature
(or suggest enabling that feature), so users see a meaningful error instead of
the generic parse_bool failure.

Comment on lines +17 to +19
if matches!(config.encryption, crate::EncryptionLevel::Strict) {
builder = builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

async-native-tls 0.5 request_alpns method TlsConnector

💡 Result:

In async-native-tls 0.5.x, request_alpns is a builder-style method on async_native_tls::TlsConnector that lets you advertise ALPN protocols (e.g. h2, http/1.1) during the TLS handshake:

use async_native_tls::TlsConnector;

let tls = TlsConnector::new()
    .request_alpns(&["h2", "http/1.1"]);

request_alpns ultimately comes from native-tls and is only available when the native-tls crate is built with its alpn feature enabled. In native-tls it’s documented as “Available on crate feature alpn only.” [2] (Reqwest uses the same underlying request_alpns call on a TlsConnector::builder() for ALPN selection.) [3]

Sources: async-native-tls::TlsConnector docs (shows request_alpns(self, protocols: &[&str]) -> Self) [1], native-tls::TlsConnectorBuilder::request_alpns (feature-gated by alpn) [2], reqwest source using tls.request_alpns(...) [3].


🏁 Script executed:

#!/bin/bash
# Check Cargo.toml for async-native-tls dependency and alpn feature
find . -name "Cargo.toml" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && grep -A 5 "async-native-tls" {}'

Repository: prisma/tiberius

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Find the TDS_ALPN_PROTOCOL_NAME constant definition
rg "TDS_ALPN_PROTOCOL_NAME" -B 2 -A 2

Repository: prisma/tiberius

Length of output: 1053


🏁 Script executed:

#!/bin/bash
# Check if there are any feature flags or default settings that might enable alpn
rg "alpn" -i --type toml

Repository: prisma/tiberius

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check async-native-tls documentation or source for default features
# Search for any build script or conditional compilation related to alpn
rg "request_alpns" -B 5 -A 5 src/client/tls_stream/native_tls_stream.rs

Repository: prisma/tiberius

Length of output: 429


Add missing alpn feature to async-native-tls dependency.

The code at lines 17-19 calls builder.request_alpns(), which requires the alpn feature on the native-tls crate. However, the async-native-tls dependency in Cargo.toml only enables runtime-async-std:

[dependencies.async-native-tls]
version = "0.5"
features = ["runtime-async-std"]

Update to:

[dependencies.async-native-tls]
version = "0.5"
features = ["runtime-async-std", "alpn"]

Without this feature flag, the request_alpns method will not be available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/tls_stream/native_tls_stream.rs` around lines 17 - 19, The
async-native-tls dependency is missing the "alpn" feature required for
builder.request_alpns to be available; update the Cargo.toml dependency entry
for async-native-tls to include the "alpn" feature in its features list (e.g.,
add "alpn" alongside "runtime-async-std") so calls like
builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]) compile successfully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 195-196: The change to the Cargo.toml default features now
enabling tds80 (default = ["tds80", "winauth", "native-tls"]) is a breaking
change for downstream users; update the CHANGELOG/release notes to clearly
document the migration: explain that tds80 is now default, that tds80 includes
tds73 but introduces new behavior (e.g., EncryptionLevel::Strict, ALPN/TLS
handshake differences and TDS 8.0 protocol capabilities), provide the exact
opt-out example using default-features = false and features = ["tds73",
"winauth", "native-tls"], and add a short guidance section listing likely
behavioral differences and how to test/restore previous behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40a6f707-ca39-4b68-9c7d-35b23def4dd3

📥 Commits

Reviewing files that changed from the base of the PR and between e839e4e and 30f0569.

📒 Files selected for processing (1)
  • Cargo.toml

Comment thread Cargo.toml
Comment on lines +195 to +196
default = ["tds80", "winauth", "native-tls"]
tds80 = ["tds73"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document the breaking change in default features.

Changing the default feature set from tds73 to tds80 is a breaking change for downstream users. Users who relied on the previous defaults will now automatically receive TDS 8.0 behavior, including:

  • New EncryptionLevel::Strict support (requires TLS handshake with ALPN)
  • TDS 8.0 protocol capabilities
  • Different connection behavior when using defaults

Users who need the previous behavior must explicitly opt out of default features and select tds73 in their Cargo.toml:

[dependencies]
tiberius = { version = "0.12", default-features = false, features = ["tds73", "winauth", "native-tls"] }

This change should be prominently documented in the CHANGELOG/release notes with migration guidance.

Note: Since tds80 = ["tds73"], the tds80 feature is additive and preserves tds73 behavior while enabling additional functionality. However, the new code paths (Strict encryption, ALPN negotiation) may introduce behavioral differences that users should be aware of.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 195 - 196, The change to the Cargo.toml default
features now enabling tds80 (default = ["tds80", "winauth", "native-tls"]) is a
breaking change for downstream users; update the CHANGELOG/release notes to
clearly document the migration: explain that tds80 is now default, that tds80
includes tds73 but introduces new behavior (e.g., EncryptionLevel::Strict,
ALPN/TLS handshake differences and TDS 8.0 protocol capabilities), provide the
exact opt-out example using default-features = false and features = ["tds73",
"winauth", "native-tls"], and add a short guidance section listing likely
behavioral differences and how to test/restore previous behavior.

MattJackson referenced this pull request in MattJackson/tiberius-ng Aug 29, 2026
… (#412, #340, #414, #224)

Adapted from upstream #413 (author @olback) onto the #419 rustls-0.23 stack:

- EncryptionLevel::Strict (TDS 8.0): TLS handshake before prelogin, ALPN
  'tds/8.0' advertised on all three TLS backends. New tds80 feature (in
  default), with compile_error if enabled without a TLS backend.
- Config::hostname_in_certificate(): validate the server cert against a
  specified name instead of the host (#340).
- Config::client_name() + default login hostname (workstation id) from the
  local machine name (#414).
- Connection-string parsing for HostNameInCertificate / WorkstationID and
  encrypt=strict.
- Deps: async-native-tls 0.4->0.5 (request_alpns), libc (unix hostname).

142 lib tests pass; clippy --features=all -D warnings clean.
joelparkerhenderson added a commit to mssql-rust/mssql-rust that referenced this pull request Aug 29, 2026
Mirrors tiberius-rs/tiberius#413, minus the parts of that PR already
superseded by earlier work this session: `hostname_in_certificate`/
`client_name` Config fields and their direct setters already existed as
`host_name_in_certificate`/`client_name` (added mirroring
mssql-tiberius-bridge#50 earlier this session) - this commit only adds
the connection-string parsing for them, which was still missing.

Resolves the open question tasks.md flagged before this could land:
whether `EncryptionLevel::Strict` needs its own distinct PRELOGIN wire
byte from `Required`. Verified against Microsoft's own current TDS 8.0
documentation (learn.microsoft.com/en-us/sql/relational-databases/
security/networking/tds-8): in Strict mode, the TLS handshake happens
before *any* TDS bytes at all - "TCP handshake -> TLS handshake -> TDS
prelogin (encrypted) and response (encrypted) -> ...". The client has
already committed to full encryption by the time PRELOGIN's ENCRYPTION
field is even sent (inside the now-established TLS session), so the
byte value in that field is documented as ignored by the server in this
mode - reusing `Required`'s wire value (as the PR itself already did) is
correct, not a placeholder needing its own value.

Added:
- `EncryptionLevel::Strict` + `as_wire_value()` (maps Strict to
  Required's byte value 3, documented above).
- `PreloginMessage::negotiated_encryption`'s Strict arm (trivially
  Strict -> Strict; no negotiation happens post-hoc).
- `Connection::connect` performs the TLS handshake immediately - before
  building the connection or sending PRELOGIN - when
  `config.encryption == Strict`; returns a clear runtime error if no TLS
  feature is compiled in (rather than a `compile_error!`, since `Strict`
  is a plain enum value, not gated behind its own Cargo feature - unlike
  the PR, which introduced a whole new `tds80` feature and flipped the
  crate's *default* TLS feature bundle to include it, a much larger and
  riskier change than this fork's TDS-8.0 support needs).
- `Connection::tls_handshake` treats Strict as a no-op (transport is
  already wrapped by that point) instead of hitting its `unreachable!()`
  arm for an unrecognized already-`Tls` transport.
- rustls requests the `tds/8.0` ALPN protocol for Strict (optional per
  the ordering-based detection above, but documented practice).
  native-tls and vendored-openssl just log that they can't - bumping
  async-native-tls to a version with ALPN support (0.5+) also renamed
  its runtime-selection features in a way that deserves its own
  deliberate migration, not folding into this change.
- `encrypt=strict` (case-insensitive) connection-string parsing.
- `hostnameincertificate`/`hostname in certificate` and `workstationid`/
  `workstation id` connection-string parsing, wired to the
  already-existing `host_name_in_certificate`/`client_name` setters.

Added unit tests for: as_wire_value's full mapping, the PRELOGIN
encode/decode asymmetry for Strict (confirmed NOT a round-trip - decoding
a Strict-encoded PRELOGIN yields Required, as expected), negotiated_
encryption ignoring the server's PRELOGIN response for Strict, and
connection-string parsing for all three new keywords (ADO.NET and JDBC).

Live-tested what this environment can support: connecting with Strict
against this fork's Azure SQL Edge test server (which doesn't speak TDS
8.0) fails cleanly with a TLS-level error rather than hanging - and
manually confirmed the no-TLS-feature build's runtime error message.
Positively verifying a successful Strict connection needs SQL Server
2022+ or Azure SQL Database; the only such image runnable here
(mcr.microsoft.com/mssql/server:2022-latest, linux/amd64 only) was
actually attempted via Docker Desktop's QEMU emulation on this arm64
host, and crashes on startup ("Invalid mapping of address ... in
reserved address space", a QEMU/SQL-Server-CLR interaction) - confirmed
by trying it, not assumed, then cleaned up. This is an environment
limitation, not something in this change; someone testing against a
real SQL Server 2022+/Azure SQL instance should verify the positive path
before relying on it in production.

Verified:
- cargo check: default, --features=rustls, --features=rustls-webpki-roots,
  --features=vendored-openssl, --no-default-features (with and without
  tds73), --features=all
- cargo clippy --all-targets -- -D warnings, for the same combinations
- cargo fmt --check
- cargo test --lib for default (198 passed) and all (206 passed)
- Live integration tests against the running Azure SQL Edge container:
  311 passed over rustls (102 bulk + 203 query + 6 custom-cert) and 311
  over vendored-openssl (confirmed stable serially; --test-threads=8
  shows occasional unrelated flakes in custom-cert.rs specifically,
  reproduced on pre-existing tests too and gone at --test-threads=1,
  so a test-parallelism/container-resource artifact, not a regression),
  plus 28 doctests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0156Di1tRRLsJK8ctU1AmAJr
joelparkerhenderson added a commit to mssql-rust/mssql-rust that referenced this pull request Aug 30, 2026
Mirrors tiberius-rs/tiberius#413, minus the parts of that PR already
superseded by earlier work this session: `hostname_in_certificate`/
`client_name` Config fields and their direct setters already existed as
`host_name_in_certificate`/`client_name` (added mirroring
mssql-tiberius-bridge#50 earlier this session) - this commit only adds
the connection-string parsing for them, which was still missing.

Resolves the open question tasks.md flagged before this could land:
whether `EncryptionLevel::Strict` needs its own distinct PRELOGIN wire
byte from `Required`. Verified against Microsoft's own current TDS 8.0
documentation (learn.microsoft.com/en-us/sql/relational-databases/
security/networking/tds-8): in Strict mode, the TLS handshake happens
before *any* TDS bytes at all - "TCP handshake -> TLS handshake -> TDS
prelogin (encrypted) and response (encrypted) -> ...". The client has
already committed to full encryption by the time PRELOGIN's ENCRYPTION
field is even sent (inside the now-established TLS session), so the
byte value in that field is documented as ignored by the server in this
mode - reusing `Required`'s wire value (as the PR itself already did) is
correct, not a placeholder needing its own value.

Added:
- `EncryptionLevel::Strict` + `as_wire_value()` (maps Strict to
  Required's byte value 3, documented above).
- `PreloginMessage::negotiated_encryption`'s Strict arm (trivially
  Strict -> Strict; no negotiation happens post-hoc).
- `Connection::connect` performs the TLS handshake immediately - before
  building the connection or sending PRELOGIN - when
  `config.encryption == Strict`; returns a clear runtime error if no TLS
  feature is compiled in (rather than a `compile_error!`, since `Strict`
  is a plain enum value, not gated behind its own Cargo feature - unlike
  the PR, which introduced a whole new `tds80` feature and flipped the
  crate's *default* TLS feature bundle to include it, a much larger and
  riskier change than this fork's TDS-8.0 support needs).
- `Connection::tls_handshake` treats Strict as a no-op (transport is
  already wrapped by that point) instead of hitting its `unreachable!()`
  arm for an unrecognized already-`Tls` transport.
- rustls requests the `tds/8.0` ALPN protocol for Strict (optional per
  the ordering-based detection above, but documented practice).
  native-tls and vendored-openssl just log that they can't - bumping
  async-native-tls to a version with ALPN support (0.5+) also renamed
  its runtime-selection features in a way that deserves its own
  deliberate migration, not folding into this change.
- `encrypt=strict` (case-insensitive) connection-string parsing.
- `hostnameincertificate`/`hostname in certificate` and `workstationid`/
  `workstation id` connection-string parsing, wired to the
  already-existing `host_name_in_certificate`/`client_name` setters.

Added unit tests for: as_wire_value's full mapping, the PRELOGIN
encode/decode asymmetry for Strict (confirmed NOT a round-trip - decoding
a Strict-encoded PRELOGIN yields Required, as expected), negotiated_
encryption ignoring the server's PRELOGIN response for Strict, and
connection-string parsing for all three new keywords (ADO.NET and JDBC).

Live-tested what this environment can support: connecting with Strict
against this fork's Azure SQL Edge test server (which doesn't speak TDS
8.0) fails cleanly with a TLS-level error rather than hanging - and
manually confirmed the no-TLS-feature build's runtime error message.
Positively verifying a successful Strict connection needs SQL Server
2022+ or Azure SQL Database; the only such image runnable here
(mcr.microsoft.com/mssql/server:2022-latest, linux/amd64 only) was
actually attempted via Docker Desktop's QEMU emulation on this arm64
host, and crashes on startup ("Invalid mapping of address ... in
reserved address space", a QEMU/SQL-Server-CLR interaction) - confirmed
by trying it, not assumed, then cleaned up. This is an environment
limitation, not something in this change; someone testing against a
real SQL Server 2022+/Azure SQL instance should verify the positive path
before relying on it in production.

Verified:
- cargo check: default, --features=rustls, --features=rustls-webpki-roots,
  --features=vendored-openssl, --no-default-features (with and without
  tds73), --features=all
- cargo clippy --all-targets -- -D warnings, for the same combinations
- cargo fmt --check
- cargo test --lib for default (198 passed) and all (206 passed)
- Live integration tests against the running Azure SQL Edge container:
  311 passed over rustls (102 bulk + 203 query + 6 custom-cert) and 311
  over vendored-openssl (confirmed stable serially; --test-threads=8
  shows occasional unrelated flakes in custom-cert.rs specifically,
  reproduced on pre-existing tests too and gone at --test-threads=1,
  so a test-parallelism/container-resource artifact, not a regression),
  plus 28 doctests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0156Di1tRRLsJK8ctU1AmAJr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant