From fadc518a7aaf2ba57ebd28a2118175c2ddf770af Mon Sep 17 00:00:00 2001 From: Ben Whaley Date: Mon, 15 Apr 2024 11:44:36 -0700 Subject: [PATCH] New option to disable IPv6 in the connectivity test (#88) --- README.md | 3 ++ functions/replace-route/app.py | 27 ++++++++++++- .../replace-route/tests/test_replace_route.py | 11 +++++ modules/terraform-aws-alternat/lambda.tf | 40 +++++++++++-------- modules/terraform-aws-alternat/variables.tf | 6 +++ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8b6ae69..e6bcc0f 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,9 @@ While we'd like for this to be available on the Terraform Registry, it requires echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf EOF ``` +- If you see errors like: `error connecting to https://www.google.com/: ` in the connectivity tester logs, you can set `lambda_has_ipv6 = false`. This will cause the lambda to request IPv4 addresses only in DNS lookups. + + ## Future work diff --git a/functions/replace-route/app.py b/functions/replace-route/app.py index 2da1461..9e4b09e 100644 --- a/functions/replace-route/app.py +++ b/functions/replace-route/app.py @@ -27,9 +27,24 @@ # Which URLs to check for connectivity DEFAULT_CHECK_URLS = ["https://www.example.com", "https://www.google.com"] - +# The timeout for the connectivity checks. REQUEST_TIMEOUT = 5 +# Whether or not use IPv6. +DEFAULT_HAS_IPV6 = True + + +# Overrides socket.getaddrinfo to perform IPv4 lookups +# See https://github.com/1debit/alternat/issues/87 +def disable_ipv6(): + prv_getaddrinfo = socket.getaddrinfo + def getaddrinfo_ipv4(*args): + modified_args = (args[0], args[1], socket.AF_INET) + args[3:] + res = prv_getaddrinfo(*modified_args) + return res + socket.getaddrinfo = getaddrinfo_ipv4 + + def get_az_and_vpc_zone_identifier(auto_scaling_group): autoscaling = boto3.client("autoscaling") @@ -156,6 +171,10 @@ def connectivity_test_handler(event, context): check_interval = int(os.getenv("CONNECTIVITY_CHECK_INTERVAL", DEFAULT_CONNECTIVITY_CHECK_INTERVAL)) check_urls = "CHECK_URLS" in os.environ and os.getenv("CHECK_URLS").split(",") or DEFAULT_CHECK_URLS + has_ipv6 = get_env_bool("HAS_IPV6", DEFAULT_HAS_IPV6) + if not has_ipv6: + disable_ipv6() + # Run connectivity checks for approximately 1 minute run = 0 num_runs = 60 / check_interval @@ -167,6 +186,12 @@ def connectivity_test_handler(event, context): break +def get_env_bool(var_name, default_value=False): + value = os.getenv(var_name, default_value) + true_values = ["t", "true", "y", "yes", "1"] + return str(value).lower() in true_values + + def handler(event, _): try: for record in event["Records"]: diff --git a/functions/replace-route/tests/test_replace_route.py b/functions/replace-route/tests/test_replace_route.py index 2bc080b..78e2d65 100644 --- a/functions/replace-route/tests/test_replace_route.py +++ b/functions/replace-route/tests/test_replace_route.py @@ -206,3 +206,14 @@ class Context: connectivity_test_handler(event=json.loads(cloudwatch_event), context=Context()) verify_nat_gateway_route(mocked_networking) + + +def test_disable_ipv6(): + with mock.patch('socket.getaddrinfo') as mock_getaddrinfo: + from app import disable_ipv6 + disable_ipv6() + socket.getaddrinfo('example.com', 80) + mock_getaddrinfo.assert_called() + call_args = mock_getaddrinfo.call_args.args + assert len(call_args) == 3, f"With IPv6 disabled, expected 3 arguments to getaddrinfo, found {len(call_args)}" + assert call_args[2] == socket.AF_INET, "Did not find AF_INET family in args to getaddrinfo" diff --git a/modules/terraform-aws-alternat/lambda.tf b/modules/terraform-aws-alternat/lambda.tf index 95723f3..48b8d0c 100644 --- a/modules/terraform-aws-alternat/lambda.tf +++ b/modules/terraform-aws-alternat/lambda.tf @@ -1,3 +1,12 @@ +locals { + autoscaling_func_env_vars = { + # Lambda function env vars cannot contain hyphens + for obj in var.vpc_az_maps + : replace(upper(obj.az), "-", "_") => join(",", obj.route_table_ids) + } + has_ipv6_env_var = { "HAS_IPV6" = var.lambda_has_ipv6 } +} + data "archive_file" "lambda" { count = var.lambda_package_type == "Zip" ? 1 : 0 type = "zip" @@ -15,7 +24,7 @@ resource "aws_lambda_function" "alternat_autoscaling_hook" { timeout = var.lambda_timeout role = aws_iam_role.nat_lambda_role.arn - layers = var.lambda_layer_arns + layers = var.lambda_layer_arns image_uri = var.lambda_package_type == "Image" ? "${var.alternat_image_uri}:${var.alternat_image_tag}" : null @@ -25,7 +34,10 @@ resource "aws_lambda_function" "alternat_autoscaling_hook" { source_code_hash = var.lambda_package_type == "Zip" ? data.archive_file.lambda[0].output_base64sha256 : null environment { - variables = merge(local.autoscaling_func_env_vars, var.lambda_environment_variables) + variables = merge( + local.autoscaling_func_env_vars, + var.lambda_environment_variables, + ) } tags = merge({ @@ -33,14 +45,6 @@ resource "aws_lambda_function" "alternat_autoscaling_hook" { }, var.tags) } -locals { - autoscaling_func_env_vars = { - # Lambda function env vars cannot contain hyphens - for obj in var.vpc_az_maps - : replace(upper(obj.az), "-", "_") => join(",", obj.route_table_ids) - } -} - resource "aws_iam_role" "nat_lambda_role" { name = var.nat_lambda_function_role_name == "" ? null : var.nat_lambda_function_role_name name_prefix = var.nat_lambda_function_role_name == "" ? "alternat-lambda-role-" : null @@ -129,7 +133,7 @@ resource "aws_lambda_function" "alternat_connectivity_tester" { timeout = var.lambda_timeout role = aws_iam_role.nat_lambda_role.arn - layers = var.lambda_layer_arns + layers = var.lambda_layer_arns image_uri = var.lambda_package_type == "Image" ? "${var.alternat_image_uri}:${var.alternat_image_tag}" : null @@ -147,11 +151,15 @@ resource "aws_lambda_function" "alternat_connectivity_tester" { } environment { - variables = merge({ - ROUTE_TABLE_IDS_CSV = join(",", each.value.route_table_ids), - PUBLIC_SUBNET_ID = each.value.public_subnet_id - CHECK_URLS = join(",", var.connectivity_test_check_urls) - }, var.lambda_environment_variables) + variables = merge( + { + ROUTE_TABLE_IDS_CSV = join(",", each.value.route_table_ids), + PUBLIC_SUBNET_ID = each.value.public_subnet_id + CHECK_URLS = join(",", var.connectivity_test_check_urls) + }, + local.has_ipv6_env_var, + var.lambda_environment_variables, + ) } vpc_config { diff --git a/modules/terraform-aws-alternat/variables.tf b/modules/terraform-aws-alternat/variables.tf index bfc959e..42c3c05 100644 --- a/modules/terraform-aws-alternat/variables.tf +++ b/modules/terraform-aws-alternat/variables.tf @@ -242,6 +242,12 @@ variable "lambda_environment_variables" { default = null } +variable "lambda_has_ipv6" { + description = "Controls whether or not the lambda function can use IPv6." + type = bool + default = true +} + variable "lambda_zip_path" { description = "The location where the generated zip file should be stored. Required when `lambda_package_type` is \"Zip\"." type = string