Skip to content

Commit d10c5a4

Browse files
authored
Fallback to IMDSv1 when IMDSv2 token request reaches hop limit (#655)
* Use URI directly * Fallback to IMDSv1 when token hop limit reached * Add warning about hop limit * Update tests to work on Julia 1.6 * Set project version to 1.90.3
1 parent bbe0f63 commit d10c5a4

File tree

3 files changed

+57
-7
lines changed

3 files changed

+57
-7
lines changed

Diff for: Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "AWS"
22
uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc"
33
license = "MIT"
4-
version = "1.90.2"
4+
version = "1.90.3"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

Diff for: src/IMDS.jl

+27-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using ..AWSExceptions: IMDSUnavailable
1515
using HTTP: HTTP
1616
using HTTP.Exceptions: ConnectError, StatusError
1717
using Mocking
18+
using URIs: URI
1819

1920
# Local-link address (https://en.wikipedia.org/wiki/Link-local_address)
2021
const IPv4_ADDRESS = "169.254.169.254"
@@ -55,8 +56,28 @@ function refresh_token!(session::Session, duration::Integer=session.duration)
5556
# For IMDSv2, you must use `/latest/api/token` when retrieving the token instead of a
5657
# version specific path.
5758
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations
58-
uri = HTTP.URI(; scheme="http", host=IPv4_ADDRESS, path="/latest/api/token")
59-
r = _http_request("PUT", uri, headers; status_exception=false)
59+
uri = URI(; scheme="http", host=IPv4_ADDRESS, path="/latest/api/token")
60+
r = try
61+
_http_request("PUT", uri, headers; status_exception=false)
62+
catch e
63+
# The IMDSv2 uses a default Time To Live (TTL) of 1 (also known as the hop limit) at
64+
# the IP layer to ensure token requests occur on the instance. When this occurs we
65+
# need to fall back to using IMDSv1. Users may wish to increase the hop limit to
66+
# allow for IMDSv2 use in container based environments:
67+
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations
68+
if is_ttl_expired_exception(e)
69+
@warn "IMDSv2 token request rejected due to reaching hop limit. Consider " *
70+
"increasing the hop limit to avoid delays upon initial use:\n" *
71+
"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/" *
72+
"instancedata-data-retrieval.html#imds-considerations"
73+
74+
session.duration = 0
75+
session.expiration = typemax(Int64) # Use IMDSv1 indefinitely
76+
return session
77+
else
78+
rethrow()
79+
end
80+
end
6081

6182
# Store the session token when we receive an HTTP 200. If we receive an HTTP 404 assume
6283
# that the server is only supports IMDSv1. Otherwise "rethrow" the `StatusError`.
@@ -87,7 +108,7 @@ function request(session::Session, method::AbstractString, path::AbstractString;
87108
# Only using the IPv4 endpoint as the IPv6 endpoint has to be explicitly enabled and
88109
# does not disable IPv4 support.
89110
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances-ipv4-ipv6-endpoints
90-
uri = HTTP.URI(; scheme="http", host=IPv4_ADDRESS, path)
111+
uri = URI(; scheme="http", host=IPv4_ADDRESS, path)
91112
return _http_request(method, uri, headers; kwargs...)
92113
end
93114

@@ -122,10 +143,12 @@ end
122143
is_connection_exception(e::ConnectError) = true
123144
is_connection_exception(e::Exception) = false
124145

146+
# https://github.com/JuliaCloud/AWS.jl/issues/654
125147
# https://github.com/JuliaCloud/AWS.jl/issues/649
126-
function is_connection_exception(e::HTTP.Exceptions.RequestError)
148+
function is_ttl_expired_exception(e::HTTP.Exceptions.RequestError)
127149
return e.error == Base.IOError("read: connection timed out (ETIMEDOUT)", -110)
128150
end
151+
is_ttl_expired_exception(e::Exception) = false
129152

130153
"""
131154
get([session::Session], path::AbstractString) -> Union{String, Nothing}

Diff for: test/IMDS.jl

+29-2
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,22 @@ function _imds_patch(router::HTTP.Router=HTTP.Router(); listening=true, enabled=
8282
end
8383

8484
@testset "IMDS" begin
85-
@testset "is_connection_exception" begin
85+
@testset "is_connection_exception / is_ttl_expired_exception" begin
8686
url = "http://169.254.169.254/latest/api/token"
8787
connect_timeout = HTTP.ConnectionPool.ConnectTimeout("169.254.169.254", 80)
8888
e = HTTP.Exceptions.ConnectError(url, connect_timeout)
8989
@test IMDS.is_connection_exception(e)
90+
@test !IMDS.is_ttl_expired_exception(e)
9091

9192
request = HTTP.Request("PUT", "/latest/api/token", [], HTTP.nobody)
9293
io_error = Base.IOError("read: connection timed out (ETIMEDOUT)", -110)
9394
e = HTTP.Exceptions.RequestError(request, io_error)
94-
@test IMDS.is_connection_exception(e)
95+
@test !IMDS.is_connection_exception(e)
96+
@test IMDS.is_ttl_expired_exception(e)
9597

9698
e = ErrorException("non-connection error")
9799
@test !IMDS.is_connection_exception(e)
100+
@test !IMDS.is_ttl_expired_exception(e)
98101
end
99102

100103
@testset "refresh_token!" begin
@@ -225,6 +228,30 @@ end
225228
@test r isa HTTP.Response
226229
@test r.status == 401
227230
end
231+
232+
# When running in a container running on an EC2 instance and the hop limit is 1 the
233+
# IMDSv2 token retrieval will fail so we should fall back to using IMDSv1.
234+
# https://github.com/JuliaCloud/AWS.jl/issues/654
235+
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations
236+
connection_timeout = function (req::HTTP.Request)
237+
io_error = Base.IOError("read: connection timed out (ETIMEDOUT)", -110)
238+
throw(HTTP.Exceptions.RequestError(request, io_error))
239+
end
240+
router = Router([
241+
Route("PUT", "/latest/api/token", connection_timeout),
242+
response_route("GET", path, HTTP.Response(instance_id)),
243+
])
244+
apply(_imds_patch(router)) do
245+
session = IMDS.Session()
246+
msg_regex = r"IMDSv2 token request rejected due to reaching hop limit"
247+
r = @test_logs (:warn, msg_regex) begin
248+
IMDS.request(session, "GET", path; status_exception=false)
249+
end
250+
@test r isa HTTP.Response
251+
@test r.status == 200
252+
@test String(r.body) == instance_id
253+
@test isempty(session.token)
254+
end
228255
end
229256

230257
@testset "get" begin

0 commit comments

Comments
 (0)