Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 37 additions & 3 deletions lib/forge.func
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
*)
Expand Down Expand Up @@ -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()
#
Expand Down Expand Up @@ -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"
Expand Down
69 changes: 69 additions & 0 deletions tests/forge-connection.sh
Original file line number Diff line number Diff line change
@@ -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))
Loading