#!/usr/bin/env bash readonly SCRIPT_NAME="${0##*/}" readonly DEPENDENCIES=("curl" "dig" "jq" "column" "openssl") 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 SIDE_BY_SIDE_GAP=4 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" "94.140.14.14" "94.140.15.15" "45.90.28.65" "45.90.30.65" "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" "AdGuard|94.140.14.14" "AdGuard|94.140.15.15" "NextDNS|45.90.28.65" "NextDNS|45.90.30.65" "Yandex|77.88.8.8" "Yandex|77.88.8.1" ) readonly TELEGRAM_DCS=( "DC1|149.154.175.50" "DC2|149.154.167.51" "DC2|95.161.76.100" "DC3|149.154.175.100" "DC4|149.154.167.91" "DC5|149.154.171.5" "DC5|91.108.56.130" ) readonly TELEGRAM_PORT=443 readonly MTPROTO_HEADER_BYTES=64 readonly MTPROTO_INTERMEDIATE_TAG="eeeeeeee" readonly MTPROTO_REQ_PQ_MULTI="f18e7ebe" readonly MTPROTO_RES_PQ="63241605" readonly MTPROTO_RES_PQ_BYTES=44 AVAILABLE_DOH=() AVAILABLE_DOT=() # Default values TIMEOUT=5 RETRIES=2 MODE="all" 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 TG_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" "github.com" "raw.githubusercontent.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" readonly MSG_SPOOFED="Spoofed" readonly MSG_CLEAN="Clean" readonly MSG_NO_RESPONSE="Unreachable" 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 if ! $JSON_OUTPUT; then printf "\r\033[K%b[%d/%d] Checking:%b %b%s%b" \ "$COLOR_BLUE" \ "$current" \ "$total" \ "$COLOR_RESET" \ "$COLOR_WHITE" \ "$domain" \ "$COLOR_RESET" >&2 fi } clear_progress() { if ! $JSON_OUTPUT; then printf "\r%80s\r" " " >&2 fi } cleanup() { clear_progress exit 130 } display_help() { cat </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 # shellcheck source=/dev/null . /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 == "both" ]]; then MODE="all" elif [[ $2 == "all" || $2 == "dpi" || $2 == "geoblock" || $2 == "dns" || $2 == "tg" ]]; then MODE=$2 else error_exit "Invalid mode: $2. Valid modes are: all, dpi, geoblock, dns, tg" 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-tg) TG_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 if [[ "$MODE" == "tg" ]] && ! $TG_CHECK; then error_exit "--no-tg cannot be used with --mode tg" fi if [[ "$MODE" == "geoblock" ]]; then DNS_CHECK=false TG_CHECK=false fi } print_proxy_setting() { if [[ -n "$PROXY" ]]; then printf "SOCKS5 proxy set to: %b%s%b\n" "$COLOR_WHITE" "$PROXY" "$COLOR_RESET" 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" ;; tg) mode="Telegram" ;; all) mode="All" ;; 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" if [[ "$MODE" == "tg" ]]; then print_proxy_setting return fi 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" print_proxy_setting 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() { printf "\n" check_encrypted_dns_servers check_dns_hijacking } print_side_by_side() { awk -v gap="$SIDE_BY_SIDE_GAP" ' NR == FNR { left[FNR] = $0 visible = $0 gsub(/\033\[[0-9;]*m/, "", visible) left_width[FNR] = length(visible) if (left_width[FNR] > width) width = left_width[FNR] left_count = FNR next } { right[FNR] = $0; right_count = FNR } END { count = left_count > right_count ? left_count : right_count for (i = 1; i <= count; i++) { if (right[i] == "") { print left[i]; continue } printf "%s%" (width - left_width[i] + gap) "s%s\n", left[i], "", right[i] } } ' "$1" "$2" } run_dns_and_telegram_checks() { local dns_table tg_table dns_table=$(mktemp) printf "\n" check_encrypted_dns_servers >"$dns_table" tg_table=$(check_telegram_dcs) print_side_by_side "$dns_table" <(printf "%s\n" "$tg_table") rm -f "$dns_table" 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[@]}" ;; all) 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" ]] } resolve_via_plain_dns() { local domain=$1 local server=$2 dig @"$server" "$domain" "$(get_record_type)" \ +timeout="$TIMEOUT" +tries=1 +noall +comments +answer 2>/dev/null | awk ' /status:/ { match($0, /status: [A-Z]+/) rcode = substr($0, RSTART + 8, RLENGTH - 8) } $4 == "A" || $4 == "AAAA" { ips = ips " " $5 } END { if (rcode) print rcode ips } ' || 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() { local domain=$1 local server_ip=$2 local transport=$3 local answer if [[ "$transport" == "udp" ]]; then answer=$(resolve_via_plain_dns "$domain" "$server_ip") else answer=$(resolve_via_dig "$domain" "$server_ip" "$transport") fi if [[ -n "$answer" ]]; then echo "available" else echo "blocked" fi } check_encrypted_dns_servers() { local test_domain="rutracker.org" local entry name ip udp_status doh_status dot_status local table_rows=() local i tmp_dir local pids=() local transports=("udp" "https" "tls") local transport AVAILABLE_DOH=() AVAILABLE_DOT=() tmp_dir=$(mktemp -d) for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do ip="${ENCRYPTED_DNS_SERVERS[$i]##*|}" for transport in "${transports[@]}"; do probe_resolver_transport "$test_domain" "$ip" "$transport" >"$tmp_dir/${i}_${transport}" & pids+=("$!") done done for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do entry="${ENCRYPTED_DNS_SERVERS[$i]}" show_progress "$((i + 1))" "${#ENCRYPTED_DNS_SERVERS[@]}" "${entry%%|*} ${entry##*|}" wait "${pids[@]:i*${#transports[@]}:${#transports[@]}}" || true done clear_progress for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do entry="${ENCRYPTED_DNS_SERVERS[$i]}" name="${entry%%|*}" ip="${entry##*|}" if [[ "$(<"$tmp_dir/${i}_udp")" == "available" ]]; then udp_status="$MSG_AVAILABLE" else udp_status="$MSG_BLOCKED" fi 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\t%s" \ "$name" "$COLOR_RESET" "$ip" \ "$(colorize_summary "$udp_status")" \ "$(colorize_summary "$doh_status")" \ "$(colorize_summary "$dot_status")")") done rm -rf "$tmp_dir" printf "%bDNS resolver availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET" { printf "\033[1m%b%s\t%s\t%s\t%s\t%s%b\033[0m\n" \ "$COLOR_WHITE" "Resolver" "IP" "UDP" "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 } classify_plain_answer() { local answer=$1 local reference_ips_name=$2 local rcode local plain_ips=() read -r -a plain_ips <<<"$answer" rcode="${plain_ips[0]:-}" plain_ips=("${plain_ips[@]:1}") if [[ "$rcode" == "NXDOMAIN" ]]; then printf "%s (NXDOMAIN)" "$MSG_SPOOFED" elif [[ ${#plain_ips[@]} -eq 0 ]]; then printf "%s" "$MSG_NO_RESPONSE" elif have_ip_intersection plain_ips "$reference_ips_name"; then printf "%s" "$MSG_CLEAN" else printf "%s (%s)" "$MSG_SPOOFED" "${plain_ips[0]}" fi } check_dns_hijacking() { local test_domains=("rutracker.org" "linkedin.com" "flibusta.is") local encrypted_ips=() local table_rows=() local hijacked_domain="" local spoofed_count=0 local test_domain domain_index i tmp_dir status local reference reference_entry reference_transport reference_name reference_ip reference=$(get_reference_resolver) printf "\n" if [[ -n "$PROXY" ]]; then printf "%b%s%b\n\n" \ "$COLOR_ORANGE" "DNS checks run from this machine, not through the proxy" "$COLOR_RESET" fi if [[ -z "$reference" ]]; then printf "%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 domain_index in "${!test_domains[@]}"; do test_domain="${test_domains[$domain_index]}" show_progress "$((domain_index + 1))" "${#test_domains[@]}" "DNS hijacking with $test_domain" mapfile -t encrypted_ips < <(resolve_via_dig "$test_domain" "$reference_ip" "$reference_transport") [[ ${#encrypted_ips[@]} -eq 0 ]] && continue spoofed_count=0 table_rows=() tmp_dir=$(mktemp -d) for i in "${!DNS_SERVERS[@]}"; do resolve_via_plain_dns "$test_domain" "${DNS_SERVERS[$i]}" >"$tmp_dir/$i" & done wait for i in "${!DNS_SERVERS[@]}"; do status=$(classify_plain_answer "$(<"$tmp_dir/$i")" encrypted_ips) if [[ "$status" == "$MSG_SPOOFED"* ]]; then spoofed_count=$((spoofed_count + 1)) fi table_rows+=("$(printf "%s\t%s" "${DNS_SERVERS[$i]}" "$(colorize_summary "$status")")") done rm -rf "$tmp_dir" if [[ $spoofed_count -gt 0 ]]; then hijacked_domain="$test_domain" break fi done clear_progress if [[ -z "$hijacked_domain" ]]; then printf "%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" return 0 fi printf "%b%s%b %s %b%s%b\n" \ "$COLOR_RED" "DNS hijacking detected!" "$COLOR_RESET" \ "$spoofed_count of ${#DNS_SERVERS[@]} plain DNS resolvers spoofed for" \ "$COLOR_WHITE" "$hijacked_domain" "$COLOR_RESET" printf "%bReference: %s (%s) over %s -> %s%b\n\n" \ "$COLOR_WHITE" "$reference_name" "$reference_ip" "${reference_transport^^}" \ "$(printf "%s, " "${encrypted_ips[@]}" | sed 's/, $//')" "$COLOR_RESET" { printf "\033[1m%b%s\t%s%b\033[0m\n" "$COLOR_WHITE" "Resolver" "Result" "$COLOR_RESET" printf "%s\n" "${table_rows[@]}" } | column -t -s $'\t' printf "\n%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" } hex_to_bin() { local bytes=() mapfile -t bytes < <(fold -w2 <<<"$1") printf "%b" "$(printf '\\x%s' "${bytes[@]}")" } bin_to_hex() { od -An -v -tx1 | tr -d ' \n' } reverse_hex() { fold -w2 <<<"$1" | tac | tr -d '\n' } le32_hex() { local value=$1 printf "%02x%02x%02x%02x" $((value & 255)) $((value >> 8 & 255)) $((value >> 16 & 255)) $((value >> 24 & 255)) } aes_ctr() { openssl enc -aes-256-ctr -K "$1" -iv "$2" -nosalt 2>/dev/null } random_obfuscated_header() { local header while true; do header=$(head -c "$MTPROTO_HEADER_BYTES" /dev/urandom | bin_to_hex) case "${header:0:8}" in ef* | 48454144 | 504f5354 | 47455420 | 4f505449 | dddddddd | eeeeeeee | 16030102) continue ;; esac if [[ "${header:8:8}" == "00000000" ]]; then continue fi echo "$header" return done } socks5_connect() { local ip=$1 local port=$2 local reply a b c d hex_to_bin "050100" >&3 reply=$(head -c 2 <&3 | bin_to_hex) if [[ "$reply" != "0500" ]]; then return 1 fi IFS=. read -r a b c d <<<"$ip" hex_to_bin "$(printf "05010001%02x%02x%02x%02x%04x" "$a" "$b" "$c" "$d" "$port")" >&3 reply=$(head -c 10 <&3 | bin_to_hex) [[ "${reply:0:4}" == "0500" ]] } mtproto_exchange() { local ip=$1 local dc_id=$2 local proxy=$3 local header nonce request key iv reversed decrypt_key decrypt_iv sealed reply plain header=$(random_obfuscated_header) header="${header:0:112}${MTPROTO_INTERMEDIATE_TAG}$(printf "%02x00" "$dc_id")${header:124:4}" nonce=$(head -c 16 /dev/urandom | bin_to_hex) # frame length, auth_key_id, msg_id, message length, constructor, nonce request="28000000""0000000000000000""00000000$(le32_hex "$(date +%s)")""14000000""${MTPROTO_REQ_PQ_MULTI}${nonce}" key=${header:16:64} iv=${header:80:32} reversed=$(reverse_hex "${header:16:96}") decrypt_key=${reversed:0:64} decrypt_iv=${reversed:64:32} sealed=$(hex_to_bin "${header}${request}" | aes_ctr "$key" "$iv" | bin_to_hex) if [[ -n "$proxy" ]]; then exec 3<>"/dev/tcp/${proxy%:*}/${proxy##*:}" || return 1 socks5_connect "$ip" "$TELEGRAM_PORT" || return 1 else exec 3<>"/dev/tcp/$ip/$TELEGRAM_PORT" || return 1 fi hex_to_bin "${header:0:112}${sealed:112}" >&3 reply=$(head -c "$MTPROTO_RES_PQ_BYTES" <&3 | bin_to_hex) if [[ ${#reply} -ne $((MTPROTO_RES_PQ_BYTES * 2)) ]]; then return 1 fi plain=$(hex_to_bin "$reply" | aes_ctr "$decrypt_key" "$decrypt_iv" | bin_to_hex) # frame length, auth_key_id, msg_id, message length, constructor, nonce [[ "${plain:48:8}" == "$MTPROTO_RES_PQ" && "${plain:56:32}" == "$nonce" ]] } probe_telegram_dc() { local ip=$1 local dc_id=$2 local attempt exchange_script exchange_script="$(declare -p TELEGRAM_PORT MTPROTO_HEADER_BYTES MTPROTO_INTERMEDIATE_TAG MTPROTO_REQ_PQ_MULTI MTPROTO_RES_PQ MTPROTO_RES_PQ_BYTES) $(declare -f hex_to_bin bin_to_hex reverse_hex le32_hex aes_ctr random_obfuscated_header socks5_connect mtproto_exchange) mtproto_exchange \"\$@\"" for ((attempt = 0; attempt <= RETRIES; attempt++)); do if timeout "$TIMEOUT" bash -c "$exchange_script" _ "$ip" "$dc_id" "$PROXY" 2>/dev/null; then echo "available" return fi done echo "blocked" } check_telegram_dcs() { local entry name ip status i tmp_dir local table_rows=() local pids=() tmp_dir=$(mktemp -d) for i in "${!TELEGRAM_DCS[@]}"; do entry="${TELEGRAM_DCS[$i]}" name="${entry%%|*}" probe_telegram_dc "${entry##*|}" "${name#DC}" >"$tmp_dir/$i" & pids+=("$!") done for i in "${!TELEGRAM_DCS[@]}"; do entry="${TELEGRAM_DCS[$i]}" show_progress "$((i + 1))" "${#TELEGRAM_DCS[@]}" "Telegram ${entry%%|*} ${entry##*|}" wait "${pids[$i]}" || true done clear_progress for i in "${!TELEGRAM_DCS[@]}"; do entry="${TELEGRAM_DCS[$i]}" name="${entry%%|*}" ip="${entry##*|}" if [[ "$(<"$tmp_dir/$i")" == "available" ]]; then status="$MSG_AVAILABLE" else status="$MSG_BLOCKED" fi table_rows+=("$(printf "%s%b\t%s\t%s" \ "$name" "$COLOR_RESET" "$ip" \ "$(colorize_summary "$status")")") done rm -rf "$tmp_dir" printf "%bTelegram DC availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET" { printf "\033[1m%b%s\t%s\t%s%b\033[0m\n" \ "$COLOR_WHITE" "DC" "IP" "MTProto" "$COLOR_RESET" printf "%s\n" "${table_rows[@]}" } | column -t -s $'\t' } 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="" 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 ;; Spoofed) first_word_color=$COLOR_RED ;; Clean) first_word_color=$COLOR_GREEN ;; *) 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 "\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" || "$MODE" == "tg" ]]; then if $JSON_OUTPUT; then error_exit "JSON output is not supported for $MODE mode" fi if $SHOW_HEADER; then print_header fi if [[ "$MODE" == "dns" ]]; then run_dns_checks else printf "\n" check_telegram_dcs fi 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 && $TG_CHECK; then run_dns_and_telegram_checks elif $DNS_CHECK; then run_dns_checks elif $TG_CHECK; then printf "\n" check_telegram_dcs fi if $SHOW_HEADER || $DNS_CHECK || $TG_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 "$@"