Files
DenozordecandCursor ca7f37f980
Docker / build (push) Failing after 21s
fix(censorcheck): показывать прогресс-бар во время проверки сайтов
JSON остаётся в stdout, бар пишет в stderr; launcher больше не перехватывает прогресс.

Co-authored-by: Cursor <[email protected]>
2026-08-22 11:15:18 +07:00

1178 lines
28 KiB
Bash

#!/usr/bin/env bash
readonly SCRIPT_NAME=$(basename "$0")
readonly DEPENDENCIES=("curl" "dig" "jq" "column")
readonly COLOR_WHITE="\033[97m"
readonly COLOR_RED="\033[31m"
readonly COLOR_GREEN="\033[32m"
readonly COLOR_BLUE="\033[36m"
readonly COLOR_ORANGE="\033[33m"
readonly COLOR_RESET="\033[0m"
readonly CURL_SEPARATOR="--UNIQUE-SEPARATOR--"
readonly DNS_SERVERS=("1.1.1.1" "1.0.0.1" "8.8.8.8" "8.8.4.4" "9.9.9.9" "9.9.9.10" "77.88.8.8" "77.88.8.1")
readonly ENCRYPTED_DNS_SERVERS=(
"Cloudflare|1.1.1.1"
"Cloudflare|1.0.0.1"
"Google|8.8.8.8"
"Google|8.8.4.4"
"Quad9|9.9.9.9"
"Quad9|9.9.9.10"
"Yandex|77.88.8.8"
"Yandex|77.88.8.1"
)
AVAILABLE_DOH=()
AVAILABLE_DOT=()
# Default values
TIMEOUT=5
RETRIES=2
MODE="both"
USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"
DOMAINS_FILE=""
IP_VERSION="4"
PROXY=""
SINGLE_DOMAIN=""
PROTOCOL="both"
JSON_OUTPUT=false
DNS_CHECK=true
SHOW_HEADER=true
readonly DPI_BLOCKED_SITES=(
"youtube.com"
"redirector.googlevideo.com/report_mapping?di=no"
"discord.com"
"instagram.com"
"facebook.com"
"x.com"
"linkedin.com"
"rutracker.org"
"digitalocean.com"
# "ntc.party"
"amnezia.org"
"getoutline.org"
"mailfence.com"
"flibusta.is"
"rezka.ag"
"api.telegram.org"
"play.google.com"
)
readonly GEO_BLOCKED_SITES=(
"spotify.com"
"netflix.com"
"patreon.com"
"swagger.io"
"snyk.io"
"mongodb.com"
"autodesk.com"
"graylog.org"
"redis.io"
"copilot.microsoft.com"
)
readonly MSG_AVAILABLE="Available"
readonly MSG_BLOCKED="Blocked"
readonly MSG_TIMEOUT="$MSG_BLOCKED / Timeout"
readonly MSG_BLOCKED_BY_IP="$MSG_BLOCKED by IP"
readonly MSG_BLOCKED_BY_PORT="$MSG_BLOCKED by port"
readonly MSG_REDIRECT="Redirected"
readonly MSG_ACCESS_DENIED="Denied"
readonly MSG_OTHER="Responded with status code"
declare -a TEXT_RESULTS=()
error_exit() {
local message="$1"
local exit_code="${2:-1}"
printf "[%b%s%b] %b%s%b\n" "$COLOR_RED" "ERROR" "$COLOR_RESET" "$COLOR_WHITE" "$message" "$COLOR_RESET" >&2
exit "$exit_code"
}
show_progress() {
local current=$1
local total=$2
local domain=$3
local width=24
local filled=0 pct=0 empty=0
local label="$domain"
local bar
if [ "$total" -gt 0 ]; then
filled=$((current * width / total))
pct=$((current * 100 / total))
fi
[ "$filled" -gt "$width" ] && filled=$width
empty=$((width - filled))
bar="$(printf '%*s' "$filled" '' | tr ' ' '=')"
if [ "$empty" -gt 0 ] && [ "$filled" -lt "$width" ]; then
bar="${bar}>"
empty=$((empty - 1))
fi
bar="${bar}$(printf '%*s' "$empty" '')"
if [ "${#label}" -gt 42 ]; then
label="${label:0:39}..."
fi
# stderr: JSON на stdout не ломаем даже в --json
printf "\r\033[K%b[%s]%b %3d%% %d/%d %b%s%b" \
"$COLOR_BLUE" \
"$bar" \
"$COLOR_RESET" \
"$pct" \
"$current" \
"$total" \
"$COLOR_WHITE" \
"$label" \
"$COLOR_RESET" >&2
}
clear_progress() {
printf "\n" >&2
}
cleanup() {
clear_progress
exit 130
}
display_help() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Checks accessibility of websites that might be blocked by DPI or geolocation restrictions
Options:
-h, --help Display this help message and exit
-m, --mode Set checking mode: 'dpi', 'geoblock', 'both' (dpi + geoblock), or 'dns' only (default: $MODE)
-t, --timeout Set connection timeout in seconds (default: $TIMEOUT)
-r, --retries Set number of connection retries (default: $RETRIES)
-u, --user-agent Set custom User-Agent string (default: $USER_AGENT)
-f, --file Read domains from specified file instead of using built-in lists
-6, --ipv6 Use IPv6 (default: IPv$IP_VERSION)
-p, --proxy Use SOCKS5 proxy (format: host:port)
-d, --domain Specify a single domain to check
--http-only Test only HTTP
--https-only Test only HTTPS
--no-dns Skip DNS checks (DoH/DoT availability and hijacking detection)
--no-header Do not print the script header with current settings
-j, --json Output results in JSON format
Examples:
$SCRIPT_NAME # Check all predefined domains with default settings
$SCRIPT_NAME --mode dpi # Check only DPI-blocked sites
$SCRIPT_NAME --timeout 10 --retries 3 # Use longer timeout and more retries
$SCRIPT_NAME --user-agent "MyAgent/1.0" # Use custom User-Agent
$SCRIPT_NAME --file my-domains.txt # Check domains from custom file
$SCRIPT_NAME --ipv6 # Use IPv6 instead of IPv4
$SCRIPT_NAME --proxy 127.0.0.1:1080 # Use SOCKS5 proxy
$SCRIPT_NAME --domain example.com # Check a single domain
$SCRIPT_NAME --http-only # Test only HTTP
$SCRIPT_NAME --https-only # Test only HTTPS
$SCRIPT_NAME --no-dns # Skip DNS checks
$SCRIPT_NAME --no-header # Do not print the script header
The domain file should contain one domain per line. Lines starting with # are ignored
EOF
}
is_installed() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1
}
check_missing_dependencies() {
local missing_pkgs=()
for pkg in "${DEPENDENCIES[@]}"; do
if ! is_installed "$pkg"; then
missing_pkgs+=("$pkg")
fi
done
echo "${missing_pkgs[@]}"
}
prompt_for_installation() {
local missing_pkgs=("$@")
echo "Missing dependencies: ${missing_pkgs[*]}"
read -r -p "Do you want to install them? [y/N]: " answer
answer=${answer,,}
case "${answer,,}" in
y | yes)
return 0
;;
*)
exit 0
;;
esac
}
get_package_manager() {
# Check if the script is running in Termux
if [[ -d /data/data/com.termux ]]; then
echo "termux"
return
fi
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
debian | ubuntu)
echo "apt"
;;
arch)
echo "pacman"
;;
fedora)
echo "dnf"
;;
centos | rhel | rocky | almalinux)
if command -v dnf >/dev/null 2>&1; then echo "dnf"; else echo "yum"; fi
;;
*)
error_exit "Unknown distribution: $ID. Please install dependencies manually."
;;
esac
else
error_exit "File /etc/os-release not found, unable to determine distribution. Please install dependencies manually."
fi
}
install_with_package_manager() {
local pkg_manager="$1"
local use_sudo=""
local packages=()
shift
for dep in "$@"; do
case "$pkg_manager" in
apt | termux)
case "$dep" in
dig) packages+=("dnsutils") ;;
column) packages+=("bsdextrautils") ;;
*) packages+=("$dep") ;;
esac
;;
pacman)
case "$dep" in
dig) packages+=("bind") ;;
column) packages+=("util-linux") ;;
*) packages+=("$dep") ;;
esac
;;
dnf | yum)
case "$dep" in
dig) packages+=("bind-utils") ;;
column) packages+=("util-linux") ;;
*) packages+=("$dep") ;;
esac
;;
*)
packages+=("$dep")
;;
esac
done
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
use_sudo="sudo"
fi
case "$pkg_manager" in
apt)
$use_sudo apt update
$use_sudo env NEEDRESTART_MODE=a apt install -y "${packages[@]}"
;;
pacman)
$use_sudo pacman -Syy --noconfirm "${packages[@]}"
;;
dnf)
$use_sudo dnf install -y "${packages[@]}"
;;
yum)
$use_sudo yum install -y "${packages[@]}"
;;
termux)
apt update
apt install -y "${packages[@]}"
;;
*)
error_exit "Unknown package manager: $pkg_manager"
;;
esac
}
install_dependencies() {
local missing_packages
local pkg_manager
read -r -a missing_packages <<<"$(check_missing_dependencies)"
if [[ ${#missing_packages[@]} -eq 0 ]]; then
return 0
fi
if $JSON_OUTPUT; then
echo "Missing dependencies: ${missing_packages[*]}" >&2
exit 1
fi
prompt_for_installation "${missing_packages[@]}"
pkg_manager=$(get_package_manager)
install_with_package_manager "$pkg_manager" "${missing_packages[@]}"
}
check_ipv6_support() {
if [[ -n $(ip -6 addr show scope global 2>/dev/null) ]]; then
return 0
fi
return 1
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h | --help)
display_help
exit 0
;;
-m | --mode)
if [[ $2 == "dpi" || $2 == "geoblock" || $2 == "dns" || $2 == "both" ]]; then
MODE=$2
else
error_exit "Invalid mode: $2. Valid modes are: dpi, geoblock, dns, both"
fi
shift 2
;;
-t | --timeout)
if [[ "$2" =~ ^[0-9]+$ ]]; then
TIMEOUT=$2
else
error_exit "Invalid timeout value: $2. Timeout must be a positive integer"
fi
shift 2
;;
-r | --retries)
if [[ "$2" =~ ^[0-9]+$ ]]; then
RETRIES=$2
else
error_exit "Invalid retries value: $2. Retry count must be a positive integer"
fi
shift 2
;;
-u | --user-agent)
if [[ -n "$2" ]]; then
USER_AGENT=$2
else
error_exit "User-Agent cannot be empty"
fi
shift 2
;;
-f | --file)
if [[ -n "${2:-}" ]]; then
if [[ -f "$2" ]]; then
DOMAINS_FILE="$2"
else
error_exit "File '$2' does not exist"
fi
else
error_exit "File path cannot be empty"
fi
shift 2
;;
-6 | --ipv6)
if ! check_ipv6_support; then
error_exit "IPv6 is not supported on this system"
fi
IP_VERSION="6"
shift
;;
-p | --proxy)
if [[ -n "${2:-}" ]]; then
PROXY="$2"
else
error_exit "Proxy address cannot be empty"
fi
shift 2
;;
-d | --domain)
if [[ -n "$2" ]]; then
SINGLE_DOMAIN="$2"
else
error_exit "Domain cannot be empty"
fi
shift 2
;;
--http-only)
PROTOCOL="http"
shift
;;
--https-only)
PROTOCOL="https"
shift
;;
--no-dns)
DNS_CHECK=false
shift
;;
--no-header)
SHOW_HEADER=false
shift
;;
-j | --json)
JSON_OUTPUT=true
shift
;;
*)
error_exit "Unknown option: $1"
;;
esac
done
if [[ "$MODE" == "dns" ]] && ! $DNS_CHECK; then
error_exit "--no-dns cannot be used with --mode dns"
fi
}
print_header() {
local mode
printf "\nTimeout set to: %b%ss%b\n" "$COLOR_WHITE" "$TIMEOUT" "$COLOR_RESET"
case $MODE in
dpi)
mode="DPI"
;;
geoblock)
mode="Geoblock"
;;
dns)
mode="DNS"
;;
both)
mode="DPI and Geoblock"
;;
esac
if [[ -z "$DOMAINS_FILE" ]] && [[ -z "$SINGLE_DOMAIN" ]]; then
printf "Mode set to: %b%s%b\n" "$COLOR_WHITE" "$mode" "$COLOR_RESET"
fi
if [[ "$MODE" == "dns" ]]; then
return
fi
printf "Retries set to: %b%s%b\n" "$COLOR_WHITE" "$RETRIES" "$COLOR_RESET"
printf "User-Agent set to: %b%s%b\n" "$COLOR_WHITE" "$USER_AGENT" "$COLOR_RESET"
if [[ -n "$DOMAINS_FILE" ]]; then
printf "Domain mode set to: %buser domains from %s%b\n" "$COLOR_WHITE" "$DOMAINS_FILE" "$COLOR_RESET"
elif [[ -n "$SINGLE_DOMAIN" ]]; then
printf "Checking single domain: %b%s%b\n" "$COLOR_WHITE" "$SINGLE_DOMAIN" "$COLOR_RESET"
else
printf "Domain mode set to: %bpredefined domains%b\n" "$COLOR_WHITE" "$COLOR_RESET"
fi
printf "IP version set to: %bIPv%s%b\n" "$COLOR_WHITE" "$IP_VERSION" "$COLOR_RESET"
if [ -n "$PROXY" ]; then
printf "SOCKS5 proxy set to: %b%s%b\n" "$COLOR_WHITE" "$PROXY" "$COLOR_RESET"
fi
case $PROTOCOL in
http)
printf "Protocol set to: %bHTTP only%b\n" "$COLOR_WHITE" "$COLOR_RESET"
;;
https)
printf "Protocol set to: %bHTTPS only%b\n" "$COLOR_WHITE" "$COLOR_RESET"
;;
both)
printf "Protocol set to: %bHTTP and HTTPS%b\n" "$COLOR_WHITE" "$COLOR_RESET"
;;
esac
}
run_dns_checks() {
check_encrypted_dns_servers
check_dns_hijacking
}
read_domains_from_file() {
local file=$1
local domains=()
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then
line=$(echo "$line" | xargs)
if [[ -n "$line" ]]; then
domains+=("$line")
fi
fi
done <"$file"
echo "${domains[@]}"
}
execute_curl() {
local url=$1
local protocol=$2
local follow_redirects=$3
local ip_version_to_use=${4:-$IP_VERSION}
local curl_output
local curl_opts=(
-s
--compressed
-o /dev/null
-w "%{http_code}${CURL_SEPARATOR}%{redirect_url}"
--retry-connrefused
--retry-all-errors
--retry "$RETRIES"
--connect-timeout "$TIMEOUT"
--max-time "$((TIMEOUT * (RETRIES + 1)))"
-"$ip_version_to_use"
-A "$USER_AGENT"
-H "Sec-Fetch-Site: none"
-H "Accept-Language: en-US,en;q=0.5"
)
if [ -n "$PROXY" ]; then
curl_opts+=(--proxy "socks5h://$PROXY")
fi
if [ "$follow_redirects" = true ]; then
curl_opts+=(-L)
fi
if curl_output=$(curl "${curl_opts[@]}" "${protocol}://${url}"); then
echo "$curl_output"
else
echo "000${CURL_SEPARATOR}"
fi
}
get_domains_to_check() {
if [[ -n "$SINGLE_DOMAIN" ]]; then
echo "$SINGLE_DOMAIN"
elif [[ -z "$DOMAINS_FILE" ]]; then
case $MODE in
# TODO: Replace echo with function
dpi) echo "${DPI_BLOCKED_SITES[@]}" ;;
geoblock) echo "${GEO_BLOCKED_SITES[@]}" ;;
both) echo "${DPI_BLOCKED_SITES[@]}" "${GEO_BLOCKED_SITES[@]}" ;;
esac
else
read_domains_from_file "$DOMAINS_FILE"
fi
}
get_single_check_result() {
local domain=$1
local protocol=$2
local follow_redirects=$3
local ip_version=$4
local response status_code redirect_url
local port
[[ "${protocol,,}" == "https" ]] && port=443 || port=80
if [[ -z "$PROXY" ]]; then
local ip
ip=$(get_domain_ip "$domain" "$ip_version")
if [[ -n "$ip" ]] && ! is_port_open "$ip" "$port"; then
jq -n '{"status": -1, "redirect_url": null}'
return
fi
fi
response=$(execute_curl "$domain" "$protocol" "$follow_redirects" "$ip_version")
status_code="${response%%$CURL_SEPARATOR*}"
redirect_url="${response#*$CURL_SEPARATOR}"
jq -n \
--argjson status "${status_code:-0}" \
--arg redirect_url "${redirect_url:-}" \
'
{
"status": ($status|tonumber),
"redirect_url": (if $redirect_url == "" then null else $redirect_url end)
}
'
}
gather_single_domain_result() {
local domain=$1
local ipv6_supported
local http_ipv4=null http_ipv6=null https_ipv4=null https_ipv6=null
check_ipv6_support && ipv6_supported=true || ipv6_supported=false
if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "http" ]]; then
http_ipv4=$(get_single_check_result "$domain" "HTTP" false 4)
if $ipv6_supported; then
http_ipv6=$(get_single_check_result "$domain" "HTTP" false 6)
fi
fi
if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "https" ]]; then
https_ipv4=$(get_single_check_result "$domain" "HTTPS" true 4)
if $ipv6_supported; then
https_ipv6=$(get_single_check_result "$domain" "HTTPS" true 6)
fi
fi
jq -n \
--arg service "$(get_domain_host "$domain")" \
--argjson http_ipv4 "$http_ipv4" \
--argjson http_ipv6 "$http_ipv6" \
--argjson https_ipv4 "$https_ipv4" \
--argjson https_ipv6 "$https_ipv6" \
'
{
"service": $service,
"http": {
"ipv4": $http_ipv4,
"ipv6": $http_ipv6
},
"https": {
"ipv4": $https_ipv4,
"ipv6": $https_ipv6
}
}
'
}
get_domain_host() {
printf "%s" "${1%%/*}"
}
get_record_type() {
local ip_version=${1:-$IP_VERSION}
[[ "$ip_version" == "6" ]] && echo "AAAA" || echo "A"
}
get_domain_ip() {
local domain
domain=$(get_domain_host "$1")
local ip_version=${2:-$IP_VERSION}
dig +short "$domain" "$(get_record_type "$ip_version")" 2>/dev/null |
awk '/^[0-9a-fA-F.:]+$/ {print; exit}' || true
}
domain_exists() {
local domain
domain=$(get_domain_host "$1")
local rcode
rcode=$(dig +noall +comments "$domain" A 2>/dev/null |
awk -F'status: ' '/status:/ {split($2, parts, ","); print parts[1]; exit}')
[[ "$rcode" != "NXDOMAIN" ]]
}
get_domain_ips_via_dns() {
local domain=$1
local server=$2
dig +short @"$server" "$domain" "$(get_record_type)" \
+timeout="$TIMEOUT" +tries=1 2>/dev/null |
awk '/^[0-9a-fA-F.:]+$/' || true
}
resolve_via_dig() {
local domain=$1
local server_ip=$2
local transport=$3
dig +short +"$transport" @"$server_ip" "$domain" "$(get_record_type)" \
+timeout="$TIMEOUT" +tries=1 2>/dev/null |
awk '/^[0-9a-fA-F.:]+$/'
}
have_ip_intersection() {
local -n first_ips=$1
local -n second_ips=$2
declare -A ip_set=()
local ip
for ip in "${first_ips[@]}"; do
ip_set["$ip"]=1
done
for ip in "${second_ips[@]}"; do
[[ -n ${ip_set["$ip"]+1} ]] && return 0
done
return 1
}
probe_resolver_transport() {
if [[ -n "$(resolve_via_dig "$1" "$2" "$3")" ]]; then
echo "available"
else
echo "blocked"
fi
}
check_encrypted_dns_servers() {
local test_domain="rutracker.org"
local entry name ip doh_status dot_status
local table_rows=()
local i tmp_dir
AVAILABLE_DOH=()
AVAILABLE_DOT=()
tmp_dir=$(mktemp -d)
for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do
ip="${ENCRYPTED_DNS_SERVERS[$i]##*|}"
probe_resolver_transport "$test_domain" "$ip" "https" >"$tmp_dir/${i}_https" &
probe_resolver_transport "$test_domain" "$ip" "tls" >"$tmp_dir/${i}_tls" &
done
wait
for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do
entry="${ENCRYPTED_DNS_SERVERS[$i]}"
name="${entry%%|*}"
ip="${entry##*|}"
if [[ "$(<"$tmp_dir/${i}_https")" == "available" ]]; then
AVAILABLE_DOH+=("$entry")
doh_status="$MSG_AVAILABLE"
else
doh_status="$MSG_BLOCKED"
fi
if [[ "$(<"$tmp_dir/${i}_tls")" == "available" ]]; then
AVAILABLE_DOT+=("$entry")
dot_status="$MSG_AVAILABLE"
else
dot_status="$MSG_BLOCKED"
fi
table_rows+=("$(printf "%s%b\t%s\t%s\t%s" \
"$name" "$COLOR_RESET" "$ip" \
"$(colorize_summary "$doh_status")" \
"$(colorize_summary "$dot_status")")")
done
rm -rf "$tmp_dir"
printf "\n%bEncrypted DNS (DoH/DoT) availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET"
{
printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \
"$COLOR_WHITE" "Resolver" "IP" "DoH" "DoT" "$COLOR_RESET"
printf "%s\n" "${table_rows[@]}"
} | column -t -s $'\t'
}
get_reference_resolver() {
if [[ ${#AVAILABLE_DOH[@]} -gt 0 ]]; then
printf "%s\thttps" "${AVAILABLE_DOH[0]}"
elif [[ ${#AVAILABLE_DOT[@]} -gt 0 ]]; then
printf "%s\ttls" "${AVAILABLE_DOT[0]}"
fi
}
check_dns_hijacking() {
local test_domains=("rutracker.org" "linkedin.com" "flibusta.is")
local regular_dns_ips=()
local encrypted_ips=()
local hijacked_domain=""
local hijacked_ip=""
local reference reference_entry reference_transport reference_name reference_ip
reference=$(get_reference_resolver)
if [[ -z "$reference" ]]; then
printf "\n%b%s%b\n" \
"$COLOR_ORANGE" \
"Cannot verify DNS spoofing: no DoH/DoT server reachable, skipping check" \
"$COLOR_RESET"
return 0
fi
reference_entry="${reference%%$'\t'*}"
reference_transport="${reference##*$'\t'}"
reference_name="${reference_entry%%|*}"
reference_ip="${reference_entry##*|}"
for test_domain in "${test_domains[@]}"; do
regular_dns_ips=()
encrypted_ips=()
for dns_server in "${DNS_SERVERS[@]}"; do
mapfile -t regular_dns_ips < <(get_domain_ips_via_dns "$test_domain" "$dns_server")
[[ ${#regular_dns_ips[@]} -gt 0 ]] && break
done
mapfile -t encrypted_ips < <(resolve_via_dig "$test_domain" "$reference_ip" "$reference_transport")
[[ ${#regular_dns_ips[@]} -eq 0 ]] || [[ ${#encrypted_ips[@]} -eq 0 ]] && continue
if ! have_ip_intersection regular_dns_ips encrypted_ips; then
hijacked_domain="$test_domain"
hijacked_ip="${regular_dns_ips[0]}"
break
fi
done
if [[ -n "$hijacked_domain" ]]; then
printf "\n%b%s%b %s %b%s%b %s %b%s%b\n" \
"$COLOR_RED" "DNS hijacking detected!" "$COLOR_RESET" \
"ISP redirects" "$COLOR_WHITE" "$hijacked_domain" "$COLOR_RESET" "to" \
"$COLOR_RED" "$hijacked_ip" "$COLOR_RESET"
printf "%bReference resolver: %s (%s) over %s%b\n\n" \
"$COLOR_WHITE" "$reference_name" "$reference_ip" "${reference_transport^^}" "$COLOR_RESET"
printf "%b%s%b\n%b%s%b\n" \
"$COLOR_ORANGE" "DNS hijacking may affect the accuracy of this check" "$COLOR_RESET" \
"$COLOR_ORANGE" "Configure encrypted DNS (DoH/DoT) on your system" "$COLOR_RESET"
else
printf "\n%b%s%b %b(reference: %s over %s)%b\n" \
"$COLOR_GREEN" "Good news, no DNS hijacking detected!" "$COLOR_RESET" \
"$COLOR_WHITE" "$reference_name" "${reference_transport^^}" "$COLOR_RESET"
fi
}
is_port_open() {
local ip="$1"
local port="$2"
timeout "$TIMEOUT" bash -c "(echo >/dev/tcp/$ip/$port)" 2>/dev/null
}
is_ip_reachable() {
local ip="$1"
is_port_open "$ip" 80 || is_port_open "$ip" 443
}
make_json_error() {
local domain="$1"
local error_code="$2"
local error_message="$3"
jq -n \
--arg service "$domain" \
--arg code "$error_code" \
--arg error "$error_message" \
'
{
"service": $service,
"error": $error,
"error_code": $code,
"http": null,
"https": null
}
'
}
summarize_status_description() {
local status_code=$1
local redirect_url=$2
local msg
if [[ "$status_code" -eq -1 ]]; then
msg="$MSG_BLOCKED_BY_PORT"
elif [[ -z "$status_code" || "$status_code" = "000" || "$status_code" -eq 0 ]]; then
msg="$MSG_TIMEOUT"
elif [[ "$status_code" -ge 300 && "$status_code" -lt 400 ]]; then
[[ -z "$redirect_url" ]] && redirect_url="<empty>"
msg=$(printf "%s (%s) -> %s" "$MSG_REDIRECT" "$status_code" "$redirect_url")
elif [[ "$status_code" -eq 200 ]]; then
msg="$MSG_AVAILABLE ($status_code)"
elif [[ "$status_code" -eq 403 ]]; then
msg="$MSG_ACCESS_DENIED ($status_code)"
else
msg="$MSG_OTHER $status_code"
fi
echo "$msg"
}
colorize_summary() {
local message="$1"
local first_word rest first_word_color
first_word="${message%% *}"
if [[ "$first_word" == "$message" ]]; then
rest=""
else
rest="${message#* }"
fi
case "$first_word" in
Blocked)
first_word_color=$COLOR_RED
;;
Available)
first_word_color=$COLOR_GREEN
;;
Redirected)
first_word_color=$COLOR_BLUE
;;
Denied)
first_word_color=$COLOR_RED
;;
*)
first_word_color=$COLOR_ORANGE
;;
esac
if [[ -z "$rest" ]]; then
printf "%b%s%b" "$first_word_color" "$first_word" "$COLOR_RESET"
else
printf "%b%s%b %s" "$first_word_color" "$first_word" "$COLOR_RESET" "$rest"
fi
}
summarize_protocol_result() {
local result_json=$1
local protocol=$2
if [[ "$PROTOCOL" != "both" && "$PROTOCOL" != "$protocol" ]]; then
echo "Skipped"
return
fi
local data
data=$(jq -c --arg protocol "$protocol" --arg ip_version "$IP_VERSION" '
if $ip_version == "6" then .[$protocol].ipv6
else .[$protocol].ipv4
end
' <<<"$result_json")
if [[ "$data" == "null" || -z "$data" ]]; then
echo "N/A"
return
fi
local status redirect
status=$(jq -r '.status' <<<"$data")
redirect=$(jq -r '.redirect_url // ""' <<<"$data")
summarize_status_description "$status" "$redirect"
}
add_text_result_row() {
local service=$1
local ip=$2
local http_cell=$3
local https_cell=$4
TEXT_RESULTS+=("$(
jq -n \
--arg service "$service" \
--arg ip "$ip" \
--arg http "$http_cell" \
--arg https "$https_cell" \
'{service: $service, ip: $ip, http: $http, https: $https}'
)")
}
add_text_result_from_json() {
local result_json=$1
local ip=$2
local service
service=$(echo "$result_json" | jq -r '.service')
local http_cell https_cell
http_cell=$(summarize_protocol_result "$result_json" "http")
https_cell=$(summarize_protocol_result "$result_json" "https")
add_text_result_row "$service" "$ip" "$http_cell" "$https_cell"
}
print_table_results() {
printf "\n"
{
printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \
"$COLOR_WHITE" \
"Service" \
"IP" \
"HTTP" \
"HTTPS" \
"$COLOR_RESET"
for row_json in "${TEXT_RESULTS[@]}"; do
local service http https
service=$(jq -r '.service' <<<"$row_json")
ip=$(jq -r '.ip' <<<"$row_json")
http=$(jq -r '.http' <<<"$row_json")
https=$(jq -r '.https' <<<"$row_json")
printf "%s%b\t%s\t%s\t%s\n" \
"$service" \
"$COLOR_RESET" \
"$ip" \
"$(colorize_summary "$http")" \
"$(colorize_summary "$https")"
done
} | column -t -s $'\t'
}
run_checks_and_print() {
local domains
local all_results_json="[]"
if [[ "$MODE" == "dns" ]]; then
if $JSON_OUTPUT; then
error_exit "JSON output is not supported for dns mode"
fi
if $SHOW_HEADER; then
print_header
fi
run_dns_checks
return
fi
read -r -a domains <<<"$(get_domains_to_check)"
local total_domains=${#domains[@]}
local current_index=0
TEXT_RESULTS=()
if ! $JSON_OUTPUT; then
if $SHOW_HEADER; then
print_header
fi
if $DNS_CHECK; then
run_dns_checks
fi
if $SHOW_HEADER || $DNS_CHECK; then
printf "\n"
fi
fi
for domain in "${domains[@]}"; do
((++current_index))
show_progress "$current_index" "$total_domains" "$domain"
local host ip_address
host=$(get_domain_host "$domain")
ip_address=$(get_domain_ip "$domain")
if [[ -z "$ip_address" ]]; then
local error_code error_message
if domain_exists "$domain"; then
error_code="no_dns_record"
error_message="No $(get_record_type) record"
else
error_code="nxdomain"
error_message="Domain does not exist"
fi
if $JSON_OUTPUT; then
all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" "$error_code" "$error_message")" '. + [$item]')
else
add_text_result_row "$host" "N/A" "$error_message" "$error_message"
fi
continue
fi
if [[ -z "$PROXY" ]] && ! is_ip_reachable "$ip_address"; then
if $JSON_OUTPUT; then
all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" blocked_by_ip "$MSG_BLOCKED_BY_IP")" '. + [$item]')
else
add_text_result_row "$host" "$ip_address" "$MSG_BLOCKED_BY_IP" "$MSG_BLOCKED_BY_IP"
fi
continue
fi
local domain_result_json
domain_result_json=$(gather_single_domain_result "$domain")
if $JSON_OUTPUT; then
all_results_json=$(echo "$all_results_json" | jq --argjson item "$domain_result_json" '. + [$item]')
else
add_text_result_from_json "$domain_result_json" "$ip_address"
fi
done
clear_progress
if $JSON_OUTPUT; then
local ipv6_supported
check_ipv6_support && ipv6_supported=true || ipv6_supported=false
local ip_version_param_val
if $ipv6_supported; then
ip_version_param_val="IPv4 & IPv6"
else
ip_version_param_val="IPv4"
fi
local params_json
params_json=$(
jq -n \
--arg timeout "${TIMEOUT}s" \
--arg retries "$RETRIES" \
--arg mode "${MODE^^}" \
--arg user_agent "$USER_AGENT" \
--arg domain_mode "$(if [[ -n "$DOMAINS_FILE" ]]; then echo "user domains from $DOMAINS_FILE"; elif [[ -n "$SINGLE_DOMAIN" ]]; then echo "single domain"; else echo "predefined domains"; fi)" \
--arg ip_version "$ip_version_param_val" \
--arg protocol "$(if [[ "$PROTOCOL" == "both" ]]; then echo "HTTP and HTTPS"; elif [[ "$PROTOCOL" == "http" ]]; then echo "HTTP only"; else echo "HTTPS only"; fi)" \
'[
{"key":"timeout", "value":$timeout},
{"key":"retries", "value":$retries},
{"key":"mode", "value":$mode},
{"key":"user_agent", "value":$user_agent},
{"key":"domain_mode", "value":$domain_mode},
{"key":"ip_version", "value":$ip_version},
{"key":"protocol", "value":$protocol}
]'
)
jq -n \
--argjson params "$params_json" \
--argjson results "$all_results_json" \
'{
"version": 1,
"params": $params,
"results": $results
}'
return
fi
print_table_results
}
main() {
set -euo pipefail
trap cleanup INT TERM
parse_arguments "$@"
install_dependencies
run_checks_and_print
if ! $JSON_OUTPUT; then
printf "\n"
fi
trap - INT TERM
}
main "$@"