Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)."
}
15 changes: 12 additions & 3 deletions awscli/customizations/ec2instanceconnect/opentunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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,
Expand Down Expand Up @@ -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 = (
Expand Down
6 changes: 4 additions & 2 deletions awscli/customizations/ec2instanceconnect/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ class SshCommand(BasicCommand):
'<li>Private IPv4</li>'
'</ul>'
'</li>'
'<li>eice: SSH using EC2 Instance Connect Endpoint. The CLI always uses the private IPv4 address.</li>'
'<li>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.</li>'
'<li>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:'
Expand Down Expand Up @@ -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 = (
Expand Down
103 changes: 103 additions & 0 deletions tests/functional/ec2instanceconnect/test_opentunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,45 @@ def describe_instance_response():
"""


@pytest.fixture
def describe_ipv6_only_instance_response():
return """
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<reservationSet>
<item>
<instancesSet>
<item>
<instanceId>i-123</instanceId>
<subnetId>subnet-123</subnetId>
<vpcId>vpc-123</vpcId>
<ipv6Address>2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27</ipv6Address>
</item>
</instancesSet>
</item>
</reservationSet>
</DescribeInstancesResponse>
"""


@pytest.fixture
def describe_no_ip_instance_response():
return """
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<reservationSet>
<item>
<instancesSet>
<item>
<instanceId>i-123</instanceId>
<subnetId>subnet-123</subnetId>
<vpcId>vpc-123</vpcId>
</item>
</instancesSet>
</item>
</reservationSet>
</DescribeInstancesResponse>
"""


@pytest.fixture
def request_params_for_describe_instance():
return {'InstanceIds': ['i-123']}
Expand Down Expand Up @@ -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
)
49 changes: 49 additions & 0 deletions tests/functional/ec2instanceconnect/test_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ def describe_ipv6_instance_response():
return get_describe_ipv6_instance_response()


def get_describe_ipv6_only_instance_response():
return """
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<reservationSet>
<item>
<instancesSet>
<item>
<instanceId>i-123</instanceId>
<subnetId>subnet-123</subnetId>
<vpcId>vpc-123</vpcId>
<ipv6Address>2600:1f10:4f8e:db01:73f5:6b9d:c0da:1c27</ipv6Address>
</item>
</instancesSet>
</item>
</reservationSet>
</DescribeInstancesResponse>
"""


@pytest.fixture
def describe_no_ip_instance_response():
return """
Expand Down Expand Up @@ -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(),
Expand Down
Loading