#!/usr/bin/env bash : <<'IRANUX_METADATA' { "standard": { "name": "iranux-script-metadata", "schema_version": "1.2" }, "script": { "id": "stormdns-server-linux-installer", "name": "StormDNS Server Linux Installer", "version": "1.0.0", "description": "Installs, updates or uninstalls the StormDNS DNS tunnel server on Linux. To free port 53 it stops and disables other DNS services, changes or stops systemd-resolved and kills any process still using port 53; it also removes port 53 redirect rules, opens port 53 in the firewall, changes kernel and file limits, generates an encryption key and creates a systemd service. It also adds a service that blocks all outgoing TCP connections to port 53 from this server.", "estimated_minutes": 5, "i18n": { "fa": { "name": "نصب سرور StormDNS روی لینوکس", "description": "سرور تونل DNS به نام StormDNS را روی لینوکس نصب، به‌روزرسانی یا حذف می‌کند. برای آزاد کردن پورت 53، سرویس‌های DNS دیگر را متوقف و غیرفعال می‌کند، systemd-resolved را تغییر می‌دهد یا متوقف می‌کند و هر برنامه‌ای را که از پورت 53 استفاده می‌کند می‌بندد. قانون‌های انتقال پورت 53 را حذف و این پورت را در فایروال باز می‌کند، تنظیمات هسته و محدودیت فایل‌ها را تغییر می‌دهد، کلید رمزنگاری می‌سازد و سرویس systemd می‌سازد. همچنین همه‌ی اتصال‌های TCP خروجی این سرور به پورت 53 را مسدود می‌کند." } } }, "risk": { "level": "dangerous" }, "requirements": { "requires_root": true, "requires_internet": true, "supported_os": [ "ubuntu", "debian", "almalinux", "rocky", "rhel", "centos" ], "required_commands": [ "systemctl" ] }, "ui": { "category": { "id": "network-services", "name": "Network Services" }, "action": { "id": "dns-tunnel-servers", "name": "DNS Tunnel Servers" }, "icon": { "library": "mdi", "name": "server-network" } } } IRANUX_METADATA : <<'IRANUX_PARAM' { "name": "action", "label": "Action", "description": "Choose whether to install or uninstall StormDNS. Install also updates an existing installation to the selected version and keeps your current settings if the new version can still use them.", "type": "enum", "required": true, "default": "install", "options": [ { "label": "Install", "value": "install" }, { "label": "Uninstall", "value": "uninstall" } ], "group": "Installation", "i18n": { "fa": { "label": "عملیات", "description": "انتخاب کنید StormDNS نصب شود یا حذف شود. اگر StormDNS از قبل نصب باشد، نصب آن را به نسخه‌ی انتخاب‌شده به‌روزرسانی می‌کند و اگر نسخه‌ی جدید بتواند از تنظیمات فعلی شما استفاده کند، این تنظیمات حفظ می‌شود." } } } IRANUX_PARAM : <<'IRANUX_PARAM' { "name": "target_version", "label": "Version", "description": "Enter the StormDNS release tag to install, for example v1.2.3. Leave empty to install the latest release. Leave it empty when you uninstall.", "type": "string", "required": false, "example": "v1.2.3", "placeholder": "v1.2.3", "validation": { "pattern": "^[A-Za-z0-9._+-]+$" }, "group": "Installation", "level": "advanced", "i18n": { "fa": { "label": "نسخه", "description": "برچسب نسخه‌ای از StormDNS را که می‌خواهید نصب شود وارد کنید، مثلاً v1.2.3. برای نصب آخرین نسخه خالی بگذارید. هنگام حذف هم این را خالی بگذارید." } } } IRANUX_PARAM : <<'IRANUX_PARAM' { "name": "local_mode", "label": "Use local files", "description": "Choose whether to install from a StormDNS server file and server_config.toml that are already in the installation folder, instead of downloading them. Do not combine this with a version.", "type": "enum", "required": false, "default": "0", "options": [ { "label": "Download", "value": "0" }, { "label": "Use local files", "value": "1" } ], "group": "Installation", "level": "advanced", "i18n": { "fa": { "label": "استفاده از فایل‌های محلی", "description": "انتخاب کنید که به‌جای دانلود، از فایل سرور StormDNS و فایل server_config.toml که از قبل در پوشه‌ی نصب هستند استفاده شود. این گزینه را همراه با نسخه انتخاب نکنید." } } } IRANUX_PARAM : <<'IRANUX_PARAM' { "name": "user_domain", "label": "Tunnel domain", "description": "Enter the domain (or subdomain) that points to this server with an NS record, for example vpn.example.com. It is needed for a new installation. It is written to server_config.toml only when no domain has been set there yet, so leave it empty if you already set one.", "type": "domain", "required": false, "example": "vpn.example.com", "placeholder": "vpn.example.com", "group": "Tunnel", "i18n": { "fa": { "label": "دامنه‌ی تونل", "description": "دامنه (یا زیردامنه‌ای) را وارد کنید که با رکورد NS به این سرور اشاره می‌کند، مثلاً vpn.example.com. برای نصب جدید لازم است. این دامنه فقط وقتی در فایل server_config.toml نوشته می‌شود که هنوز دامنه‌ای در آن تنظیم نشده باشد؛ اگر قبلاً دامنه را تنظیم کرده‌اید، خالی بگذارید." } } } IRANUX_PARAM set -euo pipefail IFS=$'\n\t' RED='\033[1;31m' GREEN='\033[1;32m' YELLOW='\033[1;33m' BLUE='\033[1;34m' MAGENTA='\033[1;35m' CYAN='\033[1;36m' BOLD='\033[1m' NC='\033[0m' log_header() { echo -e "\n${CYAN}${BOLD}>>> $1${NC}"; } log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[DONE]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } require_cmd() { command -v "$1" >/dev/null 2>&1 || log_error "Missing command: $1"; } iranux_json_string() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\t'/\\t}" s="${s//$'\r'/\\r}" s="${s//$'\n'/\\n}" printf '"%s"' "$s" } backup_file_once() { local f="$1" [[ -f "$f" && ! -f "${f}.bak" ]] && cp -a "$f" "${f}.bak" } extract_config_version() { local f="$1" [[ -f "$f" ]] || return 0 grep '^CONFIG_VERSION' "$f" | awk -F'=' '{print $2}' | tr -d ' "' | head -n1 } version_lt() { [[ "$1" == "$2" ]] && return 1 [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]] } detect_legacy_linux() { local id="${ID:-}" local version_major="${VERSION_ID%%.*}" case "$id" in ubuntu) [[ "${version_major:-0}" -le 20 ]] ;; debian) [[ "${version_major:-0}" -le 11 ]] ;; almalinux|rocky|rhel|centos) [[ "${version_major:-0}" -le 8 ]] ;; *) return 1 ;; esac } select_release_artifact() { local arch="$1" local version="${2:-}" local legacy=0 if detect_legacy_linux; then legacy=1 log_info "Legacy system detected (broader Linux compatibility mode)." fi local base_url if [[ -n "$version" ]]; then base_url="https://github.com/nullroute1970/StormDNS/releases/download/${version}" log_info "Targeting StormDNS release: ${version}" else base_url="https://github.com/nullroute1970/StormDNS/releases/latest/download" fi case "$arch" in aarch64|arm64) if [[ $legacy -eq 1 ]]; then PREFIX="StormDNS_Server_Linux-Legacy_ARM64" else PREFIX="StormDNS_Server_Linux_ARM64" fi ;; armv7l|armv7|armhf) PREFIX="StormDNS_Server_Linux_ARMV7" ;; x86_64|amd64) if [[ $legacy -eq 1 ]]; then PREFIX="StormDNS_Server_Linux-Legacy_AMD64" else PREFIX="StormDNS_Server_Linux_AMD64" fi ;; i386|i486|i586|i686|x86) PREFIX="StormDNS_Server_Linux_X86" ;; *) log_error "Unsupported architecture: $arch" ;; esac URL="${base_url}/${PREFIX}.zip" } find_local_server_binary() { local arch="$1" local search_dirs=("$INSTALL_DIR" "$INSTALL_DIR/dist") local pat case "$arch" in x86_64|amd64) pat="(A|a)(M|m)(D|d)64" ;; aarch64|arm64) pat="(A|a)(R|r)(M|m)64" ;; armv7l|armv7|armhf) pat="(A|a)(R|r)(M|m)(V|v)7" ;; i386|i486|i586|i686|x86) pat="(X|x)86" ;; *) log_error "Unsupported architecture: $arch" ;; esac for sd in "${search_dirs[@]}"; do [[ -d "$sd" ]] || continue local found found="$(find "$sd" -maxdepth 1 -name "StormDNS_Server_Linux*" -type f 2>/dev/null | grep -E "$pat" | xargs ls -t 2>/dev/null | head -n1)" if [[ -n "$found" ]]; then echo "$found" return 0 fi done return 1 } find_local_config() { local search_dirs=("$INSTALL_DIR" "$INSTALL_DIR/dist") for sd in "${search_dirs[@]}"; do if [[ -f "$sd/server_config.toml" ]]; then echo "$sd/server_config.toml" return 0 fi done if [[ -f "$INSTALL_DIR/server_config.toml.simple" ]]; then cp "$INSTALL_DIR/server_config.toml.simple" "$INSTALL_DIR/server_config.toml" echo "$INSTALL_DIR/server_config.toml" return 0 fi return 1 } print_usage() { cat <<'USAGE' StormDNS Server Linux Installer Usage: bash <(curl -Ls https://raw.githubusercontent.com/nullroute1970/StormDNS/main/server_linux_install.sh) [OPTIONS] Options: -v, --version Install a specific StormDNS release (tag), e.g. v1.2.3. If omitted, the latest release is installed. -l, --local Local/offline install: use the server binary and config found in the current directory (or dist/). No download from GitHub is performed. -u, --uninstall Uninstall StormDNS: stop and remove the systemd service, drop kernel/limit tunings, and clean up binaries and config files in the install directory. -h, --help Show this help message and exit. Examples: # Install the latest release (default behavior): bash <(curl -Ls https://raw.githubusercontent.com/nullroute1970/StormDNS/main/server_linux_install.sh) # Install a specific release version: bash <(curl -Ls https://raw.githubusercontent.com/nullroute1970/StormDNS/main/server_linux_install.sh) --version v1.2.3 # Local/offline install for testing: python build.py sudo bash server_linux_install.sh --local # Uninstall StormDNS: bash <(curl -Ls https://raw.githubusercontent.com/nullroute1970/StormDNS/main/server_linux_install.sh) --uninstall USAGE } ACTION="${ACTION:-install}" TARGET_VERSION="${TARGET_VERSION:-}" LOCAL_MODE="${LOCAL_MODE:-0}" while [[ $# -gt 0 ]]; do case "$1" in -v|--version) [[ $# -ge 2 ]] || { echo "Error: $1 requires a value" >&2; print_usage; exit 2; } TARGET_VERSION="$2" shift 2 ;; --version=*) TARGET_VERSION="${1#*=}" shift ;; -u|--uninstall) ACTION="uninstall" shift ;; -l|--local) LOCAL_MODE=1 shift ;; -h|--help) print_usage echo "IRANUX_RESULT {\"outputs\":[]}" echo "__IRANUX_REACHED_END_V1__" exit 0 ;; --) shift break ;; *) echo "Error: unknown option: $1" >&2 print_usage exit 2 ;; esac done if [[ "$LOCAL_MODE" -eq 1 && -n "$TARGET_VERSION" ]]; then echo "Error: --local cannot be combined with --version" >&2 exit 2 fi if [[ "$ACTION" == "uninstall" && -n "$TARGET_VERSION" ]]; then echo "Error: --version cannot be combined with --uninstall" >&2 exit 2 fi if [[ -n "$TARGET_VERSION" && ! "$TARGET_VERSION" =~ ^[A-Za-z0-9._+-]+$ ]]; then echo "Error: invalid version tag: $TARGET_VERSION" >&2 exit 2 fi if [[ "${EUID}" -ne 0 ]]; then log_error "Run this script as root (sudo)." fi INSTALL_DIR="$(pwd -P)" [[ -n "${PWD:-}" ]] && INSTALL_DIR="$PWD" if [[ "$INSTALL_DIR" == /dev/fd* || "$INSTALL_DIR" == /proc/*/fd* ]]; then INSTALL_DIR="$(pwd -P)" fi log_info "Installation directory: $INSTALL_DIR" cd "$INSTALL_DIR" || log_error "Cannot access install directory: $INSTALL_DIR" if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release else log_error "OS detection failed (/etc/os-release missing)." fi echo -e "${MAGENTA}${BOLD}" if [[ "$ACTION" == "uninstall" ]]; then echo -e " StormDNS Server Auto-Uninstaller${NC}" elif [[ "$LOCAL_MODE" -eq 1 ]]; then echo -e " StormDNS Server Local-Installer${NC}" else echo -e " StormDNS Server Auto-Installer${NC}" fi echo -e "${CYAN}------------------------------------------------------${NC}" do_uninstall() { log_header "Uninstalling StormDNS" if systemctl list-unit-files --all 2>/dev/null | grep -q '^stormdns.service'; then log_info "Stopping and disabling stormdns service..." systemctl stop stormdns 2>/dev/null || true systemctl disable stormdns >/dev/null 2>&1 || true systemctl reset-failed stormdns 2>/dev/null || true else log_info "No stormdns systemd unit found." fi if [[ -f /etc/systemd/system/stormdns.service ]]; then rm -f /etc/systemd/system/stormdns.service log_success "Removed /etc/systemd/system/stormdns.service" fi if [[ -f /etc/systemd/system/stormdns-egress-filter.service ]]; then systemctl stop stormdns-egress-filter.service 2>/dev/null || true systemctl disable stormdns-egress-filter.service >/dev/null 2>&1 || true rm -f /etc/systemd/system/stormdns-egress-filter.service log_success "Removed /etc/systemd/system/stormdns-egress-filter.service" fi if [[ -d /etc/systemd/system/stormdns.service.d ]]; then rm -rf /etc/systemd/system/stormdns.service.d log_success "Removed /etc/systemd/system/stormdns.service.d/" fi if [[ -f /usr/local/sbin/stormdns-egress-filter.sh ]]; then rm -f /usr/local/sbin/stormdns-egress-filter.sh log_success "Removed /usr/local/sbin/stormdns-egress-filter.sh" fi systemctl daemon-reload 2>/dev/null || true local pid cmdline while IFS= read -r pid; do [[ -z "$pid" ]] && continue cmdline="$(ps -p "$pid" -o cmd= 2>/dev/null || true)" if echo "$cmdline" | grep -qiE 'stormdns'; then log_warn "Terminating stray StormDNS process (PID: $pid)..." kill "$pid" 2>/dev/null || true sleep 1 if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi fi done < <(pgrep -fi 'stormdns' 2>/dev/null || true) if [[ -f /etc/sysctl.d/99-stormdns.conf ]]; then rm -f /etc/sysctl.d/99-stormdns.conf sysctl --system >/dev/null 2>&1 || true log_success "Removed kernel tuning (/etc/sysctl.d/99-stormdns.conf)." fi if [[ -f /etc/sysctl.d/99-stormdns-tuning.conf ]]; then rm -f /etc/sysctl.d/99-stormdns-tuning.conf sysctl --system >/dev/null 2>&1 || true log_success "Removed supplementary kernel tuning (/etc/sysctl.d/99-stormdns-tuning.conf)." fi if [[ -f /etc/security/limits.d/99-stormdns.conf ]]; then rm -f /etc/security/limits.d/99-stormdns.conf log_success "Removed file descriptor limits (/etc/security/limits.d/99-stormdns.conf)." fi if [[ -f /etc/systemd/resolved.conf.bak && -f /etc/systemd/resolved.conf ]]; then log_info "Restoring original /etc/systemd/resolved.conf from backup..." mv -f /etc/systemd/resolved.conf.bak /etc/systemd/resolved.conf systemctl restart systemd-resolved 2>/dev/null || true fi log_header "Cleaning Install Directory" log_info "Install directory: $INSTALL_DIR" shopt -s nullglob local removed=0 for f in "$INSTALL_DIR"/StormDNS_Server_Linux* "$INSTALL_DIR"/server_config.toml "$INSTALL_DIR"/server_config.toml.backup "$INSTALL_DIR"/server_config.toml.bak "$INSTALL_DIR"/server_config_*.toml "$INSTALL_DIR"/encrypt_key.txt "$INSTALL_DIR"/init_logs.tmp "$INSTALL_DIR"/*.spec; do if [[ -e "$f" ]]; then rm -f -- "$f" log_info "Removed: $f" removed=1 fi done shopt -u nullglob if [[ $removed -eq 0 ]]; then log_warn "No StormDNS files found in $INSTALL_DIR. If you installed elsewhere, run the uninstaller from that directory." fi echo -e "\n${CYAN}======================================================${NC}" echo -e " ${GREEN}${BOLD} STORMDNS UNINSTALL COMPLETED${NC}" echo -e "${CYAN}======================================================${NC}" echo -e "${YELLOW}Note:${NC} Firewall rules for port 53 (UDP/TCP) were left in place." echo -e " Remove them manually if no longer needed." } if [[ "$ACTION" == "uninstall" ]]; then do_uninstall echo "IRANUX_RESULT {\"outputs\":[{\"key\":\"install_dir\",\"label\":\"Cleaned installation folder\",\"value\":$(iranux_json_string "$INSTALL_DIR"),\"type\":\"text\",\"i18n\":{\"fa\":{\"label\":\"پوشه‌ی نصب پاک‌شده\"}}}]}" echo "__IRANUX_REACHED_END_V1__" exit 0 fi if [[ -f "server_config.toml" && -f "server_config.toml.backup" ]]; then log_error "Both server_config.toml and server_config.toml.backup exist. Remove one and retry." fi TMP_LOG="init_logs.tmp" DOWNLOAD_DIR="" cleanup() { rm -f "$TMP_LOG" 2>/dev/null || true if [[ -n "${DOWNLOAD_DIR:-}" && -d "${DOWNLOAD_DIR:-}" ]]; then rm -rf "$DOWNLOAD_DIR" 2>/dev/null || true fi } trap cleanup EXIT PM="" if command -v apt-get >/dev/null 2>&1; then PM="apt"; elif command -v dnf >/dev/null 2>&1; then PM="dnf"; elif command -v yum >/dev/null 2>&1; then PM="yum"; else log_error "No supported package manager found (apt/dnf/yum)."; fi log_header "Preparing Environment" if [[ "$LOCAL_MODE" -eq 0 ]]; then log_info "Installing dependencies..." if [[ "$PM" == "apt" ]]; then apt-get update -y >/dev/null 2>&1 apt-get install -y lsof net-tools wget unzip curl ca-certificates iproute2 procps irqbalance >/dev/null 2>&1 elif [[ "$PM" == "dnf" ]]; then dnf -y install lsof net-tools wget unzip curl ca-certificates iproute procps-ng irqbalance >/dev/null 2>&1 else yum -y install lsof net-tools wget unzip curl ca-certificates iproute procps-ng irqbalance >/dev/null 2>&1 fi else log_info "Local mode: skipping package installation." fi require_cmd ss require_cmd systemctl require_cmd sysctl log_success "System tools are ready." if systemctl list-unit-files --type=service --all 2>/dev/null | awk '{print $1}' | grep -qx 'irqbalance.service'; then log_info "Enabling irqbalance for better multi-core packet distribution..." systemctl enable --now irqbalance >/dev/null 2>&1 || log_warn "Could not enable/start irqbalance." fi check_port53() { ss -H -lun "sport = :53" 2>/dev/null | grep -q ':53' && return 0 ss -H -ltn "sport = :53" 2>/dev/null | grep -q ':53' && return 0 return 1 } show_port53_usage() { log_warn "Current listeners on port 53:" ss -lupn "sport = :53" 2>/dev/null || true ss -ltpn "sport = :53" 2>/dev/null || true lsof -nP -iUDP:53 -iTCP:53 2>/dev/null || true } get_port53_pids() { local pids_udp pids_tcp pids pids_udp="$(ss -H -lupn "sport = :53" 2>/dev/null | sed -nE 's/.*pid=([0-9]+).*/\1/p' | sort -u)" pids_tcp="$(ss -H -ltpn "sport = :53" 2>/dev/null | sed -nE 's/.*pid=([0-9]+).*/\1/p' | sort -u)" pids="$(printf '%s\n%s\n' "$pids_udp" "$pids_tcp" | sed '/^$/d' | sort -u)" if [[ -n "$pids" ]]; then echo "$pids" return 0 fi lsof -ti :53 2>/dev/null || true } stop_service_if_present() { local unit="$1" if systemctl list-unit-files --type=service --all 2>/dev/null | awk '{print $1}' | grep -qx "$unit"; then if systemctl is-active --quiet "$unit"; then log_info "Stopping conflicting service: $unit" systemctl stop "$unit" || true fi systemctl disable "$unit" >/dev/null 2>&1 || true fi } stop_socket_if_present() { local unit="$1" if systemctl list-unit-files --type=socket --all 2>/dev/null | awk '{print $1}' | grep -qx "$unit"; then if systemctl is-active --quiet "$unit"; then log_info "Stopping conflicting socket: $unit" systemctl stop "$unit" || true fi systemctl disable "$unit" >/dev/null 2>&1 || true fi } terminate_port53_pid() { local pid="$1" [[ -n "$pid" ]] || return 0 if ! kill -0 "$pid" 2>/dev/null; then return 0 fi local cmdline cmdline="$(ps -p "$pid" -o cmd= 2>/dev/null || true)" log_warn "Trying to terminate PID on port 53: $pid (${cmdline:-unknown})" kill "$pid" 2>/dev/null || true for _ in 1 2 3; do sleep 1 if ! kill -0 "$pid" 2>/dev/null; then return 0 fi done kill -9 "$pid" 2>/dev/null || true sleep 1 if kill -0 "$pid" 2>/dev/null; then log_warn "PID $pid is still alive after SIGKILL." return 1 fi return 0 } force_release_port53() { local stubborn=0 local pid while IFS= read -r pid; do [[ -z "$pid" ]] && continue terminate_port53_pid "$pid" || stubborn=1 done <<< "$(get_port53_pids)" if command -v fuser >/dev/null 2>&1 && check_port53; then log_warn "Trying fuser fallback for port 53..." fuser -k 53/udp 2>/dev/null || true fuser -k 53/tcp 2>/dev/null || true sleep 1 fi return "$stubborn" } remove_iptables_port53_redirects() { local tool="$1" command -v "$tool" >/dev/null 2>&1 || return 0 local rule delete_rule while IFS= read -r rule; do [[ -z "$rule" ]] && continue delete_rule="${rule/-A /-D }" log_warn "Removing ${tool} NAT redirect rule for port 53: $rule" # shellcheck disable=SC2086 $tool -t nat $delete_rule >/dev/null 2>&1 || true done < <("$tool" -t nat -S 2>/dev/null | grep -E '(^-A )' | grep -E -- '(-p (tcp|udp)|-p (udp|tcp)).*--dport 53([^0-9]|$)' | grep -E 'REDIRECT|DNAT' || true) } remove_nft_port53_redirects() { command -v nft >/dev/null 2>&1 || return 0 local rule while IFS= read -r rule; do [[ -z "$rule" ]] && continue log_warn "Removing nftables redirect rule for port 53: $rule" nft delete rule $rule >/dev/null 2>&1 || true done < <(nft -a list ruleset 2>/dev/null | awk ' / dport 53 / && ($0 ~ /redirect/ || $0 ~ /dnat/) { for (i = 1; i <= NF; i++) { if ($i == "table") table = $(i+1) if ($i == "chain") chain = $(i+1) if ($i == "handle") handle = $(i+1) } if (table != "" && chain != "" && handle != "") { print "ip " table " " chain " handle " handle } table = ""; chain = ""; handle = "" } ' || true) } remove_port53_forward_rules() { log_info "Checking for port 53 redirect/forward rules..." remove_iptables_port53_redirects iptables remove_iptables_port53_redirects ip6tables remove_nft_port53_redirects } stop_existing_stormdns_service() { local unit_present=0 if systemctl list-unit-files --all 2>/dev/null | grep -q '^stormdns.service'; then unit_present=1 log_info "Stopping existing StormDNS service..." systemctl stop stormdns 2>/dev/null || true for _ in 1 2 3 4 5; do if ! systemctl is-active --quiet stormdns; then break fi sleep 1 done local main_pid main_pid="$(systemctl show stormdns --property MainPID --value 2>/dev/null || true)" if [[ -n "${main_pid:-}" && "$main_pid" != "0" ]] && kill -0 "$main_pid" 2>/dev/null; then log_warn "stormdns service is still active. Trying to terminate MainPID: $main_pid" terminate_port53_pid "$main_pid" || true fi systemctl stop stormdns 2>/dev/null || true systemctl reset-failed stormdns 2>/dev/null || true fi local pid cmdline killed=0 while IFS= read -r pid; do [[ -z "$pid" ]] && continue cmdline="$(ps -p "$pid" -o cmd= 2>/dev/null || true)" if echo "$cmdline" | grep -qiE 'stormdns|stormdns_server'; then if [[ $killed -eq 0 && $unit_present -eq 0 ]]; then log_info "Stopping existing StormDNS process that was started outside systemd..." fi terminate_port53_pid "$pid" || true killed=1 fi done <<< "$(get_port53_pids)" } log_header "Stopping Existing StormDNS" stop_existing_stormdns_service log_header "Managing Network Ports (Port 53)" remove_port53_forward_rules if check_port53; then log_warn "Port 53 is occupied. Trying auto-cleanup..." show_port53_usage if systemctl is-active --quiet systemd-resolved; then log_info "Configuring systemd-resolved DNSStubListener=no ..." backup_file_once /etc/systemd/resolved.conf if grep -qE '^#?DNSStubListener=' /etc/systemd/resolved.conf; then sed -i -E 's/^#?DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf || true else echo 'DNSStubListener=no' >> /etc/systemd/resolved.conf fi if ! grep -q '^DNS=' /etc/systemd/resolved.conf; then echo 'DNS=8.8.8.8' >> /etc/systemd/resolved.conf fi systemctl restart systemd-resolved || true fi stop_socket_if_present systemd-resolved.socket stop_socket_if_present dnsmasq.socket for srv in bind9 bind9.service named named.service named-pkcs11 named-pkcs11.service dnsmasq dnsmasq.service unbound unbound.service pdns pdns.service knot-resolver kresd kresd@1.service dnscrypt-proxy dnscrypt-proxy.service smartdns smartdns.service coredns coredns.service pihole-FTL pihole-FTL.service; do stop_service_if_present "$srv" done if check_port53; then log_warn "Port 53 is still busy after stopping known services. Trying direct process termination..." force_release_port53 || true fi if check_port53 && systemctl is-active --quiet systemd-resolved; then log_warn "Port 53 is still in use. Stopping systemd-resolved completely..." systemctl stop systemd-resolved || true systemctl disable systemd-resolved >/dev/null 2>&1 || true stop_socket_if_present systemd-resolved.socket fi if check_port53; then log_warn "Port 53 still occupied. Trying one more forced cleanup pass..." force_release_port53 || true fi if check_port53; then OCC_INFO="$(ss -H -lupn 'sport = :53' 2>/dev/null | head -n1 | awk '{print $NF}' || true)" [[ -z "${OCC_INFO:-}" ]] && OCC_INFO="$(ss -H -ltn 'sport = :53' 2>/dev/null | head -n1 | awk '{print $NF}' || true)" show_port53_usage log_error "Port 53 is still occupied: ${OCC_INFO:-unknown}. Stop it manually and retry." fi fi log_success "Port 53 is available." log_header "Configuring Firewall (Port 53 UDP/TCP)" ACTIVE_FIREWALL="none" if command -v ufw >/dev/null 2>&1 && ufw status | grep -qw active; then ACTIVE_FIREWALL="ufw" ufw allow 53/udp >/dev/null 2>&1 || true ufw allow 53/tcp >/dev/null 2>&1 || true log_success "Port 53 (UDP/TCP) opened via UFW." elif command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then ACTIVE_FIREWALL="firewalld" firewall-cmd --permanent --add-port=53/udp >/dev/null 2>&1 || true firewall-cmd --permanent --add-port=53/tcp >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 || true log_success "Port 53 (UDP/TCP) opened via firewalld." elif command -v iptables >/dev/null 2>&1; then ACTIVE_FIREWALL="iptables" iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport 53 -j ACCEPT iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport 53 -j ACCEPT if command -v ip6tables >/dev/null 2>&1; then ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p udp --dport 53 -j ACCEPT ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || ip6tables -I INPUT -p tcp --dport 53 -j ACCEPT fi if command -v netfilter-persistent >/dev/null 2>&1; then netfilter-persistent save >/dev/null 2>&1 || true elif command -v iptables-save >/dev/null 2>&1 && [[ -d /etc/iptables ]]; then iptables-save > /etc/iptables/rules.v4 command -v ip6tables-save >/dev/null 2>&1 && ip6tables-save > /etc/iptables/rules.v6 fi log_success "Port 53 (UDP/TCP) rule is ready via iptables." elif command -v nft >/dev/null 2>&1; then ACTIVE_FIREWALL="nftables" if nft list table inet filter >/dev/null 2>&1; then nft add rule inet filter input udp dport 53 accept >/dev/null 2>&1 || true nft add rule inet filter input tcp dport 53 accept >/dev/null 2>&1 || true log_success "Port 53 (UDP/TCP) rule is ready via nftables." else log_warn "nftables is present but no 'inet filter' table was found. Open port 53 manually if needed." fi else log_warn "No supported firewall tool detected. Skipping firewall setup." fi log_info "Detected firewall handling: ${ACTIVE_FIREWALL}" log_header "Tuning Kernel & Limits" cat > /etc/sysctl.d/99-stormdns.conf <<'EOF' # StormDNS high-load tuning fs.file-max = 2097152 fs.nr_open = 2097152 net.core.somaxconn = 65535 net.core.netdev_max_backlog = 16384 net.core.optmem_max = 25165824 net.core.rmem_default = 262144 net.core.wmem_default = 262144 net.core.rmem_max = 33554432 net.core.wmem_max = 33554432 net.ipv4.udp_rmem_min = 16384 net.ipv4.udp_wmem_min = 16384 net.ipv4.udp_mem = 65536 131072 262144 net.netfilter.nf_conntrack_max = 262144 net.netfilter.nf_conntrack_udp_timeout = 15 net.netfilter.nf_conntrack_udp_timeout_stream = 60 net.ipv4.ip_local_port_range = 10240 65535 EOF sysctl --system >/dev/null 2>&1 || log_warn "Could not fully apply sysctl settings." cat > /etc/sysctl.d/99-stormdns-tuning.conf <<'EOF' # StormDNS performance tuning (supplementary) fs.file-max = 2097152 fs.nr_open = 2097152 net.core.rmem_max = 33554432 net.core.wmem_max = 33554432 net.core.netdev_max_backlog = 16384 net.core.somaxconn = 65535 net.ipv4.udp_rmem_min = 16384 net.ipv4.udp_wmem_min = 16384 net.ipv4.ip_local_port_range = 10240 65535 EOF sysctl --system >/dev/null 2>&1 || log_warn "Some supplementary sysctl values may not have applied." cat > /etc/security/limits.d/99-stormdns.conf <<'EOF' * soft nofile 1048576 * hard nofile 1048576 root soft nofile 1048576 root hard nofile 1048576 EOF log_success "Kernel and file descriptor limits configured." if [[ "$LOCAL_MODE" -eq 1 ]]; then log_header "Locating Local Server Binary" ARCH="$(uname -m)" LOCAL_BIN="$(find_local_server_binary "$ARCH")" [[ -z "$LOCAL_BIN" ]] && log_error "No StormDNS server binary found. Run 'python build.py' first." log_info "Found binary: $LOCAL_BIN" LOCAL_BIN_DIR="$(dirname "$LOCAL_BIN")" if [[ "$LOCAL_BIN_DIR" != "$INSTALL_DIR" ]]; then cp "$LOCAL_BIN" "$INSTALL_DIR/" log_info "Copied binary to install directory." fi EXECUTABLE="$(basename "$LOCAL_BIN")" chmod +x "$INSTALL_DIR/$EXECUTABLE" log_success "Binary ready: $EXECUTABLE" if [[ -f "$INSTALL_DIR/server_config.toml" ]]; then mv -f "$INSTALL_DIR/server_config.toml" "$INSTALL_DIR/server_config.toml.backup" log_info "Existing config backed up." fi LOCAL_CONFIG="$(find_local_config)" if [[ -n "$LOCAL_CONFIG" && "$LOCAL_CONFIG" != "$INSTALL_DIR/server_config.toml" ]]; then cp "$LOCAL_CONFIG" "$INSTALL_DIR/server_config.toml" log_info "Config copied from: $LOCAL_CONFIG" elif [[ ! -f "$INSTALL_DIR/server_config.toml" ]]; then log_error "No server_config.toml or server_config.toml.simple found. Cannot install." fi else if [[ -n "$TARGET_VERSION" ]]; then log_header "Fetching Release ${TARGET_VERSION}" else log_header "Fetching Latest Release" fi ARCH="$(uname -m)" select_release_artifact "$ARCH" "$TARGET_VERSION" if [[ -f "server_config.toml" ]]; then mv -f server_config.toml server_config.toml.backup log_info "Existing config backed up." fi log_info "Downloading server binaries..." require_cmd curl require_cmd unzip if ! DOWNLOAD_DIR="$(mktemp -d /tmp/stormdns_download.XXXXXX 2>/dev/null)"; then DOWNLOAD_DIR="$(mktemp -d "$INSTALL_DIR/stormdns_download.XXXXXX" 2>/dev/null || true)" fi [[ -n "${DOWNLOAD_DIR:-}" && -d "${DOWNLOAD_DIR:-}" ]] || log_error "Failed to create temporary download directory. Check free space and /tmp permissions." ZIP_PATH="${DOWNLOAD_DIR}/server.zip" if ! curl -fL --retry 3 --retry-delay 2 --connect-timeout 15 -o "$ZIP_PATH" "$URL"; then log_warn "curl download failed, trying wget..." wget -qO "$ZIP_PATH" "$URL" || { log_warn "Disk usage snapshot:" df -h "$INSTALL_DIR" /tmp 2>/dev/null || true log_error "Download failed." } fi [[ -s "$ZIP_PATH" ]] || log_error "Downloaded archive is missing or empty: $ZIP_PATH" unzip -q -o "$ZIP_PATH" -d "$INSTALL_DIR" || log_error "Failed to extract archive." log_success "Files extracted." EXECUTABLE="$(ls -t ${PREFIX}_v* 2>/dev/null | head -n1 || true)" [[ -z "$EXECUTABLE" ]] && log_error "Binary not found in package." chmod +x "$EXECUTABLE" fi log_header "Configuration" [[ -f "server_config.toml" ]] || log_error "server_config.toml not found." CURRENT_VERSION="$(extract_config_version server_config.toml)" if [[ -z "${CURRENT_VERSION:-}" ]]; then log_error "server_config.toml is invalid (CONFIG_VERSION missing)." fi if [[ -f "server_config.toml.backup" ]]; then BACKUP_VERSION="$(extract_config_version server_config.toml.backup)" if [[ -z "${BACKUP_VERSION:-}" ]]; then log_error "Backup config is too old (CONFIG_VERSION missing). Merge manually." fi if [[ "$BACKUP_VERSION" == "$CURRENT_VERSION" ]]; then mv -f server_config.toml.backup server_config.toml log_info "Config restored from backup." elif version_lt "$BACKUP_VERSION" "$CURRENT_VERSION"; then OLD_CFG_NAME="server_config_$(date +%Y%m%d_%H%M%S).toml" mv -f server_config.toml.backup "$OLD_CFG_NAME" log_warn "Old config version detected (backup=$BACKUP_VERSION < new=$CURRENT_VERSION)." log_warn "Previous config renamed to: $OLD_CFG_NAME" log_info "Using fresh config template; please set DOMAIN and other required fields." else log_error "Backup config version is newer than the config template (backup=$BACKUP_VERSION, new=$CURRENT_VERSION). Merge manually." fi fi USER_DOMAIN="${USER_DOMAIN:-}" RESULT_DOMAIN="" if [[ -f "server_config.toml" ]] && grep -q '"v.domain.com"' server_config.toml; then echo -e "${YELLOW}${BOLD}Attention:${NC} Set your NS domain." if [[ -n "${USER_DOMAIN:-}" ]]; then escaped_domain="$(printf '%s' "$USER_DOMAIN" | sed -e 's/[\/&|]/\\&/g')" sed -i -E "s|^DOMAIN[[:space:]]*=.*$|DOMAIN = [\"${escaped_domain}\"]|" server_config.toml RESULT_DOMAIN="${USER_DOMAIN}" fi fi log_header "Security Initialization" log_info "Starting server once to generate encryption key..." ./"$EXECUTABLE" > "$TMP_LOG" 2>&1 & APP_PID=$! READY=false for _ in {1..10}; do if grep -q "Active Encryption Key" "$TMP_LOG" 2>/dev/null; then READY=true break fi sleep 1 done kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true if [[ "$READY" != true ]]; then log_warn "Initialization log tail:" tail -n 20 "$TMP_LOG" || true log_error "Could not verify key generation. Ensure Port 53 is free." fi echo -e "${GREEN}${BOLD}------------------------------------------------------" echo -e " YOUR ENCRYPTION KEY IS STORED IN: ${NC}${CYAN}${INSTALL_DIR}/encrypt_key.txt${NC}" echo -e "${GREEN}${BOLD}------------------------------------------------------${NC}" log_header "Installing Egress Filter (Block Outbound TCP/53)" cat > /usr/local/sbin/stormdns-egress-filter.sh <<'FILTER' #!/usr/bin/env bash set -euo pipefail if command -v iptables >/dev/null 2>&1; then iptables -C OUTPUT -p tcp --dport 53 -j REJECT --reject-with tcp-reset 2>/dev/null || iptables -I OUTPUT 1 -p tcp --dport 53 -j REJECT --reject-with tcp-reset fi if command -v ip6tables >/dev/null 2>&1; then ip6tables -C OUTPUT -p tcp --dport 53 -j REJECT --reject-with tcp-reset 2>/dev/null || ip6tables -I OUTPUT 1 -p tcp --dport 53 -j REJECT --reject-with tcp-reset || true fi FILTER chmod +x /usr/local/sbin/stormdns-egress-filter.sh cat > /etc/systemd/system/stormdns-egress-filter.service <<'SERVICE' [Unit] Description=StormDNS egress filter - reject outbound TCP/53 After=network.target Before=stormdns.service [Service] Type=oneshot ExecStart=/usr/local/sbin/stormdns-egress-filter.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target SERVICE log_success "Egress filter installed." log_header "Installing System Service" SVC="/etc/systemd/system/stormdns.service" cat > "$SVC" < /etc/systemd/system/stormdns.service.d/10-stormdns-tuning.conf <<'DROPIN' [Unit] Wants=stormdns-egress-filter.service After=stormdns-egress-filter.service DROPIN systemctl daemon-reload systemctl enable --now stormdns-egress-filter.service log_success "Egress filter enabled." log_info "Killing any stuck outbound TCP/53 sockets..." ss -K state syn-sent dport = :53 >/dev/null 2>&1 || true ss -K state close-wait dport = :53 >/dev/null 2>&1 || true ss -6 -K state syn-sent dport = :53 >/dev/null 2>&1 || true ss -6 -K state close-wait dport = :53 >/dev/null 2>&1 || true systemctl enable stormdns >/dev/null 2>&1 systemctl restart stormdns if ! systemctl is-active --quiet stormdns; then journalctl -u stormdns -n 50 --no-pager || true log_error "Service failed to start. See logs above." fi log_success "StormDNS service is running." log_info "Cleaning up old server binaries..." shopt -s nullglob for old_bin in "$INSTALL_DIR"/StormDNS_Server_Linux*; do [[ "$(basename "$old_bin")" == "$EXECUTABLE" ]] && continue rm -f -- "$old_bin" log_info "Removed old binary: $(basename "$old_bin")" done shopt -u nullglob echo -e "\n${CYAN}======================================================${NC}" echo -e " ${GREEN}${BOLD} INSTALLATION COMPLETED SUCCESSFULLY!${NC}" echo -e "${CYAN}======================================================${NC}" echo -e "${BOLD}Commands:${NC}" echo -e " ${YELLOW}>${NC} Start: systemctl start stormdns" echo -e " ${YELLOW}>${NC} Stop: systemctl stop stormdns" echo -e " ${YELLOW}>${NC} Restart: systemctl restart stormdns" echo -e " ${YELLOW}>${NC} Logs: journalctl -u stormdns -f" echo -e "\n${BOLD}Files:${NC}" echo -e " ${YELLOW}>${NC} ${INSTALL_DIR}/server_config.toml" echo -e " ${YELLOW}>${NC} ${INSTALL_DIR}/encrypt_key.txt" echo -e "${YELLOW}Final Note:${NC} If config changes, run: systemctl restart stormdns" rm -f *.spec >/dev/null 2>&1 || true if [[ -n "${RESULT_DOMAIN:-}" ]]; then echo "IRANUX_RESULT {\"outputs\":[{\"key\":\"tunnel_domain\",\"label\":\"Tunnel domain\",\"value\":$(iranux_json_string "$RESULT_DOMAIN"),\"type\":\"copy\",\"i18n\":{\"fa\":{\"label\":\"دامنه‌ی تونل\"}}},{\"key\":\"dns_port\",\"label\":\"DNS port (UDP/TCP)\",\"value\":\"53\",\"type\":\"text\",\"i18n\":{\"fa\":{\"label\":\"پورت DNS (UDP/TCP)\"}}},{\"key\":\"encryption_key_file\",\"label\":\"Encryption key file\",\"value\":$(iranux_json_string "${INSTALL_DIR}/encrypt_key.txt"),\"type\":\"text\",\"i18n\":{\"fa\":{\"label\":\"فایل کلید رمزنگاری\"}}},{\"key\":\"config_file\",\"label\":\"Configuration file\",\"value\":$(iranux_json_string "${INSTALL_DIR}/server_config.toml"),\"type\":\"copy\",\"i18n\":{\"fa\":{\"label\":\"فایل پیکربندی\"}}},{\"key\":\"service_name\",\"label\":\"Service name\",\"value\":\"stormdns\",\"type\":\"copy\",\"i18n\":{\"fa\":{\"label\":\"نام سرویس\"}}}]}" else echo "IRANUX_RESULT {\"outputs\":[{\"key\":\"dns_port\",\"label\":\"DNS port (UDP/TCP)\",\"value\":\"53\",\"type\":\"text\",\"i18n\":{\"fa\":{\"label\":\"پورت DNS (UDP/TCP)\"}}},{\"key\":\"encryption_key_file\",\"label\":\"Encryption key file\",\"value\":$(iranux_json_string "${INSTALL_DIR}/encrypt_key.txt"),\"type\":\"text\",\"i18n\":{\"fa\":{\"label\":\"فایل کلید رمزنگاری\"}}},{\"key\":\"config_file\",\"label\":\"Configuration file\",\"value\":$(iranux_json_string "${INSTALL_DIR}/server_config.toml"),\"type\":\"copy\",\"i18n\":{\"fa\":{\"label\":\"فایل پیکربندی\"}}},{\"key\":\"service_name\",\"label\":\"Service name\",\"value\":\"stormdns\",\"type\":\"copy\",\"i18n\":{\"fa\":{\"label\":\"نام سرویس\"}}}]}" fi echo "__IRANUX_REACHED_END_V1__" exit 0