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.
This commit is contained in:
@@ -0,0 +1,951 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user