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))