Skip to content

Commit

Permalink
Add IPv6 support
Browse files Browse the repository at this point in the history
This change is mostly focused around the addition of IPv6 connection
functionality to Broker's Host Sessions.
Included are options for an ipv6 connection preference with an opt-out
ipv6 connection fallback.
These have also been added to the settings file.
  • Loading branch information
JacobCallahan committed Feb 2, 2024
1 parent f84c820 commit 4295397
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 12 deletions.
35 changes: 27 additions & 8 deletions broker/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ def __init__(self, **kwargs):
"""Create a Host instance.
Expected kwargs:
hostname: str - Hostname or IP address of the host, required
name: str - Name of the host
username: str - Username to use for SSH connection
password: str - Password to use for SSH connection
connection_timeout: int - Timeout for SSH connection
port: int - Port to use for SSH connection
key_filename: str - Path to SSH key file to use for SSH connection
hostname: (str) - Hostname or IP address of the host, required
name: (str) - Name of the host
username: (str) - Username to use for SSH connection
password: (str) - Password to use for SSH connection
connection_timeout: (int) - Timeout for SSH connection
port: (int) - Port to use for SSH connection
key_filename: (str) - Path to SSH key file to use for SSH connection
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
"""
logger.debug(f"Constructing host using {kwargs=}")
self.hostname = kwargs.get("hostname") or kwargs.get("ip")
Expand All @@ -59,6 +61,8 @@ def __init__(self, **kwargs):
self.timeout = kwargs.pop("connection_timeout", settings.HOST_CONNECTION_TIMEOUT)
self.port = kwargs.pop("port", settings.HOST_SSH_PORT)
self.key_filename = kwargs.pop("key_filename", settings.HOST_SSH_KEY_FILENAME)
self.ipv6 = kwargs.pop("ipv6", settings.HOST_IPV6)
self.ipv4_fallback = kwargs.pop("ipv4_fallback", settings.HOST_IPV4_FALLBACK)
self.__dict__.update(kwargs) # Make every other kwarg an attribute
self._session = None

Expand All @@ -84,7 +88,16 @@ def session(self):
self.connect()
return self._session

def connect(self, username=None, password=None, timeout=None, port=22, key_filename=None):
def connect(
self,
username=None,
password=None,
timeout=None,
port=22,
key_filename=None,
ipv6=False,
ipv4_fallback=True,
):
"""Connect to the host using SSH.
Args:
Expand All @@ -93,6 +106,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
timeout (int): The timeout for the SSH connection in seconds.
port (int): The port to use for the SSH connection. Defaults to 22.
key_filename (str): The path to the private key file to use for the SSH connection.
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
"""
username = username or self.username
password = password or self.password
Expand All @@ -103,6 +118,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
if ":" in self.hostname:
_hostname, port = self.hostname.split(":")
_port = int(port)
ipv6 = ipv6 or self.ipv6
ipv4_fallback = ipv4_fallback or self.ipv4_fallback
self.close()
self._session = Session(
hostname=_hostname,
Expand All @@ -111,6 +128,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
port=_port,
key_filename=key_filename,
timeout=timeout,
ipv6=ipv6,
ipv4_fallback=ipv4_fallback,
)

def close(self):
Expand Down
64 changes: 60 additions & 4 deletions broker/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,54 @@
FILE_FLAGS = ssh2_sftp.LIBSSH2_FXF_CREAT | ssh2_sftp.LIBSSH2_FXF_WRITE


def _create_connect_socket(host, port, timeout, ipv6=False, ipv4_fallback=True, sock=None):
"""Create a socket and establish a connection to the specified host and port.
Args:
host (str): The hostname or IP address of the remote server.
port (int): The port number to connect to.
timeout (float): The timeout value in seconds for the socket connection.
ipv6 (bool, optional): Whether to use IPv6. Defaults to False.
ipv4_fallback (bool, optional): Whether to fallback to IPv4 if IPv6 fails. Defaults to True.
sock (socket.socket, optional): An existing socket object to use. Defaults to None.
Returns:
socket.socket: The connected socket object.
bool: True if IPv6 was used, False otherwise.
Raises:
exceptions.ConnectionError: If unable to establish a connection to the host.
"""
if ipv6 and not sock:
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
except OSError as err:
if ipv4_fallback:
logger.warning(f"IPv6 failed with {err}. Falling back to IPv4.")
return _create_connect_socket(host, port, timeout, ipv6=False)
else:
raise exceptions.ConnectionError(
f"Unable to establish IPv6 connection to {host}."
) from err
elif not sock:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
if ipv6:
try:
sock.connect((host, port))
except socket.gaierror as err:
if ipv4_fallback:
logger.warning(f"IPv6 connection failed to {host}. Falling back to IPv4.")
return _create_connect_socket(host, port, timeout, ipv6=False, sock=sock)
else:
raise exceptions.ConnectionError(
f"Unable to establish IPv6 connection to {host}."
) from err
else:
sock.connect((host, port))
return sock, ipv6


class Session:
"""Wrapper around ssh2-python's auth/connection system."""

Expand All @@ -43,22 +91,30 @@ def __init__(self, **kwargs):
port (int): The port number to connect to. Defaults to 22.
key_filename (str): The path to the private key file to use for authentication.
password (str): The password to use for authentication.
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
Raises:
AuthException: If no password or key file is provided.
ConnectionError: If the connection fails.
FileNotFoundError: If the key file is not found.
"""
host = kwargs.get("hostname", "localhost")
user = kwargs.get("username", "root")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(kwargs.get("timeout"))
port = kwargs.get("port", 22)
key_filename = kwargs.get("key_filename")
password = kwargs.get("password")
timeout = kwargs.get("timeout", 60)
helpers.simple_retry(sock.connect, [(host, port)], max_timeout=timeout)
# create the socket
self.sock, self.is_ipv6 = _create_connect_socket(
host,
port,
timeout,
ipv6=kwargs.get("ipv6", False),
ipv4_fallback=kwargs.get("ipv4_fallback", True),
)
self.session = ssh2_Session()
self.session.handshake(sock)
self.session.handshake(self.sock)
try:
if key_filename:
auth_type = "Key"
Expand Down
2 changes: 2 additions & 0 deletions broker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def init_settings(settings_path, interactive=False):
Validator("HOST_CONNECTION_TIMEOUT", default=60),
Validator("HOST_SSH_PORT", default=22),
Validator("HOST_SSH_KEY_FILENAME", default=None),
Validator("HOST_IPV6", default=False),
Validator("HOST_IPV4_FALLBACK", default=True),
Validator("LOGGING", is_type_of=dict),
Validator(
"LOGGING.CONSOLE_LEVEL",
Expand Down
4 changes: 4 additions & 0 deletions broker_settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ host_username: root
host_password: "<password>"
host_ssh_port: 22
host_ssh_key_filename: "</path/to/the/ssh-key>"
# Default all host ssh connections to IPv6
host_ipv6: False
# If IPv6 connection attempts fail, fallback to IPv4
host_ipv4_fallback: True
# Provider settings
AnsibleTower:
base_url: "https://<ansible tower host>/"
Expand Down

0 comments on commit 4295397

Please sign in to comment.