diff --git a/.changes/next-release/bugfix-ec2-instance-connect-ssh-IPv6.json b/.changes/next-release/bugfix-ec2-instance-connect-ssh-IPv6.json
new file mode 100644
index 000000000000..2398806295ee
--- /dev/null
+++ b/.changes/next-release/bugfix-ec2-instance-connect-ssh-IPv6.json
@@ -0,0 +1,5 @@
+{
+ "type": "bugfix",
+ "category": "``ec2-instance-connect``",
+ "description": "Fall back to the instance's IPv6 address in ``ec2-instance-connect ssh --connection-type eice`` and ``ec2-instance-connect open-tunnel`` when the instance has no private IPv4 address (e.g. an IPv6-only subnet)."
+}
diff --git a/awscli/customizations/ec2instanceconnect/opentunnel.py b/awscli/customizations/ec2instanceconnect/opentunnel.py
index 2d301b77b3a6..0de7849e58b9 100644
--- a/awscli/customizations/ec2instanceconnect/opentunnel.py
+++ b/awscli/customizations/ec2instanceconnect/opentunnel.py
@@ -30,7 +30,7 @@ class OpenTunnelCommand(BasicCommand):
NAME = "open-tunnel"
DESCRIPTION = (
- "Opens a websocket tunnel to the specified EC2 Instance or private ip."
+ "Opens a websocket tunnel to the specified EC2 Instance or IP address."
)
ARG_TABLE = [
@@ -55,7 +55,7 @@ class OpenTunnelCommand(BasicCommand):
{
"name": "private-ip-address",
"help_text": (
- "Specify the private ip address to open a tunnel for. "
+ "Specify the private IPv4 (or IPv6) address to open a tunnel for. "
"If this is specified, you must specify instance-connect-endpoint-id."
),
"required": False,
@@ -114,7 +114,16 @@ def _run_main(self, parsed_args, parsed_globals):
)["Reservations"][0]["Instances"][0]
instance_vpc_id = instance_metadata["VpcId"]
instance_subnet_id = instance_metadata["SubnetId"]
- private_ip_address = instance_metadata["PrivateIpAddress"]
+ # Fall back to IPv6 when the instance has no private IPv4
+ # (e.g. an IPv6-only subnet). The EICE accepts either.
+ private_ip_address = instance_metadata.get(
+ "PrivateIpAddress"
+ ) or instance_metadata.get("Ipv6Address")
+ if not private_ip_address:
+ raise ParamValidationError(
+ "Unable to find any IP address on the instance to "
+ "connect to."
+ )
instance_connect_endpoint_id = parsed_args.instance_connect_endpoint_id
instance_connect_endpoint_dns_name = (
diff --git a/awscli/customizations/ec2instanceconnect/ssh.py b/awscli/customizations/ec2instanceconnect/ssh.py
index 09e3363e022b..46c195325074 100644
--- a/awscli/customizations/ec2instanceconnect/ssh.py
+++ b/awscli/customizations/ec2instanceconnect/ssh.py
@@ -86,7 +86,9 @@ class SshCommand(BasicCommand):
'
Private IPv4'
''
''
- 'eice: SSH using EC2 Instance Connect Endpoint. The CLI always uses the private IPv4 address.'
+ 'eice: SSH using EC2 Instance Connect Endpoint. '
+ 'The CLI tries to connect using the private IPv4 address, '
+ 'falling back to IPv6 if the instance has no private IPv4.'
'auto: The CLI automatically determines the connection type (direct or eice) '
'to use based on the instance info. Currently the CLI tries to connect using the IP addresses '
'in the following order and with the corresponding connection type:'
@@ -171,7 +173,7 @@ def _run_main(self, parsed_args, parsed_globals):
ip_address_to_connect = parsed_args.instance_ip
elif parsed_args.connection_type == 'eice' or parsed_args.eice_options:
use_open_tunnel = True
- ip_address_to_connect = private_ip_address
+ ip_address_to_connect = private_ip_address or ipv6_address
elif parsed_args.connection_type == 'direct':
use_open_tunnel = False
ip_address_to_connect = (
diff --git a/tests/functional/ec2instanceconnect/test_opentunnel.py b/tests/functional/ec2instanceconnect/test_opentunnel.py
index 744aea670273..e1742665e97a 100644
--- a/tests/functional/ec2instanceconnect/test_opentunnel.py
+++ b/tests/functional/ec2instanceconnect/test_opentunnel.py
@@ -278,6 +278,45 @@ def describe_instance_response():
"""
+@pytest.fixture
+def describe_ipv6_only_instance_response():
+ return """
+
+
+ -
+
+
-
+ i-123
+ subnet-123
+ vpc-123
+ 2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27
+
+
+
+
+
+ """
+
+
+@pytest.fixture
+def describe_no_ip_instance_response():
+ return """
+
+
+ -
+
+
-
+ i-123
+ subnet-123
+ vpc-123
+
+
+
+
+
+ """
+
+
@pytest.fixture
def request_params_for_describe_instance():
return {'InstanceIds': ['i-123']}
@@ -816,3 +855,67 @@ def test_command_fails_when_empty_fips_endpoint_available_to_connect(
assert 253 == result.rc
assert "Unable to find FIPS Endpoint" in result.stderr
+
+ def test_falls_back_to_ipv6_when_no_private_ipv4(
+ self,
+ cli_runner,
+ mock_crt_websocket,
+ connect_patch,
+ io_patch,
+ describe_ipv6_only_instance_response,
+ describe_eice_response,
+ dns_name,
+ datetime_utcnow_patch,
+ ):
+ cli_runner.env["AWS_USE_FIPS_ENDPOINT"] = "false"
+ cmdline = [
+ "ec2-instance-connect",
+ "open-tunnel",
+ "--instance-id",
+ "i-123",
+ "--max-tunnel-duration",
+ "1",
+ ]
+ cli_runner.add_response(
+ HTTPResponse(body=describe_ipv6_only_instance_response)
+ )
+ cli_runner.add_response(HTTPResponse(body=describe_eice_response))
+
+ test_server_input = b"Test Server Output"
+ mock_crt_websocket.add_output_from_server(test_server_input)
+ mock_crt_websocket.add_shutdown_from_server()
+
+ with connect_patch, io_patch:
+ result = cli_runner.run(cmdline)
+
+ assert 0 == result.rc
+ assert_url(dns_name, mock_crt_websocket.url)
+ parsed_qs = urllib.parse.parse_qs(
+ urllib.parse.urlparse(mock_crt_websocket.url).query
+ )
+ assert parsed_qs["privateIpAddress"] == [
+ "2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27"
+ ]
+
+ def test_command_fails_when_instance_has_no_ip_address(
+ self,
+ cli_runner,
+ describe_no_ip_instance_response,
+ ):
+ cmdline = [
+ "ec2-instance-connect",
+ "open-tunnel",
+ "--instance-id",
+ "i-123",
+ ]
+ cli_runner.add_response(
+ HTTPResponse(body=describe_no_ip_instance_response)
+ )
+
+ result = cli_runner.run(cmdline)
+
+ assert 252 == result.rc
+ assert (
+ "Unable to find any IP address on the instance to connect to."
+ in result.stderr
+ )
diff --git a/tests/functional/ec2instanceconnect/test_ssh.py b/tests/functional/ec2instanceconnect/test_ssh.py
index f65109d76e57..63813d75afcf 100644
--- a/tests/functional/ec2instanceconnect/test_ssh.py
+++ b/tests/functional/ec2instanceconnect/test_ssh.py
@@ -128,6 +128,25 @@ def describe_ipv6_instance_response():
return get_describe_ipv6_instance_response()
+def get_describe_ipv6_only_instance_response():
+ return """
+
+
+ -
+
+
-
+ i-123
+ subnet-123
+ vpc-123
+ 2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27
+
+
+
+
+
+ """
+
+
@pytest.fixture
def describe_no_ip_instance_response():
return """
@@ -525,6 +544,36 @@ class TestSSHCommand:
],
id='Open-Tunnel: select private IP when connection type eice',
),
+ pytest.param(
+ get_describe_ipv6_only_instance_response(),
+ get_describe_eice_response(),
+ get_request_params_for_describe_eice(),
+ [
+ "ec2-instance-connect",
+ "ssh",
+ "--instance-id",
+ "i-123",
+ "--private-key-file",
+ "/tmp/ssh-file",
+ "--connection-type",
+ "eice",
+ ],
+ [
+ 'ssh',
+ '-o',
+ 'ServerAliveInterval=5',
+ '-p',
+ '22',
+ '-i',
+ '/tmp/ssh-file',
+ '-o',
+ 'ProxyCommand=aws ec2-instance-connect open-tunnel --instance-id i-123 '
+ '--private-ip-address 2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27 --remote-port 22 '
+ '--instance-connect-endpoint-id eice-123 --instance-connect-endpoint-dns-name dns.com',
+ 'ec2-user@2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27',
+ ],
+ id='Open-Tunnel: fall back to IPv6 when connection type eice and no private IPv4',
+ ),
pytest.param(
get_describe_private_instance_response(),
get_describe_eice_response(),