From 01b227739f05d91ff4e5a15803eac5f96f6386ed Mon Sep 17 00:00:00 2001 From: mhorky Date: Fri, 24 Nov 2023 15:04:23 +0100 Subject: [PATCH] CCT-176: Do not wait until the server closes the TLS connection * Card ID: CCT-176 * Card ID: RHEL-17345 Starting with TLS 1.1, it is not required of the server to send the `close_notify` alert before closing their the connection. TLS 1.3 uses half-close policy which extends this behavior. Candlepin is migrating to Quarkus which does not send these messages by default. This makes subscription-manager hang during the `.sock.unwrap()` because it waits for the message until it timeouts. --- src/rhsm/connection.py | 50 +++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/rhsm/connection.py b/src/rhsm/connection.py index 82cf04ea2a..1f51879049 100644 --- a/src/rhsm/connection.py +++ b/src/rhsm/connection.py @@ -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 @@ -670,19 +673,37 @@ def close_connection(self) -> None: 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 + return + + # 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. + log.debug(f"Closing TLS connection {self.__conn.sock}") + + 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}") + + log.debug(f"Closing TCP connection to {self.__conn.host}") + self.__conn.close() def _get_cert_key_list(self) -> List[Tuple[str, str]]: """ @@ -1227,7 +1248,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: """