Skip to content

Commit

Permalink
ci/cd: fix IPv6 timeout with force-ipv4 action
Browse files Browse the repository at this point in the history
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.
Detais:
- nodejs/node¤41625
- nodejs/undici¤1531
  • Loading branch information
undergroundwires committed Mar 29, 2024
1 parent 8a5592f commit 9257539
Show file tree
Hide file tree
Showing 16 changed files with 700 additions and 573 deletions.
31 changes: 31 additions & 0 deletions .github/actions/force-ipv4/README.md
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions .github/actions/force-ipv4/action.yml
Original file line number Diff line number Diff line change
@@ -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 }}
78 changes: 78 additions & 0 deletions .github/actions/force-ipv4/force-ipv4.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash

main() {
if is_linux; then
echo 'Configuring Linux...'
setup_warp_and_exclude_ipv6_on_linux
prefer_ipv4_on_linux # Not required with WARP, and it does not address the issue without WARP
# Considered alternatives:
# - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent Node 18 exits (code: 13) when using `fetch`.
elif is_macos; then
echo 'Configuring macOS...'
setup_warp_and_exclude_ipv6_on_macos
disable_ipv6_on_macos
# Considered alternatives:
# - Using `networksetup` to iterate interfaces and disabling IPv6 for each did not resolve the issue.
fi
echo "IPv4: $(curl -s4m8 --retry 3 -A Mozilla https://api.ip.sb/geoip)"
echo "IPv6: $(curl -s6m8 --retry 3 -A Mozilla https://api.ip.sb/geoip)"
}

is_linux() {
[[ "$(uname -s)" == "Linux" ]]
}

is_macos() {
[[ "$(uname -s)" == "Darwin" ]]
}

setup_warp_and_exclude_ipv6_on_linux() {
install_warp_on_debian
configure_warp_to_drop_ipv6
}

setup_warp_and_exclude_ipv6_on_macos() {
brew install cloudflare-warp
configure_warp_to_drop_ipv6
}

configure_warp_to_drop_ipv6() {
echo 'Configuring Cloudflare WARP client...'
echo 'Initiating client registration with Cloudflare...'
warp-cli --accept-tos registration new
echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...'
warp-cli --accept-tos mode warp+doh
echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...'
warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution
# `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834
echo 'Establishing WARP connection...'
warp-cli --accept-tos connect
}

install_warp_on_debian() {
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
sudo apt-get update
sudo apt-get install -y cloudflare-warp
}

disable_ipv6_on_macos() {
networksetup -listallnetworkservices \
| tail -n +2 \
| while IFS= read -r interface; do
echo "Disabling IPv6 on: $interface"
networksetup -setv6off "$interface"
done
}

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."
}

main
127 changes: 65 additions & 62 deletions .github/workflows/checks.build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,69 @@ on:
pull_request:

jobs:
build-web:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
development, # Used by `dev` command
production, # Used by `build` command
# Vitest mode: https://vitest.dev/guide/cli.html
test, # Used by Vitest
]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Build web
run: npm run build -- --mode ${{ matrix.mode }}
-
name: Verify web build artifacts
run: npm run check:verify-build-artifacts -- --web
# build-web:
# strategy:
# matrix:
# os: [ macos, ubuntu, windows ]
# mode: [
# # Vite mode: https://vitejs.dev/guide/env-and-mode.html
# development, # Used by `dev` command
# production, # Used by `build` command
# # Vitest mode: https://vitest.dev/guide/cli.html
# test, # Used by Vitest
# ]
# fail-fast: false # Allows to see results from other combinations
# runs-on: ${{ matrix.os }}-latest
# steps:
# -
# name: Checkout
# uses: actions/checkout@v4
# -
# name: Setup node
# uses: ./.github/actions/setup-node
# -
# name: Install dependencies
# uses: ./.github/actions/npm-install-dependencies
# -
# name: Build web
# run: npm run build -- --mode ${{ matrix.mode }}
# -
# name: Verify web build artifacts
# run: npm run check:verify-build-artifacts -- --web

build-desktop:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
development, # Used by `dev` command
production, # Used by `build` and `preview` commands
]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Prebuild desktop
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
-
name: Verify unbundled desktop build artifacts
run: npm run check:verify-build-artifacts -- --electron-unbundled
-
name: Build (bundle and package) desktop application
run: npm run electron:build -- --publish never
-
name: Verify bundled desktop build artifacts
run: npm run check:verify-build-artifacts -- --electron-bundled
# build-desktop:
# strategy:
# matrix:
# os: [ macos, ubuntu, windows ]
# mode: [
# # electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
# development, # Used by `dev` command
# production, # Used by `build` and `preview` commands
# ]
# fail-fast: false # Allows to see results from other combinations
# runs-on: ${{ matrix.os }}-latest
# steps:
# -
# name: Checkout
# uses: actions/checkout@v4
# -
# name: Setup node
# uses: ./.github/actions/setup-node
# -
# name: Install dependencies
# uses: ./.github/actions/npm-install-dependencies
# -
# name: Prebuild desktop
# run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
# -
# name: Verify unbundled desktop build artifacts
# run: npm run check:verify-build-artifacts -- --electron-unbundled
# -
# name: Build (bundle and package) desktop application
# run: npm run electron:build -- --publish never
# -
# name: Verify bundled desktop build artifacts
# run: npm run check:verify-build-artifacts -- --electron-bundled

build-docker:
strategy:
Expand Down Expand Up @@ -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: >-
Expand Down
Loading

0 comments on commit 9257539

Please sign in to comment.