diff --git a/CHANGELOG.md b/CHANGELOG.md index ff72b97..16bbc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## 2026-06-XX - Tiny Cloud v3.3.3 -* Support IPv6 and multiple endpoints [#68](https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud/-/work_items/68) +* Support IPv6 and multiple endpoints + [#68](https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud/-/work_items/68) +* Check for IP routes to IMDS endpoints before trying them; retry if none are + routable. Fixes race condition between `dhcpcd` starting and attempting to + reach IMDS before routes are resolved. ## 2026-06-08 - Tiny Cloud v3.3.2 diff --git a/README.md b/README.md index cba82e1..e35d602 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,10 @@ must use URL-style brackets: IMDS_ENDPOINTS="169.254.169.254 [fd00:ec2::254]" ``` +Tiny Cloud checks for routes to configured IMDS endpoints before trying metadata +requests. `IMDS_ENDPOINT_WAIT_ATTEMPTS` controls how many route checks are made +before metadata requests are tried anyway. + ### Metadata API Version Each provider's API has a built-in default version. You can override the diff --git a/bin/imds b/bin/imds index 5c5613c..1d0efe1 100755 --- a/bin/imds +++ b/bin/imds @@ -55,6 +55,7 @@ unset -f \ : "${IMDS_ENDPOINT:=169.254.169.254}" : "${IMDS_ENDPOINTS:=$IMDS_ENDPOINT}" : "${IMDS_ENDPOINT_CACHE:=$TINY_CLOUD_VAR/.imds-endpoint}" +: "${IMDS_ENDPOINT_WAIT_ATTEMPTS:=10}" # Common to AWS and NoCloud(ish) IMDS_HOSTNAME="meta-data/hostname" @@ -107,9 +108,37 @@ _imds_host_port() { echo "$port" } +_imds_has_route() { + local host + set -- $(_imds_host_port "$1") + host="$1" + case "$host" in + *:*) ip -6 route get "$host" >/dev/null 2>&1 ;; + [0-9]*.[0-9]*.[0-9]*.[0-9]*) ip route get "$host" >/dev/null 2>&1 ;; + *) return 0 ;; + esac +} + _imds() { - local endpoint - for endpoint in $(_imds_endpoints); do + local endpoint endpoints routed attempts=1 + while :; do + endpoints= + routed= + for endpoint in $(_imds_endpoints); do + if _imds_has_route "$endpoint"; then + endpoints="$endpoints $endpoint" + routed=1 + fi + done + [ -n "$routed" ] && break + [ "$attempts" -ge "$IMDS_ENDPOINT_WAIT_ATTEMPTS" ] && { + endpoints="$(_imds_endpoints)" + break + } + sleep 1 + attempts=$((attempts + 1)) + done + for endpoint in $endpoints; do IMDS_ENDPOINT="$endpoint" IMDS_CURRENT_ENDPOINT="$endpoint" wget --quiet --timeout 1 --output-document - \ diff --git a/docs/imds.1.scd b/docs/imds.1.scd index 5fc3e9d..afaa608 100644 --- a/docs/imds.1.scd +++ b/docs/imds.1.scd @@ -106,6 +106,10 @@ inside *@nic:* queries. Whitespace-separated provider metadata endpoint list. IPv6 endpoints must use brackets, for example *[fd00:ec2::254]*. +*IMDS_ENDPOINT_WAIT_ATTEMPTS* + Number of times to check for routes to any IMDS endpoint before trying + metadata requests anyway. The default is *10*. + # EXIT STATUS *0* diff --git a/docs/tiny-cloud.conf.5.scd b/docs/tiny-cloud.conf.5.scd index f8f16aa..e50fc71 100644 --- a/docs/tiny-cloud.conf.5.scd +++ b/docs/tiny-cloud.conf.5.scd @@ -47,6 +47,10 @@ Blank lines and shell comments are ignored. first on later queries. IPv6 endpoints must use brackets, for example *[fd00:ec2::254]* or *[fd00:ec2::254]:80*. +*IMDS_ENDPOINT_WAIT_ATTEMPTS*= + Number of times to check for routes to any IMDS endpoint before trying + metadata requests anyway. The default is *10*. + *IMDS_TOKEN_TTL*= Metadata token lifetime in seconds for AWS metadata access. This is only used by the AWS provider. The default is *5*. diff --git a/lib/tiny-cloud/tiny-cloud.conf b/lib/tiny-cloud/tiny-cloud.conf index 50c5222..16528d6 100644 --- a/lib/tiny-cloud/tiny-cloud.conf +++ b/lib/tiny-cloud/tiny-cloud.conf @@ -17,6 +17,10 @@ # IPv6 endpoints must use brackets: [fd00:ec2::254] or [fd00:ec2::254]:80 #IMDS_ENDPOINTS="169.254.169.254 [fd00:ec2::254]" +# Number of times to check for routes to any IMDS endpoint before trying +# metadata requests anyway. +#IMDS_ENDPOINT_WAIT_ATTEMPTS=10 + # IMDS API version # Most providers have a default version, overrideable here if necessary #IMDS_API_VERSION="" diff --git a/tests/imds.test b/tests/imds.test index ff4558d..a8388a3 100755 --- a/tests/imds.test +++ b/tests/imds.test @@ -13,6 +13,8 @@ init_tests \ imds_endpoint_fallback \ imds_endpoint_cache \ imds_endpoint_ipv6 \ + imds_endpoint_route_skip \ + imds_endpoint_route_wait \ \ imds_hostname_aws \ imds_hostname_azure \ @@ -71,7 +73,8 @@ imds_endpoint_fallback_body() { IMDS_API_VERSION=2009-04-04 CLOUD=aws fake_metadata aws <<-EOF hostname: myhostname EOF - IMDS_API_VERSION=2009-04-04 IMDS_ENDPOINTS="fail 169.254.169.254" CLOUD=aws atf_check \ + IMDS_API_VERSION=2009-04-04 IMDS_ENDPOINT_WAIT_ATTEMPTS=0 \ + IMDS_ENDPOINTS="fail 169.254.169.254" CLOUD=aws atf_check \ -o match:"myhostname" \ imds @hostname atf_check -o match:"^169.254.169.254$" cat var/lib/cloud/.imds-endpoint @@ -86,7 +89,8 @@ imds_endpoint_cache_body() { cat > first.example.yaml <<-EOF hostname: first-hostname EOF - IMDS_API_VERSION=2009-04-04 WGET_STRIP_PREFIX="/2009-04-04/meta-data" \ + IMDS_API_VERSION=2009-04-04 IMDS_ENDPOINT_WAIT_ATTEMPTS=0 \ + WGET_STRIP_PREFIX="/2009-04-04/meta-data" \ WGET_HOST_LOG="$PWD/hosts.log" \ IMDS_ENDPOINTS="first.example cached.example" CLOUD=aws atf_check \ -o match:"cached-hostname" \ @@ -98,7 +102,8 @@ imds_endpoint_ipv6_body() { cat > "fd00:ec2::254.yaml" <<-EOF hostname: ipv6-hostname EOF - IMDS_API_VERSION=2009-04-04 WGET_STRIP_PREFIX="/2009-04-04/meta-data" \ + IMDS_API_VERSION=2009-04-04 IMDS_ENDPOINT_WAIT_ATTEMPTS=0 \ + WGET_STRIP_PREFIX="/2009-04-04/meta-data" \ WGET_HOST_LOG="$PWD/hosts.log" \ IMDS_ENDPOINTS="[fd00:ec2::254]" CLOUD=aws atf_check \ -o match:"ipv6-hostname" \ @@ -106,6 +111,39 @@ imds_endpoint_ipv6_body() { atf_check -o match:"^\\[fd00:ec2::254\\]$" cat hosts.log } +imds_endpoint_route_skip_body() { + IMDS_API_VERSION=2009-04-04 CLOUD=aws fake_metadata aws <<-EOF + hostname: myhostname + EOF + fake_bin ip <<-'EOF' + #!/bin/sh + [ "$3" = 169.254.169.254 ] + EOF + IMDS_API_VERSION=2009-04-04 \ + IMDS_ENDPOINTS="192.0.2.1 169.254.169.254" CLOUD=aws atf_check \ + -o match:"myhostname" \ + imds @hostname + atf_check -o match:"^169.254.169.254$" cat var/lib/cloud/.imds-endpoint +} + +imds_endpoint_route_wait_body() { + IMDS_API_VERSION=2009-04-04 CLOUD=aws fake_metadata aws <<-EOF + hostname: myhostname + EOF + fake_bin ip <<-'EOF' + #!/bin/sh + mkdir -p tmp + count=$(cat tmp/route-count 2>/dev/null || echo 0) + count=$((count + 1)) + echo "$count" > tmp/route-count + [ "$count" -gt 1 ] + EOF + IMDS_API_VERSION=2009-04-04 IMDS_ENDPOINT_WAIT_ATTEMPTS=5 CLOUD=aws atf_check \ + -o match:"myhostname" \ + imds @hostname + atf_check -o match:"^2$" cat tmp/route-count +} + check_hostname() { fake_metadata "$1" <<-EOF # aws, digitalocean, hetzner, nocloud diff --git a/tests/test_env.sh b/tests/test_env.sh index f96dc26..70dab9a 100644 --- a/tests/test_env.sh +++ b/tests/test_env.sh @@ -6,6 +6,7 @@ PATH="$atf_srcdir/bin:$srcdir/bin:$srcdir/sbin:$PATH" export TINY_CLOUD_BASEDIR="$srcdir" export ROOT="$PWD" +export IMDS_ENDPOINT_WAIT_ATTEMPTS=0 init_tests() {