If you are setting up Microsoft Office or Google Workspace, you’re constantly having to check DMARC, DKIM and SPF values and then check if the edited values have propagated. Here’s a script that does just that. It even saves main zone records in a new folder with a symlink of the latest output. All output is saved.
This script was developed with a large language model.
Features #
- Displays DKIM, SPF, DMARC and nameserver values.
- Polls against Cloudflare and Google servers.
- Saves a little bit more output to a folder with the domain name.
- Symlnks the latest file with ’latest’ in the name.
This script is tested on WSL running Ubuntu 24. Please run it at your own discretion.
How to run #
- Make your script executable. (chmod +x scriptname)
- Enter like this on the terminal: ./dns-check.sh example.com
#!/usr/bin/env bash
# dns-check.sh (summary + zone snapshot only)
# Usage:
# ./dns-check.sh [-r 1.1.1.1] domain.com
# Env:
# RESOLVER=1.1.1.1
# RESOLVERS=1.1.1.1,8.8.8.8,9.9.9.9
set -euo pipefail
# ----- args -----
resolver_cli=""
if [[ "${1:-}" == "-r" || "${1:-}" == "--resolver" ]]; then
resolver_cli="${2:-}"; shift 2 || true
fi
domain="${1:-}"
if [[ -z "${domain}" ]]; then
echo "Usage: $0 [-r 1.1.1.1] domain.com"; exit 1
fi
# ----- resolvers -----
if [[ -n "${resolver_cli}" ]]; then
resolvers=("${resolver_cli}")
elif [[ -n "${RESOLVER:-}" ]]; then
resolvers=("${RESOLVER}")
elif [[ -n "${RESOLVERS:-}" ]]; then
IFS=',' read -r -a resolvers <<< "$RESOLVERS"
else
resolvers=(1.1.1.1 8.8.8.8 9.9.9.9)
fi
# ----- deps -----
if ! command -v dig >/dev/null 2>&1; then
echo "Installing dnsutils..."; sudo apt update -y && sudo apt install -y dnsutils
fi
# ----- dig helpers (caching + single active resolver) -----
dig_quick_opts="+short +time=1 +tries=1"
dig_dump_opts="+noall +answer +time=1 +tries=1"
ACTIVE_RESOLVER=""
choose_resolver() {
for r in "${resolvers[@]}"; do
if dig ${dig_quick_opts} @"$r" NS . >/dev/null 2>&1; then
ACTIVE_RESOLVER="$r"; return
fi
done
ACTIVE_RESOLVER="${resolvers[0]}"
}
choose_resolver
declare -A CACHE_Q # key "TYPE|NAME" -> +short text
declare -A CACHE_F # key "TYPE|NAME" -> +answer text
_q_raw() { # raw +short
local rr="${1-}" name="${2-}"
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
dig ${dig_quick_opts} @"$ACTIVE_RESOLVER" "$rr" "$name" 2>/dev/null || true
}
_f_raw() { # raw +answer
local rr="${1-}" name="${2-}"
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
dig ${dig_dump_opts} @"$ACTIVE_RESOLVER" "$rr" "$name" 2>/dev/null || true
}
q() { # cached +short
local rr="${1-}" name="${2-}" k
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
k="${rr}|${name}"
if [[ -z "${CACHE_Q[$k]+x}" ]]; then
CACHE_Q[$k]="$(_q_raw "$rr" "$name")"
fi
printf "%s" "${CACHE_Q[$k]}"
}
f() { # cached +answer
local rr="${1-}" name="${2-}" k
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
k="${rr}|${name}"
if [[ -z "${CACHE_F[$k]+x}" ]]; then
CACHE_F[$k]="$(_f_raw "$rr" "$name")"
fi
printf "%s" "${CACHE_F[$k]}"
}
# ----- TXT helpers -----
get_txt_raw_lines() { q TXT "$1" || true; }
get_txt_records() {
local raw line
raw="$(get_txt_raw_lines "$1")"
[[ -z "$raw" ]] && return 0
while IFS= read -r line; do
printf "%s\n" "$line" | sed 's/^"//; s/"$//' | tr -d '"'
done <<< "$raw"
}
wrap_txt() { fold -s -w 70 | sed 's/^/ /'; }
ttl_of() { # parse TTL from cached full answer
local rr="${1-}" name="${2-}" ttl
[[ -z "$rr" || -z "$name" ]] && { printf "Auto"; return 0; }
ttl="$(f "$rr" "$name" | awk 'NR==1{print $2}')"
[[ -n "$ttl" ]] && printf "%s" "$ttl" || printf "Auto"
}
# ----- paths -----
mkdir -p "$domain" "names"
ts="$(date +"%Y-%m-%d_%H-%M-%S")"
zonesfile="$domain/${domain}_${ts}.zones"
zones_latest="$domain/latest.zones"
# ====== HUMAN READABLE SUMMARY ======
echo "Checking DNS records for: $domain"
echo "Resolver: $ACTIVE_RESOLVER (chosen from: ${resolvers[*]})"
echo "--------------------------------------------------"
# SPF
echo "[SPF]"
mapfile -t spf_list < <(get_txt_records "$domain" | awk 'BEGIN{IGNORECASE=1} /^v=spf1/ {print}')
if (( ${#spf_list[@]} == 0 )); then echo "No SPF record found"
elif (( ${#spf_list[@]} == 1 )); then echo "${spf_list[0]}"
else
echo "⚠️ Multiple SPF records found (${#spf_list[@]}). Merge into one:"
for i in "${!spf_list[@]}"; do printf " [%d] %s\n" "$((i+1))" "${spf_list[$i]}"; done
fi
echo
# DMARC
echo "[DMARC]"
mapfile -t dmarc_list < <(get_txt_records "_dmarc.$domain" | awk 'BEGIN{IGNORECASE=1} /^v=DMARC1/ {print}')
if (( ${#dmarc_list[@]} == 0 )); then echo "No DMARC record found"
elif (( ${#dmarc_list[@]} == 1 )); then echo "${dmarc_list[0]}"
else
echo "⚠️ Multiple DMARC records found (${#dmarc_list[@]}). Merge into a single record."
for i in "${!dmarc_list[@]}"; do printf " [%d] %s\n" "$((i+1))" "${dmarc_list[$i]}"; done
fi
echo
# MX
echo "[MX]"
mx="$(q MX "$domain" || true)"
[[ -n "$mx" ]] && echo "$mx" | sort -n -k1 || echo "No MX records found"
echo
# NS
echo "[Nameservers]"
ns="$(q NS "$domain" || true)"
[[ -n "$ns" ]] && echo "$ns" | sort || echo "No NS records found"
echo
# DKIM
echo "[DKIM]"
policy_recs="$(get_txt_records "_domainkey.$domain" || true)"
if [[ -n "$policy_recs" ]]; then
echo "Policy (_domainkey.$domain):"
while IFS= read -r pline; do echo " $pline"; done <<< "$policy_recs"
echo
fi
selectors=(s1 s2 selector1 selector2 default google mail k1 k2 mx dkim)
found_txt=0; found_cname=0; any_found=false
dkim_found_names=()
for sel in "${selectors[@]}"; do
rec="$sel._domainkey.$domain"
txt_lines="$(get_txt_records "$rec" || true)"
cname="$(q CNAME "$rec" || true)"
if [[ -n "$cname" ]]; then
any_found=true; ((found_cname++)) || true
echo "Selector: $sel"; echo " Record: $rec"; echo " CNAME → $cname"; echo
dkim_found_names+=("$rec")
fi
if [[ -n "$txt_lines" ]]; then
while IFS= read -r tline; do
if echo "$tline" | grep -qi '^v=DKIM1'; then
any_found=true; ((found_txt++)) || true
echo "Selector: $sel"; echo " Record: $rec"; echo " TXT →"
echo "$tline" | wrap_txt; echo
dkim_found_names+=("$rec")
fi
done <<< "$txt_lines"
fi
done
[[ "$any_found" != true ]] && echo "No DKIM records found with common selectors."
echo "Summary: DKIM TXT=$found_txt, CNAME=$found_cname"
echo "--------------------------------------------------"
# ===== ZONE SNAPSHOT (full answers; uses cached f()) =====
{
echo "; Zone snapshot for $domain"
echo "; Generated: $(date) | Resolver: $ACTIVE_RESOLVER"
echo
types=(SOA NS A AAAA CNAME MX TXT SRV CAA DS DNSKEY)
names=("$domain" "_dmarc.$domain" "_domainkey.$domain" "www.$domain" \
"autodiscover.$domain" "enterpriseenrollment.$domain" "enterpriseregistration.$domain" \
"lyncdiscover.$domain" "sip.$domain" "_sip._tls.$domain" "_sipfederationtls._tcp.$domain" \
"s1._domainkey.$domain" "s2._domainkey.$domain" "selector1._domainkey.$domain" "selector2._domainkey.$domain" \
"apple-domain-verification.$domain")
if [[ -f "names/$domain.txt" ]]; then
while IFS= read -r lbl; do
[[ -z "$lbl" || "$lbl" =~ ^# ]] && continue
names+=("$lbl.$domain")
done < "names/$domain.txt"
fi
if (( ${#dkim_found_names[@]} )); then names+=("${dkim_found_names[@]}"); fi
readarray -t names < <(printf "%s\n" "${names[@]}" | awk '!seen[$0]++')
for n in "${names[@]}"; do
echo ";; ===== $n ====="
for rr in "${types[@]}"; do
ans="$(f "$rr" "$n" || true)"
[[ -n "$ans" ]] && { echo ";; -- $rr"; echo "$ans"; }
done
echo
done
} > "$zonesfile"
# latest pointer for zones
if ln -sfn "$(basename "$zonesfile")" "$zones_latest" 2>/dev/null; then :; else cp -f "$zonesfile" "$zones_latest"; fi
echo "Zone snapshot written to: $zonesfile"
echo "Updated latest → $zones_latest"🔵