Docker / build (push) Failing after 21s
JSON остаётся в stdout, бар пишет в stderr; launcher больше не перехватывает прогресс. Co-authored-by: Cursor <cursoragent@cursor.com>
1178 lines
28 KiB
Bash
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 "$@"
|