From 5d4bc18cfe242718a2b32db13a8cc84a41d01ff8 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Thu, 28 Mar 2024 20:39:14 +0100 Subject: [PATCH] ci/cd: fix IPv6 timeout with `force-ipv4` action This commit introduces the `force-ipv4` GitHub action to address connectivity issues caused by the lack of IPv6 support in GitHub runners. Details: - actions/runner$3138 - actions/runner-images$668 This change solves connection problems when Node's `fetch` API fails due to `UND_ERR_CONNECT_TIMEOUT` errors. Details: - actions/runner-images$9540 - actions/runner$3213 This action disables IPv6 at the system level, ensuring all outging requests use IPv4. Resolving connectivity issues when running external URL checks and Docker build checks. This solution is a temporary workaround until GitHub runners support IPv6 or Node `fetch` API has a working solution such as Happy Eyeball: - nodejs/nodei$41625 - nodejs/undici$1531 --- .github/actions/force-ipv4/README.md | 31 ++++++++++++ .github/actions/force-ipv4/action.yml | 12 +++++ .github/actions/force-ipv4/force-ipv4.sh | 52 +++++++++++++++++++++ .github/workflows/checks.build.yaml | 3 ++ .github/workflows/checks.external-urls.yaml | 3 ++ scripts/verify-web-server-status.js | 19 +++++++- 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .github/actions/force-ipv4/README.md create mode 100644 .github/actions/force-ipv4/action.yml create mode 100755 .github/actions/force-ipv4/force-ipv4.sh diff --git a/.github/actions/force-ipv4/README.md b/.github/actions/force-ipv4/README.md new file mode 100644 index 000000000..9a7c35747 --- /dev/null +++ b/.github/actions/force-ipv4/README.md @@ -0,0 +1,31 @@ +# force-ipv4 + +## Overview + +This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2]. + +## Background + +Some applications attempt network connections over IPv6. +Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` errors [3] [4]. +This happens when the software cannot handle this such as by using Happy Eyeballs [5] [6]. + +## Usage + +To use this action in your GitHub workflow, add the following step before any job that requires network access: + +```yaml +- name: Enforce IPv4 Connectivity + uses: ./.github/actions/force-ipv4 +``` + +## Note + +This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future. + +[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com" +[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com" +[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com" +[4]: https://archive.today/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com" +[5]: https://archive.today/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com" +[6]: https://archive.today/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com" diff --git a/.github/actions/force-ipv4/action.yml b/.github/actions/force-ipv4/action.yml new file mode 100644 index 000000000..23c9ef5cc --- /dev/null +++ b/.github/actions/force-ipv4/action.yml @@ -0,0 +1,12 @@ +inputs: + project-root: + required: false + default: '.' +runs: + using: composite + steps: + - + name: Run prefer IPv4 script + shell: bash + run: ./.github/actions/force-ipv4/force-ipv4.sh + working-directory: ${{ inputs.project-root }} diff --git a/.github/actions/force-ipv4/force-ipv4.sh b/.github/actions/force-ipv4/force-ipv4.sh new file mode 100755 index 000000000..06ddbd50b --- /dev/null +++ b/.github/actions/force-ipv4/force-ipv4.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +main() { + if is_linux; then + echo 'Configuring Linux...' + disable_ipv6_on_linux + elif is_macos; then + echo 'Configuring macOS...' + disable_ipv6_on_macos + fi + nslookup localhost # TODO: Delete +} + +is_linux() { + [[ "$(uname -s)" == "Linux" ]] +} + +is_macos() { + [[ "$(uname -s)" == "Darwin" ]] +} + +disable_ipv6_on_linux() { + # Temporarily and immediately disable IPv6 + echo '1' | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6 > /dev/null + echo '1' | sudo tee /proc/sys/net/ipv6/conf/default/disable_ipv6 > /dev/null + # Other approaches considered: + # - Prefer IPv4 in `/etc/gai.conf`: Requires proper configuration (just `precedence ::ffff:0:0/96 100`) is not enough. + # - Using `sysctl` command: Results in node to exit with code `13`. + # - Writing to `/proc/sys/net/`: Results in node to exit with code `13`. + # - Writing to `/etc/sysctl.conf`: Results in node to exit with code `13`. +} + +prefer_ipv4_on_linux() { + local -r gai_conf="/etc/gai.conf" + if [ ! -f "$gai_conf" ]; then + echo "Creating $gai_conf since it doesn't exist..." + touch "$gai_conf" + fi + echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_conf" > /dev/null + echo "Configuration complete." +} + +disable_ipv6_on_macos() { + networksetup -listallnetworkservices \ + | tail -n +2 \ + | while IFS= read -r interface; do + echo "Disabling IPv6 on: $interface" + networksetup -setv6off "$interface" + done +} + +main diff --git a/.github/workflows/checks.build.yaml b/.github/workflows/checks.build.yaml index 3efa472c3..1be8a26e5 100644 --- a/.github/workflows/checks.build.yaml +++ b/.github/workflows/checks.build.yaml @@ -95,6 +95,9 @@ jobs: - name: Run Docker image on port 8080 run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest + - + name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts. + uses: ./.github/actions/force-ipv4 - name: Check server is up and returns HTTP 200 run: >- diff --git a/.github/workflows/checks.external-urls.yaml b/.github/workflows/checks.external-urls.yaml index 8d8b46c85..1a3d9fd6a 100644 --- a/.github/workflows/checks.external-urls.yaml +++ b/.github/workflows/checks.external-urls.yaml @@ -17,6 +17,9 @@ jobs: - name: Install dependencies uses: ./.github/actions/npm-install-dependencies + - + name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts. + uses: ./.github/actions/force-ipv4 - name: Test run: npm run check:external-urls diff --git a/scripts/verify-web-server-status.js b/scripts/verify-web-server-status.js index f1ad10fbb..761fdcffc 100644 --- a/scripts/verify-web-server-status.js +++ b/scripts/verify-web-server-status.js @@ -20,6 +20,15 @@ const RETRY_DELAY_IN_SECONDS = 3; const PARAMETER_NAME_URL = '--url'; const PARAMETER_NAME_MAX_RETRIES = '--max-retries'; +async function main() { + try { + await checkServer(); + } catch (error) { + exitWithError('Uncaught exception:', error); + exitWithError('Verification failed with unexpected error.'); + } +} + async function checkServer(currentRetryCount = 1) { const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL); const maxRetries = parseNumber( @@ -84,4 +93,12 @@ function exitWithError(message) { process.exit(1); } -await checkServer(); +await main(); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception:', error); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +});