Created
May 4, 2026 15:57
-
-
Save adamamyl/470cd8142fc1d4db8b6c3b819a2743cf to your computer and use it in GitHub Desktop.
find-ubuntu-mirrors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # find-ubuntu-mirror | |
| # | |
| # Detects the host's real public IP via the container default gateway, | |
| # geolocates it, then selects and benchmarks regional Ubuntu mirrors. | |
| # Writes a DEB822-format sources file (.sources) for the running Ubuntu release. | |
| # | |
| # Works under: plain Docker, RunPod, LXC, and bare metal. | |
| # Requires: curl, ip (iproute2), awk, sort, head — all standard on Ubuntu. | |
| set -euo pipefail | |
| # -- config ------------------------------------------------------------------- | |
| # resolved at runtime — see get_codename() | |
| CODENAME="" | |
| readonly COMPONENTS="main restricted universe multiverse" | |
| readonly SOURCES_FILE="/etc/apt/sources.list.d/ubuntu.sources" | |
| readonly BENCHMARK_COUNT=5 # how many mirror candidates to time | |
| readonly BENCHMARK_FILE="ls-lR.gz" # standard large file present on all mirrors | |
| readonly TIMEOUT=5 # curl timeout per request (seconds) | |
| # Akamai's CDN-backed mirror — fast globally, anycast-routed, solid default | |
| readonly AKAMAI_MIRROR="https://ubuntu.mirror.constant.com/ubuntu" | |
| # canonical upstream — last-resort only | |
| readonly FALLBACK_MIRROR="https://archive.ubuntu.com/ubuntu" | |
| # -- helpers ------------------------------------------------------------------ | |
| log() { echo "[mirror] $*" >&2; } | |
| warn() { echo "[mirror] WARN: $*" >&2; } | |
| die() { echo "[mirror] ERROR: $*" >&2; exit 1; } | |
| require() { | |
| local cmd | |
| for cmd in "$@"; do | |
| command -v "$cmd" &>/dev/null || die "required command not found: $cmd" | |
| done | |
| } | |
| # -- codename detection ------------------------------------------------------- | |
| # Resolves the Ubuntu release codename dynamically. | |
| # Tries lsb_release first, then /etc/os-release, then the apt cache. | |
| get_codename() { | |
| local name="" | |
| # lsb_release is the canonical source; may not be installed in minimal images | |
| if command -v lsb_release &>/dev/null; then | |
| name="$(lsb_release -cs 2>/dev/null)" || true | |
| fi | |
| # /etc/os-release is present on virtually every modern Ubuntu image | |
| if [[ -z "$name" && -r /etc/os-release ]]; then | |
| name="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}")" || true | |
| fi | |
| # last resort: scrape from the existing apt sources | |
| if [[ -z "$name" ]]; then | |
| name="$( | |
| grep -rhP '^\s*(deb|Types:)' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null \ | |
| | grep -oP '(?<=ubuntu\s)\w+' \ | |
| | head -n1 | |
| )" || true | |
| fi | |
| [[ -n "$name" ]] || die "could not determine Ubuntu codename — is this Ubuntu?" | |
| echo "$name" | |
| } | |
| # -- gateway / host IP detection ---------------------------------------------- | |
| # Finds the default gateway IP — works in Docker (eth0) and RunPod (various). | |
| get_gateway_ip() { | |
| ip route show default 2>/dev/null \ | |
| | awk '/default via/ { print $3; exit }' | |
| } | |
| # Resolves the *public* IP and country code in a single ipinfo.io call. | |
| # ipinfo.io returns a JSON blob with .ip and .country fields. | |
| # Falls back to plain IP-echo services if ipinfo is unreachable, then | |
| # to the gateway IP itself for a best-effort region guess. | |
| get_ipinfo() { | |
| local gw="$1" | |
| local json="" | |
| json="$(curl -sf --max-time "$TIMEOUT" "https://ipinfo.io/json" 2>/dev/null)" || true | |
| if [[ -n "$json" ]]; then | |
| # parse without jq — ipinfo's JSON is simple enough for awk | |
| local ip country | |
| ip="$(echo "$json" | grep -oP '"ip"\s*:\s*"\K[^"]+' || true)" | |
| country="$(echo "$json" | grep -oP '"country"\s*:\s*"\K[^"]+' || true)" | |
| if [[ -n "$ip" && -n "$country" ]]; then | |
| echo "$ip" "$country" | |
| return | |
| fi | |
| fi | |
| # ipinfo unreachable — fall back to plain IP echo + separate geo lookup | |
| warn "ipinfo.io unreachable; trying fallback IP detection" | |
| local pub_ip="" | |
| local -a echo_services=( | |
| "https://ifconfig.me/ip" | |
| "https://api.ipify.org" | |
| "https://icanhazip.com" | |
| ) | |
| local svc | |
| for svc in "${echo_services[@]}"; do | |
| pub_ip="$(curl -sf --max-time "$TIMEOUT" "$svc" 2>/dev/null | tr -d '[:space:]')" || true | |
| [[ "$pub_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && break | |
| pub_ip="" | |
| done | |
| # try ip-api.com for country if we have a usable IP | |
| local country="" | |
| if [[ -n "$pub_ip" ]]; then | |
| country="$( | |
| curl -sf --max-time "$TIMEOUT" \ | |
| "http://ip-api.com/line/${pub_ip}?fields=countryCode" 2>/dev/null \ | |
| | tr -d '[:space:]' | |
| )" || true | |
| fi | |
| # absolute last resort: gateway IP, GB default | |
| echo "${pub_ip:-$gw}" "${country:-GB}" | |
| } | |
| # -- mirror candidates -------------------------------------------------------- | |
| # Fetches the live mirror list for a country code from mirrors.ubuntu.com. | |
| # Ubuntu maintains per-CC text files at http://mirrors.ubuntu.com/CC.txt — | |
| # one http:// URL per line, geolocation-curated by Canonical. | |
| # Falls back to GB if the country has no list, then to mirrors.txt (global). | |
| fetch_mirror_list() { | |
| local country="${1^^}" | |
| local url="http://mirrors.ubuntu.com/${country}.txt" | |
| local result="" | |
| result="$(curl -sf --max-time "$TIMEOUT" "$url" 2>/dev/null)" || true | |
| if [[ -z "$result" ]]; then | |
| warn "falling back to global mirrors.txt" | |
| result="$(curl -sf --max-time "$TIMEOUT" "http://mirrors.ubuntu.com/mirrors.txt" 2>/dev/null)" || true | |
| fi | |
| # strip blank lines, whitespace, limit to sane count | |
| echo "$result" | awk 'NF { gsub(/[[:space:]]/, ""); print }' | head -20 | |
| } | |
| # Returns a prioritised list of mirror URLs to benchmark. | |
| # Akamai's anycast mirror always leads — fast globally regardless of region. | |
| # Live country mirrors from mirrors.ubuntu.com follow. | |
| # Canonical upstream is the last-resort backstop. | |
| get_mirror_candidates() { | |
| local country="${1^^}" | |
| # Akamai CDN mirror — anycast, globally distributed, always first | |
| local -a mirrors=("$AKAMAI_MIRROR") | |
| # pull live CC list and append | |
| mapfile -t cc_mirrors < <(fetch_mirror_list "$country") | |
| mirrors+=("${cc_mirrors[@]}") | |
| # canonical upstream always last — only reached if everything else times out | |
| mirrors+=("$FALLBACK_MIRROR") | |
| # deduplicate while preserving order | |
| local seen=() | |
| local m | |
| for m in "${mirrors[@]}"; do | |
| [[ " ${seen[*]} " =~ " ${m} " ]] || seen+=("$m") | |
| done | |
| printf '%s\n' "${seen[@]}" | |
| } | |
| # -- benchmarking ------------------------------------------------------------- | |
| # Times a single mirror by fetching a known large index file. | |
| # Prints "milliseconds url" so results can be sorted numerically. | |
| benchmark_mirror() { | |
| local mirror="$1" | |
| local url="${mirror%/}/dists/${CODENAME}/${BENCHMARK_FILE}" | |
| local ms | |
| ms="$( | |
| curl -sf --max-time "$TIMEOUT" \ | |
| -o /dev/null \ | |
| -w '%{time_total}' \ | |
| "$url" 2>/dev/null | |
| )" || { echo "9999 $mirror"; return; } | |
| # convert fractional seconds → integer milliseconds for sorting | |
| printf '%d %s\n' "$(awk "BEGIN { printf \"%d\", $ms * 1000 }")" "$mirror" | |
| } | |
| # Runs benchmarks against the first N candidates, returns the fastest URL. | |
| pick_fastest_mirror() { | |
| local -a candidates=("$@") | |
| local results=() | |
| local i=0 | |
| log "benchmarking up to ${BENCHMARK_COUNT} mirrors..." | |
| local m | |
| for m in "${candidates[@]}"; do | |
| (( i >= BENCHMARK_COUNT )) && break | |
| log " timing: $m" | |
| results+=("$(benchmark_mirror "$m")") | |
| (( i++ )) || true | |
| done | |
| # sort by time ascending, pick winner | |
| local winner | |
| winner="$(printf '%s\n' "${results[@]}" | sort -n | head -n1 | awk '{print $2}')" | |
| echo "$winner" | |
| } | |
| # -- write sources file ------------------------------------------------------- | |
| # Writes a DEB822-format .sources file (replaces the old one-liner .list format). | |
| write_sources_file() { | |
| local mirror="$1" | |
| log "writing DEB822 sources to: $SOURCES_FILE" | |
| cat > "$SOURCES_FILE" <<EOF | |
| # Ubuntu ${CODENAME^} — auto-generated by find-ubuntu-mirror.sh | |
| # Mirror selected based on host geolocation. Regenerate at any time. | |
| Types: deb | |
| URIs: ${mirror} | |
| Suites: ${CODENAME} ${CODENAME}-updates ${CODENAME}-backports | |
| Components: ${COMPONENTS} | |
| Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg | |
| Types: deb | |
| URIs: http://security.ubuntu.com/ubuntu | |
| Suites: ${CODENAME}-security | |
| Components: ${COMPONENTS} | |
| Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg | |
| EOF | |
| } | |
| # -- main --------------------------------------------------------------------- | |
| main() { | |
| require curl ip awk sort head | |
| # resolve codename dynamically | |
| CODENAME="$(get_codename)" | |
| log "Ubuntu codename: $CODENAME" | |
| # detect gateway | |
| local gw | |
| gw="$(get_gateway_ip)" | |
| [[ -n "$gw" ]] || die "no default gateway found — is networking up?" | |
| log "default gateway: $gw" | |
| # resolve public IP and country via ipinfo.io | |
| local public_ip country | |
| read -r public_ip country < <(get_ipinfo "$gw") | |
| log "public IP: $public_ip country: $country" | |
| # get mirror candidates for region | |
| mapfile -t candidates < <(get_mirror_candidates "$country") | |
| log "found ${#candidates[@]} candidate mirror(s)" | |
| # benchmark and pick fastest | |
| local best_mirror | |
| best_mirror="$(pick_fastest_mirror "${candidates[@]}")" | |
| log "fastest mirror: $best_mirror" | |
| # write sources | |
| write_sources_file "$best_mirror" | |
| log "done — run 'apt-get update' to apply" | |
| echo "$best_mirror" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment