a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
351 lines
15 KiB
Bash
Executable File
351 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### dns-propagation-checker.sh — Check DNS propagation across public resolvers ####
|
|
#### Queries Cloudflare, Google, Quad9, OpenDNS, compares results ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./dns-propagation-checker.sh example.com ####
|
|
#### ./dns-propagation-checker.sh example.com --type MX ####
|
|
#### ./dns-propagation-checker.sh example.com --watch 30 ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
DOMAIN=""
|
|
RECORD_TYPE="A"
|
|
TIMEOUT=5
|
|
COLOR="auto"
|
|
JSON_OUTPUT="false"
|
|
WATCH_INTERVAL=0
|
|
EXPECTED=""
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
|
|
# ── Built-in Resolvers ───────────────────────────────────────────────
|
|
RESOLVER_NAMES=("Cloudflare" "Google" "Quad9" "OpenDNS" "Cloudflare-2" "Google-2")
|
|
RESOLVER_IPS=("1.1.1.1" "8.8.8.8" "9.9.9.9" "208.67.222.222" "1.0.0.1" "8.8.4.4")
|
|
CUSTOM_RESOLVERS=()
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USAGE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${SCRIPT_NAME} — Check DNS propagation across public resolvers
|
|
|
|
USAGE:
|
|
${SCRIPT_NAME} DOMAIN [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--type TYPE Record type: A, AAAA, MX, CNAME, TXT, NS, SOA, PTR (default: A)
|
|
--resolver IP Add a custom resolver (repeatable)
|
|
--expected VALUE Expected answer to verify against
|
|
--json Output results in JSON format
|
|
--watch SECONDS Re-check every N seconds until all resolvers agree
|
|
--timeout SECONDS Per-query timeout in seconds (default: 5)
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
EXAMPLES:
|
|
# Check A record propagation
|
|
./${SCRIPT_NAME} example.com
|
|
|
|
# Check MX records
|
|
./${SCRIPT_NAME} example.com --type MX
|
|
|
|
# Watch until propagated
|
|
./${SCRIPT_NAME} example.com --type A --watch 30
|
|
|
|
# Verify specific value
|
|
./${SCRIPT_NAME} example.com --expected "203.0.113.50"
|
|
|
|
# Add custom resolvers
|
|
./${SCRIPT_NAME} example.com --resolver 4.2.2.1 --resolver 77.88.8.8
|
|
|
|
# JSON output
|
|
./${SCRIPT_NAME} example.com --json
|
|
EOF
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ARGUMENT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--type)
|
|
RECORD_TYPE="${2^^}"; shift 2 ;;
|
|
--resolver)
|
|
CUSTOM_RESOLVERS+=("$2"); shift 2 ;;
|
|
--expected)
|
|
EXPECTED="$2"; shift 2 ;;
|
|
--json)
|
|
JSON_OUTPUT="true"; shift ;;
|
|
--watch)
|
|
WATCH_INTERVAL="$2"; shift 2 ;;
|
|
--timeout)
|
|
TIMEOUT="$2"; shift 2 ;;
|
|
--no-color)
|
|
COLOR="never"; shift ;;
|
|
--help|-h)
|
|
setup_colors
|
|
usage
|
|
exit 0 ;;
|
|
-*)
|
|
err "Unknown option: $1"
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1 ;;
|
|
*)
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
DOMAIN="$1"
|
|
else
|
|
err "Unexpected argument: $1"
|
|
exit 1
|
|
fi
|
|
shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
err "Domain name is required"
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local valid_types="A AAAA MX CNAME TXT NS SOA PTR"
|
|
if [[ ! " $valid_types " =~ " $RECORD_TYPE " ]]; then
|
|
err "Invalid record type: $RECORD_TYPE"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# DNS QUERY
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
query_resolver() {
|
|
local resolver_ip="$1" domain="$2" rtype="$3" timeout="$4"
|
|
local output ttl_output answer ttl
|
|
|
|
output=$(dig +time="$timeout" +tries=1 +short "@${resolver_ip}" "$domain" "$rtype" 2>/dev/null) || true
|
|
ttl_output=$(dig +time="$timeout" +tries=1 +noall +answer "@${resolver_ip}" "$domain" "$rtype" 2>/dev/null) || true
|
|
answer=$(echo "$output" | tr '\n' ' ' | sed 's/ *$//')
|
|
ttl=$(echo "$ttl_output" | awk 'NR==1{print $2}')
|
|
|
|
if [[ -z "$answer" ]]; then
|
|
echo "FAIL||"; return
|
|
fi
|
|
echo "${answer}|${ttl:-?}|"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAJORITY ANSWER
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
find_majority() {
|
|
local -n answers_ref=$1
|
|
local -A counts
|
|
local max_count=0 majority=""
|
|
for answer in "${answers_ref[@]}"; do
|
|
[[ "$answer" == "FAIL" ]] && continue
|
|
counts["$answer"]=$(( ${counts["$answer"]:-0} + 1 ))
|
|
if [[ ${counts["$answer"]} -gt $max_count ]]; then
|
|
max_count=${counts["$answer"]}; majority="$answer"
|
|
fi
|
|
done
|
|
echo "$majority"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# RUN CHECK
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
run_check() {
|
|
local all_names=("${RESOLVER_NAMES[@]}")
|
|
local all_ips=("${RESOLVER_IPS[@]}")
|
|
|
|
for custom in "${CUSTOM_RESOLVERS[@]}"; do
|
|
all_names+=("Custom-${custom}")
|
|
all_ips+=("$custom")
|
|
done
|
|
|
|
local total=${#all_names[@]}
|
|
local answers=() ttls=()
|
|
|
|
for i in $(seq 0 $(( total - 1 ))); do
|
|
local result
|
|
result=$(query_resolver "${all_ips[$i]}" "$DOMAIN" "$RECORD_TYPE" "$TIMEOUT")
|
|
answers+=("$(echo "$result" | cut -d'|' -f1)")
|
|
ttls+=("$(echo "$result" | cut -d'|' -f2)")
|
|
done
|
|
|
|
local majority
|
|
majority=$(find_majority answers)
|
|
local compare_to="${EXPECTED:-$majority}"
|
|
local agree_count=0 statuses=()
|
|
|
|
for i in $(seq 0 $(( total - 1 ))); do
|
|
if [[ "${answers[$i]}" == "FAIL" ]]; then
|
|
statuses+=("FAIL")
|
|
elif [[ "${answers[$i]}" == "$compare_to" ]]; then
|
|
statuses+=("MATCH"); agree_count=$((agree_count + 1))
|
|
else
|
|
statuses+=("MISMATCH")
|
|
fi
|
|
done
|
|
|
|
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
|
print_json all_names all_ips answers ttls statuses "$agree_count" "$total" "$majority"
|
|
else
|
|
print_table all_names all_ips answers ttls statuses "$agree_count" "$total" "$majority" "$compare_to"
|
|
fi
|
|
[[ "$agree_count" -eq "$total" ]] && return 0 || return 1
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# OUTPUT: TABLE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
print_table() {
|
|
local -n names_ref=$1 ips_ref=$2 ans_ref=$3 ttl_ref=$4 stat_ref=$5
|
|
local agree="$6" total="$7" majority="$8" compare="$9"
|
|
|
|
echo ""
|
|
echo -e "${BOLD}DNS Propagation Check — ${DOMAIN} (${RECORD_TYPE})${RESET}"
|
|
echo -e "${DIM}$(date -u '+%Y-%m-%d %H:%M:%S UTC')${RESET}"
|
|
echo ""
|
|
printf " ${BOLD}%-20s %-17s %-22s %-6s %s${RESET}\n" "RESOLVER" "IP" "RESULT" "TTL" "STATUS"
|
|
printf " %s\n" "$(printf '%.0s─' {1..78})"
|
|
|
|
local count=${#names_ref[@]}
|
|
for i in $(seq 0 $(( count - 1 ))); do
|
|
local color status_str
|
|
case "${stat_ref[$i]}" in
|
|
MATCH) color="$GREEN"; status_str="MATCH" ;;
|
|
MISMATCH) color="$YELLOW"; status_str="MISMATCH" ;;
|
|
FAIL) color="$RED"; status_str="FAIL" ;;
|
|
esac
|
|
|
|
local display_answer="${ans_ref[$i]}"
|
|
if [[ ${#display_answer} -gt 20 ]]; then
|
|
display_answer="${display_answer:0:17}..."
|
|
fi
|
|
|
|
printf " %-20s %-17s %b%-22s%b %-6s %b%s%b\n" \
|
|
"${names_ref[$i]}" \
|
|
"${ips_ref[$i]}" \
|
|
"$color" "$display_answer" "$RESET" \
|
|
"${ttl_ref[$i]}" \
|
|
"$color" "$status_str" "$RESET"
|
|
done
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Summary${RESET}"
|
|
if [[ -n "$EXPECTED" ]]; then
|
|
printf " %-20s %s\n" "Expected answer:" "$EXPECTED"
|
|
fi
|
|
printf " %-20s %s\n" "Majority answer:" "${majority:-N/A}"
|
|
printf " %-20s %s\n" "Agree:" "${agree}/${total} resolvers"
|
|
|
|
if [[ "$agree" -eq "$total" ]]; then
|
|
printf " %-20s " "Status:"; echo -e "${GREEN}PROPAGATION COMPLETE${RESET}"
|
|
else
|
|
printf " %-20s " "Status:"; echo -e "${YELLOW}PROPAGATION PENDING${RESET}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# OUTPUT: JSON
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
print_json() {
|
|
local -n jnames=$1 jips=$2 jans=$3 jttls=$4 jstats=$5
|
|
local agree="$6" total="$7" majority="$8"
|
|
local count=${#jnames[@]} propagated="false"
|
|
[[ "$agree" -eq "$total" ]] && propagated="true"
|
|
|
|
printf '{"domain":"%s","type":"%s","timestamp":"%s","results":[' \
|
|
"$DOMAIN" "$RECORD_TYPE" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
for i in $(seq 0 $(( count - 1 ))); do
|
|
[[ $i -gt 0 ]] && printf ','
|
|
local escaped_answer
|
|
escaped_answer=$(echo "${jans[$i]}" | sed 's/"/\\"/g')
|
|
printf '{"resolver":"%s","ip":"%s","answer":"%s","ttl":"%s","status":"%s"}' \
|
|
"${jnames[$i]}" "${jips[$i]}" "$escaped_answer" "${jttls[$i]}" "${jstats[$i]}"
|
|
done
|
|
local escaped_majority
|
|
escaped_majority=$(echo "$majority" | sed 's/"/\\"/g')
|
|
printf '],"summary":{"majority":"%s","agree":%d,"total":%d,"propagated":%s}}\n' \
|
|
"$escaped_majority" "$agree" "$total" "$propagated"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
if ! command -v dig &>/dev/null; then
|
|
err "dig is required but not found. Install dnsutils (Debian/Ubuntu) or bind-utils (RHEL/CentOS)."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$WATCH_INTERVAL" -gt 0 ]]; then
|
|
local cycle=1
|
|
while true; do
|
|
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
|
[[ $cycle -gt 1 ]] && echo -e "${DIM}────────────────────────────────────────────────${RESET}"
|
|
echo -e "${DIM}Watch cycle ${cycle} — checking every ${WATCH_INTERVAL}s (Ctrl+C to stop)${RESET}"
|
|
fi
|
|
if run_check; then
|
|
[[ "$JSON_OUTPUT" != "true" ]] && echo -e " ${GREEN}All resolvers agree. Propagation complete.${RESET}\n"
|
|
exit 0
|
|
fi
|
|
cycle=$((cycle + 1))
|
|
sleep "$WATCH_INTERVAL"
|
|
done
|
|
else
|
|
run_check && exit 0 || exit 1
|
|
fi
|
|
}
|
|
|
|
main "$@"
|