Skip to content

Commit 4295397

Browse files
committed
Add IPv6 support
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.
1 parent f84c820 commit 4295397

File tree

4 files changed

+93
-12
lines changed

4 files changed

+93
-12
lines changed

broker/hosts.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ def __init__(self, **kwargs):
3535
"""Create a Host instance.
3636
3737
Expected kwargs:
38-
hostname: str - Hostname or IP address of the host, required
39-
name: str - Name of the host
40-
username: str - Username to use for SSH connection
41-
password: str - Password to use for SSH connection
42-
connection_timeout: int - Timeout for SSH connection
43-
port: int - Port to use for SSH connection
44-
key_filename: str - Path to SSH key file to use for SSH connection
38+
hostname: (str) - Hostname or IP address of the host, required
39+
name: (str) - Name of the host
40+
username: (str) - Username to use for SSH connection
41+
password: (str) - Password to use for SSH connection
42+
connection_timeout: (int) - Timeout for SSH connection
43+
port: (int) - Port to use for SSH connection
44+
key_filename: (str) - Path to SSH key file to use for SSH connection
45+
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
46+
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
4547
"""
4648
logger.debug(f"Constructing host using {kwargs=}")
4749
self.hostname = kwargs.get("hostname") or kwargs.get("ip")
@@ -59,6 +61,8 @@ def __init__(self, **kwargs):
5961
self.timeout = kwargs.pop("connection_timeout", settings.HOST_CONNECTION_TIMEOUT)
6062
self.port = kwargs.pop("port", settings.HOST_SSH_PORT)
6163
self.key_filename = kwargs.pop("key_filename", settings.HOST_SSH_KEY_FILENAME)
64+
self.ipv6 = kwargs.pop("ipv6", settings.HOST_IPV6)
65+
self.ipv4_fallback = kwargs.pop("ipv4_fallback", settings.HOST_IPV4_FALLBACK)
6266
self.__dict__.update(kwargs) # Make every other kwarg an attribute
6367
self._session = None
6468

@@ -84,7 +88,16 @@ def session(self):
8488
self.connect()
8589
return self._session
8690

87-
def connect(self, username=None, password=None, timeout=None, port=22, key_filename=None):
91+
def connect(
92+
self,
93+
username=None,
94+
password=None,
95+
timeout=None,
96+
port=22,
97+
key_filename=None,
98+
ipv6=False,
99+
ipv4_fallback=True,
100+
):
88101
"""Connect to the host using SSH.
89102
90103
Args:
@@ -93,6 +106,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
93106
timeout (int): The timeout for the SSH connection in seconds.
94107
port (int): The port to use for the SSH connection. Defaults to 22.
95108
key_filename (str): The path to the private key file to use for the SSH connection.
109+
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
110+
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
96111
"""
97112
username = username or self.username
98113
password = password or self.password
@@ -103,6 +118,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
103118
if ":" in self.hostname:
104119
_hostname, port = self.hostname.split(":")
105120
_port = int(port)
121+
ipv6 = ipv6 or self.ipv6
122+
ipv4_fallback = ipv4_fallback or self.ipv4_fallback
106123
self.close()
107124
self._session = Session(
108125
hostname=_hostname,
@@ -111,6 +128,8 @@ def connect(self, username=None, password=None, timeout=None, port=22, key_filen
111128
port=_port,
112129
key_filename=key_filename,
113130
timeout=timeout,
131+
ipv6=ipv6,
132+
ipv4_fallback=ipv4_fallback,
114133
)
115134

116135
def close(self):

broker/session.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,54 @@
3030
FILE_FLAGS = ssh2_sftp.LIBSSH2_FXF_CREAT | ssh2_sftp.LIBSSH2_FXF_WRITE
3131

3232

33+
def _create_connect_socket(host, port, timeout, ipv6=False, ipv4_fallback=True, sock=None):
34+
"""Create a socket and establish a connection to the specified host and port.
35+
36+
Args:
37+
host (str): The hostname or IP address of the remote server.
38+
port (int): The port number to connect to.
39+
timeout (float): The timeout value in seconds for the socket connection.
40+
ipv6 (bool, optional): Whether to use IPv6. Defaults to False.
41+
ipv4_fallback (bool, optional): Whether to fallback to IPv4 if IPv6 fails. Defaults to True.
42+
sock (socket.socket, optional): An existing socket object to use. Defaults to None.
43+
44+
Returns:
45+
socket.socket: The connected socket object.
46+
bool: True if IPv6 was used, False otherwise.
47+
48+
Raises:
49+
exceptions.ConnectionError: If unable to establish a connection to the host.
50+
"""
51+
if ipv6 and not sock:
52+
try:
53+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
54+
except OSError as err:
55+
if ipv4_fallback:
56+
logger.warning(f"IPv6 failed with {err}. Falling back to IPv4.")
57+
return _create_connect_socket(host, port, timeout, ipv6=False)
58+
else:
59+
raise exceptions.ConnectionError(
60+
f"Unable to establish IPv6 connection to {host}."
61+
) from err
62+
elif not sock:
63+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
64+
sock.settimeout(timeout)
65+
if ipv6:
66+
try:
67+
sock.connect((host, port))
68+
except socket.gaierror as err:
69+
if ipv4_fallback:
70+
logger.warning(f"IPv6 connection failed to {host}. Falling back to IPv4.")
71+
return _create_connect_socket(host, port, timeout, ipv6=False, sock=sock)
72+
else:
73+
raise exceptions.ConnectionError(
74+
f"Unable to establish IPv6 connection to {host}."
75+
) from err
76+
else:
77+
sock.connect((host, port))
78+
return sock, ipv6
79+
80+
3381
class Session:
3482
"""Wrapper around ssh2-python's auth/connection system."""
3583

@@ -43,22 +91,30 @@ def __init__(self, **kwargs):
4391
port (int): The port number to connect to. Defaults to 22.
4492
key_filename (str): The path to the private key file to use for authentication.
4593
password (str): The password to use for authentication.
94+
ipv6 (bool): Whether or not to use IPv6. Defaults to False.
95+
ipv4_fallback (bool): Whether or not to fallback to IPv4 if IPv6 fails. Defaults to True.
4696
4797
Raises:
4898
AuthException: If no password or key file is provided.
99+
ConnectionError: If the connection fails.
49100
FileNotFoundError: If the key file is not found.
50101
"""
51102
host = kwargs.get("hostname", "localhost")
52103
user = kwargs.get("username", "root")
53-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
54-
sock.settimeout(kwargs.get("timeout"))
55104
port = kwargs.get("port", 22)
56105
key_filename = kwargs.get("key_filename")
57106
password = kwargs.get("password")
58107
timeout = kwargs.get("timeout", 60)
59-
helpers.simple_retry(sock.connect, [(host, port)], max_timeout=timeout)
108+
# create the socket
109+
self.sock, self.is_ipv6 = _create_connect_socket(
110+
host,
111+
port,
112+
timeout,
113+
ipv6=kwargs.get("ipv6", False),
114+
ipv4_fallback=kwargs.get("ipv4_fallback", True),
115+
)
60116
self.session = ssh2_Session()
61-
self.session.handshake(sock)
117+
self.session.handshake(self.sock)
62118
try:
63119
if key_filename:
64120
auth_type = "Key"

broker/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ def init_settings(settings_path, interactive=False):
9292
Validator("HOST_CONNECTION_TIMEOUT", default=60),
9393
Validator("HOST_SSH_PORT", default=22),
9494
Validator("HOST_SSH_KEY_FILENAME", default=None),
95+
Validator("HOST_IPV6", default=False),
96+
Validator("HOST_IPV4_FALLBACK", default=True),
9597
Validator("LOGGING", is_type_of=dict),
9698
Validator(
9799
"LOGGING.CONSOLE_LEVEL",

broker_settings.yaml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ host_username: root
99
host_password: "<password>"
1010
host_ssh_port: 22
1111
host_ssh_key_filename: "</path/to/the/ssh-key>"
12+
# Default all host ssh connections to IPv6
13+
host_ipv6: False
14+
# If IPv6 connection attempts fail, fallback to IPv4
15+
host_ipv4_fallback: True
1216
# Provider settings
1317
AnsibleTower:
1418
base_url: "https://<ansible tower host>/"

0 commit comments

Comments
 (0)