Script for installing recursive DNS on Ubuntu Version 3.0

Script for installing recursive DNS on Ubuntu Version 3.0

I have written many times that ISPs should be running their own local recursive DNS servers. Today I am releasing this new version of my DNS recursive script for Ubuntu to the public. If you use this and find any bugs please let me know. If you find this helpful please consider any of the following

  1. Subscribe to my Youtube channel
  2. Donate via Paypal
  3. Subscribe to my Patreon.

This version does the following:

  • Checks to see if bind is installed. If not runs apt-get to install
  • Entering local IPv4 + IPv6 subnets
  • Builds a BIND ACL from your ranges
  • Allows recursion only from those ranges
  • Does NOT use forwarders. Root hints only
  • Fixes Ubuntu service names
  • Validating user-entered CIDRs (v4 or v6)
  • writing BIND ACLs that include v6
  • adding UFW rules for DNS over UDP/TCP 53 for both v4 and v6
  • making sure UFW IPV4 & IPv6 is installed and enabled
  • Checks UFW firewall rules to ensure ssh access

Notes at the bottom of this post.

#!/usr/bin/env bash
set -euo pipefail

# ---------------------------
# Helpers
# ---------------------------
is_cidr_v4() {
  [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]
}

is_cidr_v6() {
  # Practical IPv6 CIDR check
  [[ "$1" =~ ^[0-9A-Fa-f:]+/[0-9]{1,3}$ ]]
}

is_cidr() {
  is_cidr_v4 "$1" || is_cidr_v6 "$1"
}

dedupe_list() {
  printf "%s\n" "$@" | awk 'NF' | sort -u
}

ensure_pkg() {
  local pkg="$1"
  if ! dpkg -s "$pkg" >/dev/null 2>&1; then
    echo "Installing $pkg..."
    sudo apt-get update
    sudo apt-get install -y "$pkg"
  fi
}

# ---------------------------
# Install required packages
# ---------------------------
ensure_pkg "bind9"
ensure_pkg "bind9-utils"
ensure_pkg "dnsutils"
ensure_pkg "ufw"
ensure_pkg "curl"

# ---------------------------
# Discover local subnets (v4 + v6)
# ---------------------------
predefined_ranges=()

while IFS= read -r cidr; do
  predefined_ranges+=("$cidr")
done < <(ip -o -f inet addr show scope global | awk '{print $4}' | sort -u)

while IFS= read -r cidr; do
  predefined_ranges+=("$cidr")
done < <(ip -o -f inet6 addr show scope global | awk '{print $4}' | sort -u)

if [[ "${#predefined_ranges[@]}" -eq 0 ]]; then
  v4_route_cidr="$(ip -4 route list scope link | awk 'NF{print $1; exit}' || true)"
  v6_route_cidr="$(ip -6 route list scope link | awk 'NF{print $1; exit}' || true)"
  [[ -n "${v4_route_cidr:-}" && "${v4_route_cidr}" != "default" ]] && predefined_ranges+=("$v4_route_cidr")
  [[ -n "${v6_route_cidr:-}" && "${v6_route_cidr}" != "default" ]] && predefined_ranges+=("$v6_route_cidr")
fi

echo "The following local subnets will be automatically included for recursion:"
for range in "${predefined_ranges[@]}"; do
  echo " - $range"
done

echo
echo "You can add additional IP ranges (IPv4 or IPv6 CIDR)."
echo "Enter 'done' when you are finished."

additional_ranges=()
while :; do
  read -rp "Enter IP range in CIDR (or 'done'): " ip_range
  [[ "$ip_range" == "done" ]] && break

  if ! is_cidr "$ip_range"; then
    echo "Invalid CIDR: $ip_range"
    echo "Examples: 38.86.64.0/22   2001:db8:1234::/48"
    continue
  fi

  additional_ranges+=("$ip_range")
done

mapfile -t all_ranges < <(dedupe_list "${predefined_ranges[@]}" "${additional_ranges[@]}")

echo
echo "Final allowed recursion ranges:"
for range in "${all_ranges[@]}"; do
  echo " - $range"
done

# ---------------------------
# Configure BIND
# ---------------------------
acl_lines=""
for range in "${all_ranges[@]}"; do
  acl_lines+="        ${range};"$'\n'
done

sudo tee /etc/bind/named.conf.options >/dev/null </dev/null || sudo systemctl restart named

# ---------------------------
# SSH port change (safer flow)
# ---------------------------
read -rp "Enter custom SSH port (e.g., 2222): " ssh_port
if [[ ! "$ssh_port" =~ ^[0-9]+$ ]] || (( ssh_port < 1 || ssh_port > 65535 )); then
  echo "Invalid SSH port: $ssh_port"
  exit 1
fi

sudo sed -i -E "s/^[#[:space:]]*Port[[:space:]]+[0-9]+/Port ${ssh_port}/" /etc/ssh/sshd_config

if ! grep -qE '^[[:space:]]*Port[[:space:]]+' /etc/ssh/sshd_config; then
  echo "Port ${ssh_port}" | sudo tee -a /etc/ssh/sshd_config >/dev/null
fi

sudo systemctl restart ssh

# ---------------------------
# UFW safe defaults + rules
# ---------------------------
# Ensure UFW IPv6 rules apply
sudo sed -i -E 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw

# Safe defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH on the new port BEFORE enabling
sudo ufw allow "${ssh_port}"/tcp

# Allow DNS (UDP + TCP 53) from allowed ranges (IPv4 + IPv6)
for range in "${all_ranges[@]}"; do
  sudo ufw allow from "$range" to any port 53 proto udp
  sudo ufw allow from "$range" to any port 53 proto tcp
done

# Enable UFW (non-interactive)
sudo ufw --force enable

# ---------------------------
# Final message + checks
# ---------------------------
echo
echo "Setup complete!"
echo "SSH is now on port: $ssh_port"
echo "BIND recursion is allowed ONLY from the listed IPv4/IPv6 ranges."
echo
echo "Quick checks:"
echo " - DNS: dig @127.0.0.1 google.com +short"
echo " - Service: systemctl status bind9 --no-pager | head -n 20"
echo " - Firewall: sudo ufw status verbose"
echo
echo "IMPORTANT:"
echo "Open a NEW terminal and test SSH before closing your current session:"
echo "  ssh -p $ssh_port @"

Paste the script into a file on your server. Save it as a .sh file, Chmod the file to 755, and run it. For example, if you named it dnsinstall.sh you would do ./dnsinstall.sh and follow the prompts.

What the script actually configures

  1. Client asks your server for j2sw.com
  2. BIND asks:
    • the root servers
    • then the TLD servers
    • then the authoritative servers
  3. BIND caches the answer and replies

That is classic recursive resolution using root hints.

This is a basic script that gets a dns recursive server up and going very quickly. As with any software package, you can tune the performance, add security options, etc.

j2networks family of sites
https://j2sw.com
https://startawisp.info
https://indycolo.net
#packetsdownrange #routethelight