Skip to content

Commit

Permalink
CCT-176: Handle incorrectly closed TLS connections
Browse files Browse the repository at this point in the history
* Card ID: CCT-176
* Card ID: RHEL-17345

Some servers, like Quarkus, do not send the `close_notify` alert before
closing their connection by default. This would cause
subscription-manager to freeze until the TLS connection timeout was
reached.

This patch ensures we set our own timeout equal to 3x the response time
of the connection, to kill the connection if we don't get a response
back.
  • Loading branch information
m-horky committed Jan 11, 2024
1 parent 51f75db commit eab5b64
Showing 1 changed file with 51 additions and 15 deletions.
66 changes: 51 additions & 15 deletions src/rhsm/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
#

import base64
import math

from rhsm import certificate
import datetime
import dateutil.parser
import locale
import logging
import os
import signal
import socket
import sys
import time
Expand Down Expand Up @@ -666,23 +669,53 @@ def __init__(
self.headers["Authorization"] = "Bearer " + token

def close_connection(self) -> None:
"""Try to close connection to the server.
Because the server's TLS stack may misbehave (there are behavioral
differences between 1.2 and 1.3, for example), we handle timeouts of
closing TLS connection manually. If the server does not respond within
'3 * average return time' seconds, we force the connection down.
After TLS connection has been closed, we close the TCP connection.
"""
Try to close connection to server
:return: None
"""
if self.__conn is not None:
# Do proper TLS shutdown handshake (TLS tear down) first
if self.__conn.sock is not None:
log.debug(f"Closing HTTPS connection {self.__conn.sock}")
try:
self.__conn.sock.unwrap()
except ssl.SSLError as err:
log.debug(f"Unable to close TLS connection properly: {err}")
else:
log.debug("TLS connection closed")
# Then it is possible to close TCP connection
if self.__conn is None:
return

if self.__conn.sock is None:
log.debug(f"Closing TCP connection to {self.__conn.host}")
self.__conn.close()
self.__conn = None
log.debug("TCP connection has been closed")
return

log.debug(f"Closing TLS connection {self.__conn.sock}")

# The server may not send the `close_notify` alert when it closes the
# connection on its side.
# Here we set a timeout equal to three times the measured response time
# to prevent the connection from waiting until TCP timeout.
# See RHEL-17345.
response_time: float = self.smoothed_rt or 0.5
timeout_time: int = math.ceil(response_time * 3)

def on_timeout(signum, frame) -> None:
raise TimeoutError(f"Did not get response in {timeout_time}s")

signal.signal(signalnum=signal.SIGALRM, handler=on_timeout)
signal.alarm(timeout_time)

try:
self.__conn.sock.unwrap()
except (ssl.SSLError, TimeoutError) as err:
log.debug(f"TLS connection could not be closed: {err}")
else:
signal.alarm(0)
log.debug("TLS connection has been closed")

log.debug(f"Closing TCP connection to {self.__conn.host}")
self.__conn.close()
self.__conn = None
log.debug("TCP connection has been closed")

def _get_cert_key_list(self) -> List[Tuple[str, str]]:
"""
Expand Down Expand Up @@ -1227,7 +1260,10 @@ def _update_smoothed_response_time(self, response_time: float):
self.smoothed_rt = response_time
else:
self.smoothed_rt = (self.ALPHA * self.smoothed_rt) + ((1 - self.ALPHA) * response_time)
log.debug("Response time: %s, Smoothed response time: %s" % (response_time, self.smoothed_rt))
log.debug(
f"Latest response time was {response_time:.5f}s, "
f"smoothed response time is {self.smoothed_rt:.5f}s"
)

def validateResult(self, result: dict, request_type: str = None, handler: str = None) -> None:
"""
Expand Down

0 comments on commit eab5b64

Please sign in to comment.