Unverified Commit 8264ad1a authored by Philip Taron's avatar Philip Taron Committed by GitHub
Browse files

nixos/virtualization/ec2: add IPv6 IMDS fetch capability (#476897)

parents 23e0843f cfaddae8
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -31,7 +31,7 @@ in
      ;
    inherit (config.image) baseName;
    additionalSpace = "1024M";
    pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
    pkgs = import ../../../.. { inherit (pkgs.stdenv.hostPlatform) system; }; # ensure we use the regular qemu-kvm package
    configFile = pkgs.writeText "configuration.nix" ''
      {
        imports = [ <nixpkgs/nixos/modules/virtualisation/openstack-config.nix> ];
+19 −8
Original line number Diff line number Diff line
@@ -3,7 +3,12 @@ mkdir -p "$metaDir"
chmod 0755 "$metaDir"
rm -f "$metaDir/*"

IMDS_ENDPOINTS="http://169.254.169.254 http://[fd00:ec2::254]"
IMDS_BASE_URL="http://169.254.169.254"
IMDS_TOKEN=""

get_imds_token() {
  local endpoint=$1
  # retry-delay of 1 selected to give the system a second to get going,
  # but not add a lot to the bootup time
  curl \
@@ -15,10 +20,11 @@ get_imds_token() {
    -X PUT \
    --connect-timeout 1 \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 600" \
    http://169.254.169.254/latest/api/token
    "$endpoint/latest/api/token"
}

preflight_imds_token() {
  local endpoint=$1
  # retry-delay of 1 selected to give the system a second to get going,
  # but not add a lot to the bootup time
  curl \
@@ -30,13 +36,18 @@ preflight_imds_token() {
    --connect-timeout 1 \
    -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" \
    -o /dev/null \
    http://169.254.169.254/1.0/meta-data/instance-id
    "$endpoint/1.0/meta-data/instance-id"
}

try=1
while [ $try -le 3 ]; do
  echo "(attempt $try/3) getting an EC2 instance metadata service v2 token..."
  IMDS_TOKEN=$(get_imds_token) && break
  for endpoint in $IMDS_ENDPOINTS; do
    IMDS_TOKEN=$(get_imds_token "$endpoint") && IMDS_BASE_URL=$endpoint && break
  done
  if [ -n "$IMDS_TOKEN" ]; then
    break
  fi
  try=$((try + 1))
  sleep 1
done
@@ -48,7 +59,7 @@ fi
try=1
while [ $try -le 10 ]; do
  echo "(attempt $try/10) validating the EC2 instance metadata service v2 token..."
  preflight_imds_token && break
  preflight_imds_token "$IMDS_BASE_URL" && break
  try=$((try + 1))
  sleep 1
done
@@ -85,7 +96,7 @@ try_decompress() {
  fi
}

get_imds -o "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
(umask 077 && get_imds -o "$metaDir/user-data" http://169.254.169.254/1.0/user-data && try_decompress "$metaDir/user-data")
get_imds -o "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
get_imds -o "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
get_imds -o "$metaDir/ami-manifest-path" "$IMDS_BASE_URL/1.0/meta-data/ami-manifest-path"
(umask 077 && get_imds -o "$metaDir/user-data" "$IMDS_BASE_URL/1.0/user-data" && try_decompress "$metaDir/user-data")
get_imds -o "$metaDir/hostname" "$IMDS_BASE_URL/1.0/meta-data/hostname"
get_imds -o "$metaDir/public-keys-0-openssh-key" "$IMDS_BASE_URL/1.0/meta-data/public-keys/0/openssh-key"
+12 −3
Original line number Diff line number Diff line
@@ -2,12 +2,17 @@

with pkgs.lib;

let
  imdsServer = import ./imds-server.nix { inherit pkgs; };
in
{
  inherit imdsServer;

  makeEc2Test =
    {
      name,
      image,
      userData,
      userData ? null,
      script,
      hostname ? "ec2-instance",
      sshPublicKey ? null,
@@ -18,9 +23,13 @@ with pkgs.lib;
        name = "metadata";
        buildCommand = ''
          mkdir -p $out/1.0/meta-data
          ln -s ${pkgs.writeText "userData" userData} $out/1.0/user-data
          ${optionalString (
            userData != null
          ) "ln -s ${pkgs.writeText "userData" userData} $out/1.0/user-data"}
          ${optionalString (userData == null) "touch $out/1.0/user-data"}
          echo "${hostname}" > $out/1.0/meta-data/hostname
          echo "(unknown)" > $out/1.0/meta-data/ami-manifest-path
          echo "i-1234567890abcdef0" > $out/1.0/meta-data/instance-id
        ''
        + optionalString (sshPublicKey != null) ''
          mkdir -p $out/1.0/meta-data/public-keys/0
@@ -67,7 +76,7 @@ with pkgs.lib;
        start_command = (
            "qemu-kvm -m 1024"
            + " -device virtio-net-pci,netdev=vlan0"
            + " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'"
            + " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${getExe imdsServer} ${metaData}'"
            + f" -drive file={disk_image},if=virtio,werror=report"
            + " $QEMU_OPTS"
        )
+4 −0
Original line number Diff line number Diff line
# Minimal IMDSv2-compatible metadata server for NixOS EC2 tests.
# Runs in inetd mode (stdin/stdout), drop-in for micro_httpd in
# QEMU guestfwd and socat contexts.
{ pkgs }: pkgs.writers.writePython3Bin "imds-server" { } (builtins.readFile ./imds-server.py)
+102 −0
Original line number Diff line number Diff line
"""Minimal IMDSv2-compatible metadata server for NixOS EC2 tests.

Runs in inetd mode: reads one HTTP request from stdin, writes the
response to stdout. Drop-in replacement for micro_httpd in QEMU
guestfwd and socat contexts.

Usage: imds-server <metadata-directory>

The metadata directory should contain:
  latest/api/token                        - Token value (returned on PUT)
  1.0/meta-data/hostname                  - Instance hostname
  1.0/meta-data/ami-manifest-path         - AMI manifest path
  1.0/meta-data/instance-id               - Instance ID
  1.0/meta-data/public-keys/0/openssh-key - SSH public key
  1.0/user-data                           - User data
"""

import os
import sys


def read_request():
    """Read and parse one HTTP request from stdin (inetd mode)."""
    request_line = sys.stdin.readline()
    if not request_line:
        sys.exit(0)

    parts = request_line.strip().split()
    method = parts[0] if parts else ""
    path = parts[1] if len(parts) > 1 else "/"

    headers = {}
    while True:
        line = sys.stdin.readline()
        if not line or line.strip() == "":
            break
        if ":" in line:
            key, _, value = line.partition(":")
            headers[key.strip().lower()] = value.strip()

    return method, path, headers


def respond(status, body):
    """Write an HTTP response to stdout."""
    if isinstance(body, str):
        body = body.encode()
    header = (
        f"HTTP/1.1 {status}\r\n"
        f"Content-Type: text/plain\r\n"
        f"Content-Length: {len(body)}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    ).encode()
    sys.stdout.buffer.write(header + body)
    sys.stdout.buffer.flush()


def main():
    base_dir = sys.argv[1] if len(sys.argv) > 1 else "."

    # Load expected token from file. If no token file exists, IMDSv2
    # authentication is disabled — requests are served without tokens.
    # This supports both EC2 (IMDSv2 with tokens) and OpenStack (plain GET)
    # metadata fetchers.
    token_path = os.path.join(base_dir, "latest", "api", "token")
    if os.path.isfile(token_path):
        with open(token_path) as f:
            expected_token = f.read().strip()
    else:
        expected_token = None

    method, path, headers = read_request()
    rel_path = path.lstrip("/")

    # PUT /latest/api/token — IMDSv2 token acquisition
    if method == "PUT" and rel_path == "latest/api/token":
        if expected_token is not None:
            respond("200 OK", expected_token)
        else:
            respond("404 Not Found", "IMDSv2 token endpoint not configured\n")
        return

    # Token validation (only when a token file is present)
    if expected_token is not None:
        request_token = headers.get("x-aws-ec2-metadata-token", "")
        if request_token != expected_token:
            respond("401 Unauthorized", "Invalid or missing IMDSv2 token\n")
            return

    # Serve file from the metadata directory
    file_path = os.path.join(base_dir, rel_path)
    if os.path.isfile(file_path):
        with open(file_path, "rb") as f:
            content = f.read()
        respond("200 OK", content)
    else:
        respond("404 Not Found", f"Not found: {path}\n")


if __name__ == "__main__":
    main()
Loading