Files
linux-scripts/install-unbound.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
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.
2026-05-25 03:31:08 +02:00

952 lines
28 KiB
Bash

#!/bin/bash
################################################################################
# Script Name: install-unbound.sh
# Version: 1.1
# Description: Install and configure Unbound recursive DNS resolver with
# DNSSEC validation, DNS-over-TLS forwarding, local zones,
# unbound-control, and optional NSD stub-zone pairing
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
# Date: 2026-03-31
#
# Supported OS:
# - Ubuntu / Debian
# - RHEL / AlmaLinux / Rocky Linux / Fedora
#
# Usage:
# sudo ./install-unbound.sh
# sudo ./install-unbound.sh --forward cloudflare --dot
# sudo ./install-unbound.sh --forward google --local-zone home.lab
# sudo ./install-unbound.sh --nsd-stub example.com --nsd-port 5353
# sudo ./install-unbound.sh --uninstall
# ./install-unbound.sh --dry-run
#
################################################################################
set -euo pipefail
# ============================================================================
# CONFIGURATION
# ============================================================================
readonly VERSION="1.0"
readonly SCRIPT_NAME="${0##*/}"
readonly LOG_FILE="/var/log/unbound-install.log"
# Defaults
MODE="recursive" # recursive (root hints) or forward
FORWARDER="" # cloudflare, google, quad9, or custom IP
USE_DOT=false # DNS-over-TLS for forwarding
LOCAL_ZONES=() # Local zone names to create
LOCAL_ENTRIES=() # Individual local-data entries
NSD_STUB_ZONES=() # Zones to stub to NSD
NSD_PORT="5353" # Port NSD listens on
LISTEN_IP="127.0.0.1" # Default: localhost only
LISTEN_ALL=false # Listen on all interfaces
THREADS="" # Auto-detect from CPU cores
CACHE_SIZE="" # Auto-detect from RAM
UNINSTALL=false
DRY_RUN=false
# Paths (set after OS detection)
UNBOUND_CONF=""
UNBOUND_CONF_DIR=""
UNBOUND_USER=""
TRUST_ANCHOR=""
ROOT_HINTS=""
TLS_BUNDLE=""
# OS detection
OS=""
OS_FAMILY=""
# ============================================================================
# COLOR OUTPUT
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
print_success() { echo -e "${GREEN}$1${NC}"; }
print_error() { echo -e "${RED}$1${NC}" >&2; }
print_warning() { echo -e "${YELLOW}$1${NC}"; }
print_info() { echo -e "$1"; }
print_header() { echo -e "\n${CYAN}━━━ $1 ━━━${NC}"; }
# ============================================================================
# LOGGING
# ============================================================================
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
if [[ "$DRY_RUN" == "false" ]] && [[ -w "$(dirname "$LOG_FILE")" || -w "$LOG_FILE" ]]; then
echo "$msg" >> "$LOG_FILE"
fi
echo "$msg"
}
log_error() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1"
if [[ "$DRY_RUN" == "false" ]] && [[ -w "$(dirname "$LOG_FILE")" || -w "$LOG_FILE" ]]; then
echo "$msg" >> "$LOG_FILE"
fi
print_error "$1"
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
show_help() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Install and configure Unbound recursive DNS resolver.
MODE OPTIONS:
--recursive Full recursion from root hints (default)
--forward PROVIDER Forward to upstream: cloudflare, google, quad9, or IP
--dot Use DNS-over-TLS for forwarding (requires --forward)
NETWORK:
--listen IP IP to listen on (default: 127.0.0.1)
--listen-all Listen on all interfaces (0.0.0.0)
--allow-net CIDR Allow queries from network (repeatable, default: 127.0.0.0/8)
LOCAL ZONES:
--local-zone NAME Create a local zone (repeatable)
--local-data "RECORD" Add a local-data entry (repeatable)
Format: "hostname.zone. A 10.0.1.10"
NSD INTEGRATION:
--nsd-stub ZONE Add stub-zone for NSD (repeatable)
--nsd-port PORT NSD port (default: 5353)
TUNING:
--threads N Number of threads (default: auto-detect CPU cores)
--cache-size SIZE Message cache size in MB (default: auto-detect)
ACTIONS:
--uninstall Remove Unbound and configuration
--dry-run Show what would be done without executing
-h, --help Show this help message
--version Show version
EXAMPLES:
$SCRIPT_NAME
$SCRIPT_NAME --forward cloudflare --dot
$SCRIPT_NAME --forward google --listen-all --allow-net 10.0.0.0/8
$SCRIPT_NAME --local-zone home.lab --local-data "nas.home.lab. A 192.168.1.100"
$SCRIPT_NAME --nsd-stub example.com --nsd-port 5353
$SCRIPT_NAME --uninstall
EOF
exit 0
}
show_version() {
echo "$SCRIPT_NAME version $VERSION"
exit 0
}
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root (use sudo)"
exit 1
fi
}
# ============================================================================
# OS DETECTION
# ============================================================================
detect_os() {
if [[ ! -f /etc/os-release ]]; then
log_error "Cannot detect OS — /etc/os-release not found"
exit 1
fi
OS=$({ grep ^ID= /etc/os-release || true; } | cut -d= -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]')
case "$OS" in
ubuntu|debian)
OS_FAMILY="debian"
UNBOUND_CONF="/etc/unbound/unbound.conf"
UNBOUND_CONF_DIR="/etc/unbound/unbound.conf.d"
UNBOUND_USER="unbound"
TRUST_ANCHOR="/var/lib/unbound/root.key"
ROOT_HINTS="/usr/share/dns/root.hints"
TLS_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
;;
rhel|centos|rocky|almalinux|fedora)
OS_FAMILY="rhel"
UNBOUND_CONF="/etc/unbound/unbound.conf"
UNBOUND_CONF_DIR="/etc/unbound/conf.d"
UNBOUND_USER="unbound"
TRUST_ANCHOR="/var/lib/unbound/root.key"
ROOT_HINTS="/etc/unbound/root.hints"
TLS_BUNDLE="/etc/pki/tls/certs/ca-bundle.crt"
;;
*)
log_error "Unsupported OS: $OS"
echo "Supported: Ubuntu, Debian, RHEL, AlmaLinux, Rocky Linux, Fedora"
exit 1
;;
esac
log "Detected OS: $OS (family: $OS_FAMILY)"
}
# ============================================================================
# SYSTEMD-RESOLVED HANDLING
# ============================================================================
disable_resolved() {
if [[ "$OS" != "ubuntu" ]]; then
return
fi
if systemctl is-active --quiet systemd-resolved 2>/dev/null; then
print_warning "systemd-resolved is running and binds to port 53"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would disable systemd-resolved"
return
fi
log "Disabling systemd-resolved to free port 53"
systemctl stop systemd-resolved
systemctl disable systemd-resolved
if [[ -L /etc/resolv.conf ]]; then
rm -f /etc/resolv.conf
echo "nameserver 1.1.1.1" > /etc/resolv.conf
fi
print_success "Disabled systemd-resolved"
else
print_info "systemd-resolved not running — no action needed"
fi
}
# ============================================================================
# INSTALL UNBOUND
# ============================================================================
install_unbound() {
print_header "Installing Unbound"
if command -v unbound &>/dev/null; then
print_info "Unbound is already installed"
return
fi
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would install unbound packages"
return
fi
log "Installing Unbound packages"
case "$OS_FAMILY" in
debian)
apt-get update -qq
apt-get install -y -qq unbound unbound-host dns-root-data > /dev/null
;;
rhel)
if [[ "$OS" != "fedora" ]]; then
dnf install -y -q epel-release > /dev/null 2>&1 || true
fi
dnf install -y -q unbound > /dev/null
;;
esac
print_success "Unbound installed"
}
# ============================================================================
# AUTO-DETECT TUNING
# ============================================================================
detect_tuning() {
if [[ -z "$THREADS" ]]; then
THREADS=$(nproc 2>/dev/null || echo 2)
fi
if [[ -z "$CACHE_SIZE" ]]; then
local total_mb
total_mb=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 1024)
if (( total_mb <= 1024 )); then
CACHE_SIZE="16"
elif (( total_mb <= 2048 )); then
CACHE_SIZE="32"
elif (( total_mb <= 4096 )); then
CACHE_SIZE="64"
else
CACHE_SIZE="128"
fi
fi
# Slabs must be power of 2 >= threads
local slabs=2
while (( slabs < THREADS )); do
slabs=$((slabs * 2))
done
SLAB_COUNT=$slabs
local rrset_size=$((CACHE_SIZE * 2))
log "Tuning: threads=$THREADS, msg-cache=${CACHE_SIZE}m, rrset-cache=${rrset_size}m, slabs=$SLAB_COUNT"
}
# ============================================================================
# SETUP ROOT HINTS
# ============================================================================
setup_root_hints() {
if [[ "$MODE" != "recursive" ]]; then
return
fi
print_info "Setting up root hints"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would download root hints"
return
fi
local hints_dir
hints_dir=$(dirname "$ROOT_HINTS")
mkdir -p "$hints_dir"
if wget -qO "$ROOT_HINTS.tmp" https://www.internic.net/domain/named.cache 2>/dev/null; then
mv "$ROOT_HINTS.tmp" "$ROOT_HINTS"
chown "$UNBOUND_USER":"$UNBOUND_USER" "$ROOT_HINTS" 2>/dev/null || true
print_success "Root hints downloaded"
else
print_warning "Could not download root hints — using package default"
fi
}
# ============================================================================
# SETUP TRUST ANCHOR
# ============================================================================
setup_trust_anchor() {
print_info "Setting up DNSSEC trust anchor"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would set up DNSSEC trust anchor"
return
fi
local anchor_dir
anchor_dir=$(dirname "$TRUST_ANCHOR")
mkdir -p "$anchor_dir"
chown "$UNBOUND_USER":"$UNBOUND_USER" "$anchor_dir"
if command -v unbound-anchor &>/dev/null; then
unbound-anchor -a "$TRUST_ANCHOR" 2>/dev/null || true
chown "$UNBOUND_USER":"$UNBOUND_USER" "$TRUST_ANCHOR" 2>/dev/null || true
print_success "DNSSEC trust anchor configured"
else
print_warning "unbound-anchor not found — DNSSEC trust anchor not configured"
fi
}
# ============================================================================
# SETUP UNBOUND-CONTROL
# ============================================================================
setup_control() {
print_info "Setting up unbound-control"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would generate unbound-control keys"
return
fi
if [[ -f /etc/unbound/unbound_control.key ]]; then
print_info "unbound-control keys already exist"
return
fi
if command -v unbound-control-setup &>/dev/null; then
unbound-control-setup > /dev/null 2>&1
print_success "unbound-control keys generated"
else
print_warning "unbound-control-setup not found"
fi
}
# ============================================================================
# GENERATE CONFIGURATION
# ============================================================================
generate_config() {
print_header "Generating Configuration"
local rrset_size=$((CACHE_SIZE * 2))
local listen_addr="$LISTEN_IP"
if [[ "$LISTEN_ALL" == "true" ]]; then
listen_addr="0.0.0.0"
fi
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would write $UNBOUND_CONF"
print_info " Listen: $listen_addr"
print_info " Mode: $MODE"
[[ -n "$FORWARDER" ]] && print_info " Forwarder: $FORWARDER (DoT: $USE_DOT)"
print_info " Threads: $THREADS, Cache: ${CACHE_SIZE}m"
[[ ${#LOCAL_ZONES[@]} -gt 0 ]] && print_info " Local zones: ${LOCAL_ZONES[*]}"
[[ ${#NSD_STUB_ZONES[@]} -gt 0 ]] && print_info " NSD stub zones: ${NSD_STUB_ZONES[*]} (port $NSD_PORT)"
return
fi
# Backup existing config
if [[ -f "$UNBOUND_CONF" ]]; then
cp "$UNBOUND_CONF" "${UNBOUND_CONF}.bak.$(date +%Y%m%d%H%M%S)"
print_info "Backed up existing config"
fi
mkdir -p "$UNBOUND_CONF_DIR"
# ── Main config ──
cat > "$UNBOUND_CONF" <<CONF
# Unbound configuration — generated by $SCRIPT_NAME v$VERSION
# Date: $(date '+%Y-%m-%d %H:%M:%S')
server:
# Network
interface: $listen_addr
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# Access control
access-control: 127.0.0.0/8 allow
CONF
if [[ "$LISTEN_ALL" == "true" ]]; then
cat >> "$UNBOUND_CONF" <<CONF
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
CONF
fi
# Add any extra --allow-net entries
for net in "${ALLOW_NETS[@]:-}"; do
[[ -n "$net" ]] && echo " access-control: $net allow" >> "$UNBOUND_CONF"
done
cat >> "$UNBOUND_CONF" <<CONF
# DNSSEC
auto-trust-anchor-file: "$TRUST_ANCHOR"
# Security
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
use-caps-for-id: yes
qname-minimisation: yes
aggressive-nsec: yes
# Cache
cache-min-ttl: 300
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 86400
# Performance
num-threads: $THREADS
msg-cache-slabs: $SLAB_COUNT
rrset-cache-slabs: $SLAB_COUNT
infra-cache-slabs: $SLAB_COUNT
key-cache-slabs: $SLAB_COUNT
msg-cache-size: ${CACHE_SIZE}m
rrset-cache-size: ${rrset_size}m
outgoing-range: 4096
num-queries-per-thread: 2048
so-reuseport: yes
# Logging
verbosity: 1
log-queries: no
log-replies: no
logfile: ""
# Private addresses
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: fd00::/8
private-address: fe80::/10
# Runtime
username: "$UNBOUND_USER"
directory: "/etc/unbound"
chroot: ""
pidfile: "/run/unbound/unbound.pid"
CONF
# Root hints (recursive mode only)
if [[ "$MODE" == "recursive" ]]; then
echo " root-hints: \"$ROOT_HINTS\"" >> "$UNBOUND_CONF"
echo "" >> "$UNBOUND_CONF"
fi
# TLS bundle (for DoT)
if [[ "$USE_DOT" == "true" ]]; then
echo " tls-cert-bundle: \"$TLS_BUNDLE\"" >> "$UNBOUND_CONF"
echo "" >> "$UNBOUND_CONF"
fi
# Include directory
echo " include: \"$UNBOUND_CONF_DIR/*.conf\"" >> "$UNBOUND_CONF"
echo "" >> "$UNBOUND_CONF"
# Remote control
cat >> "$UNBOUND_CONF" <<CONF
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
CONF
print_success "Main config written to $UNBOUND_CONF"
# ── Forwarding config ──
if [[ "$MODE" == "forward" && -n "$FORWARDER" ]]; then
generate_forward_config
fi
# ── Local zones config ──
if [[ ${#LOCAL_ZONES[@]} -gt 0 || ${#LOCAL_ENTRIES[@]} -gt 0 ]]; then
generate_local_zones_config
fi
# ── NSD stub zones config ──
if [[ ${#NSD_STUB_ZONES[@]} -gt 0 ]]; then
generate_nsd_stub_config
fi
}
# ============================================================================
# FORWARDING CONFIGURATION
# ============================================================================
generate_forward_config() {
local conf_file="$UNBOUND_CONF_DIR/forwarding.conf"
local dot_flag=""
[[ "$USE_DOT" == "true" ]] && dot_flag="yes" || dot_flag="no"
cat > "$conf_file" <<CONF
# Forwarding configuration — generated by $SCRIPT_NAME
CONF
case "$FORWARDER" in
cloudflare)
if [[ "$USE_DOT" == "true" ]]; then
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 1.0.0.1@853#cloudflare-dns.com
CONF
else
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-addr: 1.1.1.1
forward-addr: 1.0.0.1
CONF
fi
;;
google)
if [[ "$USE_DOT" == "true" ]]; then
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 8.8.8.8@853#dns.google
forward-addr: 8.8.4.4@853#dns.google
CONF
else
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-addr: 8.8.8.8
forward-addr: 8.8.4.4
CONF
fi
;;
quad9)
if [[ "$USE_DOT" == "true" ]]; then
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
CONF
else
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-addr: 9.9.9.9
forward-addr: 149.112.112.112
CONF
fi
;;
*)
# Custom IP address
cat >> "$conf_file" <<CONF
forward-zone:
name: "."
forward-tls-upstream: $dot_flag
forward-addr: $FORWARDER
CONF
;;
esac
print_success "Forwarding config written (upstream: $FORWARDER, DoT: $USE_DOT)"
}
# ============================================================================
# LOCAL ZONES CONFIGURATION
# ============================================================================
generate_local_zones_config() {
local conf_file="$UNBOUND_CONF_DIR/local-zones.conf"
cat > "$conf_file" <<CONF
# Local zones — generated by $SCRIPT_NAME
server:
CONF
for zone in "${LOCAL_ZONES[@]}"; do
echo " local-zone: \"${zone}.\" static" >> "$conf_file"
done
for entry in "${LOCAL_ENTRIES[@]}"; do
echo " local-data: \"$entry\"" >> "$conf_file"
done
print_success "Local zones config written (${#LOCAL_ZONES[@]} zones, ${#LOCAL_ENTRIES[@]} records)"
}
# ============================================================================
# NSD STUB ZONE CONFIGURATION
# ============================================================================
generate_nsd_stub_config() {
local conf_file="$UNBOUND_CONF_DIR/nsd-stubs.conf"
cat > "$conf_file" <<CONF
# NSD stub zones — generated by $SCRIPT_NAME
# Forwards these zones to local NSD on port $NSD_PORT
CONF
for zone in "${NSD_STUB_ZONES[@]}"; do
cat >> "$conf_file" <<CONF
stub-zone:
name: "$zone"
stub-addr: 127.0.0.1@$NSD_PORT
CONF
done
print_success "NSD stub zones written (${#NSD_STUB_ZONES[@]} zones → port $NSD_PORT)"
}
# ============================================================================
# FIREWALL CONFIGURATION
# ============================================================================
configure_firewall() {
if [[ "$LISTEN_ALL" != "true" && "$LISTEN_IP" == "127.0.0.1" ]]; then
print_info "Listening on localhost only — skipping firewall"
return
fi
print_info "Configuring firewall"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would open port 53/tcp and 53/udp"
return
fi
if command -v ufw &>/dev/null && ufw status | grep -q "active"; then
ufw allow 53/tcp > /dev/null 2>&1
ufw allow 53/udp > /dev/null 2>&1
print_success "Opened ports 53/tcp and 53/udp (ufw)"
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-service=dns > /dev/null 2>&1
firewall-cmd --reload > /dev/null 2>&1
print_success "Opened DNS ports (firewalld)"
else
print_warning "No active firewall detected — skipping"
fi
}
# ============================================================================
# START AND VALIDATE
# ============================================================================
validate_and_start() {
print_header "Validating and Starting Unbound"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would validate config and start unbound"
return
fi
# Create pid directory
mkdir -p /run/unbound
chown "$UNBOUND_USER":"$UNBOUND_USER" /run/unbound
# Set ownership
chown -R "$UNBOUND_USER":"$UNBOUND_USER" /etc/unbound/ 2>/dev/null || true
chmod 640 "$UNBOUND_CONF"
# Validate
if unbound-checkconf "$UNBOUND_CONF" > /dev/null 2>&1; then
print_success "Configuration validated (unbound-checkconf)"
else
print_error "Configuration validation failed:"
unbound-checkconf "$UNBOUND_CONF"
exit 1
fi
# Enable and start
systemctl enable unbound > /dev/null 2>&1
systemctl restart unbound
if systemctl is-active --quiet unbound; then
print_success "Unbound is running"
else
print_error "Unbound failed to start"
journalctl -u unbound --no-pager -n 10
exit 1
fi
# Quick test
sleep 1
if dig @127.0.0.1 example.com A +short +time=5 > /dev/null 2>&1; then
print_success "DNS resolution test passed"
else
print_warning "DNS resolution test failed — check logs with: journalctl -u unbound"
fi
}
# ============================================================================
# UNINSTALL
# ============================================================================
do_uninstall() {
print_header "Uninstalling Unbound"
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY RUN] Would remove Unbound and configuration"
return
fi
systemctl stop unbound 2>/dev/null || true
systemctl disable unbound 2>/dev/null || true
case "$OS_FAMILY" in
debian)
apt-get remove --purge -y -qq unbound unbound-host dns-root-data > /dev/null 2>&1 || true
apt-get autoremove -y -qq > /dev/null 2>&1 || true
;;
rhel)
dnf remove -y -q unbound > /dev/null 2>&1 || true
;;
esac
rm -rf /etc/unbound
rm -f /var/lib/unbound/root.key
print_success "Unbound removed"
log "Unbound uninstalled"
}
# ============================================================================
# SUMMARY
# ============================================================================
print_summary() {
local rrset_size=$((CACHE_SIZE * 2))
print_header "Installation Complete"
echo ""
echo " Unbound recursive DNS resolver is running."
echo ""
echo " Config: $UNBOUND_CONF"
echo " Config dir: $UNBOUND_CONF_DIR"
echo " Listen: ${LISTEN_ALL:+0.0.0.0}${LISTEN_ALL:-$LISTEN_IP}:53"
echo " Mode: $MODE"
[[ -n "$FORWARDER" ]] && echo " Forwarder: $FORWARDER (DoT: $USE_DOT)"
echo " Threads: $THREADS"
echo " Cache: msg=${CACHE_SIZE}m, rrset=${rrset_size}m"
echo " DNSSEC: enabled"
[[ ${#LOCAL_ZONES[@]} -gt 0 ]] && echo " Local zones: ${LOCAL_ZONES[*]}"
[[ ${#NSD_STUB_ZONES[@]} -gt 0 ]] && echo " NSD stubs: ${NSD_STUB_ZONES[*]} (port $NSD_PORT)"
echo " Log: $LOG_FILE"
echo ""
echo " Useful commands:"
echo " sudo unbound-control status"
echo " sudo unbound-control stats_noreset"
echo " dig @127.0.0.1 example.com A"
echo " sudo journalctl -u unbound -f"
echo ""
}
# ============================================================================
# PARSE ARGUMENTS
# ============================================================================
ALLOW_NETS=()
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--recursive)
MODE="recursive"
shift
;;
--forward)
MODE="forward"
FORWARDER="${2:?'--forward requires a provider (cloudflare, google, quad9, or IP)'}"
shift 2
;;
--dot)
USE_DOT=true
shift
;;
--listen)
LISTEN_IP="${2:?'--listen requires an IP address'}"
shift 2
;;
--listen-all)
LISTEN_ALL=true
shift
;;
--allow-net)
ALLOW_NETS+=("${2:?'--allow-net requires a CIDR'}")
shift 2
;;
--local-zone)
LOCAL_ZONES+=("${2:?'--local-zone requires a zone name'}")
shift 2
;;
--local-data)
LOCAL_ENTRIES+=("${2:?'--local-data requires a record'}")
shift 2
;;
--nsd-stub)
NSD_STUB_ZONES+=("${2:?'--nsd-stub requires a zone name'}")
shift 2
;;
--nsd-port)
NSD_PORT="${2:?'--nsd-port requires a port number'}"
shift 2
;;
--threads)
THREADS="${2:?'--threads requires a number'}"
shift 2
;;
--cache-size)
CACHE_SIZE="${2:?'--cache-size requires a size in MB'}"
shift 2
;;
--uninstall)
UNINSTALL=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_help
;;
--version)
show_version
;;
*)
log_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Validate
if [[ "$USE_DOT" == "true" && "$MODE" != "forward" ]]; then
log_error "--dot requires --forward (DoT is for forwarding to upstream)"
exit 1
fi
}
# ============================================================================
# MAIN
# ============================================================================
main() {
parse_args "$@"
echo ""
echo " ╔══════════════════════════════════════════╗"
echo " ║ Unbound Installer v$VERSION"
echo " ║ https://mylinux.work ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
[[ "$DRY_RUN" == "true" ]] && print_warning "DRY RUN MODE — no changes will be made"
# Root check (skip for dry-run)
if [[ "$DRY_RUN" == "false" ]]; then
check_root
fi
detect_os
if [[ "$UNINSTALL" == "true" ]]; then
do_uninstall
exit 0
fi
detect_tuning
disable_resolved
install_unbound
setup_trust_anchor
setup_root_hints
setup_control
generate_config
configure_firewall
validate_and_start
print_summary
log "Installation complete"
}
main "$@"