#!/usr/bin/env bash # dnsperftest - Test DNS performances # Version: 0.0.5 -- Last update: Tue Apr 21 14:15:24 2026 # (C) 2026 F-Hauri.ch - http://www.f-hauri.ch # Rewrite, based on https://raw.githubusercontent.com/cleanbrowsing/dnsperftest/master/dnstest.sh # Run all test parallelized in order to speed up whole process. # Add a very useful spinner... shopt -s extglob command -v bc > /dev/null || { echo "error: bc was not found. Please install bc."; exit 1; } { command -v drill > /dev/null && function req() { timeout 1 drill "$@" ; } ; } || { command -v dig > /dev/null && function req() { dig +tries=1 +time=2 +stats "$@";};} export -f req &>/dev/null || { echo "error: dig was not found. Please install dnsutils."; exit 1; } # Spinner from https://f-hauri.ch/vrac/spinnerBg.sh # --- Minimal code for simple backgrounded spinner without json begin here! --- declare spinnerShapes=( ⢈⡱ ⢄⡱ ⢆⡰ ⢎⡠ ⢎⡁ ⢎⠑ ⠎⠱ ⠊⡱ ) declare -i doSpinner=0 # File descriptor to address spinner as background task declare -xi spinnerFD # File descriptor for spinner output, in order to not [[ -t $spinnerFD ]] || exec {spinnerFD}>&1 # use any of STDOUT nor STDERR. spinner() { # spinner as job. ( Use global spinnerShapes) local -i pnt while ! read -rsn 1 -t .06 _; do # while no input until delay of .06 sec # Save cursor position, render frame, restore cursor position printf >&$spinnerFD '\e7%b\e8' \ "${spinnerShapes[pnt++%${#spinnerShapes[@]}]}" done } startSpinner() { # startSpinner # no arguments printf '\e[?25l' >&$spinnerFD # Cursor invisible exec {doSpinner}> >(spinner) # Run spinner with dedicated FD to stop } stopSpinner() { # stopSpinner # no arguments if (( doSpinner )) && echo >&"$doSpinner"; then # Echo newline in FD exec {doSpinner}>&- # Close doSpinner FD doSpinner=0 printf '\e[?25h\r\e[K' >&$spinnerFD # Cursor visible fi } # Run bc as background process and set scale=2 coproc BC { bc -l ;} echo >&${BC[1]} "scale=2" providersV4=( 1.1.1.1/cloudflare 4.2.2.1/level3 8.8.8.8/google 9.9.9.9/quad9 80.80.80.80/freenom 208.67.222.123/opendns 199.85.126.20/norton 185.228.168.168/cleanbrowsing 77.88.8.7/yandex 156.154.70.3/neustar 8.26.56.26/comodo 45.90.28.202/nextdns ) providersV6=( 2606:4700:4700::1111/cloudflare-v6 2001:4860:4860::8888/google-v6 2620:fe::fe/quad9-v6 2620:119:35::35/opendns-v6 2a0d:2a00:1::1/cleanbrowsing-v6 2a02:6b8::feed:0ff/yandex-v6 2a00:5a60::ad1:0ff/adguard-v6 2610:a1:1018::3/neustar-v6 ) hasipv6() { # Testing for IPv6 (As a function, to be executed only if asked for) req @2a0d:2a00:1::1 www.google.com |& grep -q 216.239.38.120 && return 0 return 1 } # Read 1st command line argument case $1 in ipv6 ) if ! hasipv6; then echo "error: IPv6 support not found. Unable to do the ipv6 test." exit 1 fi providers=("${providersV6[@]}") ;; all ) providers=("${providersV4[@]}") hasipv6 && providers+=("${providersV6[@]}") ;; * ) providers=("${providersV4[@]}") ;; esac # Domains to test. Duplicated domains are ok domains2test=( www.google.com amazon.com facebook.com www.youtube.com www.reddit.com wikipedia.org twitter.com gmail.com www.google.com whatsapp.com cartou.ch enfi.ch github.com stackexchange.com ) # Print header line and prepare some variables declare -i totaldomains=0 domcnt=0 pids=() printf -v headerLine "%-20s" "" for d in "${domains2test[@]}"; do totaldomains+=1 printf -v headerLine "%s%8s" "$headerLine" "test$totaldomains" strAvgs+=("test$totaldomains") # List of domains(tests) for averages declare -i "test$totaldomains" # by domain (Use bash integer variables) done printf -v headerLine '%s%11s' "$headerLine" "Average" startSpinner printf >&$spinnerFD '\rList local DNS... \e[K' # Get all nameservers, which answer to ping, in a bash array mapfile -t nameservers < <(. <( sed -ne '/^nameserver \([0-9.]\+\)$/{ s@@ping -nc 1 \1 \&>/dev/null \&\& echo \1 \&@p }' /etc/resolv.conf) wait ) printf >&$spinnerFD '\rDo DNS performances tests... \e[K' for p in "${nameservers[@]}" "${providers[@]}"; do IFS=/ read -r pip pname <<<"$p" [[ $pname ]] || pname=$pip exec {fd[domcnt++]}< <( declare -i ftime=0 printf -v string "%-21s" "$pname" # Build string to send once to parent for d in "${domains2test[@]}"; do ttime=$( req @$pip $d | sed -une 's/^;; Query time: \([0-9]\+\) msec/\1/p') if [[ -z $ttime ]]; then #let's have time out be 1s = 1000ms ttime=1000 elif [[ $ttime == 0 ]]; then ttime=1 fi printf -v string "%s%4.0f ms " "$string" "$ttime" ftime+=ttime done echo >&${BC[1]} "$ftime/$totaldomains" # Use backgrounded BC read -ru ${BC[0]} avg # to compute average printf '%s%7.2f ms\n' "$string" "$avg" # Send builded string to parent exit 0 ) pids[$!]= done # Wait until all subprocess finish wait "${!pids[@]}" declare -i testAvg # integer variable for overall average # Prepare output to be sorted testResults=() for ((i=0;i<$domcnt;i++));do read -ru ${fd[i]} "line" testResults+=("$line") printf -v tmpStr '%s+=%%s ' "${strAvgs[@]}" "testAvg" line=${line#* } printf -v tmpStr "$tmpStr" ${line//+( ms|.)} # Drop radix point, multiply by 100 eval "$tmpStr" done # Compute averages by domain eval "echo \"${strAvgs[@]//*/\$&\/$domcnt\;}\" $testAvg/$domcnt/100 >&${BC[1]}" mapfile -t -n $(( 1 + totaldomains )) -u ${BC[0]} Avgs stopSpinner echo "$headerLine" sort -nk $(( 2 * totaldomains + 2 )) < <( printf '%s\n' "${testResults[@]}" ) echo "Averages by domain:" printf -v tmpStr ' - %-9s %%-21s %%%%6.2f\n' "${strAvgs[@]}" Overall printf -v tmpStr "$tmpStr" "${domains2test[@]}" '' printf "$tmpStr" "${Avgs[@]}" | column