From 47ae993915d5f7f79ab206394adf3e53f5166821 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:28:54 +0200 Subject: [PATCH] Say why a forge could not be reached instead of 'connection failed' The release lookup ran curl silently and reported every transport failure as 'Connection to github failed', exit 7. Reproduce the failure once with curl's message visible, classify the exit code (DNS, no route, timeout, TLS, reset), probe IPv4 against IPv6 for the common broken-v6 case, and return curl's real code so the error table matches. --- .github/workflows/verify-split-files.yml | 5 ++ lib/API.txt | 1 + lib/forge.func | 40 ++++++++++++-- tests/forge-connection.sh | 69 ++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 tests/forge-connection.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index f39b146..98415b1 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -30,6 +30,7 @@ on: - "tests/docker-lxcfs.sh" - "tests/external-installer.sh" - "tests/forge-version-key.sh" + - "tests/forge-connection.sh" - ".github/workflows/verify-split-files.yml" push: branches: [main] @@ -51,6 +52,7 @@ on: - "tests/docker-lxcfs.sh" - "tests/external-installer.sh" - "tests/forge-version-key.sh" + - "tests/forge-connection.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -96,6 +98,9 @@ jobs: - name: Forge version key regression tests run: bash tests/forge-version-key.sh + - name: Forge connection diagnosis tests + run: bash tests/forge-connection.sh + - name: Prefetch lists cover every engine file run: | set -euo pipefail diff --git a/lib/API.txt b/lib/API.txt index fabe0f7..c9ba53e 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -17,6 +17,7 @@ _forge_asset_urls _forge_curl _forge_descriptor _forge_designated_release +_forge_explain_failure _forge_highest_version _forge_http_get _forge_is_current diff --git a/lib/forge.func b/lib/forge.func index d4e3de4..0fced73 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -238,7 +238,7 @@ github_api_call() { continue fi msg_error "GitHub API connection failed (no response)." - msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit" + _forge_explain_failure "api.github.com" return 22 ;; *) @@ -1293,6 +1293,35 @@ _forge_curl() { printf '%s' "${code:-000}" } +# The fetch helpers run curl with -s and only see 000. Reproduce the failure +# once with the message visible and say what the exit code points to. +_forge_explain_failure() { + local host="${1:?host}" msg rc v4 + msg="$(curl -sS -o /dev/null --connect-timeout 5 --max-time 15 "https://${host}/" 2>&1)" + rc=$? + msg="$(printf '%s' "$msg" | tail -n1 | sed 's/^curl: ([0-9]*) //')" + [[ -n "$msg" ]] && msg_error "curl: ${msg}" + case "$rc" in + 0) msg_error "${host} answers now; the failure looks transient, run the script again" ;; + 6) msg_error "${host} does not resolve here: check /etc/resolv.conf and whether a DNS filter (Pi-hole, AdGuard) blocks it" ;; + 7 | 28) + v4="$(curl -4 -sS -o /dev/null --connect-timeout 5 --max-time 15 -w '%{http_code}' "https://${host}/" 2>/dev/null)" || v4="000" + if [[ "$v4" != "000" ]] && ip -6 addr show scope global 2>/dev/null | grep -q inet6; then + msg_error "IPv4 reaches ${host}, IPv6 does not: this container has an IPv6 address without a working route. Disable IPv6 for it or fix the route." + elif [[ "$v4" != "000" ]]; then + msg_error "${host} answered an IPv4 probe just now; the failure looks transient, run the script again" + elif ((rc == 28)); then + msg_error "Connection to ${host} timed out: a firewall or proxy is holding the traffic (set HTTP_PROXY if one is required)" + else + msg_error "No route to ${host}: check the gateway, firewall and proxy (HTTP_PROXY)" + fi + ;; + 35 | 60) msg_error "TLS handshake with ${host} failed: check the clock ($(date -u '+%F %T') UTC), ca-certificates, and whether something intercepts TLS" ;; + 56) msg_error "Connection to ${host} was reset: a filtering proxy or a regional block" ;; + *) msg_error "curl exit ${rc} reaching ${host}; try: curl -v https://${host}/" ;; + esac +} + # ------------------------------------------------------------------------------ # _forge_http_get() # @@ -1562,9 +1591,14 @@ _forge_release_json() { if [[ "$ok" != "true" ]]; then if [[ "$code" == "000" ]]; then - msg_error "Connection to ${forge} failed" rm -f "$raw" - return 7 + if ((curl_rc == 0)); then + msg_error "${forge} answered, but not with JSON: a captive portal or a proxy error page is in the way" + return 22 + fi + msg_error "Connection to ${forge} failed" + _forge_explain_failure "$host" + return "$curl_rc" fi msg_error "${forge} API returned HTTP ${code} for ${repo}" rm -f "$raw" diff --git a/tests/forge-connection.sh b/tests/forge-connection.sh new file mode 100644 index 0000000..adc7d50 --- /dev/null +++ b/tests/forge-connection.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_forge_explain_failure\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +LOG="$TEST_DIR/log" +passed=0 failed=0 + +msg_error() { echo "$*" >>"$LOG"; } +# The probe fails like the real transfer did; the -4 probe answers per MOCK_V4. +curl() { + if [[ " $* " == *" -4 "* ]]; then + [[ "${MOCK_V4:-000}" != "000" ]] || return 7 + printf '%s' "$MOCK_V4" + return 0 + fi + [[ -n "${MOCK_MSG:-}" ]] && echo "curl: (${MOCK_RC}) ${MOCK_MSG}" >&2 + return "${MOCK_RC:-0}" +} +ip() { [[ -n "${MOCK_V6:-}" ]] && echo " inet6 2001:db8::1/64 scope global"; } + +run() { + : >"$LOG" + _forge_explain_failure api.github.com +} +expect() { + if grep -qF -- "$2" "$LOG"; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: missing <$2>" >&2 + sed 's/^/ /' "$LOG" >&2 + failed=$((failed + 1)) + fi +} + +MOCK_RC=6 MOCK_MSG="Could not resolve host: api.github.com" run +expect "curl message is shown" "curl: Could not resolve host: api.github.com" +expect "dns failure points at the resolver" "does not resolve" + +MOCK_RC=7 MOCK_MSG="Failed to connect" MOCK_V4=200 MOCK_V6=1 run +expect "v4 works, v6 configured: blames the v6 route" "IPv6 does not" + +MOCK_RC=7 MOCK_MSG="Failed to connect" MOCK_V4=200 run +expect "v4 works, no v6 address: transient" "answered an IPv4 probe" + +MOCK_RC=7 MOCK_MSG="Failed to connect" MOCK_V4=000 run +expect "nothing reachable: no route" "No route to api.github.com" + +MOCK_RC=28 MOCK_MSG="Connection timed out" MOCK_V4=000 run +expect "timeout" "timed out" + +MOCK_RC=35 MOCK_MSG="SSL certificate problem" run +expect "tls" "TLS handshake" + +MOCK_RC=56 MOCK_MSG="Recv failure: Connection reset by peer" run +expect "reset" "was reset" + +MOCK_RC=0 run +expect "probe succeeds: transient" "answers now" + +MOCK_RC=99 MOCK_MSG="something odd" run +expect "unknown code falls back to curl -v" "curl -v https://api.github.com/" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0))