#!/usr/bin/env bash # # BIND9 Web UI — API backend installer. # # This service is the API-only backend; the user interface lives in the # companion webui facade (himalsimkhada/webui), which proxies to it. # # Offers two deployment modes: # 1. Full Docker stack : BIND9 container + API container # 2. Host BIND + Docker : manage an existing HOST BIND with the API container # # The one-liner does NOT clone the repository: it creates a directory # (default ~/bind9-webui) and fetches only the compose file (+ shared BIND # config for mode 1) needed for the mode you pick. # # Usage: sudo ./install.sh (or: ./install.sh --check | --help) # One-liner: curl -fsSL https://raw.githubusercontent.com/himalsimkhada/bind9-webui/main/install.sh | bash set -euo pipefail REPO_URL="https://github.com/himalsimkhada/bind9-webui.git" RAW_BASE="https://raw.githubusercontent.com/himalsimkhada/bind9-webui/main" WEBUI_REPO_URL="https://github.com/himalsimkhada/webui.git" WEBUI_PORT="${WEBUI_PORT:-8080}" DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" # ── Output helpers ─────────────────────────────────────────────────────── if [ -t 1 ]; then C_RESET=$'\e[0m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'; C_BOLD=$'\e[1m' else C_RESET=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_BOLD="" fi info() { printf '%s\n' "${C_BOLD}==>${C_RESET} $*"; } ok() { printf '%s%s%s\n' "${C_GREEN} $*${C_RESET}"; } warn() { printf '%s%s%s\n' "${C_YELLOW}!! $*${C_RESET}"; } die() { printf '%s%s%s\n' "${C_RED}FATAL:$*${C_RESET}" >&2; exit 1; } has_cmd() { command -v "$1" >/dev/null 2>&1; } # ── Input helpers ──────────────────────────────────────────────────────── # Under `curl ... | bash` stdin is the script stream, so bare `read` never # sees a prompt. Read from the controlling terminal when one exists. For # headless runs (no terminal) the variables below can be pre-set in the # environment instead of prompting. have_tty() { ( exec /dev/null; } read_input() { local var="$1" prompt="${2:-}" local envval="" case "$var" in target) envval="${TARGET_DIR:-}";; choice) envval="${BIND9_MODE:-}";; ans) envval="${BIND9_YES:-}";; esac if [ -n "$envval" ]; then printf -v "$var" '%s' "$envval" return 0 fi if have_tty; then read -r -p "$prompt" "$var" < /dev/tty return 0 fi if [ "$var" = "target" ]; then printf -v "$var" '%s' "$PWD/bind9-webui" # headless default: ./bind9-webui subdir return 0 fi die "No terminal available and \$$var was not set (${prompt%:}). Re-run from a terminal or set the env var." } read_input_silent() { local var="$1" prompt="${2:-}" if have_tty; then read -r -s -p "$prompt" "$var" < /dev/tty else die "No terminal available for password input. Set WEBUI_PASSWORD=... and re-run." fi echo "" } # ── Streaming bootstrap ────────────────────────────────────────────────── # Runs when the script is streamed (curl ... | bash) and there is no local # checkout: ask where to install, create that directory and fetch the files # for the chosen mode later (ensure_mode_files). No full repo clone. if [ ! -f "$DIR/docker-compose.yml" ] && [ ! -f "$DIR/docker-compose-w-bind9.yml" ]; then echo "==> One-liner install: no project checkout in \"$DIR\"." has_cmd curl || has_cmd wget || die "curl or wget is required for the one-liner install." has_cmd git || warn "git is not installed — only the files needed by the installer will be fetched." default_target="$PWD/bind9-webui" target="" read_input target "Install the project into [$default_target]: " target="${target:-$default_target}" mkdir -p "$target" cd "$target" DIR="$target" info "Project dir: $DIR (the compose file is fetched for the mode you choose)" fi # ── File fetch (no repo clone) ─────────────────────────────────────────── fetch_file() { local rel="$1" local dest="$DIR/$rel" mkdir -p "$(dirname "$dest")" if has_cmd curl; then curl -fsSL "$RAW_BASE/$rel" -o "$dest" elif has_cmd wget; then wget -qO "$dest" "$RAW_BASE/$rel" else die "curl or wget is required to fetch $rel" fi } ensure_mode_files() { # The compose file is always saved as docker-compose.yml — whatever mode is # picked, `docker compose up` picks it up with no -f flag. local mode="$1" if [ "$mode" = "1" ]; then # The full-stack manifest ships in the repo as docker-compose-w-bind9.yml. if [ -f "$DIR/docker-compose-w-bind9.yml" ]; then cp -f "$DIR/docker-compose-w-bind9.yml" "$DIR/docker-compose.yml" elif has_cmd curl; then curl -fsSL "$RAW_BASE/docker-compose-w-bind9.yml" -o "$DIR/docker-compose.yml" elif has_cmd wget; then wget -qO "$DIR/docker-compose.yml" "$RAW_BASE/docker-compose-w-bind9.yml" else die "curl or wget is required to fetch docker-compose.yml" fi for f in named.conf named.conf.options named.conf.local named.conf.default-zones rndc.key root.hints db.local db.127 db.0 db.255; do [ -f "$DIR/docker/bind/$f" ] || fetch_file "docker/bind/$f" done else [ -f "$DIR/docker-compose.yml" ] || fetch_file "docker-compose.yml" fi ok "Mode $mode files present in $DIR (compose file: docker-compose.yml)" } # ── System detection ───────────────────────────────────────────────────── OS_ID="unknown" OS_NAME="unknown" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-unknown}" OS_NAME="${NAME:-$OS_ID}" fi is_deb() { case "$OS_ID" in debian|ubuntu|linuxmint|pop|elementary|kali|raspbian) return 0;; *) return 1;; esac; } is_rpm() { case "$OS_ID" in rhel|fedora|centos|almalinux|rocky|ol|amazon) return 0;; *) return 1;; esac; } is_arch() { case "$OS_ID" in arch|manjaro|endeavouros) return 0;; *) return 1;; esac; } pkg_install() { if is_deb; then sudo apt-get update -qq sudo apt-get install -y -qq "$@" elif is_rpm; then if command -v dnf >/dev/null 2>&1; then sudo dnf install -y "$@" else sudo yum install -y "$@"; fi elif is_arch; then sudo pacman -S --noconfirm --needed "$@" else die "Unsupported distro ($OS_NAME). Please install dependencies manually." fi } # BIND package names / paths per family (overridable via BIND_CONF_DIR env). bind_packages() { if is_deb; then echo "bind9 bind9utils bind9-dnsutils" elif is_rpm; then echo "bind bind-utils" elif is_arch; then echo "bind" fi } bind_conf_dir() { if [ -n "${BIND_CONF_DIR:-}" ]; then echo "$BIND_CONF_DIR"; return; fi if is_deb; then echo "/etc/bind"; else echo "/etc/named"; fi } bind_log_dir() { if [ -n "${HOST_BIND_LOG_DIR:-}" ]; then echo "$HOST_BIND_LOG_DIR"; return; fi if is_deb; then echo "/var/log/bind"; else echo "/var/log/named"; fi } rndc_key_path() { local dir; dir="$(bind_conf_dir)" if [ -n "${RNDC_KEY:-}" ]; then echo "$RNDC_KEY"; return; fi if is_deb; then echo "$dir/rndc.key"; else echo "/etc/rndc.key"; fi } # ── Generic helpers ────────────────────────────────────────────────────── random_secret() { if has_cmd openssl; then openssl rand -hex 32 else od -An -N32 -tx1 /dev/urandom | tr -d ' \n' fi } ask_password() { # Accepts WEBUI_PASSWORD from the environment (headless runs) or prompts. if [ -z "${WEBUI_PASSWORD:-}" ]; then WEBUI_PASSWORD="" while [ -z "$WEBUI_PASSWORD" ]; do read_input_silent WEBUI_PASSWORD " Web UI password (used to log in): " if [ -z "$WEBUI_PASSWORD" ]; then warn "Password cannot be empty." else read_input_silent WEBUI_PASSWORD_CONFIRM " Confirm password: " if [ "$WEBUI_PASSWORD" != "$WEBUI_PASSWORD_CONFIRM" ]; then warn "Passwords do not match. Try again." WEBUI_PASSWORD="" fi fi done else ok "Using WEBUI_PASSWORD from the environment" fi SECRET_KEY="$(random_secret)" } write_env_file() { # Writes .env from an array of KEY=VALUE lines passed on stdin. local envpath="$DIR/.env" info "Writing $envpath" cat > "$envpath" chmod 600 "$envpath" 2>/dev/null || true } # ── webui admin facade (companion dashboard) ──────────────────────────── webui_detected() { # True when the webui admin facade is already answering on the expected port. if has_cmd curl && curl -fsS --max-time 3 "http://127.0.0.1:${WEBUI_PORT}/healthz" >/dev/null 2>&1; then return 0 fi if has_cmd docker && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^webui-admin$'; then return 0 fi return 1 } offer_webui_portal() { # The dashboard UI lives in the companion webui facade. If it is not running, # offer to install it now. local tgt="$HOME/webui" if webui_detected; then ok "webui admin dashboard already running on http://127.0.0.1:${WEBUI_PORT}" return fi if [ -d "$tgt" ] && [ -f "$tgt/install.sh" ]; then warn "The webui admin dashboard is NOT running but a checkout exists at $tgt (port $WEBUI_PORT)." else warn "The webui admin dashboard (himalsimkhada/webui) was not detected on http://127.0.0.1:${WEBUI_PORT}." fi info "The BIND dashboard is hosted by the webui facade; without it you only get the API." ans="" read_input ans " Install the webui admin dashboard now? [Y/n] " if [ "${ans:-y}" = "n" ] || [ "${ans:-y}" = "N" ]; then warn "Install it later with: curl -fsSL https://raw.githubusercontent.com/himalsimkhada/webui/main/install.sh | bash" return fi has_cmd git || die "git is required to clone the webui dashboard." mkdir -p "$HOME" if [ ! -f "$tgt/install.sh" ]; then info "Cloning $WEBUI_REPO_URL into $tgt" git clone --quiet --depth 1 "$WEBUI_REPO_URL" "$tgt" fi info "Running the webui dashboard installer (choose Docker or Manual mode)" bash "$tgt/install.sh" ok "webui admin dashboard installed. Open http://localhost:${WEBUI_PORT}" } ensure_named_running() { info "Starting named if it is not running" sudo systemctl enable named >/dev/null 2>&1 || true sudo systemctl start named 2>/dev/null || sudo named 2>/dev/null || true sleep 1 if ! sudo rndc status >/dev/null 2>&1; then warn "named may not be running. Check with: sudo rndc status" else ok "named is running" fi } ensure_rndc_key() { local key; key="$(rndc_key_path)" local dir; dir="$(bind_conf_dir)" if [ ! -f "$key" ]; then info "Generating rndc key at $key" sudo rndc-confgen -a fi if [ -d "$dir" ] && [ -f "$key" ]; then sudo chmod 640 "$key" 2>/dev/null || true sudo chown root:bind "$key" 2>/dev/null || sudo chown root:named "$key" 2>/dev/null || true fi } add_logging_channel() { # Adds a file channel + default category so the Logs tab has content. local dir; dir="$(bind_conf_dir)" local conf="$dir/named.conf.options" local logfile="$(bind_log_dir)/named.log" local marker="# BIND9-WEBUI-LOGGING" if [ ! -f "$conf" ]; then conf="$dir/named.conf" fi if ! sudo grep -qF "$marker" "$conf" 2>/dev/null; then info "Adding a file logging channel ($logfile) to $conf" { echo "" echo "$marker" echo "logging {" echo " channel bind_webui_file { file \"$logfile\" versions 3 size 5m; severity info; };" echo " category default { bind_webui_file; };" echo "};" } | sudo tee -a "$conf" >/dev/null sudo systemctl restart named || true else ok "Logging channel already present" fi local ldir; ldir="$(bind_log_dir)" sudo mkdir -p "$ldir" sudo chown root:bind "$ldir" 2>/dev/null || sudo chown root:named "$ldir" 2>/dev/null || true } add_rndc_controls() { # Ensures host named listens on TCP 953 for the web-UI container, restricted # to loopback + Docker bridge subnets. local dir; dir="$(bind_conf_dir)" local keyfile; keyfile="$(rndc_key_path)" local keyname keyname="$(sudo sed -n 's/^key *"\([^"]*\)".*/\1/p' "$keyfile" 2>/dev/null | head -n1)" keyname="${keyname:-rndc-key}" local marker="# BIND9-WEBUI-CONTROLS" local conf="$dir/named.conf" if ! grep -q 'port 953' "$conf" 2>/dev/null && ! sudo grep -qF "$marker" "$conf" 2>/dev/null; then warn "Host '$dir/named.conf' has no rndc 'controls' block listening on TCP 953," warn "which the web-UI container needs to manage BIND. Adding a restricted one:" { echo "" echo "$marker" echo "controls { inet 0.0.0.0 port 953 allow { 127.0.0.1; ::1; 172.16.0.0/12; } keys { \"$keyname\"; }; };" } | sudo tee -a "$conf" >/dev/null info "Restarting named to apply the controls block" sudo systemctl restart named ok "rndc TCP 953 control channel enabled (restricted to loopback + 172.16.0.0/12)" else ok "rndc 953 controls block already present" fi } ensure_docker() { if ! has_cmd docker || ! docker compose version >/dev/null 2>&1; then warn "Docker with the compose plugin is required but not installed." ans="" read_input ans " Install Docker now? [y/N] " if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then die "Docker is required for this mode. Re-run after installing Docker." fi info "Installing Docker" if is_deb; then sudo apt-get update -qq sudo apt-get install -y -qq docker.io docker-compose-v2 elif is_rpm; then sudo dnf install -y moby-engine docker-compose-plugin 2>/dev/null \ || sudo dnf install -y moby-engine || \ warn "Could not auto-install Docker. Install it manually and re-run." elif is_arch; then sudo pacman -S --noconfirm --needed docker docker-compose 2>/dev/null \ || sudo pacman -S --noconfirm --needed docker else die "Unsupported distro for automatic Docker install. Install Docker manually." fi fi if ! docker compose version >/dev/null 2>&1; then die "Docker compose plugin is missing. Install docker-compose and re-run." fi if ! docker info >/dev/null 2>&1; then warn "Docker daemon is not reachable. Starting the docker service..." sudo systemctl enable --now docker >/dev/null 2>&1 || true sleep 2 fi docker info >/dev/null 2>&1 \ || die "Docker daemon is not reachable. Start it (sudo systemctl start docker) and re-run." ok "Docker + compose plugin available and the daemon is running" } ensure_host_bind() { if ! has_cmd named; then warn "BIND9 (named) is not installed." ans="" read_input ans " Install BIND9 now? [y/N] " if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then die "BIND9 is required for this mode." fi # shellcheck disable=SC2046 pkg_install $(bind_packages) fi ok "BIND9 present" ensure_rndc_key } # ── Mode 1: Full Docker stack ──────────────────────────────────────────── seed_bind_volume() { # The full-stack compose keeps the whole /etc/bind in one named volume # (bind9-vol). The ISC image's own /etc/bind only ships bind.keys + # named.conf, so on first run we copy the shipped config files in. local vol="bind9-vol" if ! docker volume inspect "$vol" >/dev/null 2>&1; then info "Initializing $vol from the shipped BIND config (docker/bind)" docker run --rm --entrypoint sh \ -v "$vol":/etc/bind \ -v "$DIR/docker/bind":/seed:ro \ internetsystemsconsortium/bind9:9.18 -c '\ cp -a /seed/. /etc/bind/ && \ mkdir -p /etc/bind/zones && \ chown -R 53:53 /etc/bind/zones' ok "bind9-vol seeded — edit config with: docker run --rm -it -v bind9-vol:/etc/bind sh" else ok "bind9-vol already initialized" fi } mode_docker_full() { info "Mode 1: Full Docker stack (BIND9 + API containers)" ensure_mode_files 1 ensure_docker seed_bind_volume ask_password write_env_file </dev/null 2>&1 || true # Containers keep the pinned names across projects; compose can only remove # its own, so clear the names when an overlapping install left them behind. docker rm -f bind9 bind9-webui >/dev/null 2>&1 || true docker compose -f docker-compose.yml up -d ok "Deployed. API at http://localhost:5000 (UI lives in the webui facade)" ok "DNS is published on host 127.0.0.1:5353 (rndc on 127.0.0.1:9353)" offer_webui_portal } # ── Mode 2: Host BIND + Docker API ──────────────────────────────────────── mode_host_bind_docker() { info "Mode 2: Host BIND9 + API container" ensure_mode_files 2 ensure_docker ensure_host_bind ensure_named_running add_rndc_controls add_logging_channel local bdir; bdir="$(bind_conf_dir)" local ldir; ldir="$(bind_log_dir)" ask_password write_env_file </dev/null 2>&1 || true docker rm -f bind9-webui >/dev/null 2>&1 || true docker compose -f docker-compose.yml up -d ok "Deployed. API at http://localhost:5000 (UI lives in the webui facade)" ok "Mounted host BIND config from: $bdir" ok "Tailing logs from: $ldir/named.log" offer_webui_portal } # ── Main menu / flags ──────────────────────────────────────────────────── show_menu() { echo "" echo "Select how you want to run the BIND9 backend:" echo "" echo " 1) Full Docker stack - BIND9 and the API both in containers" echo " 2) Host BIND + Docker - API container managing BIND installed on this machine" echo "" while :; do choice="" read_input choice "Enter your choice [1-2]: " case "$choice" in 1) mode_docker_full; return;; 2) mode_host_bind_docker; return;; *) warn "Please choose 1 or 2.";; esac done } do_check() { echo "── System check ─────────────────────────────" echo "Distro : $OS_NAME ($OS_ID)" echo "Package tool : $(is_deb && echo 'apt' || (is_rpm && echo 'rpm/dnf' || (is_arch && echo 'pacman' || echo 'unknown')))" echo "BIND dir : $(bind_conf_dir)" echo "Log dir : $(bind_log_dir)" echo "rndc key : $(rndc_key_path)" echo "" if has_cmd named; then echo "BIND9 : installed" else echo "BIND9 : NOT installed" fi if has_cmd docker && docker compose version >/dev/null 2>&1; then echo "Docker+compose: available" else echo "Docker+compose: missing" fi echo "" } case "${1:-}" in --check|-c) do_check; exit 0;; --help|-h) sed -n '1,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0;; "") if [ "$(id -u)" -eq 0 ]; then warn "Running as root. Prefer running as a normal sudo user on some distros." fi show_menu echo "" echo "Done!" ;; *) echo "Unknown option: $1 (use --help)"; exit 1;; esac