@@ -596,17 +596,27 @@ static async Task OpenCore(
596596 bool async ,
597597 CancellationToken cancellationToken )
598598 {
599- await conn . RawOpen ( sslMode , gssEncMode , timeout , async , cancellationToken ) . ConfigureAwait ( false) ;
600- timeout . CheckAndApply ( conn ) ;
601- conn. WriteStartupMessage( username) ;
602- await conn. Flush( async, cancellationToken ) . ConfigureAwait ( false) ;
599+ // If we fail to connect to the socket, there is no reason to retry even if SslMode/GssEncryption allows it
600+ await conn . RawOpen ( timeout , async , cancellationToken ) . ConfigureAwait ( false) ;
603601
604- using var cancellationRegistration = conn . StartCancellableOperation ( cancellationToken , attemptPgCancellation : false) ;
605602 try
606603 {
604+ await conn . SetupEncryption ( sslMode , gssEncMode , timeout , async , cancellationToken ) . ConfigureAwait ( false) ;
605+ timeout . CheckAndApply ( conn ) ;
606+ conn. WriteStartupMessage( username) ;
607+ await conn. Flush( async, cancellationToken ) . ConfigureAwait ( false) ;
608+
609+ using var cancellationRegistration = conn . StartCancellableOperation ( cancellationToken , attemptPgCancellation : false) ;
607610 await conn . Authenticate ( username , timeout , async , cancellationToken ) . ConfigureAwait ( false) ;
608611 }
612+ // We handle any exception here because on Windows while receiving a response from Postgres
613+ // We might hit connection reset, in which case the actual error will be lost
614+ // And we only read some IO error
615+ // In addition, this behavior mimics libpq, where it retries as long as GssEncryptionMode and SslMode allows it
609616 catch ( Exception e) when
617+ // We might also get here OperationCancelledException/TimeoutException
618+ // But it's fine to fall down and retry because we'll immediately exit with the exact same exception
619+ //
610620 // Any error after trying with GSS encryption
611621 ( gssEncMode == GssEncryptionMode. Prefer ||
612622 // Auth error with/without SSL
@@ -620,9 +630,6 @@ static async Task OpenCore(
620630 else
621631 sslMode = sslMode == SslMode . Prefer ? SslMode . Disable : SslMode . Require ;
622632
623- cancellationRegistration. Dispose ( ) ;
624- Debug. Assert ( ! conn . IsBroken ) ;
625-
626633 conn. Cleanup ( ) ;
627634
628635 // If Prefer was specified and we failed (with SSL), retry without SSL.
@@ -696,6 +703,8 @@ internal async ValueTask<GssEncryptionResult> GSSEncrypt(bool async, bool isRequ
696703 default :
697704 throw new NpgsqlException( $ "Received unknown response { response } for GSSEncRequest (expecting G or N)") ;
698705 case 'N':
706+ if ( isRequired )
707+ throw new NpgsqlException( "GGS encryption requested. No GSS encryption enabled connection from this host is configured." ) ;
699708 return GssEncryptionResult. NegotiateFailure ;
700709 case 'G':
701710 break ;
@@ -907,7 +916,7 @@ async ValueTask<string> GetUsernameAsyncInternal()
907916 }
908917 }
909918
910- async Task RawOpen( SslMode sslMode , GssEncryptionMode gssEncryptionMode , NpgsqlTimeout timeout , bool async , CancellationToken cancellationToken )
919+ async Task RawOpen( NpgsqlTimeout timeout , bool async , CancellationToken cancellationToken )
911920 {
912921 try
913922 {
@@ -939,59 +948,6 @@ async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlT
939948
940949 IsSslEncrypted = false ;
941950 IsGssEncrypted = false ;
942-
943- var gssEncryptResult = await TryNegotiateGssEncryption ( gssEncryptionMode , async , cancellationToken ) . ConfigureAwait( false) ;
944- if ( gssEncryptResult == GssEncryptionResult. Success)
945- return ;
946-
947- timeout. CheckAndApply( this ) ;
948-
949- if ( GetSslNegotiation( Settings) == SslNegotiation. Direct)
950- {
951- // We already check that in NpgsqlConnectionStringBuilder.PostProcessAndValidate, but since we also allow environment variables...
952- if ( Settings . SslMode is not SslMode . Require and not SslMode . VerifyCA and not SslMode . VerifyFull )
953- throw new ArgumentException( "SSL Mode has to be Require or higher to be used with direct SSL Negotiation" ) ;
954- if ( gssEncryptResult == GssEncryptionResult . NegotiateFailure )
955- {
956- // We can be here only if it's fallback from preferred (but failed) gss encryption
957- // In this case, direct encryption isn't going to work anymore, so we throw a bogus exception to retry again without gss
958- // Alternatively, we can instead just go with the usual route of writing SslRequest, ignoring direct ssl
959- // But this is how libpq works
960- Debug . Assert ( gssEncryptionMode == GssEncryptionMode . Prefer ) ;
961- // The exception message doesn't matter since we're going to retry again
962- throw new NpgsqlException ( ) ;
963- }
964-
965- await DataSource. TransportSecurityHandler . NegotiateEncryption ( async, this , sslMode , timeout , cancellationToken ) . ConfigureAwait ( false ) ;
966- if ( ReadBuffer . ReadBytesLeft > 0 )
967- throw new NpgsqlException( "Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack." ) ;
968- }
969- else if ( ( sslMode is SslMode . Prefer && DataSource . TransportSecurityHandler . SupportEncryption ) ||
970- sslMode is SslMode . Require or SslMode . VerifyCA or SslMode . VerifyFull )
971- {
972- WriteSslRequest( ) ;
973- await Flush( async, cancellationToken ) . ConfigureAwait ( false ) ;
974-
975- await ReadBuffer. Ensure ( 1 , async ) . ConfigureAwait ( false ) ;
976- var response = ( char ) ReadBuffer . ReadByte ( ) ;
977- timeout. CheckAndApply ( this ) ;
978-
979- switch ( response )
980- {
981- default :
982- throw new NpgsqlException( $ "Received unknown response { response } for SSLRequest (expecting S or N)") ;
983- case 'N':
984- if ( sslMode != SslMode . Prefer )
985- throw new NpgsqlException( "SSL connection requested. No SSL enabled connection from this host is configured." ) ;
986- break ;
987- case 'S':
988- await DataSource. TransportSecurityHandler . NegotiateEncryption ( async, this , sslMode , timeout , cancellationToken ) . ConfigureAwait ( false ) ;
989- break ;
990- }
991-
992- if ( ReadBuffer . ReadBytesLeft > 0 )
993- throw new NpgsqlException( "Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack." ) ;
994- }
995951 }
996952 catch
997953 {
@@ -1008,12 +964,80 @@ async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlT
1008964 }
1009965 }
1010966
967+ async Task SetupEncryption ( SslMode sslMode , GssEncryptionMode gssEncryptionMode , NpgsqlTimeout timeout , bool async , CancellationToken cancellationToken )
968+ {
969+ var gssEncryptResult = await TryNegotiateGssEncryption ( gssEncryptionMode , async , cancellationToken ) . ConfigureAwait( false) ;
970+ if ( gssEncryptResult == GssEncryptionResult. Success)
971+ return ;
972+
973+ // TryNegotiateGssEncryption should already throw a much more meaningful exception
974+ // if GSS encryption is required but for some reason we can't negotiate it.
975+ // But since we have to return a specific result instead of generic true/false
976+ // To make absolutely sure we didn't miss anything, recheck again
977+ if ( gssEncryptionMode == GssEncryptionMode. Require)
978+ throw new NpgsqlException ( $ "Unable to negotiate GSS encryption: { gssEncryptResult } ") ;
979+
980+ timeout. CheckAndApply ( this ) ;
981+
982+ if ( GetSslNegotiation ( Settings ) == SslNegotiation . Direct )
983+ {
984+ // We already check that in NpgsqlConnectionStringBuilder.PostProcessAndValidate, but since we also allow environment variables...
985+ if ( Settings . SslMode is not SslMode . Require and not SslMode . VerifyCA and not SslMode . VerifyFull )
986+ throw new ArgumentException( "SSL Mode has to be Require or higher to be used with direct SSL Negotiation" ) ;
987+ if ( gssEncryptResult == GssEncryptionResult . NegotiateFailure )
988+ {
989+ // We can be here only if it's fallback from preferred (but failed) gss encryption
990+ // In this case, direct encryption isn't going to work anymore, so we throw a bogus exception to retry again without gss
991+ // Alternatively, we can instead just go with the usual route of writing SslRequest, ignoring direct ssl
992+ // But this is how libpq works
993+ Debug . Assert ( gssEncryptionMode == GssEncryptionMode . Prefer ) ;
994+ // The exception message doesn't matter since we're going to retry again
995+ throw new NpgsqlException ( ) ;
996+ }
997+
998+ await DataSource. TransportSecurityHandler . NegotiateEncryption ( async, this , sslMode , timeout , cancellationToken ) . ConfigureAwait ( false ) ;
999+ if ( ReadBuffer . ReadBytesLeft > 0 )
1000+ throw new NpgsqlException( "Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack." ) ;
1001+ }
1002+ else if ( ( sslMode is SslMode . Prefer && DataSource . TransportSecurityHandler . SupportEncryption ) ||
1003+ sslMode is SslMode . Require or SslMode . VerifyCA or SslMode . VerifyFull )
1004+ {
1005+ WriteSslRequest( ) ;
1006+ await Flush( async, cancellationToken ) . ConfigureAwait ( false ) ;
1007+
1008+ await ReadBuffer. Ensure ( 1 , async ) . ConfigureAwait ( false ) ;
1009+ var response = ( char ) ReadBuffer . ReadByte ( ) ;
1010+ timeout. CheckAndApply ( this ) ;
1011+
1012+ switch ( response )
1013+ {
1014+ default :
1015+ throw new NpgsqlException( $ "Received unknown response { response } for SSLRequest (expecting S or N)") ;
1016+ case 'N':
1017+ if ( sslMode != SslMode . Prefer )
1018+ throw new NpgsqlException( "SSL connection requested. No SSL enabled connection from this host is configured." ) ;
1019+ break ;
1020+ case 'S':
1021+ await DataSource. TransportSecurityHandler . NegotiateEncryption ( async, this , sslMode , timeout , cancellationToken ) . ConfigureAwait ( false ) ;
1022+ break ;
1023+ }
1024+
1025+ if ( ReadBuffer . ReadBytesLeft > 0 )
1026+ throw new NpgsqlException( "Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack." ) ;
1027+ }
1028+ }
1029+
10111030 async ValueTask< GssEncryptionResult > TryNegotiateGssEncryption ( GssEncryptionMode gssEncryptionMode , bool async , CancellationToken cancellationToken )
10121031 {
10131032 // GetCredentialFailure is essentially a nop (since we didn't send anything over the wire)
10141033 // So we can proceed further as if gss encryption wasn't even attempted
10151034 if ( gssEncryptionMode == GssEncryptionMode . Disable ) return GssEncryptionResult. GetCredentialFailure ;
10161035
1036+ // Same thing as above, though in this case user doesn't require GSS encryption but didn't enable encryption
1037+ // Most of the time they're using the default value, in which case also exit without throwing an error
1038+ if ( gssEncryptionMode == GssEncryptionMode . Prefer && ! DataSource . TransportSecurityHandler . SupportEncryption )
1039+ return GssEncryptionResult. GetCredentialFailure ;
1040+
10171041 if ( ConnectedEndPoint ! . AddressFamily == AddressFamily . Unix )
10181042 {
10191043 if ( gssEncryptionMode == GssEncryptionMode . Prefer )
@@ -1038,7 +1062,9 @@ static SslNegotiation GetSslNegotiation(NpgsqlConnectionStringBuilder settings)
10381062 return sslNegotiation;
10391063 }
10401064
1041- return SslNegotiation . Postgres ;
1065+ // If user hasn't provided the value via connection string or environment variable
1066+ // Retrieve the default value from property
1067+ return settings. SslNegotiation ;
10421068 }
10431069
10441070 static GssEncryptionMode GetGssEncMode( NpgsqlConnectionStringBuilder settings )
@@ -1052,7 +1078,9 @@ static GssEncryptionMode GetGssEncMode(NpgsqlConnectionStringBuilder settings)
10521078 return gssEncMode;
10531079 }
10541080
1055- return GssEncryptionMode . Disable ;
1081+ // If user hasn't provided the value via connection string or environment variable
1082+ // Retrieve the default value from property
1083+ return settings. GssEncryptionMode ;
10561084 }
10571085
10581086 internal async Task NegotiateEncryption( SslMode sslMode , NpgsqlTimeout timeout , bool async , CancellationToken cancellationToken )
@@ -2187,9 +2215,13 @@ void DoCancelRequest(int backendProcessId, int backendSecretKey)
21872215 {
21882216 try
21892217 {
2190- RawOpen ( Settings . SslMode , gssEncMode , new NpgsqlTimeout ( TimeSpan . FromSeconds ( ConnectionTimeout ) ) , false ,
2218+ var timeout = new NpgsqlTimeout( TimeSpan . FromSeconds ( ConnectionTimeout ) ) ;
2219+ RawOpen( timeout , false ,
21912220 CancellationToken . None )
21922221 . GetAwaiter ( ) . GetResult ( ) ;
2222+ SetupEncryption( Settings . SslMode , gssEncMode , timeout , false ,
2223+ CancellationToken . None ) .
2224+ GetAwaiter ( ) . GetResult ( ) ;
21932225 }
21942226 catch ( Exception e ) when ( gssEncMode == GssEncryptionMode . Prefer )
21952227 {
@@ -2198,9 +2230,13 @@ void DoCancelRequest(int backendProcessId, int backendSecretKey)
21982230
21992231 // If we hit an error with gss encryption
22002232 // Retry again without it
2201- RawOpen ( Settings . SslMode , GssEncryptionMode . Disable , new NpgsqlTimeout ( TimeSpan . FromSeconds ( ConnectionTimeout ) ) , false ,
2233+ var timeout = new NpgsqlTimeout( TimeSpan . FromSeconds ( ConnectionTimeout ) ) ;
2234+ RawOpen( timeout , false ,
22022235 CancellationToken . None )
22032236 . GetAwaiter ( ) . GetResult ( ) ;
2237+ SetupEncryption( Settings . SslMode , GssEncryptionMode . Disable , timeout , false ,
2238+ CancellationToken . None ) .
2239+ GetAwaiter ( ) . GetResult ( ) ;
22042240 }
22052241
22062242 WriteCancelRequest( backendProcessId , backendSecretKey ) ;
0 commit comments