#!/usr/bin/env bash
#
# remcheck - multithreaded proxy checker for Linux and macOS
# Modes: connect (alive and exit IP), rbl (17 DNSBL zones) and target
# (Google, LinkedIn, Avito and other services or any URL)
#
# Version:    1.3, 2026-10-01 (first release 2026-09-28)
# Home page:  https://remproxy.com/proxy-checker-linux
# Download:   https://remproxy.com/files/remcheck.sh
# Support:    hello@remproxy.com, Telegram @remproxybot
#
# Copyright (c) 2026 RemProxy, https://remproxy.com
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

REMCHECK_VERSION="1.3"
POOL_SRC=""
TARGETS=""
SAMPLE=0
OK_MARK=""
BUILTIN_TARGETS="google youtube bing yandex linkedin instagram facebook x tiktok amazon ebay aliexpress walmart avito ozon wildberries"
BROWSER_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
GEN_CAPTCHA='g-recaptcha|recaptcha/api|hcaptcha\.com|smartcaptcha|showcaptcha|/sorry/index|captcha-delivery\.com|px-captcha|_pxCaptcha|Attention Required! \| Cloudflare'
GEN_HOP='showcaptcha|/sorry/index|captcha|/challenge'
GEN_ANTIBOT='triggerInterstitialChallenge|challenge-platform|cf-chl-|cf_chl_opt|Just a moment\.\.\.|_Incapsula_Resource|Incapsula incident|awsWafCookieDomainList|gokuProps|wbaas/challenges|abt-challenge|Antibot Challenge|errors\.edgesuite\.net|firewallPow'
MODE="connect"
LANES=64
WAIT_S=10
PROTO="http"
ECHO_URL="https://ipv4.icanhazip.com,https://api.ipify.org,https://checkip.amazonaws.com,https://ifconfig.me/ip,https://ipinfo.io/ip"
TRIES=2
DEST=""
QUIET=0
FORCE_CONNECT=0
PANEL_SUBS="https://panel.remproxy.com/subscriptions"
REMPROXY_LISTS="https://proxy.remproxy.com"
SUB_ID_FORM='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}([.][Cc][Ss][Vv])?$'
DNSBL_ZONES="bl.spamcop.net b.barracudacentral.org psbl.surriel.com dnsbl-1.uceprotect.net dnsbl-2.uceprotect.net dnsbl.dronebl.org bl.blocklist.de bl.mailspike.net truncate.gbudb.net dnsbl.justspam.org bl.nordspam.com bl.0spam.org rbl.interserver.net all.s5h.net dnsbl.spfbl.net bl.suomispam.net dnsrbl.swinog.ch"
WIDE_ZONES="dnsbl-2.uceprotect.net"
SOFT_CODES="dnsbl.spfbl.net=127.0.0.3 dnsbl.spfbl.net=127.0.0.4 dnsbl.spfbl.net=127.0.0.5"
DOH_A="https://dns.google/resolve"
DOH_B="https://cloudflare-dns.com/dns-query"
RBL_LANES=128
RBL_LANES_MAX=256
LANES_SET=0
COLON_FORM='^[^:@/]+:[0-9]+:.+'
IPV4_FORM='^([0-9]{1,3}\.){3}[0-9]{1,3}$'

usage() {
  cat <<'TXT'
remcheck - multithreaded proxy checker for Linux and macOS
(Windows: remcheck.ps1, https://remproxy.com/proxy-checker-windows)

Usage: remcheck.sh -f LIST [options]

  -f LIST   proxy list: file, http(s) URL, - for stdin, or a RemProxy
            subscription ID with or without .csv, for example
            -f 0f8fad5b-d9cb-469f-a165-70867728950e.csv; in rbl mode a bare
            ID takes the .csv list to read exit IPs without connecting
  -m MODE   connect (default), rbl or target
  -t LIST   target mode: services and URLs separated by commas, for example
            -t google,linkedin,https://example.com/page or -t all
  -k TEXT   target mode: a page of your own URL counts as working only
            when it contains TEXT
  -S NUM    connect and target modes: check a random sample of NUM proxies
  -n NUM    proxies checked at once, default 64; in rbl mode also the
            number of proxies looked up at once, default 128, up to 256
  -T SEC    timeout per proxy in seconds, default 10; -Timeout SEC works
            too, the same name as in remcheck.ps1
  -P PROTO  http (default), socks5 or socks4 for lines without a scheme
  -e URLS   IP echo pages opened through the proxies, separated by commas;
            each must answer with the caller IP. Default: ipv4.icanhazip.com,
            api.ipify.org, checkip.amazonaws.com, ifconfig.me/ip, ipinfo.io/ip.
            Every proxy starts on its own page, so the load spreads out
  -r NUM    attempts per proxy in connect mode, default 2, up to 5: when the
            echo page fails, answers with an error or without an IP, the
            next page is tried; a proxy that refuses the connection, rejects
            the password or stays silent is not retried
  -d DIR    folder for results, default ./remcheck-DATE-TIME
  -C        rbl mode with a RemProxy CSV: connect through every proxy anyway
            instead of taking exit IPs from the real_ip column
  -q        no progress line
  -V        print version
  -h        this help

List lines, one proxy per line:
  host:port
  host:port:user:pass
  user:pass@host:port
  socks5://user:pass@host:port
  ip;port;country;speed;uptime;real_ip    (RemProxy CSV)

Modes:
  connect   opens the page through every proxy, keeps the ones that answer
  rbl       looks up the exit IP of every proxy in 17 DNSBL zones over
            DNS-over-HTTPS (Google and Cloudflare); exit IPs come from the
            real_ip column of a RemProxy CSV, for other lists from a
            connect pass first
  target    opens every service through every proxy with browser headers
            and sorts the answers: ok, captcha (the site wants this IP to
            solve a captcha), antibot (a JavaScript browser check that no
            script passes from any IP), blocked (403, 451, LinkedIn 999),
            limit (429), noanswer (the proxy did not answer), other.
            First each service is opened once from this machine without
            a proxy: when that also fails, the site rejects the client,
            not the proxy IPs

Services for -t: google youtube bing yandex linkedin instagram facebook x
  tiktok amazon ebay aliexpress walmart avito ozon wildberries; -t all for
  every one; any URL, also with a name: -t shop=https://example.com/

Files in DIR:
  alive.txt     working proxies, fastest first (connect pass)
  dead.txt      proxies that did not answer (connect pass)
  results.tsv   one row per proxy with exit IP, time or CSV data, RBL columns;
                no_answer lists zones that neither resolver answered, the IP
                counts as clean for them
  clean.txt     rbl mode: proxies whose exit IP is in no list
  listed.txt    rbl mode: proxies whose exit IP is listed
  ok-NAME.txt   target mode: proxies that opened service NAME, fastest first
  summary.txt   target mode: the table printed at the end

Exit code: 0 success, 1 no working or clean proxy, 2 usage error,
3 the list server rejected this machine (IP not in the whitelist).
TXT
}

die() {
  printf 'remcheck: %s\n' "$1" >&2
  exit 2
}

is_num() {
  case "$1" in '' | *[!0-9]*) return 1 ;; esac
  [ "$1" -gt 0 ]
}

lower() {
  printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
}

pct() {
  awk -v a="$1" -v b="$2" 'BEGIN { if (b > 0) printf "%.1f", a * 100 / b; else printf "0.0" }'
}

need_tools() {
  local miss="" t os=""
  for t in curl awk sed grep sort cut tr wc mktemp mkfifo date head; do
    command -v "$t" >/dev/null 2>&1 || miss="$miss $t"
  done
  [ -z "$miss" ] && return 0
  printf 'remcheck: missing tools:%s\n' "$miss" >&2
  [ -r /etc/os-release ] && os=$(. /etc/os-release; printf '%s %s' "${ID:-}" "${ID_LIKE:-}")
  case "$os" in
    *alpine*) printf 'Install them with: apk add bash curl coreutils gawk grep sed\n' >&2 ;;
    *debian* | *ubuntu*) printf 'Install them with: sudo apt-get update && sudo apt-get install -y curl gawk coreutils grep sed\n' >&2 ;;
    *rhel* | *fedora* | *centos* | *rocky* | *alma*) printf 'Install them with: sudo dnf install -y curl gawk coreutils grep sed\n' >&2 ;;
    *arch*) printf 'Install them with: sudo pacman -S --needed curl gawk coreutils grep sed\n' >&2 ;;
    *) printf 'Install them with the package manager of your system.\n' >&2 ;;
  esac
  exit 2
}

whitelist_guard() {
  local text seen allowed
  text=$(head -c 2000 "$1" | tr -d '\r')
  case "$text" in
    *"not authorized"* | *"Not authorized"*) ;;
    *) return 0 ;;
  esac
  seen=$(printf '%s\n' "$text" | awk '{
    i = index(tolower($0), "not authorized"); if (!i) next
    s = substr($0, i + 14)
    if (match(s, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) || match(s, /[0-9a-fA-F]*:[0-9a-fA-F:]+/)) { print substr(s, RSTART, RLENGTH); exit }
  }')
  allowed=$(printf '%s\n' "$text" | awk '{
    i = index(tolower($0), "allowed only from:"); if (!i) next
    s = substr($0, i + 18); sub(/^[ \t]+/, "", s); print s; exit
  }')
  [ -n "$seen" ] || seen="?"
  [ -n "$allowed" ] || allowed="-"
  printf '\nremcheck: the list server rejected this machine, its IP is not in the subscription whitelist.\n' >&2
  printf '  IP of this machine:        %s\n  Allowed IPs of the plan:   %s\n\n' "$seen" "$allowed" >&2
  printf 'Open %s, choose this subscription,\nadd %s to Allowed IPs in the Access section and run the check again.\n\n' "$PANEL_SUBS" "$seen" >&2
  exit 3
}

for arg in "$@"; do
  shift
  case "$(lower "$arg")" in
    -timeout | --timeout) set -- "$@" -T ;;
    -timeout=* | --timeout=*) set -- "$@" -T "${arg#*=}" ;;
    *) set -- "$@" "$arg" ;;
  esac
done

while getopts ":f:m:n:T:P:e:r:d:t:k:S:CqVh" opt; do
  case "$opt" in
    f) POOL_SRC="$OPTARG" ;;
    m) MODE=$(lower "$OPTARG") ;;
    n) LANES="$OPTARG"; LANES_SET=1 ;;
    T) WAIT_S="$OPTARG" ;;
    P) PROTO=$(lower "$OPTARG") ;;
    e) ECHO_URL="$OPTARG" ;;
    r) TRIES="$OPTARG" ;;
    d) DEST="$OPTARG" ;;
    t) TARGETS="$OPTARG" ;;
    k) OK_MARK="$OPTARG" ;;
    S) SAMPLE="$OPTARG" ;;
    C) FORCE_CONNECT=1 ;;
    q) QUIET=1 ;;
    V) printf 'remcheck %s\n' "$REMCHECK_VERSION"; exit 0 ;;
    h) usage; exit 0 ;;
    :) die "option -$OPTARG needs a value" ;;
    *) die "unknown option -$OPTARG, see -h" ;;
  esac
done

[ -n "$POOL_SRC" ] || { usage >&2; exit 2; }
need_tools
case "$MODE" in connect | rbl | target) ;; *) die "mode must be connect, rbl or target" ;; esac
[ "$MODE" = "target" ] && [ -z "$TARGETS" ] && die "target mode needs -t, for example -t google,linkedin or -t all, see -h"
[ "$SAMPLE" = 0 ] || is_num "$SAMPLE" || die "-S needs a positive number"
case "$PROTO" in http | https | socks5 | socks4) ;; *) die "proto must be http, https, socks5 or socks4" ;; esac
is_num "$LANES" || die "-n needs a positive number"
is_num "$WAIT_S" || die "-T needs a positive number"
[ "$LANES" -gt 1000 ] && LANES=1000
is_num "$TRIES" || die "-r needs a positive number"
[ "$TRIES" -gt 5 ] && TRIES=5
JUDGES=()
for j in $(printf '%s' "$ECHO_URL" | tr ',' ' '); do
  case "$j" in http://* | https://*) JUDGES+=("$j") ;; *) die "-e takes http(s) URLs separated by commas, got $j" ;; esac
done
[ "${#JUDGES[@]}" -gt 0 ] || die "-e needs at least one URL"
[ -t 2 ] || QUIET=1

WORK=$(mktemp -d "${TMPDIR:-/tmp}/remcheck.XXXXXX") || die "cannot create a temp folder"
TICKER=""
finish() {
  [ -n "$TICKER" ] && kill "$TICKER" 2>/dev/null
  rm -rf "$WORK" 2>/dev/null
}
PHASE=""
STOPPED=0
stopped() {
  trap '' INT TERM
  [ -n "$TICKER" ] && kill "$TICKER" 2>/dev/null
  TICKER=""
  kill $(jobs -p) 2>/dev/null
  STOPPED=1
  printf '\r%60s\r' '' >&2
  if [ -z "$PHASE" ]; then
    printf 'remcheck: stopped before any proxy was checked\n' >&2
    exit 130
  fi
  printf 'remcheck: stopped, saving what was checked\n' >&2
  case "$PHASE" in
    csv) finish_rbl_csv ;;
    target) finish_target ;;
    *) finish_connect ;;
  esac
  exit 130
}
trap finish EXIT
trap stopped INT TERM

if [ ! -e "$POOL_SRC" ] && [[ $POOL_SRC =~ $SUB_ID_FORM ]]; then
  SUB_ID="$POOL_SRC"
  case "$SUB_ID" in
    *.[Cc][Ss][Vv]) SUB_ID="${SUB_ID%.*}.csv" ;;
    *) [ "$MODE" = "rbl" ] && [ "$FORCE_CONNECT" = 0 ] && SUB_ID="$SUB_ID.csv" ;;
  esac
  POOL_SRC="$REMPROXY_LISTS/$SUB_ID"
  [ "$QUIET" = 1 ] || printf 'remcheck: list %s\n' "$POOL_SRC" >&2
fi

case "$POOL_SRC" in
  http://* | https://*)
    HTTP_CODE=$(curl -sSL --max-time 90 -o "$WORK/src" -w '%{http_code}' "$POOL_SRC") || die "cannot download $POOL_SRC"
    whitelist_guard "$WORK/src"
    case "$HTTP_CODE" in 2??) ;; *) die "the list link answered with HTTP $HTTP_CODE" ;; esac
    ;;
  -) cat > "$WORK/src" ;;
  *) [ -r "$POOL_SRC" ] || die "cannot read $POOL_SRC"; cat "$POOL_SRC" > "$WORK/src" ;;
esac
whitelist_guard "$WORK/src"

: > "$WORK/csvmap"
tr -d '\r' < "$WORK/src" | awk -F';' -v map="$WORK/csvmap" '
  { sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
  $0 == "" || substr($0, 1, 1) == "#" { next }
  NF >= 2 {
    if ($2 !~ /^[0-9]+$/) next
    key = $1 ":" $2
    if (seen[key]++) next
    print key
    if (NF >= 6 && $6 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) print key "\t" $6 "\t" $3 "\t" $4 "\t" $5 > map
    next
  }
  { if (!seen[$0]++) print }
' > "$WORK/pool"

TOTAL=$(wc -l < "$WORK/pool" | tr -d ' ')
[ "$TOTAL" -gt 0 ] || die "no proxies found in $POOL_SRC"
FULL=$TOTAL
[ "$MODE" = "rbl" ] && [ "$SAMPLE" -gt 0 ] && printf 'remcheck: -S works in connect and target modes, rbl checks the whole list\n' >&2
if [ "$MODE" != "rbl" ] && [ "$SAMPLE" -gt 0 ] && [ "$SAMPLE" -lt "$TOTAL" ]; then
  awk 'BEGIN { srand() } { printf "%.9f\t%s\n", rand(), $0 }' "$WORK/pool" | sort -n | head -n "$SAMPLE" | cut -f2- > "$WORK/pool.s"
  mv "$WORK/pool.s" "$WORK/pool"
  TOTAL=$SAMPLE
fi
MAPPED=$(wc -l < "$WORK/csvmap" | tr -d ' ')
CSV_FAST=0
[ "$MODE" = "rbl" ] && [ "$FORCE_CONNECT" = 0 ] && [ "$MAPPED" = "$TOTAL" ] && CSV_FAST=1
[ -n "$DEST" ] || DEST="remcheck-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$DEST" || die "cannot create $DEST"
: > "$WORK/raw"
: > "$WORK/rblraw"

probe_one() {
  local spec="$1" n="${#JUDGES[@]}" idx digits try=0 url out tail body code tun app secs rc ip ms="-" state="dead" judge="-" got_ip=""
  parse_spec "$spec"
  digits="${spec//[!0-9]/}"
  [ ${#digits} -gt 2 ] && digits="${digits:${#digits}-2}"
  idx=$(( (10#${digits:-0} + ${#spec}) % n ))
  while [ "$try" -lt "$TRIES" ]; do
    try=$((try + 1))
    url="${JUDGES[$idx]}"
    idx=$(( (idx + 1) % n ))
    set -- -s -o - -w '\n%{http_code} %{http_connect} %{time_appconnect} %{time_total}' --max-time "$WAIT_S" --connect-timeout "$WAIT_S" \
      -A "remcheck/$REMCHECK_VERSION" -x "$P_SCHEME://$P_HOST"
    [ -n "$P_CRED" ] && set -- "$@" --proxy-user "$P_CRED"
    out=$(curl "$@" "$url" 2>/dev/null)
    rc=$?
    tail="${out##*$'\n'}"
    body="${out%$'\n'*}"
    code="${tail%% *}"
    tail="${tail#* }"
    tun="${tail%% *}"
    tail="${tail#* }"
    app="${tail%% *}"
    secs="${tail#* }"
    ip=$(printf '%s' "$body" | tr -d ' \r\n\t' | grep -Eo '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^[0-9A-Fa-f:]{3,39}$' | head -n 1)
    case "$code" in
      2?? | 3??)
        if [ "$state" != "ok" ] || [ -n "$ip" ]; then
          state="ok"
          got_ip="$ip"
          ms=$(awk -v s="$secs" 'BEGIN { printf "%d", s * 1000 }')
          judge="${url#*://}"
          judge="${judge%%/*}"
        fi
        [ -n "$ip" ] && break
        continue
        ;;
    esac
    case "$rc" in 5 | 7 | 97) break ;; esac
    [ "$tun" = "407" ] && break
    if [ "$state" != "ok" ] && [ "$rc" = 28 ] && [ "$code" = "000" ]; then
      case "$app" in 0 | 0.000000 | "") break ;; esac
    fi
  done
  printf '%s\t%s\t%s\t%s\t%s\n' "$state" "$spec" "${got_ip:--}" "$ms" "$judge" >> "$WORK/raw"
}

parse_spec() {
  local spec="$1" rest="$1" a b c d
  P_SCHEME="$PROTO"
  P_CRED=""
  case "$spec" in
    *://*) P_SCHEME="${spec%%://*}"; rest="${spec#*://}" ;;
  esac
  if [[ $rest =~ $COLON_FORM ]]; then
    IFS=: read -r a b c d <<< "$rest"
    P_HOST="$a:$b"
    P_CRED="$c:$d"
  elif [[ $rest == *@* ]]; then
    P_CRED="${rest%@*}"
    P_HOST="${rest##*@}"
  else
    P_HOST="$rest"
  fi
  case "$P_SCHEME" in
    socks5 | socks5h) P_SCHEME="socks5h" ;;
    socks4 | socks4a) P_SCHEME="socks4a" ;;
    https) P_SCHEME="https" ;;
    *) P_SCHEME="http" ;;
  esac
}

target_def() {
  T_URL="$2"
  T_HOP=""
  T_CAPTCHA=""
  T_LIMIT=""
  T_BLOCK=""
  T_ANTIBOT=""
  T_WALL=""
  T_CUSTOM=0
  if [ -n "$2" ]; then
    T_CUSTOM=1
    return 0
  fi
  case "$1" in
    google) T_URL="https://www.google.com/search?q=weather&hl=en"; T_HOP="/sorry/"; T_CAPTCHA='unusual traffic from your computer|id="captcha-form"' ;;
    youtube) T_URL="https://www.youtube.com/results?search_query=weather"; T_HOP="/sorry/"; T_CAPTCHA="unusual traffic from your computer" ;;
    bing) T_URL="https://www.bing.com/search?q=weather"; T_HOP="/turing/"; T_CAPTCHA="b_captcha|turing/captcha" ;;
    yandex) T_URL="https://yandex.ru/search/?text=weather"; T_HOP="showcaptcha|checkcaptcha"; T_CAPTCHA="showcaptcha|smartcaptcha" ;;
    linkedin) T_URL="https://www.linkedin.com/jobs/search?keywords=manager"; T_HOP="checkpoint/challenge|login-captcha"; T_CAPTCHA="captcha-internal"; T_WALL="authwall|/login|/signup" ;;
    instagram) T_URL="https://www.instagram.com/"; T_HOP="/challenge/"; T_LIMIT="Please wait a few minutes before you try again" ;;
    facebook) T_URL="https://www.facebook.com/"; T_HOP="/checkpoint/" ;;
    x) T_URL="https://x.com/" ;;
    tiktok) T_URL="https://www.tiktok.com/"; T_CAPTCHA="captcha_container|secsdk-captcha" ;;
    amazon) T_URL="https://www.amazon.com/"; T_HOP="validateCaptcha"; T_CAPTCHA="validateCaptcha|Type the characters you see|opfcaptcha"; T_ANTIBOT="awsWafCookieDomainList|gokuProps|challenge-container|triggerInterstitialChallenge" ;;
    ebay) T_URL="https://www.ebay.com/"; T_HOP="splashui/challenge|/captcha"; T_CAPTCHA="splashui/challenge"; T_ANTIBOT='Error Page [|] eBay' ;;
    aliexpress) T_URL="https://www.aliexpress.com/"; T_HOP="punish|_____tmd_____"; T_CAPTCHA="x5secdata|_____tmd_____/punish|baxia-punish" ;;
    walmart) T_URL="https://www.walmart.com/"; T_HOP="/blocked"; T_CAPTCHA="Robot or human" ;;
    avito) T_URL="https://www.avito.ru/"; T_BLOCK="js-firewall-form"; T_ANTIBOT="firewall-container|firewallPow" ;;
    ozon) T_URL="https://www.ozon.ru/"; T_ANTIBOT="Antibot Challenge|abt-challenge|fab_chlg_|incidentId" ;;
    wildberries) T_URL="https://www.wildberries.ru/"; T_ANTIBOT="wbaas|antibot" ;;
    *) return 1 ;;
  esac
}

build_targets() {
  local item name url list=""
  set -f
  for item in $(printf '%s' "$TARGETS" | tr ',' ' '); do
    case "$(lower "$item")" in
      all) list="$list $BUILTIN_TARGETS" ;;
      *) list="$list $item" ;;
    esac
  done
  : > "$WORK/targets"
  for item in $list; do
    case "$item" in
      http://* | https://*)
        url="$item"
        name=$(printf '%s' "$url" | sed -E 's#^https?://##; s#^www\.##; s#[/?\#:].*$##')
        ;;
      *=http://* | *=https://*)
        name="${item%%=*}"
        url="${item#*=}"
        ;;
      *)
        name=$(lower "$item")
        url=""
        target_def "$name" "" || die "unknown service $item, built-in ones: $BUILTIN_TARGETS"
        ;;
    esac
    name=$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_')
    [ -n "$name" ] || die "cannot make a name for $item, use name=URL"
    awk -F'\t' -v n="$name" '$1 == n { f = 1 } END { exit !f }' "$WORK/targets" && continue
    printf '%s\t%s\n' "$name" "$url" >> "$WORK/targets"
  done
  set +f
  [ -s "$WORK/targets" ] || die "-t lists no services"
}

curl_reason() {
  case "$1" in
    5) printf 'cannot resolve the proxy host' ;;
    6) printf 'the site address does not resolve' ;;
    7) printf 'cannot connect to the proxy' ;;
    35) printf 'TLS handshake failed' ;;
    52) printf 'empty reply' ;;
    56) printf 'connection reset' ;;
    60) printf 'untrusted TLS certificate, the exit may intercept HTTPS' ;;
    97) printf 'SOCKS handshake failed' ;;
    *) printf 'curl error %s' "$1" ;;
  esac
}

body_sig() {
  LC_ALL=C grep -aoiE "$2" "$1" 2>/dev/null | head -n 1 | tr -d '\t\r\n' | cut -c1-60
}

target_hit() {
  local name="$1" url="$2" spec="$3" body out code tunnel pre secs size final ms verdict="" detail="" hit=""
  target_def "$name" "$url"
  body=$(mktemp "$WORK/b.XXXXXX") || return 0
  set -- -s -L -b "" --compressed --max-redirs 10 --max-filesize 3000000 --max-time "$WAIT_S" --connect-timeout "$WAIT_S" \
    -A "$BROWSER_UA" -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
    -H 'Accept-Language: en-US,en;q=0.9,ru;q=0.8' -H 'Upgrade-Insecure-Requests: 1' \
    -H 'Sec-Fetch-Dest: document' -H 'Sec-Fetch-Mode: navigate' -H 'Sec-Fetch-Site: none' -H 'Sec-Fetch-User: ?1' \
    -H 'sec-ch-ua: "Chromium";v="129", "Google Chrome";v="129", "Not=A?Brand";v="24"' -H 'sec-ch-ua-mobile: ?0' \
    -H 'sec-ch-ua-platform: "Windows"' \
    -o "$body" -w '%{http_code} %{http_connect} %{time_pretransfer} %{time_total} %{size_download} %{url_effective}'
  if [ "$spec" != "-" ]; then
    parse_spec "$spec"
    set -- "$@" -x "$P_SCHEME://$P_HOST"
    [ -n "$P_CRED" ] && set -- "$@" --proxy-user "$P_CRED"
  fi
  out=$(curl "$@" "$T_URL" 2>/dev/null)
  LAST_RC=$?
  case "$LAST_RC" in 5 | 7 | 97) LAST_DEAD=1 ;; esac
  [ -n "$out" ] || out="000 000 0 0 0 -"
  code=${out%% *}
  out=${out#* }
  tunnel=${out%% *}
  out=${out#* }
  pre=${out%% *}
  out=${out#* }
  secs=${out%% *}
  out=${out#* }
  size=${out%% *}
  final=${out#* }
  ms=$(awk -v s="$secs" 'BEGIN { printf "%d", s * 1000 }')
  case "$size" in '' | *[!0-9]*) size=0 ;; esac
  if [ "$tunnel" = "407" ]; then
    verdict="noanswer"; detail="proxy auth failed (407)"
    LAST_DEAD=1
  elif [ "$code" = "000" ] && [ -n "$tunnel" ] && [ "$tunnel" != "000" ] && [ "$tunnel" != "200" ]; then
    verdict="noanswer"; detail="proxy refused the tunnel ($tunnel)"
  elif [ "$code" = "000" ] && [ "$LAST_RC" = 28 ]; then
    verdict="noanswer"; detail="timeout after ${WAIT_S}s"
    case "$pre" in 0 | 0.000000 | "") LAST_SILENT=1; detail="timeout after ${WAIT_S}s, nothing came through the proxy" ;; esac
  elif [ "$code" = "000" ]; then
    verdict="noanswer"; detail="$(curl_reason "$LAST_RC") (curl $LAST_RC)"
  elif [ "$code" = "407" ]; then
    verdict="noanswer"; detail="proxy auth failed (407)"
    LAST_DEAD=1
  elif [ -n "$T_HOP" ] && printf '%s' "$final" | LC_ALL=C grep -qiE "$T_HOP"; then
    verdict="captcha"; detail="redirect to the captcha page"
  fi
  if [ -z "$verdict" ] && [ -n "$T_WALL" ] && printf '%s' "$final" | LC_ALL=C grep -qiE "$T_WALL"; then
    verdict="blocked"; detail="sent to the login page"
  fi
  if [ -z "$verdict" ] && [ -n "$T_CAPTCHA" ]; then hit=$(body_sig "$body" "$T_CAPTCHA"); [ -n "$hit" ] && { verdict="captcha"; detail="$hit"; }; fi
  if [ -z "$verdict" ] && [ -n "$T_LIMIT" ]; then hit=$(body_sig "$body" "$T_LIMIT"); [ -n "$hit" ] && { verdict="limit"; detail="$hit"; }; fi
  if [ -z "$verdict" ] && [ -n "$T_BLOCK" ]; then hit=$(body_sig "$body" "$T_BLOCK"); [ -n "$hit" ] && { verdict="blocked"; detail="IP block page"; }; fi
  if [ -z "$verdict" ] && [ -n "$T_ANTIBOT" ]; then hit=$(body_sig "$body" "$T_ANTIBOT"); [ -n "$hit" ] && { verdict="antibot"; detail="$hit"; }; fi
  if [ -z "$verdict" ] && printf '%s' "$final" | LC_ALL=C grep -qiE "$GEN_HOP"; then
    verdict="captcha"; detail="redirect to a captcha page"
  fi
  if [ -z "$verdict" ]; then
    case "$code" in
      999) verdict="blocked"; detail="http 999" ;;
      498) verdict="antibot"; detail="http 498" ;;
    esac
  fi
  if [ -z "$verdict" ]; then
    hit=""
    case "$code" in
      2??) [ "$size" -lt 60000 ] && hit=$(body_sig "$body" "$GEN_CAPTCHA|$GEN_ANTIBOT") ;;
      *) hit=$(body_sig "$body" "$GEN_CAPTCHA|$GEN_ANTIBOT") ;;
    esac
    if [ -n "$hit" ]; then
      if printf '%s' "$hit" | LC_ALL=C grep -qiE "$GEN_ANTIBOT"; then verdict="antibot"; else verdict="captcha"; fi
      detail="$hit"
    fi
  fi
  if [ -z "$verdict" ]; then
    case "$code" in
      2??)
        verdict="ok"
        if [ "$T_CUSTOM" = 1 ] && [ -n "$OK_MARK" ] && ! LC_ALL=C grep -qaF -- "$OK_MARK" "$body"; then
          verdict="other"; detail="text from -k not found"
        fi
        ;;
      429) verdict="limit"; detail="http 429" ;;
      401 | 403 | 451) verdict="blocked"; detail="http $code" ;;
      3??) verdict="other"; detail="redirect loop" ;;
      *) verdict="other"; detail="http $code" ;;
    esac
  fi
  rm -f "$body"
  printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$spec" "$name" "$verdict" "$code" "$ms" "$detail"
}

proxy_relays() {
  local code
  set -- -s -o /dev/null -w '%{http_code}' --max-time "$WAIT_S" --connect-timeout "$WAIT_S" -A "remcheck/$REMCHECK_VERSION" -x "$P_SCHEME://$P_HOST"
  [ -n "$P_CRED" ] && set -- "$@" --proxy-user "$P_CRED"
  code=$(curl "$@" "${JUDGES[0]}" 2>/dev/null)
  [ -n "$code" ] && [ "$code" != "000" ]
}

target_one() {
  local spec="$1" name url answered=0 probed=0
  PROXY_DOWN=0
  while IFS="$TAB" read -r name url; do
    if [ "$PROXY_DOWN" = 1 ]; then
      printf '%s\t%s\tnoanswer\t000\t0\tskipped, the proxy is down\n' "$spec" "$name" >> "$WORK/raw"
      continue
    fi
    LAST_SILENT=0
    LAST_DEAD=0
    target_hit "$name" "$url" "$spec" >> "$WORK/raw"
    if [ "$LAST_DEAD" = 1 ]; then
      [ "$answered" = 0 ] && PROXY_DOWN=1
    elif [ "$LAST_SILENT" != 1 ]; then
      answered=1
    elif [ "$answered" = 0 ] && [ "$probed" = 0 ]; then
      probed=1
      if proxy_relays; then answered=1; else PROXY_DOWN=1; fi
    fi
  done < "$WORK/targets"
}

doh_batch() {
  local base="$1" names name
  shift
  names="$*"
  set --
  for name in $names; do
    set -- "$@" "$base?name=$name&type=A"
  done
  curl -s --connect-timeout 5 --max-time 8 -H 'accept: application/dns-json' -w '\n' "$@" 2>/dev/null |
    awk '{
      gsub(/"Status" ?:/, "\001&")
      n = split($0, part, "\001")
      for (i = 2; i <= n; i++) {
        p = part[i]
        if (!match(p, /"Status" ?: ?[0-9]+/)) continue
        st = substr(p, RSTART, RLENGTH); gsub(/[^0-9]/, "", st)
        if (st != "0" && st != "3") continue
        if (!match(p, /"name" ?: ?"[^"]+"/)) continue
        nm = tolower(substr(p, RSTART, RLENGTH)); sub(/^"name" ?: ?"/, "", nm); sub(/"$/, "", nm); sub(/\.$/, "", nm)
        code = ""
        if (st == "0" && match(p, /"data" ?: ?"127\.[0-9.]+"/)) { code = substr(p, RSTART, RLENGTH); sub(/^"data" ?: ?"/, "", code); sub(/"$/, "", code) }
        print nm "\t" code
      }
    }'
}

rbl_group() {
  local key="${1%% *}" ips="${1##* }" ip o1 o2 o3 o4 zone names="" first="$DOH_A" second="$DOH_B" got left
  for ip in $ips; do
    IFS=. read -r o1 o2 o3 o4 <<< "$ip"
    for zone in $DNSBL_ZONES; do
      names="$names $o4.$o3.$o2.$o1.$zone"
    done
  done
  if [ $((10#$o4 % 2)) -eq 1 ]; then
    first="$DOH_B"
    second="$DOH_A"
  fi
  got=$(doh_batch "$first" $names)
  left=$(printf '%s\n' "$got" | awk -F'\t' -v names="$names" '
    { seen[$1] = 1 }
    END { n = split(names, z, " "); for (i = 1; i <= n; i++) if (!(z[i] in seen)) printf "%s ", z[i] }')
  [ -n "$left" ] && got="$got
$(doh_batch "$second" $left)"
  printf '%s\n' "$got" | awk -F'\t' -v key="$key" -v ips="$ips" -v zones="$DNSBL_ZONES" -v widez=" $WIDE_ZONES " -v soft=" $SOFT_CODES " '
    $1 != "" { seen[$1] = 1; code[$1] = $2 }
    END {
      m = split(ips, list, " ")
      n = split(zones, z, " ")
      for (j = 1; j <= m; j++) {
        split(list[j], o, ".")
        rev = o[4] "." o[3] "." o[2] "." o[1]
        hits = ""; wide = ""; miss = ""
        for (i = 1; i <= n; i++) {
          k = z[i]
          nm = rev "." k
          if (!(nm in seen)) { miss = miss " " k; continue }
          c = code[nm]
          if (c == "" || c ~ /^127\.255\.255\./) continue
          if (index(widez, " " k " ") || index(soft, " " k "=" c " ")) wide = wide " " k
          else hits = hits " " k
        }
        sub(/^ /, "", hits); sub(/^ /, "", wide); sub(/^ /, "", miss)
        printf "%s\t%s\t%s\t%s\n", key, hits, wide, miss
      }
    }' >> "$WORK/rblraw"
}

run_lanes() {
  local list="$1" job="$2" width="$3" gate="$WORK/gate.$2" i item
  mkfifo "$gate" || die "cannot create a fifo in $WORK"
  exec 7<> "$gate"
  rm -f "$gate"
  i=0
  while [ "$i" -lt "$width" ]; do
    printf '.' >&7
    i=$((i + 1))
  done
  while IFS= read -r item || [ -n "$item" ]; do
    read -r -n 1 -u 7 _
    { "$job" "$item"; printf '.' >&7; } &
  done < "$list"
  wait
  exec 7>&-
}

ticker() {
  local file="$1" total="$2" label="$3" done_n alive_n
  while :; do
    done_n=$(wc -l < "$file" | tr -d ' ')
    alive_n=$(grep -c '^ok' "$file" 2>/dev/null)
    if [ "$label" = "rbl" ]; then
      printf '\r  looked up %s/%s   ' "$done_n" "$total" >&2
    elif [ "$label" = "target" ]; then
      alive_n=$(awk -F'\t' '$3 == "ok"' "$file" | wc -l | tr -d ' ')
      printf '\r  requests %s/%s, opened %s   ' "$done_n" "$total" "$alive_n" >&2
    else
      printf '\r  checked %s/%s, alive %s   ' "$done_n" "$total" "$alive_n" >&2
    fi
    sleep 1
  done
}

start_ticker() {
  [ "$QUIET" = 1 ] && return
  ticker "$@" &
  TICKER=$!
  disown "$TICKER" 2>/dev/null
}

stop_ticker() {
  [ -n "$TICKER" ] || return
  kill "$TICKER" 2>/dev/null
  TICKER=""
  printf '\r%60s\r' '' >&2
}

rbl_run() {
  local ips="$1" width=$RBL_LANES
  [ "$LANES_SET" = 1 ] && width=$LANES
  [ "$width" -gt "$RBL_LANES_MAX" ] && width=$RBL_LANES_MAX
  IPS=$(wc -l < "$ips" | tr -d ' ')
  [ "$IPS" -gt 0 ] || return 0
  start_ticker "$WORK/rblraw" "$IPS" rbl
  run_lanes "$ips" rbl_group "$width"
  stop_ticker
}

rbl_merge() {
  [ -f "$WORK/ips" ] || : > "$WORK/ips"
  awk -F'\t' -v OFS='\t' -v zones="$DNSBL_ZONES" '
    function addset(arr, ip, list,   n, z, j) { n = split(list, z, " "); for (j = 1; j <= n; j++) arr[ip SUBSEP z[j]] = 1 }
    FILENAME == ARGV[1] { split($0, a, " "); ipk[a[1]] = a[2]; next }
    {
      k = $1; i = ipk[k]; order[++m] = k; cnt[i]++
      addset(H, i, $2); addset(W, i, $3)
      n = split($4, z, " "); for (j = 1; j <= n; j++) G[i SUBSEP z[j]]++
    }
    END {
      nz = split(zones, Z, " ")
      for (r = 1; r <= m; r++) {
        k = order[r]; i = ipk[k]; h = ""; w = ""; g = ""
        for (q = 1; q <= nz; q++) {
          zn = Z[q]
          if ((i SUBSEP zn) in H) h = h " " zn
          else if ((i SUBSEP zn) in W) w = w " " zn
          else if (G[i SUBSEP zn] == cnt[i]) g = g " " zn
        }
        sub(/^ /, "", h); sub(/^ /, "", w); sub(/^ /, "", g)
        print k, h, w, g
      }
    }' "$WORK/ips" "$WORK/rblraw" > "$WORK/rblraw.u" && mv "$WORK/rblraw.u" "$WORK/rblraw"
  RBL_DONE=$(wc -l < "$WORK/rblraw" | tr -d ' ')
  IPS_GAPS=$(awk -F'\t' '$4 != ""' "$WORK/rblraw" | wc -l | tr -d ' ')
}

rbl_summary() {
  local base="$1" label="$2"
  printf 'Clean:   %s%s (%s%%)\n' "$CLEAN" "$label" "$(pct "$CLEAN" "$base")"
  printf 'Listed:  %s%s (%s%%)\n' "$LISTED" "$label" "$(pct "$LISTED" "$base")"
  [ "$IPS_GAPS" -gt 0 ] && printf 'Note:    %s proxies missed an answer from some lists, see the no_answer column\n' "$IPS_GAPS"
  return 0
}

done_line() {
  SPENT=$SECONDS
  if [ "$STOPPED" = 1 ]; then
    printf 'Stopped after %dm %02ds, partial results saved\n' $((SPENT / 60)) $((SPENT % 60))
  else
    printf 'Done in %dm %02ds%s\n' $((SPENT / 60)) $((SPENT % 60)) "$1"
  fi
}

finish_rbl_csv() {
  rbl_merge
  {
    printf 'proxy\texit_ip\tcountry\tspeed\tuptime\tlisted_in\tnetwork_flagged\tno_answer\n'
    awk -F'\t' -v OFS='\t' 'NR == FNR { seen[$1] = 1; hit[$1] = $2; wide[$1] = $3; gap[$1] = $4; next }
      ($1 in seen) { print $1, $2, $3, $4, $5, hit[$1], wide[$1], gap[$1] }' "$WORK/rblraw" "$WORK/csvmap"
  } > "$DEST/results.tsv"
  awk -F'\t' 'NR == FNR { seen[$1] = 1; if ($2 != "") bad[$1] = 1; next } ($1 in seen) && !($1 in bad) { print $1 }' "$WORK/rblraw" "$WORK/csvmap" > "$DEST/clean.txt"
  awk -F'\t' 'NR == FNR { if ($2 != "") bad[$1] = 1; next } ($1 in bad) { print $1 }' "$WORK/rblraw" "$WORK/csvmap" > "$DEST/listed.txt"
  CLEAN=$(wc -l < "$DEST/clean.txt" | tr -d ' ')
  LISTED=$(wc -l < "$DEST/listed.txt" | tr -d ' ')
  done_line ", exit IPs taken from the CSV without connecting"
  if [ "$STOPPED" = 1 ]; then
    printf 'Proxies: %s checked of %s\n' "$RBL_DONE" "$TOTAL"
  else
    printf 'Proxies: %s\n' "$TOTAL"
  fi
  rbl_summary "$RBL_DONE" ""
  printf 'Results: %s\n' "$DEST"
}

finish_target() {
  local name url checked head
  {
    printf 'proxy\tservice\tverdict\thttp_code\tms\tdetail\n'
    sort -t "$TAB" -k2,2 -k3,3 -k5,5n "$WORK/raw"
  } > "$DEST/results.tsv"
  while IFS="$TAB" read -r name url; do
    awk -F'\t' -v n="$name" '$2 == n && $3 == "ok" { print $5 "\t" $1 }' "$WORK/raw" | sort -n | cut -f2 > "$DEST/ok-$name.txt"
  done < "$WORK/targets"
  checked=$(cut -f1 "$WORK/raw" | sort -u | wc -l | tr -d ' ')
  SPENT=$SECONDS
  if [ "$STOPPED" = 1 ]; then
    head=$(printf 'Stopped after %dm %02ds, partial results saved: %s of %s proxies%s x %s %s' $((SPENT / 60)) $((SPENT % 60)) "$checked" "$TOTAL" "$SAMPLED" "$NT" "$SVC")
  else
    head=$(printf 'Done in %dm %02ds: %s proxies%s x %s %s' $((SPENT / 60)) $((SPENT % 60)) "$TOTAL" "$SAMPLED" "$NT" "$SVC")
  fi
  {
    printf '%s\n' "$head"
    [ "$STOPPED" = 1 ] && printf 'Note: fast proxies finish first, so slow and dead ones are under-counted here\n'
    printf '\n'
    awk -F'\t' '
      FILENAME == ARGV[1] { order[++n] = $1; next }
      FILENAME == ARGV[2] { base[$2] = $3; next }
      { c[$2 SUBSEP $3]++; rows[$2]++; px[$1] = 1; if ($3 != "noanswer") live[$1] = 1 }
      END {
        for (p in px) np++
        for (p in live) nl++
        printf "Alive proxies: %d of %d (%.1f%%), they answered at least one service\n\n", nl, np, (np ? nl * 100 / np : 0)
        printf "%-14s %-9s %-17s %8s %8s %8s %8s %8s %9s %6s\n", "service", "direct", "working", "of alive", "captcha", "antibot", "blocked", "limit", "noanswer", "other"
        for (i = 1; i <= n; i++) {
          s = order[i]
          ok = c[s SUBSEP "ok"] + 0
          printf "%-14s %-9s %6d (%5.1f%%)   %7.1f%% %8d %8d %8d %8d %9d %6d\n", s, base[s], ok, (rows[s] ? ok * 100 / rows[s] : 0), (nl ? ok * 100 / nl : 0), c[s SUBSEP "captcha"] + 0, c[s SUBSEP "antibot"] + 0, c[s SUBSEP "blocked"] + 0, c[s SUBSEP "limit"] + 0, c[s SUBSEP "noanswer"] + 0, c[s SUBSEP "other"] + 0
        }
      }' "$WORK/targets" "$WORK/base" "$WORK/raw"
    awk -F'\t' '
      FILENAME == ARGV[1] { if ($3 == "ok") okn[$2]++; next }
      $3 == "antibot" && okn[$2] > 0 { printf "\nNote: %s gave antibot to this machine without a proxy too (%s). The site checks the browser, so most script requests stop there; got through anyway: %d, see ok-%s.txt.\n", $2, $6, okn[$2], $2 }
      $3 == "antibot" && !okn[$2] { printf "\nNote: %s gave antibot to this machine without a proxy too (%s). The site checks the browser, so a script cannot rate proxies there; test it in a browser or an antidetect.\n", $2, $6 }
      $3 == "captcha" && okn[$2] > 0 { printf "\nNote: %s shows a captcha to this machine without a proxy too (%s); opened it anyway: %d, see ok-%s.txt.\n", $2, $6, okn[$2], $2 }
      $3 == "captcha" && !okn[$2] { printf "\nNote: %s shows a captcha to this machine without a proxy too (%s). Either this server IP is flagged or the site distrusts scripts; compare the proxies with that.\n", $2, $6 }
      $3 == "blocked" || $3 == "limit" || $3 == "other" { printf "\nNote: %s answered %s to this machine without a proxy (%s), keep that in mind when reading its row.\n", $2, $3, $6 }
      $3 == "noanswer" { printf "\nNote: this machine cannot open %s directly (%s), the hoster may block it; the proxy results still count.\n", $2, $6 }
    ' "$WORK/raw" "$WORK/base"
    printf '\nWorking proxies, fastest first:\n'
    while IFS="$TAB" read -r name url; do
      printf '  ok-%s.txt  %s\n' "$name" "$(wc -l < "$DEST/ok-$name.txt" | tr -d ' ')"
    done < "$WORK/targets"
    printf 'Every answer with its reason: results.tsv\n'
  } | tee "$DEST/summary.txt"
  printf 'Results: %s\n' "$DEST"
  OKN=$(awk -F'\t' '$3 == "ok"' "$WORK/raw" | wc -l | tr -d ' ')
}

http_hint() {
  local n
  [ "$ALIVE" = 0 ] || return 0
  n=$(awk -F'\t' -v p="$PROTO" '$2 ~ /^http:\/\// || ($2 !~ /:\/\// && p == "http")' "$WORK/bad" | wc -l | tr -d ' ')
  [ "$n" -ge 20 ] || return 0
  printf 'Hint:    no proxy answered over plain HTTP. Some networks drop HTTP proxy\n'
  printf '         traffic on the way; if the proxies also speak SOCKS5, run with -P socks5\n'
}

finish_connect() {
  local checked rbl_ran=0
  awk -F'\t' '$1 == "ok"' "$WORK/raw" | sort -t "$TAB" -k4,4n > "$WORK/ok"
  awk -F'\t' '$1 != "ok"' "$WORK/raw" > "$WORK/bad"
  cut -f2 "$WORK/ok" > "$DEST/alive.txt"
  cut -f2 "$WORK/bad" > "$DEST/dead.txt"
  ALIVE=$(wc -l < "$WORK/ok" | tr -d ' ')
  DEAD=$(wc -l < "$WORK/bad" | tr -d ' ')
  checked=$((ALIVE + DEAD))
  if [ "$MODE" = "rbl" ] && [ "$PHASE" = "rbl" ]; then
    rbl_ran=1
    rbl_merge
    {
      printf 'proxy\tstatus\texit_ip\tms\tlisted_in\tnetwork_flagged\tno_answer\n'
      awk -F'\t' -v OFS='\t' 'NR == FNR { hit[$1] = $2; wide[$1] = $3; gap[$1] = $4; next }
        { print $2, $1, $3, $4, hit[$2], wide[$2], gap[$2] }' "$WORK/rblraw" "$WORK/ok" "$WORK/bad"
    } > "$DEST/results.tsv"
    awk -F'\t' -v partial="$STOPPED" 'NR == FNR { seen[$1] = 1; if ($2 != "") bad[$1] = 1; next } (partial == 0 || ($2 in seen)) && !($2 in bad) { print $2 }' "$WORK/rblraw" "$WORK/ok" > "$DEST/clean.txt"
    awk -F'\t' 'NR == FNR { if ($2 != "") bad[$1] = 1; next } ($2 in bad) { print $2 }' "$WORK/rblraw" "$WORK/ok" > "$DEST/listed.txt"
    CLEAN=$(wc -l < "$DEST/clean.txt" | tr -d ' ')
    LISTED=$(wc -l < "$DEST/listed.txt" | tr -d ' ')
  else
    {
      printf 'proxy\tstatus\texit_ip\tms\techo_page\n'
      awk -F'\t' -v OFS='\t' '{ print $2, $1, $3, $4, $5 }' "$WORK/ok" "$WORK/bad"
    } > "$DEST/results.tsv"
  fi
  done_line ""
  if [ "$STOPPED" = 1 ] && [ "$checked" -lt "$TOTAL" ]; then
    printf 'Note:    fast proxies finish first, so slow and dead ones are under-counted here\n'
    printf 'Proxies: %s checked of %s\n' "$checked" "$TOTAL"
  else
    printf 'Proxies: %s\n' "$TOTAL"
  fi
  printf 'Alive:   %s (%s%%)\n' "$ALIVE" "$(pct "$ALIVE" "$checked")"
  printf 'Dead:    %s (%s%%)\n' "$DEAD" "$(pct "$DEAD" "$checked")"
  if [ "$rbl_ran" = 1 ]; then
    if [ "$STOPPED" = 1 ]; then
      printf 'RBL:     %s of %s alive proxies looked up\n' "$RBL_DONE" "$ALIVE"
      rbl_summary "$RBL_DONE" " of looked up"
    else
      rbl_summary "$ALIVE" " of alive"
    fi
  elif [ "$MODE" = "rbl" ]; then
    printf 'RBL:     not started, the run stopped during the connect pass\n'
  fi
  http_hint
  printf 'Results: %s\n' "$DEST"
}

TAB=$(printf '\t')
IPS=0
IPS_GAPS=0
RBL_DONE=0
SAMPLED=""
NT=0
SVC="services"

if [ "$CSV_FAST" = 1 ]; then
  awk -F'\t' '{ print $1 " " $2 }' "$WORK/csvmap" > "$WORK/ips"
  [ "$QUIET" = 1 ] || printf 'remcheck %s: %s proxies from a RemProxy CSV, exit IPs from the real_ip column, rbl only\n' "$REMCHECK_VERSION" "$TOTAL" >&2
  PHASE=csv
  rbl_run "$WORK/ips"
  finish_rbl_csv
  [ "$CLEAN" -gt 0 ] || exit 1
  exit 0
fi

if [ "$MODE" = "target" ]; then
  build_targets
  NT=$(wc -l < "$WORK/targets" | tr -d ' ')
  [ "$NT" = 1 ] && SVC="service"
  [ "$TOTAL" -lt "$FULL" ] && SAMPLED=" (random sample of $FULL)"
  if [ "$QUIET" != 1 ]; then
    printf 'remcheck %s: %s proxies%s x %s %s, %s at once\n' "$REMCHECK_VERSION" "$TOTAL" "$SAMPLED" "$NT" "$SVC" "$LANES" >&2
    printf '  control request to each service without a proxy, for the direct column\n' >&2
  fi
  : > "$WORK/base"
  while IFS="$TAB" read -r name url; do
    LAST_RC=0
    target_hit "$name" "$url" - >> "$WORK/base"
    [ "$LAST_RC" = 6 ] && die "the address of $name does not resolve from this machine, check it: ${url:-built-in service}"
  done < "$WORK/targets"
  PHASE=target
  start_ticker "$WORK/raw" $((TOTAL * NT)) target
  run_lanes "$WORK/pool" target_one "$LANES"
  stop_ticker
  finish_target
  [ "$OKN" -gt 0 ] || exit 1
  exit 0
fi

[ "$QUIET" = 1 ] || printf 'remcheck %s: %s proxies, mode %s, %s at once, %s echo pages, up to %s attempts\n' "$REMCHECK_VERSION" "$TOTAL" "$MODE" "$LANES" "${#JUDGES[@]}" "$TRIES" >&2
PHASE=connect
start_ticker "$WORK/raw" "$TOTAL" connect
run_lanes "$WORK/pool" probe_one "$LANES"
stop_ticker
if [ "$MODE" = "rbl" ]; then
  awk -F'\t' '$1 == "ok" && $3 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print $2 " " $3 }' "$WORK/raw" > "$WORK/ips"
  PHASE=rbl
  rbl_run "$WORK/ips"
fi
finish_connect
[ "$ALIVE" -gt 0 ] || exit 1
exit 0
