Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature | Adding TDS 8 support #1608

Merged
merged 46 commits into from
Jun 15, 2022
Merged

Conversation

JRahnama
Copy link
Contributor

@JRahnama JRahnama commented May 9, 2022

TDS8 support is in preview mode for SQL server 2022. This PR is to add client-side support.

Preview MS-TDS spec changes:
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-winprotlp/8a9c667b-2825-46a8-8066-a80681233c33

Behavior:
To use TDS 8, users should specify Encrypt=Strict in the connection string. Strict mode disables TrustServerCertificate (always treated as False in Strict mode). HostNameInCertificate has been added to help some Strict mode scenarios. In the future, the plan is also to add a ServerCertificate setting, which points to a certificate file that will be directly compared to the server certificate for validation. This will allow secure use of any certificate (self-signed, for example) on a connection without opening up the connection to MITM attacks by blanket trusting of all certificates (as TrustServerCertificate=true would do).

The currently proposed code also retains API backwards compatibility in SqlConnectionStringBuilder.Encrypt. Encrypt has changed from a bool to a SqlConnectionEncryptOption object, but implicit conversion operators have been added so that it can still be treated like a bool. Comparing SqlConnectionEncryptOption to a bool will return true for SqlConnectionEncryptOption.Mandatory or SqlConnectionEncryptOption.Strict. Converting a bool to SqlConnectionEncryptOption will produce SqlConnectionEncryptOption.Mandatory for true and SqlConnectionEncryptOption.Optional for false.

* Adding TDS8 support to netcore

* Doc changes for TDS 8

Co-authored-by: JRahnama <v-jarahn@microsoft.com>
_sslOverTdsStream = new SslOverTdsStream(_tcpStream, _connectionId);
stream = _sslOverTdsStream;
}
_sslStream = new SNISslStream(stream, true, new RemoteCertificateValidationCallback(ValidateServerCertificate));
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ValidateServerCertificate an instance method that we need to create a delegate for each time we create the connection? If it's static or takes a context parameter we could avoid the allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Wraith2 thanks for looking into this, ValidateServerCertificate is not static and is following the pattern mentioned for RemoteCertificateValidationCallback Delegate. What do you suggest to avoid the allocation?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you could check the sender argument in the callback and find if it's the SNISslStream that'll tell you whether it could be used as the context object here. If it is then you don't need the instance and you can use a static callback.
This isn't really that important and it'll only happen once per connection so don't worry about it too much, I'd say it's more important to get everything working first.

SslClientAuthenticationOptions sslClientOptions = new()
{
TargetHost = _serverIndicationName,
ApplicationProtocols = new List<SslApplicationProtocol>() { new(TdsEnums.TDS8) },
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be a per instance collection? Is if going to be modified by the caller? if it isn't going to be modified we could have a single static instance that we reuse. If it has to be instantiated consider using the ctor that specifies the initial size because the default initial size is 5 and if we know this will almost never grow we waste some memory each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can you check the new changes I made?

if (_isTDS8)
{
#if NETCOREAPP
SslApplicationProtocol TDS8 = new("tds/8.0");
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: naming would be tds8 since it's a local not a public member. I think I'd probably just call it protocol since tds is the value not the description of the variable that holds it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would define a static list of client-supported protocols somewhere above (private static clientApplicationProtocols s_Protocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("tds/8.0") }) that you can reference on line 333 and remove this local variable completely.

EDIT: I now see that Wraith already suggested the same thing in SNITcpHandle.cs. We can use the same static reference here and there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can you check the new changes I made?

Copy link
Contributor

Choose a reason for hiding this comment

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

in SNIPhysicalHandle create a new field private static s_clientApplicationProtocols s_Protocols = new List<SslApplicationProtocol>(1) { new SslApplicationProtocol("tds/8.0") }; as David suggested and then in SNINpHandle and SNITcpHandle use it like this:

SslClientAuthenticationOptions sslClientOptions = new()
{
    TargetHost = _serverNameIndication,
    ApplicationProtocols = SNIPhysicalHandle.s_clientApplicationProtocols ,
    EnabledSslProtocols = SupportedProtocols,
    ClientCertificates = null,
};

Copy link
Contributor

@Wraith2 Wraith2 left a comment

Choose a reason for hiding this comment

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

Looks ok in general.
Have you considered using an int protocol version instead of a bool? It seems sensible to think there may be later versions and make switching on the version cleaner.

Copy link
Contributor

@David-Engel David-Engel left a comment

Choose a reason for hiding this comment

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

Not done reviewing, but wanted to submit some partial feedback.

As we discussed offline, we should also try to maintain a backwards compatible connection string, where possible. Specifically, with the new encrypt enum and Optional/Mandatory values, when we output a connection string, we should continue to output Encrypt=true or Encrypt=false for Mondatory/Optional respectively. This will preserve some backwards compatibility between versions in scenarios where an application is passing connection strings around. This is more important since Encrypt is such a common/well-used option.

if (_isTDS8)
{
#if NETCOREAPP
SslApplicationProtocol TDS8 = new("tds/8.0");
Copy link
Contributor

Choose a reason for hiding this comment

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

I would define a static list of client-supported protocols somewhere above (private static clientApplicationProtocols s_Protocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("tds/8.0") }) that you can reference on line 333 and remove this local variable completely.

EDIT: I now see that Wraith already suggested the same thing in SNITcpHandle.cs. We can use the same static reference here and there.

Co-authored-by: David Engel <dengel1012@gmail.com>
EnabledSslProtocols = SupportedProtocols,
ClientCertificates = null,
};
_sslStream.AuthenticateAsClientAsync(sslClientOptions).Wait();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is using an async function and then awaiting it synchronously, if @roji sees you doing this you'll make him sad. See if there is a naturally synchronous AuthenticateAsClient method you can use instead and avoid sync-over-async.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is no AuthenticateAsClient that accepts SslClientAuthenticationOption as an argument in netcore 3.1. It started from net5 and after.

Copy link
Contributor

Choose a reason for hiding this comment

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

Netcore 3.1 goes out of support at the end of this year (it's LTS) and net5 is already unsupported. https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core

You can use compiler ifdefs to use the current pattern only on 3.1 and lower and then use the sync overload from net5 onwards. In this case I think it's acceptable to do that because it will avoid more threading problems.

Copy link
Contributor

@David-Engel David-Engel left a comment

Choose a reason for hiding this comment

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

Another partial review. In general, where we were previously passing bool encrypt internally and we are now passing SqlConnectionEncryptionOption encrypt plus bool isTDS8, we could simplify the code by just passing bool encrypt plus bool isTDS8 (or more accurately bool tlsFirst). This would make for fewer code changes related to the change of the encrypt parameter type.

Alternatively, just pass SqlConnectionEncryptionOption encrypt and base all the if (isTDS8) logic off of if (encrypt == SqlConnectionEncryptionOption.Strict).

Since I haven't been all the way through the code, feel free to point to to why either of the above wouldn't work.

David-Engel and others added 4 commits June 2, 2022 12:03
Update error message
Co-authored-by: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com>
@DavoudEshtehari DavoudEshtehari changed the title Feature | Adding support to TDS8 in NetCore Feature | Adding TDS 8 support Jun 2, 2022
Copy link
Contributor

@DavoudEshtehari DavoudEshtehari left a comment

Choose a reason for hiding this comment

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

Generally, I believe there are some opportunities to add more event source logs, especially in the new paths that would be better if we could include them with this change.

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.

5 participants