Find DMARC DKIM SPF Nameservers With One Script

Published: Aug 28, 2025

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

This script is tested on WSL running Ubuntu 24. Please run it at your own discretion.

How to run

#!/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"

🔵

Follow us: Prasna IT · LinkedIn · Facebook · Instagram

Previous Post
Next Post