#!/bin/sh # B4 Installer — Universal Linux installer with wizard interface # Supports desktop Linux, OpenWRT, MerlinWRT, Keenetic, Mikrotik, Docker, and more # # AUTO-GENERATED — Do not edit directly # Edit files in installer2/ and run: make build-installer # set -e # Ensure sane PATH (Entware paths first for wget-ssl/curl from /opt/bin) export PATH="/opt/bin:/opt/sbin:$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:$PATH" if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC='' fi QUIET_MODE=0 log_info() { [ "$QUIET_MODE" -eq 1 ] && return printf "${BLUE}[INFO]${NC} %s\n" "$1" >&2 } log_ok() { [ "$QUIET_MODE" -eq 1 ] && return printf "${GREEN}[ OK ]${NC} %s\n" "$1" >&2 } log_warn() { [ "$QUIET_MODE" -eq 1 ] && return printf "${YELLOW}[WARN]${NC} %s\n" "$1" >&2 } log_err() { printf "${RED}[ERR ]${NC} %s\n" "$1" >&2 } log_header() { [ "$QUIET_MODE" -eq 1 ] && return printf "\n${MAGENTA}${BOLD}%s${NC}\n" "$1" >&2 } log_detail() { [ "$QUIET_MODE" -eq 1 ] && return printf " ${CYAN}%-22s${NC}: %b\n" "$1" "$2" >&2 } log_sep() { [ "$QUIET_MODE" -eq 1 ] && return printf "${DIM}%s${NC}\n" "─────────────────────────────────────────" >&2 } REPO_OWNER="DanielLavrushin" REPO_NAME="b4" BINARY_NAME="b4" TEMP_DIR="/tmp/b4_install_$$" WGET_INSECURE="" PROXY_BASE_URL="https://proxy.lavrush.in/github" B4_BIN_DIR="" B4_DATA_DIR="" B4_CONFIG_FILE="" B4_SERVICE_TYPE="" B4_SERVICE_DIR="" B4_SERVICE_NAME="" B4_PKG_MANAGER="" B4_PLATFORM="" command_exists() { command -v "$1" >/dev/null 2>&1 || which "$1" >/dev/null 2>&1 } _byte_to_dec() { _btd_oct=$(od -b | head -1 | awk '{print $2}') [ -z "$_btd_oct" ] && return 1 printf '%d\n' "0$_btd_oct" } check_root() { if [ "$(id -u 2>/dev/null)" = "0" ]; then return 0 fi if [ "$USER" = "root" ]; then return 0 fi if touch /etc/.b4_root_test 2>/dev/null; then rm -f /etc/.b4_root_test return 0 fi log_err "This script must be run as root" exit 1 } get_avail_kb() { _path="$1" df -Pk "$_path" 2>/dev/null | awk 'NR==2 {print $4}' } TEMP_MIN_KB=20000 setup_temp() { _tmp_avail=$(get_avail_kb /tmp) if [ -n "$_tmp_avail" ] && [ "$_tmp_avail" -gt "$TEMP_MIN_KB" ] 2>/dev/null; then TEMP_DIR="/tmp/b4_install_$$" else _fallback="" if [ -n "$B4_BIN_DIR" ] && [ -d "$B4_BIN_DIR" ] && [ -w "$B4_BIN_DIR" ]; then _fb_avail=$(get_avail_kb "$B4_BIN_DIR") if [ -n "$_fb_avail" ] && [ "$_fb_avail" -gt "$TEMP_MIN_KB" ] 2>/dev/null; then _fallback="$B4_BIN_DIR" fi fi for _fb_dir in /opt /var/tmp /root "$HOME"; do [ -z "$_fallback" ] || break [ -d "$_fb_dir" ] && [ -w "$_fb_dir" ] || continue _fb_avail=$(get_avail_kb "$_fb_dir") if [ -n "$_fb_avail" ] && [ "$_fb_avail" -gt "$TEMP_MIN_KB" ] 2>/dev/null; then _fallback="$_fb_dir" fi done if [ -z "$_fallback" ]; then log_err "Not enough disk space — /tmp has ${_tmp_avail:-?}KB free (need ${TEMP_MIN_KB}KB)" log_err "No writable fallback directory found." log_info "Free space or re-run with --bin-dir on external storage." exit 1 else TEMP_DIR="${_fallback}/.b4_install_$$" log_info "Using ${_fallback} for temp files (/tmp too small)" fi fi rm -rf "$TEMP_DIR" 2>/dev/null || true mkdir -p "$TEMP_DIR" || { log_err "Cannot create temp dir: $TEMP_DIR" exit 1 } } cleanup_temp() { rm -rf "$TEMP_DIR" 2>/dev/null || true } trap cleanup_temp EXIT INT TERM detect_pkg_manager() { if [ -n "$B4_PKG_MANAGER" ]; then return 0 fi if command_exists apt-get; then B4_PKG_MANAGER="apt" elif command_exists dnf; then B4_PKG_MANAGER="dnf" elif command_exists yum; then B4_PKG_MANAGER="yum" elif command_exists pacman; then B4_PKG_MANAGER="pacman" elif command_exists apk; then B4_PKG_MANAGER="apk" elif command_exists opkg; then B4_PKG_MANAGER="opkg" fi } pkg_install() { detect_pkg_manager case "$B4_PKG_MANAGER" in apt) apt-get update -qq >/dev/null 2>&1 apt-get install -y -qq "$@" >/dev/null 2>&1 ;; dnf) dnf install -y -q "$@" >/dev/null 2>&1 ;; yum) yum install -y -q "$@" >/dev/null 2>&1 ;; pacman) pacman -S --noconfirm --needed "$@" >/dev/null 2>&1 ;; apk) apk add --quiet "$@" >/dev/null 2>&1 ;; opkg) opkg update >/dev/null 2>&1 opkg install "$@" >/dev/null 2>&1 ;; *) log_warn "No package manager detected" return 1 ;; esac } detect_architecture() { arch=$(uname -m) case "$arch" in x86_64 | amd64) echo "amd64" ;; i386 | i486 | i586 | i686) echo "386" ;; aarch64 | arm64) echo "arm64" ;; armv7 | armv7l) if [ -f /proc/cpuinfo ] && grep -qE "(vfpv[3-9])" /proc/cpuinfo 2>/dev/null && grep -qE "CPU architecture:\s*7" /proc/cpuinfo 2>/dev/null; then echo "armv7" else echo "armv5" fi ;; armv6*) echo "armv6" ;; armv5*) echo "armv5" ;; arm*) if [ -f /proc/cpuinfo ]; then if grep -qE "CPU architecture:\s*7" /proc/cpuinfo 2>/dev/null; then echo "armv7" elif grep -qE "CPU architecture:\s*6" /proc/cpuinfo 2>/dev/null; then echo "armv6" else echo "armv5" fi else echo "armv5" fi ;; mips64*) variant="mips64" if is_little_endian; then variant="mips64le"; fi if is_softfloat; then variant="${variant}_softfloat"; fi echo "$variant" ;; mips*) variant="mips" if is_little_endian; then variant="mipsle"; fi if is_softfloat; then variant="${variant}_softfloat"; fi echo "$variant" ;; ppc64le) echo "ppc64le" ;; ppc64) echo "ppc64" ;; riscv64) echo "riscv64" ;; s390x) echo "s390x" ;; loongarch64) echo "loong64" ;; *) log_err "Unsupported architecture: $arch" exit 1 ;; esac } is_little_endian() { uname -m | grep -qi "el" && return 0 [ -f /sys/kernel/cpu_byteorder ] && grep -qi "little" /sys/kernel/cpu_byteorder 2>/dev/null && return 0 [ -f /proc/cpuinfo ] && grep -qi "little.endian\|byteorder.*little" /proc/cpuinfo 2>/dev/null && return 0 command_exists opkg && opkg print-architecture 2>/dev/null | grep -qi "mipsel\|mips64el" && return 0 [ "$(dd if=/bin/sh bs=1 skip=5 count=1 2>/dev/null | _byte_to_dec)" = "1" ] && return 0 return 1 } is_softfloat() { if [ -f /etc/openwrt_release ]; then _sf_owrt_arch=$(sed -n "s/^DISTRIB_ARCH=['\"\`]*\([^'\"\`]*\).*/\1/p" /etc/openwrt_release 2>/dev/null) if [ -n "$_sf_owrt_arch" ]; then case "$_sf_owrt_arch" in *_softfloat* | *_nofpu* | *soft*) return 0 ;; esac if echo "$_sf_owrt_arch" | grep -qE '_[a-z]*[0-9]+k?f$'; then return 1 fi case "$_sf_owrt_arch" in mips_* | mipsel_* | mips64_* | mips64el_*) return 0 ;; esac fi fi if command_exists opkg; then _sf_opkg_arch="$(opkg print-architecture 2>/dev/null)" echo "$_sf_opkg_arch" | grep -qi "softfloat\|_nofpu\|soft_float" && return 0 if echo "$_sf_opkg_arch" | grep -qiE "mips(el|64|64el)?_[a-z]*[0-9]+k?f( |$)"; then return 1 fi echo "$_sf_opkg_arch" | grep -qi "mips" && return 0 fi if [ -f /proc/cpuinfo ]; then grep -qi "nofpu\|no fpu\|soft.float" /proc/cpuinfo 2>/dev/null && return 0 fi _sf_elf_bin="" for _sf_b in /bin/sh /bin/busybox /bin/ls; do [ -f "$_sf_b" ] && _sf_elf_bin="$_sf_b" && break done if [ -n "$_sf_elf_bin" ]; then _sf_ei_class=$(dd if="$_sf_elf_bin" bs=1 skip=4 count=1 2>/dev/null | _byte_to_dec) _sf_ei_data=$(dd if="$_sf_elf_bin" bs=1 skip=5 count=1 2>/dev/null | _byte_to_dec) _sf_flags_off="" [ "$_sf_ei_class" = "1" ] && _sf_flags_off=36 [ "$_sf_ei_class" = "2" ] && _sf_flags_off=48 if [ -n "$_sf_flags_off" ]; then if [ "$_sf_ei_data" = "1" ]; then _sf_check_off=$((_sf_flags_off + 1)) else _sf_check_off=$((_sf_flags_off + 2)) fi _sf_flag_byte=$(dd if="$_sf_elf_bin" bs=1 skip="$_sf_check_off" count=1 2>/dev/null | _byte_to_dec) if [ -n "$_sf_flag_byte" ]; then [ $((_sf_flag_byte & 8)) -ne 0 ] && return 0 return 1 fi fi fi if command_exists file; then _sf_file_out="$(file /bin/sh 2>/dev/null)" echo "$_sf_file_out" | grep -qi "soft.float" && return 0 echo "$_sf_file_out" | grep -qi "MIPS\|ELF" && return 1 fi if command_exists readelf; then readelf -A /bin/sh 2>/dev/null | grep -qi "soft.float\|softfloat" && return 0 fi return 1 } check_https_support() { if command_exists curl && curl -sI --max-time 5 "https://github.com" >/dev/null 2>&1; then return 0 fi if command_exists wget && wget --spider -q --timeout=5 "https://github.com" 2>/dev/null; then return 0 fi if command_exists wget && wget --spider -q --timeout=5 --no-check-certificate "https://github.com" 2>/dev/null; then WGET_INSECURE="--no-check-certificate" log_warn "HTTPS works only with --no-check-certificate (CA certs missing)" return 0 fi return 1 } ensure_https_support() { if check_https_support; then return 0 fi log_warn "HTTPS not available — trying to install SSL support" if command_exists opkg; then opkg update >/dev/null 2>&1 || true opkg install ca-certificates >/dev/null 2>&1 || true opkg install wget-ssl >/dev/null 2>&1 || true hash -r 2>/dev/null || true if check_https_support; then return 0; fi fi log_err "HTTPS not available. Cannot download from GitHub." log_info "On Entware/OpenWrt: opkg install wget-ssl ca-certificates" return 1 } convert_to_proxy_url() { url="$1" case "$url" in https://raw.githubusercontent.com/${REPO_OWNER}/* | \ https://github.com/${REPO_OWNER}/* | \ https://api.github.com/repos/${REPO_OWNER}/*) echo "${PROXY_BASE_URL}/${url}" ;; *) echo "$url" ;; esac } _wget_supports() { wget --help 2>&1 | grep -q "$1" } _do_fetch() { _fetch_url="$1" _fetch_out="$2" if [ -t 2 ] && [ "$QUIET_MODE" -ne 1 ]; then if command_exists curl && curl -fL --progress-bar --max-time 120 -o "$_fetch_out" "$_fetch_url" 2>&1; then return 0; fi if command_exists wget; then _wget_args="$WGET_INSECURE" _wget_supports "--show-progress" && _wget_args="$_wget_args --show-progress -q" _wget_supports "--timeout" && _wget_args="$_wget_args --timeout=120" wget $_wget_args -O "$_fetch_out" "$_fetch_url" 2>&1 && return 0 fi else if command_exists curl && curl -sfL --max-time 120 -o "$_fetch_out" "$_fetch_url" 2>/dev/null; then return 0; fi if command_exists wget; then _wget_args="-q $WGET_INSECURE" _wget_supports "--timeout" && _wget_args="$_wget_args --timeout=120" wget $_wget_args -O "$_fetch_out" "$_fetch_url" 2>/dev/null && return 0 fi fi return 1 } fetch_file() { url="$1" output="$2" if ! command_exists curl && ! command_exists wget; then log_err "Neither curl nor wget found" return 1 fi if _do_fetch "$url" "$output"; then return 0; fi proxy_url=$(convert_to_proxy_url "$url") if [ "$proxy_url" != "$url" ]; then log_warn "Direct download failed, trying proxy..." if _do_fetch "$proxy_url" "$output"; then return 0; fi fi log_err "Failed to download: $url" return 1 } fetch_stdout() { url="$1" if command_exists curl; then result=$(curl -sfL --max-time 15 "$url" 2>/dev/null) && [ -n "$result" ] && echo "$result" && return 0 fi if command_exists wget; then result=$(wget -qO- $WGET_INSECURE --timeout=15 "$url" 2>/dev/null) && [ -n "$result" ] && echo "$result" && return 0 fi proxy_url=$(convert_to_proxy_url "$url") if [ "$proxy_url" != "$url" ]; then if command_exists curl; then result=$(curl -sfL --max-time 15 "$proxy_url" 2>/dev/null) && [ -n "$result" ] && echo "$result" && return 0 fi if command_exists wget; then result=$(wget -qO- $WGET_INSECURE --timeout=15 "$proxy_url" 2>/dev/null) && [ -n "$result" ] && echo "$result" && return 0 fi fi return 1 } get_latest_version() { api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" version=$(fetch_stdout "$api_url" | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) if [ -z "$version" ]; then log_err "Failed to fetch latest version" exit 1 fi echo "$version" } verify_checksum() { file="$1" checksum_url="$2" checksum_file="${file}.sha256" if ! fetch_file "$checksum_url" "$checksum_file"; then rm -f "$checksum_file" return 1 fi expected=$(awk '{print $1}' "$checksum_file") rm -f "$checksum_file" [ -z "$expected" ] && return 1 if ! command_exists sha256sum; then log_warn "sha256sum not found, skipping verification" return 1 fi actual=$(sha256sum "$file" | awk '{print $1}') if [ "$expected" = "$actual" ]; then log_ok "SHA256 verified: $actual" return 0 else log_err "SHA256 mismatch! Expected: $expected Got: $actual" return 2 fi } is_lxc_container() { if [ -f /proc/1/environ ]; then tr '\0' '\n' /dev/null | grep -q '^container=lxc' && return 0 fi [ -f /run/systemd/container ] && grep -q "lxc" /run/systemd/container 2>/dev/null && return 0 return 1 } _kmod_builtin() { _mod="$1" _kver=$(uname -r) for _f in "/lib/modules/${_kver}/modules.builtin" "/lib/modules/${_kver}/modules.builtin.modinfo"; do [ -f "$_f" ] && grep -q "${_mod}" "$_f" 2>/dev/null && return 0 done [ -d "/sys/module/${_mod}" ] && return 0 return 1 } _kmod_available() { lsmod 2>/dev/null | grep -q "^$1" && return 0 _kmod_builtin "$1" && return 0 return 1 } is_b4_running() { for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ]; then _pid=$(cat "$pf" 2>/dev/null) [ -n "$_pid" ] && kill -0 "$_pid" 2>/dev/null && return 0 fi done if command_exists pgrep; then pgrep -x "$BINARY_NAME" >/dev/null 2>&1 && return 0 fi _mypid=$$ _ps_out=$(ps w 2>/dev/null || ps 2>/dev/null) || true if [ -n "$_ps_out" ]; then echo "$_ps_out" | grep -v grep | grep -v "$_mypid" | grep -q "[/ ]${BINARY_NAME}$" && return 0 echo "$_ps_out" | grep -v grep | grep -v "$_mypid" | grep -q "[/ ]${BINARY_NAME} " && return 0 fi return 1 } stop_b4() { if ! is_b4_running; then return 0; fi log_info "Stopping running b4 process..." for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ]; then _pid=$(cat "$pf" 2>/dev/null) [ -n "$_pid" ] && kill "$_pid" 2>/dev/null || true fi done if command_exists pkill; then pkill -x "$BINARY_NAME" 2>/dev/null || true fi sleep 2 } is_writable_dir() { dir="$1" [ -d "$dir" ] && [ -w "$dir" ] && return 0 mkdir -p "$dir" 2>/dev/null && [ -w "$dir" ] && return 0 return 1 } ensure_dir() { dir="$1" label="$2" if ! mkdir -p "$dir" 2>/dev/null; then log_err "Cannot create ${label}: ${dir}" return 1 fi if [ ! -w "$dir" ]; then log_err "${label} not writable: ${dir}" return 1 fi return 0 } check_exit() { case "$1" in [eEqQ] | exit | EXIT | quit | QUIT) echo "" log_info "Aborted by user." exit 0 ;; esac } _INPUT="" read_input() { prompt="$1" default="$2" if [ "$QUIET_MODE" -eq 1 ] 2>/dev/null; then _INPUT="$default" return 0 fi printf "${CYAN}%b${NC}" "$prompt" >&2 read _INPUT || _INPUT="$default" _INPUT=$(printf '%s' "$_INPUT" | tr -d '\r') check_exit "$_INPUT" [ -z "$_INPUT" ] && _INPUT="$default" return 0 } confirm() { prompt="$1" default="${2:-y}" # default yes if [ "$default" = "y" ]; then hint="Y/n/e" else hint="y/N/e" fi read_input "${prompt} (${hint}): " "$default" case "$_INPUT" in [yY] | [yY][eE][sS]) return 0 ;; [nN] | [nN][oO]) return 1 ;; *) [ "$default" = "y" ] && return 0 || return 1 ;; esac } WIZARD_MODE="" # "auto" or "manual" wizard_start() { echo "" printf "${BOLD}" echo " ╔═══════════════════════════════════════╗" echo " ║ B4 Universal Installer ║" echo " ╚═══════════════════════════════════════╝" printf "${NC}" echo "" while true; do log_sep echo "" printf " ${BOLD}1${NC}) Automatic detection ${DIM}(recommended)${NC}\n" printf " ${BOLD}2${NC}) Manual configuration\n" printf " ${BOLD}3${NC}) System info\n" printf " ${DIM}e) Exit${NC}\n" echo "" read_input "Select mode [1]: " "1" case "$_INPUT" in 2) WIZARD_MODE="manual"; return 0 ;; 3) action_sysinfo echo "" read_input "Press Enter to return to menu..." "" echo "" ;; *) WIZARD_MODE="auto"; return 0 ;; esac done } wizard_auto_detect() { log_header "Detecting system..." echo "" platform_auto_detect if [ -z "$B4_PLATFORM" ]; then log_err "Could not detect platform" log_info "Try manual mode or set B4_PLATFORM environment variable" exit 1 fi _user_bin_dir="$B4_BIN_DIR" _user_data_dir="$B4_DATA_DIR" platform_call info [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" B4_ARCH=$(detect_architecture) detect_pkg_manager wizard_show_config echo "" if ! confirm "Proceed with these settings?"; then log_info "Switching to manual mode..." WIZARD_MODE="manual" wizard_manual_configure fi } wizard_manual_configure() { log_header "Manual configuration" echo "" while true; do echo " Available platforms:" idx=1 for p in $REGISTERED_PLATFORMS; do pname=$(platform_dispatch "$p" name) printf " ${BOLD}%d${NC}) %s\n" "$idx" "$pname" idx=$((idx + 1)) done echo "" read_input "Select platform [1]: " "1" idx=1 for p in $REGISTERED_PLATFORMS; do if [ "$idx" = "$_INPUT" ]; then B4_PLATFORM="$p" break fi idx=$((idx + 1)) done if [ -n "$B4_PLATFORM" ]; then break fi log_warn "Invalid selection, please try again" echo "" done platform_call info read_input "Binary directory [${B4_BIN_DIR}]: " "$B4_BIN_DIR" B4_BIN_DIR="$_INPUT" read_input "Data directory [${B4_DATA_DIR}]: " "$B4_DATA_DIR" B4_DATA_DIR="$_INPUT" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" echo "" echo " Service types: systemd, openrc, procd, sysv, entware, none" read_input "Service type [${B4_SERVICE_TYPE}]: " "$B4_SERVICE_TYPE" B4_SERVICE_TYPE="$_INPUT" auto_arch=$(detect_architecture) B4_SUPPORTED_ARCHS="amd64 arm64 armv7 armv6 armv5 386 mips mipsle mips_softfloat mipsle_softfloat mips64 mips64le loong64 ppc64 ppc64le riscv64 s390x" _arch_default=1 _arch_idx=1 for a in $B4_SUPPORTED_ARCHS; do if [ "$a" = "$auto_arch" ]; then _arch_default=$_arch_idx break fi _arch_idx=$((_arch_idx + 1)) done while true; do echo " Available architectures:" _arch_idx=1 for a in $B4_SUPPORTED_ARCHS; do if [ "$a" = "$auto_arch" ]; then printf " ${BOLD}%2d${NC}) %s ${DIM}(detected)${NC}\n" "$_arch_idx" "$a" else printf " ${BOLD}%2d${NC}) %s\n" "$_arch_idx" "$a" fi _arch_idx=$((_arch_idx + 1)) done echo "" read_input "Select architecture [${_arch_default}]: " "$_arch_default" _arch_idx=1 B4_ARCH="" for a in $B4_SUPPORTED_ARCHS; do if [ "$_arch_idx" = "$_INPUT" ]; then B4_ARCH="$a" break fi _arch_idx=$((_arch_idx + 1)) done if [ -n "$B4_ARCH" ]; then break fi log_warn "Invalid selection, please try again" echo "" done detect_pkg_manager read_input "Package manager [${B4_PKG_MANAGER:-none}]: " "$B4_PKG_MANAGER" B4_PKG_MANAGER="$_INPUT" echo "" wizard_show_config echo "" if ! confirm "Proceed with these settings?"; then log_info "Aborted." exit 0 fi } wizard_show_config() { log_sep pname="" if [ -n "$B4_PLATFORM" ]; then pname=$(platform_dispatch "$B4_PLATFORM" name) fi log_detail "Platform" "${BOLD}${pname}${NC} (${B4_PLATFORM})" log_detail "Architecture" "${B4_ARCH}" log_detail "Binary directory" "${B4_BIN_DIR}" log_detail "Data directory" "${B4_DATA_DIR}" log_detail "Config file" "${B4_CONFIG_FILE}" log_detail "Service type" "${B4_SERVICE_TYPE}" log_detail "Package manager" "${B4_PKG_MANAGER:-none}" if [ -n "$REGISTERED_FEATURES" ]; then echo "" log_detail "Features" "" for f in $REGISTERED_FEATURES; do fname=$(feature_dispatch "$f" name) fdesc=$(feature_dispatch "$f" description) printf " ${GREEN}+${NC} %s ${DIM}— %s${NC}\n" "$fname" "$fdesc" >&2 done fi log_sep } wizard_select_features() { if [ -z "$REGISTERED_FEATURES" ]; then return 0 fi log_header "Optional features" echo "" for f in $REGISTERED_FEATURES; do fname=$(feature_dispatch "$f" name) fdesc=$(feature_dispatch "$f" description) fdefault=$(feature_dispatch "$f" default_enabled) if [ "$fdefault" = "yes" ]; then def="y" else def="n" fi if confirm " Enable ${BOLD}${fname}${NC}? ${DIM}(${fdesc})${NC}" "$def"; then ENABLED_FEATURES="${ENABLED_FEATURES} ${f}" fi done } REGISTERED_PLATFORMS="" register_platform() { id="$1" REGISTERED_PLATFORMS="${REGISTERED_PLATFORMS} ${id}" } platform_call() { func="$1" shift platform_dispatch "$B4_PLATFORM" "$func" "$@" } platform_dispatch() { pid="$1" func="$2" shift 2 fn="platform_${pid}_${func}" if type "$fn" >/dev/null 2>&1; then "$fn" "$@" else log_warn "Platform '${pid}' does not implement '${func}'" return 1 fi } platform_auto_detect() { if [ -n "$B4_PLATFORM" ]; then for p in $REGISTERED_PLATFORMS; do if [ "$p" = "$B4_PLATFORM" ]; then log_ok "Using user-specified platform: $B4_PLATFORM" return 0 fi done log_err "Unknown platform: $B4_PLATFORM" log_info "Available: $REGISTERED_PLATFORMS" exit 1 fi _fallback="" for p in $REGISTERED_PLATFORMS; do [ "$p" = "generic_linux" ] && _fallback="generic_linux" && continue if platform_dispatch "$p" match 2>/dev/null; then B4_PLATFORM="$p" pname=$(platform_dispatch "$p" name) log_ok "Detected platform: ${pname}" return 0 fi done if [ -n "$_fallback" ] && platform_dispatch "generic_linux" match 2>/dev/null; then B4_PLATFORM="generic_linux" log_ok "Detected platform: Generic Linux" return 0 fi if [ -n "$_fallback" ]; then B4_PLATFORM="generic_linux" log_warn "Could not detect specific platform, defaulting to Generic Linux" return 0 fi return 1 } platform_generic_linux_name() { echo "Generic Linux (Ubuntu/Debian/Fedora/Arch/Alpine)" } platform_generic_linux_match() { [ "$(uname -s)" = "Linux" ] || return 1 [ -f /etc/openwrt_release ] && return 1 [ -f /etc/merlinwrt_release ] && return 1 [ -d /jffs ] && [ -d /opt/etc/init.d ] && return 1 # Merlin with Entware [ -d /etc/storage ] && [ -d /etc_ro ] && return 1 # Padavan [ -d /var/run/ndm ] && return 1 # Keenetic NDMS command_exists ndmc && return 1 # Keenetic NDMS command_exists nvram && nvram get firmver 2>/dev/null | grep -qi "merlin" && return 1 [ -f /proc/device-tree/model ] && grep -qi "keenetic" /proc/device-tree/model 2>/dev/null && return 1 command_exists systemctl && return 0 [ -d /etc/init.d ] && return 0 return 0 } platform_generic_linux_info() { B4_BIN_DIR="/usr/local/bin" B4_DATA_DIR="/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" if command_exists systemctl && systemctl list-units >/dev/null 2>&1; then B4_SERVICE_TYPE="systemd" B4_SERVICE_DIR="/etc/systemd/system" B4_SERVICE_NAME="b4.service" elif [ -f /sbin/openrc-run ] || command_exists openrc-run; then B4_SERVICE_TYPE="openrc" B4_SERVICE_DIR="/etc/init.d" B4_SERVICE_NAME="b4" elif [ -d /etc/init.d ]; then B4_SERVICE_TYPE="sysv" B4_SERVICE_DIR="/etc/init.d" B4_SERVICE_NAME="b4" else B4_SERVICE_TYPE="none" fi detect_pkg_manager } platform_generic_linux_check_deps() { _generic_linux_check_lxc missing="" if ! command_exists curl && ! command_exists wget; then missing="${missing} wget" fi command_exists tar || missing="${missing} tar" if [ -n "$missing" ]; then log_warn "Missing required:${missing}" if confirm "Install missing packages?"; then pkg_install $missing || log_warn "Some packages failed to install" else log_err "Cannot continue without:${missing}" exit 1 fi fi ensure_https_support || exit 1 _generic_linux_check_kmods _generic_linux_check_recommended } _generic_linux_check_lxc() { is_lxc_container || return 0 echo "" log_warn "Running inside an LXC container" log_info "B4 requires netfilter/NFQUEUE support from the host kernel." log_info "The LXC container config (on the host) must include:" echo "" >&2 printf " ${BOLD}lxc.cgroup2.devices.allow: c 10:200 rwm${NC}\n" >&2 printf " ${BOLD}lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file${NC}\n" >&2 printf " ${BOLD}lxc.prlimit.nofile: 1048576${NC}\n" >&2 printf " ${BOLD}features: nesting=1,keyctl=1${NC}\n" >&2 echo "" >&2 log_info "On Proxmox: edit /etc/pve/lxc/.conf and restart the container." echo "" if ! confirm "Continue installation?"; then log_info "Aborted. Apply the LXC config changes first, then re-run the installer." exit 0 fi } _generic_linux_check_kmods() { for mod in nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq; do _kmod_available "$mod" && continue modprobe "$mod" 2>/dev/null || true done if ! _kmod_available "xt_NFQUEUE" && ! _kmod_available "nfnetlink_queue" && ! _kmod_available "nft_queue"; then log_warn "No netfilter queue module available" case "$B4_PKG_MANAGER" in apt) log_info "Try: apt install xtables-addons-common" ;; dnf | yum) log_info "Try: dnf install xtables-addons" ;; pacman) log_info "Try: pacman -S xtables-addons" ;; apk) log_info "Try: apk add iptables-nft" ;; *) ;; esac fi } _generic_linux_check_recommended() { rec_missing="" command_exists jq || rec_missing="${rec_missing} jq" if ! command_exists iptables && ! command_exists nft; then if [ "$B4_PKG_MANAGER" = "apk" ]; then rec_missing="${rec_missing} nftables" else rec_missing="${rec_missing} iptables" fi fi if [ -n "$rec_missing" ]; then log_warn "Recommended but missing:${rec_missing}" if confirm "Install recommended packages?"; then pkg_install $rec_missing || true fi fi } platform_generic_linux_find_storage() { return 0 } register_platform "generic_linux" platform_keenetic_name() { echo "Keenetic (NDMS)" } platform_keenetic_match() { if [ -f /proc/device-tree/model ] && grep -qi "keenetic" /proc/device-tree/model 2>/dev/null; then return 0 fi if [ -d /var/run/ndm ] || command_exists ndmc; then return 0 fi if [ -d "/opt/sbin" ] && [ -w "/opt/sbin" ] && [ ! -w "/etc" ] && [ ! -d "/jffs" ] && [ ! -f /etc/openwrt_release ]; then [ -d /tmp/ndm ] && return 0 fi return 1 } platform_keenetic_info() { B4_BIN_DIR="/opt/sbin" B4_DATA_DIR="/opt/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" B4_SERVICE_TYPE="entware" B4_SERVICE_DIR="/opt/etc/init.d" B4_SERVICE_NAME="S99b4" B4_PKG_MANAGER="opkg" if [ ! -d "/opt/etc/init.d" ] && [ ! -f "/opt/bin/opkg" ]; then log_warn "Entware not detected!" log_info "Entware is required on Keenetic. To install:" log_info " 1. Go to router admin panel > System Settings" log_info " 2. Enable OPKG package manager component" log_info " 3. For older models: plug in a USB drive and install Entware" log_info " More info: https://help.keenetic.com/hc/en-us/articles/360021214160" if [ -d "/tmp" ] && [ -w "/tmp" ]; then log_warn "Falling back to /tmp (non-persistent, will not survive reboot)" B4_BIN_DIR="/tmp/b4" B4_DATA_DIR="/tmp/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" B4_SERVICE_TYPE="none" fi fi } platform_keenetic_check_deps() { if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" if command_exists opkg; then log_info "Installing wget-ssl..." pkg_install wget-ssl || true fi fi command_exists tar || { log_warn "tar not found" command_exists opkg && pkg_install tar || true } ensure_https_support || exit 1 _keenetic_load_kmods _keenetic_check_recommended } _keenetic_load_kmods() { for mod in nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq; do _kmod_available "$mod" && continue modprobe "$mod" 2>/dev/null && continue kver=$(uname -r) mod_path=$(find /lib/modules/"$kver" -name "${mod}.ko*" 2>/dev/null | head -1) [ -n "$mod_path" ] && insmod "$mod_path" 2>/dev/null || true done if ! _kmod_available "xt_NFQUEUE" && ! _kmod_available "nfnetlink_queue" && ! _kmod_available "nft_queue"; then log_warn "No netfilter queue module available — b4 may not work" log_info "Check that your Keenetic firmware supports Netfilter Queue" log_info "You may need to enable 'Kernel modules for Netfilter' in the package manager" fi } _keenetic_check_recommended() { if ! command_exists opkg; then log_warn "opkg not available — cannot install recommended packages" return 0 fi rec_missing="" command_exists jq || rec_missing="${rec_missing} jq" command_exists iptables || rec_missing="${rec_missing} iptables" command_exists nohup || rec_missing="${rec_missing} coreutils-nohup" if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" fi if ! opkg list-installed 2>/dev/null | grep -q "^wget-ssl "; then if ! command_exists curl || ! curl -sI --max-time 3 "https://github.com" >/dev/null 2>&1; then rec_missing="${rec_missing} wget-ssl" fi fi if [ -n "$rec_missing" ]; then log_warn "Recommended but missing:${rec_missing}" if confirm "Install recommended packages?"; then opkg update >/dev/null 2>&1 || true for pkg in $rec_missing; do log_info "Installing ${pkg}..." opkg install "$pkg" >/dev/null 2>&1 && log_ok "Installed ${pkg}" || log_warn "Failed: ${pkg}" done fi fi } platform_keenetic_find_storage() { if [ -d "/opt" ] && [ -w "/opt" ]; then return 0 fi log_err "No writable persistent storage found (/opt not available)" log_info "Ensure Entware is installed:" log_info " - Newer models: Enable OPKG in system settings" log_info " - Older models: Plug in a USB drive and install Entware" return 1 } register_platform "keenetic" platform_merlinwrt_name() { echo "Asus Merlin (Asuswrt-Merlin)" } platform_merlinwrt_match() { if command_exists nvram; then fw=$(nvram get firmver 2>/dev/null) bw=$(nvram get buildno 2>/dev/null) if echo "$fw $bw" | grep -qi "merlin"; then return 0 fi fi if [ -d "/jffs" ] && [ -w "/jffs" ] && [ -d "/opt/etc/init.d" ]; then [ -f "/opt/etc/init.d/rc.func" ] && return 0 fi [ -f "/etc/merlinwrt_release" ] && return 0 return 1 } platform_merlinwrt_info() { B4_BIN_DIR="/opt/sbin" B4_DATA_DIR="/opt/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" B4_SERVICE_TYPE="entware" B4_SERVICE_DIR="/opt/etc/init.d" B4_SERVICE_NAME="S99b4" B4_PKG_MANAGER="opkg" if [ ! -d "/opt/etc/init.d" ]; then log_warn "Entware not detected!" log_info "Entware is required for MerlinWRT. Install it first:" log_info " 1. Plug in a USB drive and format it via the router admin panel" log_info " 2. Open SSH and run: amtm" log_info " 3. Select option 'ep' to install Entware" log_info " More info: https://diversion.ch/amtm.html" if [ -d "/jffs" ] && [ -w "/jffs" ]; then log_warn "Falling back to /jffs (limited space, Entware recommended)" B4_BIN_DIR="/jffs/b4" B4_DATA_DIR="/jffs/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" B4_SERVICE_TYPE="none" fi fi } platform_merlinwrt_check_deps() { if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" if command_exists opkg; then log_info "Installing wget-ssl..." pkg_install wget-ssl || true fi fi command_exists tar || { log_warn "tar not found" command_exists opkg && pkg_install tar || true } ensure_https_support || exit 1 _merlinwrt_load_kmods _merlinwrt_check_recommended } _merlinwrt_load_kmods() { for mod in nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq; do _kmod_available "$mod" && continue modprobe "$mod" 2>/dev/null && continue kver=$(uname -r) mod_path=$(find /lib/modules/"$kver" -name "${mod}.ko*" 2>/dev/null | head -1) [ -n "$mod_path" ] && insmod "$mod_path" 2>/dev/null || true done if ! _kmod_available "xt_NFQUEUE" && ! _kmod_available "nfnetlink_queue" && ! _kmod_available "nft_queue"; then log_warn "No netfilter queue module available — b4 may not work" log_info "Check your firmware version supports NFQUEUE" fi } _merlinwrt_check_recommended() { if ! command_exists opkg; then log_warn "opkg not available — cannot install recommended packages" return 0 fi rec_missing="" command_exists jq || rec_missing="${rec_missing} jq" command_exists iptables || rec_missing="${rec_missing} iptables" command_exists nohup || rec_missing="${rec_missing} coreutils-nohup" if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" fi if [ -n "$rec_missing" ]; then log_warn "Recommended but missing:${rec_missing}" if confirm "Install recommended packages?"; then opkg update >/dev/null 2>&1 || true for pkg in $rec_missing; do log_info "Installing ${pkg}..." opkg install "$pkg" >/dev/null 2>&1 && log_ok "Installed ${pkg}" || log_warn "Failed: ${pkg}" done fi fi } platform_merlinwrt_find_storage() { if [ -d "/opt" ] && [ -w "/opt" ]; then return 0 fi if [ -d "/jffs" ] && [ -w "/jffs" ]; then log_warn "Entware /opt not available, using /jffs (limited space)" B4_BIN_DIR="/jffs/b4" B4_DATA_DIR="/jffs/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" return 0 fi log_err "No writable persistent storage found" log_info "Please install Entware via amtm (run 'amtm' in SSH, select 'ep')" return 1 } register_platform "merlinwrt" platform_openwrt_name() { echo "OpenWrt" } platform_openwrt_match() { [ -f /etc/openwrt_release ] && return 0 if [ -f /etc/os-release ]; then grep -qi "openwrt" /etc/os-release 2>/dev/null && return 0 fi [ -f /etc/board.json ] && return 0 return 1 } platform_openwrt_info() { B4_BIN_DIR="/usr/bin" B4_DATA_DIR="/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" if command_exists apk; then B4_PKG_MANAGER="apk" else B4_PKG_MANAGER="opkg" fi if [ -f /sbin/procd ] || command_exists procd; then B4_SERVICE_TYPE="procd" B4_SERVICE_DIR="/etc/init.d" B4_SERVICE_NAME="b4" elif [ -d /etc/init.d ]; then B4_SERVICE_TYPE="sysv" B4_SERVICE_DIR="/etc/init.d" B4_SERVICE_NAME="b4" else B4_SERVICE_TYPE="none" fi if [ -d "/opt" ] && [ -w "/opt" ]; then _opt_avail=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_opt_avail" ] && [ "$_opt_avail" -gt 10000 ] 2>/dev/null; then B4_BIN_DIR="/opt/bin" B4_DATA_DIR="/opt/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" fi fi if [ "$B4_BIN_DIR" = "/usr/bin" ]; then for mnt in /mnt/sda1 /mnt/sda2 /mnt/mmcblk* /mnt/usb*; do if [ -d "$mnt" ] && [ -w "$mnt" ]; then _mnt_avail=$(df "$mnt" 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_mnt_avail" ] && [ "$_mnt_avail" -gt 10000 ] 2>/dev/null; then log_info "External storage found: $mnt" B4_BIN_DIR="${mnt}/b4" B4_DATA_DIR="${mnt}/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" break fi fi done fi } platform_openwrt_check_deps() { if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" log_info "Installing wget-ssl..." pkg_install wget-ssl ca-certificates || true fi command_exists tar || { log_warn "tar not found" pkg_install tar || true } ensure_https_support || exit 1 _openwrt_load_kmods _openwrt_check_recommended } _openwrt_load_kmods() { for mod in nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq; do _kmod_available "$mod" && continue modprobe "$mod" 2>/dev/null && continue kver=$(uname -r) mod_path=$(find /lib/modules/"$kver" -name "${mod}.ko*" 2>/dev/null | head -1) [ -n "$mod_path" ] && insmod "$mod_path" 2>/dev/null || true done if ! _kmod_available "xt_NFQUEUE" && ! _kmod_available "nfnetlink_queue" && ! _kmod_available "nft_queue"; then log_warn "No netfilter queue module available — b4 may not work" if [ "$B4_PKG_MANAGER" = "apk" ]; then log_info "Try: apk add kmod-nft-queue kmod-nft-nat kmod-nft-compat" else log_info "Try: opkg install kmod-nfnetlink-queue kmod-ipt-nfqueue iptables-mod-nfqueue kmod-ipt-conntrack-extra iptables-mod-conntrack-extra" fi fi } _openwrt_check_recommended() { rec_missing="" command_exists jq || rec_missing="${rec_missing} jq" if ! command_exists iptables && ! command_exists nft; then if [ "$B4_PKG_MANAGER" = "apk" ]; then rec_missing="${rec_missing} nftables" else rec_missing="${rec_missing} iptables" fi fi if [ "$B4_PKG_MANAGER" = "apk" ]; then if ! _kmod_available "nft_queue"; then rec_missing="${rec_missing} kmod-nft-queue" fi if ! _kmod_available "nf_nat"; then rec_missing="${rec_missing} kmod-nft-nat" fi fi if ! command_exists curl || ! curl -sI --max-time 3 "https://github.com" >/dev/null 2>&1; then if [ "$B4_PKG_MANAGER" = "apk" ]; then command_exists wget || rec_missing="${rec_missing} wget" [ -d /etc/ssl/certs ] && [ -n "$(ls /etc/ssl/certs/ 2>/dev/null)" ] || rec_missing="${rec_missing} ca-certificates" else if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" fi if ! opkg list-installed 2>/dev/null | grep -q "^wget-ssl "; then rec_missing="${rec_missing} wget-ssl" fi fi fi if [ -n "$rec_missing" ]; then log_warn "Recommended but missing:${rec_missing}" if confirm "Install recommended packages?"; then if [ "$B4_PKG_MANAGER" = "apk" ]; then for pkg in $rec_missing; do log_info "Installing ${pkg}..." apk add "$pkg" >/dev/null 2>&1 && log_ok "Installed ${pkg}" || log_warn "Failed: ${pkg}" done else opkg update >/dev/null 2>&1 || true for pkg in $rec_missing; do log_info "Installing ${pkg}..." opkg install "$pkg" >/dev/null 2>&1 && log_ok "Installed ${pkg}" || log_warn "Failed: ${pkg}" done fi fi fi } platform_openwrt_find_storage() { if [ -d "/opt" ] && [ -w "/opt" ]; then _opt_avail=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_opt_avail" ] && [ "$_opt_avail" -gt 10000 ] 2>/dev/null; then return 0 fi fi for mnt in /mnt/sda1 /mnt/sda2 /mnt/mmcblk* /mnt/usb*; do if [ -d "$mnt" ] && [ -w "$mnt" ]; then return 0 fi done _root_avail=$(df / 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_root_avail" ] && [ "$_root_avail" -lt 2000 ] 2>/dev/null; then log_warn "Root filesystem has very little space ($(df -h / 2>/dev/null | tail -1 | awk '{print $4}') available)" log_info "Consider using extroot or USB storage" log_info "See: https://openwrt.org/docs/guide-user/additional-software/extroot_configuration" fi return 0 } register_platform "openwrt" REGISTERED_FEATURES="" ENABLED_FEATURES="" register_feature() { id="$1" REGISTERED_FEATURES="${REGISTERED_FEATURES} ${id}" } feature_dispatch() { fid="$1" func="$2" shift 2 fn="feature_${fid}_${func}" if type "$fn" >/dev/null 2>&1; then "$fn" "$@" else log_warn "Feature '${fid}' does not implement '${func}'" return 1 fi } features_run() { for f in $ENABLED_FEATURES; do fname=$(feature_dispatch "$f" name) log_header "Feature: ${fname}" feature_dispatch "$f" run || log_warn "Feature '${fname}' had issues" done } features_remove() { _geo_files_to_remove="" _geo_files_display="" for f in $REGISTERED_FEATURES; do case "$f" in geoip|geosite) _gpath=$(_geo_find_file_path "$f") if [ -n "$_gpath" ]; then _geo_files_to_remove="${_geo_files_to_remove} ${_gpath}" _geo_files_display="${_geo_files_display}\n ${_gpath}" fi ;; *) feature_dispatch "$f" remove || true ;; esac done if [ -n "$_geo_files_to_remove" ]; then log_info "Found geodata files:${_geo_files_display}" if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove geodata files?" "y"; then for _gf in $_geo_files_to_remove; do rm -f "$_gf" && log_info "Removed: $_gf" done else log_info "Keeping geodata files" fi fi } feature_auth_name() { echo "Web UI authentication" } feature_auth_description() { echo "Protect the web interface with a login/password" } feature_auth_default_enabled() { echo "no" } feature_auth_run() { log_info "Set up credentials for the B4 web interface" echo "" read_input " Username: " "" _auth_user="$_INPUT" if [ -z "$_auth_user" ]; then log_info "No username provided, skipping authentication setup" return 0 fi while true; do printf " Password: " >&2 stty -echo 2>/dev/null || true read -r _auth_pass stty echo 2>/dev/null || true echo "" >&2 if [ -z "$_auth_pass" ]; then log_warn "Password cannot be empty" continue fi printf " Confirm password: " >&2 stty -echo 2>/dev/null || true read -r _auth_pass2 stty echo 2>/dev/null || true echo "" >&2 if [ "$_auth_pass" != "$_auth_pass2" ]; then log_warn "Passwords do not match, try again" continue fi break done if ! command_exists jq; then log_warn "jq not found — please update config manually:" log_info " Set system.web_server.username = $_auth_user" log_info " Set system.web_server.password = " return 0 fi if [ ! -f "$B4_CONFIG_FILE" ]; then ensure_dir "$(dirname "$B4_CONFIG_FILE")" "Config directory" || return 1 jq -n \ --arg user "$_auth_user" \ --arg pass "$_auth_pass" \ '{ system: { web_server: { username: $user, password: $pass } } }' \ >"$B4_CONFIG_FILE" else tmp="${B4_CONFIG_FILE}.tmp" if jq --arg user "$_auth_user" --arg pass "$_auth_pass" \ '.system.web_server.username = $user | .system.web_server.password = $pass' \ "$B4_CONFIG_FILE" >"$tmp" 2>/dev/null; then mv "$tmp" "$B4_CONFIG_FILE" else rm -f "$tmp" log_warn "Failed to update config" return 1 fi fi log_ok "Web UI authentication configured for user '${_auth_user}'" } feature_auth_remove() { return 0 } register_feature "auth" GEOIP_SOURCES="1|Loyalsoldier|https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download 2|RUNET Freedom|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release 3|B4 GeoIP (recommended)|https://github.com/DanielLavrushin/b4geoip/releases/latest/download" feature_geoip_name() { echo "GeoIP data" } feature_geoip_description() { echo "Download geoip.dat for IP-based filtering" } feature_geoip_default_enabled() { echo "yes" } feature_geoip_run() { base_url=$(echo "$GEOIP_SOURCES" | grep "^3|" | cut -d'|' -f3) save_dir="$B4_DATA_DIR" if [ "$QUIET_MODE" -ne 1 ]; then log_sep echo "" echo " Available geoip sources:" echo "$GEOIP_SOURCES" | while IFS='|' read -r num name _url; do [ -n "$num" ] && printf " ${BOLD}%s${NC}) %s\n" "$num" "$name" done echo "" read_input "Select source [3]: " "3" _sel_url=$(echo "$GEOIP_SOURCES" | grep "^${_INPUT}|" | cut -d'|' -f3) || true [ -n "$_sel_url" ] && base_url="$_sel_url" || log_warn "Invalid selection, using default" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then existing=$(jq -r '.system.geo.ipdat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geoip path: $save_dir" fi fi read_input "Save directory [${save_dir}]: " "$save_dir" save_dir="$_INPUT" fi ensure_dir "$save_dir" "GeoIP directory" || return 1 log_info "Downloading geoip.dat..." if ! fetch_file "${base_url}/geoip.dat" "${save_dir}/geoip.dat"; then log_err "Failed to download geoip.dat" return 1 fi [ ! -s "${save_dir}/geoip.dat" ] && log_err "geoip.dat is empty" && return 1 log_ok "geoip.dat downloaded to ${save_dir}" _geo_update_config "ipdat_path" "${save_dir}/geoip.dat" "ipdat_url" "${base_url}/geoip.dat" } feature_geoip_remove() { _geo_remove_file "ipdat_path" "geoip.dat" } register_feature "geoip" GEOSITE_SOURCES="1|Loyalsoldier|https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download 2|RUNET Freedom (recommended)|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release" feature_geosite_name() { echo "GeoSite data" } feature_geosite_description() { echo "Download geosite.dat for domain categorization" } feature_geosite_default_enabled() { echo "yes" } feature_geosite_run() { base_url=$(echo "$GEOSITE_SOURCES" | grep "^2|" | cut -d'|' -f3) save_dir="$B4_DATA_DIR" if [ "$QUIET_MODE" -ne 1 ]; then log_sep echo "" echo " Available geosite sources:" echo "$GEOSITE_SOURCES" | while IFS='|' read -r num name _url; do [ -n "$num" ] && printf " ${BOLD}%s${NC}) %s\n" "$num" "$name" done echo "" read_input "Select source [2]: " "2" _sel_url=$(echo "$GEOSITE_SOURCES" | grep "^${_INPUT}|" | cut -d'|' -f3) || true [ -n "$_sel_url" ] && base_url="$_sel_url" || log_warn "Invalid selection, using default" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then existing=$(jq -r '.system.geo.sitedat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geosite path: $save_dir" fi fi read_input "Save directory [${save_dir}]: " "$save_dir" save_dir="$_INPUT" fi ensure_dir "$save_dir" "GeoSite directory" || return 1 log_info "Downloading geosite.dat..." if ! fetch_file "${base_url}/geosite.dat" "${save_dir}/geosite.dat"; then log_err "Failed to download geosite.dat" return 1 fi [ ! -s "${save_dir}/geosite.dat" ] && log_err "geosite.dat is empty" && return 1 log_ok "geosite.dat downloaded to ${save_dir}" _geo_update_config "sitedat_path" "${save_dir}/geosite.dat" "sitedat_url" "${base_url}/geosite.dat" } feature_geosite_remove() { _geo_remove_file "sitedat_path" "geosite.dat" } register_feature "geosite" _geo_update_config() { path_key="$1" path_val="$2" url_key="$3" url_val="$4" if ! command_exists jq; then log_warn "jq not found — please update config manually:" log_info " Set system.geo.${path_key} = ${path_val}" return 0 fi if [ ! -f "$B4_CONFIG_FILE" ]; then jq -n \ --arg pv "$path_val" \ --arg uv "$url_val" \ "{ system: { geo: { ${path_key}: \$pv, ${url_key}: \$uv } } }" \ >"$B4_CONFIG_FILE" log_ok "Created config with ${path_key}" return 0 fi tmp="${B4_CONFIG_FILE}.tmp" if jq \ --arg pv "$path_val" \ --arg uv "$url_val" \ ".system.geo = (.system.geo // {}) + { \"${path_key}\": \$pv, \"${url_key}\": \$uv }" \ "$B4_CONFIG_FILE" >"$tmp" 2>/dev/null; then mv "$tmp" "$B4_CONFIG_FILE" log_ok "Config updated: ${path_key}" else rm -f "$tmp" log_warn "Failed to update config, please set ${path_key} manually" fi } _geo_find_file_path() { _feat="$1" case "$_feat" in geoip) _cfg_key="ipdat_path"; _fname="geoip.dat" ;; geosite) _cfg_key="sitedat_path"; _fname="geosite.dat" ;; *) return 1 ;; esac for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -f "$cfg" ] || continue if command_exists jq; then fpath=$(jq -r ".system.geo.${_cfg_key} // empty" "$cfg" 2>/dev/null) || true if [ -n "$fpath" ] && [ -f "$fpath" ]; then echo "$fpath" return 0 fi fi done for dir in /etc/b4 /opt/etc/b4 "$B4_DATA_DIR"; do [ -z "$dir" ] && continue if [ -f "${dir}/${_fname}" ]; then echo "${dir}/${_fname}" return 0 fi done } _geo_remove_file() { config_key="$1" filename="$2" for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -f "$cfg" ] || continue if command_exists jq; then fpath=$(jq -r ".system.geo.${config_key} // empty" "$cfg" 2>/dev/null) || true if [ -n "$fpath" ] && [ -f "$fpath" ]; then log_info "Found ${filename}: ${fpath}" if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove ${filename}?" "y"; then rm -f "$fpath" && log_info "Removed: $fpath" else log_info "Keeping ${filename}" fi return 0 fi fi done for dir in /etc/b4 /opt/etc/b4; do if [ -f "${dir}/${filename}" ]; then log_info "Found ${filename}: ${dir}/${filename}" if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove ${filename}?" "y"; then rm -f "${dir}/${filename}" && log_info "Removed: ${dir}/${filename}" else log_info "Keeping ${filename}" fi return 0 fi done } feature_https_name() { echo "HTTPS web interface" } feature_https_description() { echo "Enable HTTPS for B4 web UI using detected TLS certificates" } feature_https_default_enabled() { _https_detect_certs >/dev/null 2>&1 && echo "yes" || echo "no" } feature_https_run() { cert_info=$(_https_detect_certs) || true if [ -z "$cert_info" ]; then log_info "No compatible TLS certificates found on this system" log_info "You can configure HTTPS later in B4 Web UI > Settings > Web Server" _https_remove_config return 0 fi cert_path=$(echo "$cert_info" | cut -d'|' -f1) key_path=$(echo "$cert_info" | cut -d'|' -f2) cert_source=$(echo "$cert_info" | cut -d'|' -f3) log_info "Found TLS certificate: ${cert_source}" log_detail "Certificate" "$cert_path" log_detail "Key" "$key_path" if ! confirm "Enable HTTPS with this certificate?"; then _https_remove_config return 0 fi if ! command_exists jq; then log_warn "jq not found — please update config manually:" log_info " Set system.web_server.tls_cert = $cert_path" log_info " Set system.web_server.tls_key = $key_path" return 0 fi if [ ! -f "$B4_CONFIG_FILE" ]; then ensure_dir "$(dirname "$B4_CONFIG_FILE")" "Config directory" || return 1 jq -n \ --arg cert "$cert_path" \ --arg key "$key_path" \ '{ system: { web_server: { tls_cert: $cert, tls_key: $key } } }' \ >"$B4_CONFIG_FILE" else tmp="${B4_CONFIG_FILE}.tmp" if jq --arg cert "$cert_path" --arg key "$key_path" \ '.system.web_server.tls_cert = $cert | .system.web_server.tls_key = $key' \ "$B4_CONFIG_FILE" >"$tmp" 2>/dev/null; then mv "$tmp" "$B4_CONFIG_FILE" else rm -f "$tmp" log_warn "Failed to update config" return 1 fi fi log_ok "HTTPS enabled" } _https_detect_certs() { _https_check_pair "/etc/uhttpd.crt" "/etc/uhttpd.key" "OpenWrt uhttpd" && return 0 _https_check_pair "/etc/cert.pem" "/etc/key.pem" "System default" && return 0 _https_check_pair "/etc/ssl/certs/server.crt" "/etc/ssl/private/server.key" "System SSL" && return 0 return 1 } _https_check_pair() { cert="$1" key="$2" label="$3" [ -f "$cert" ] && [ -f "$key" ] || return 1 grep -q "BEGIN" "$cert" 2>/dev/null && grep -q "BEGIN" "$key" 2>/dev/null || { log_warn "Skipping ${label} certificate — not in PEM format (possibly DER-encoded)" return 1 } echo "${cert}|${key}|${label}" return 0 } _https_remove_config() { if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$tls" ]; then tmp="${B4_CONFIG_FILE}.tmp" if jq 'del(.system.web_server.tls_cert, .system.web_server.tls_key)' \ "$B4_CONFIG_FILE" >"$tmp" 2>/dev/null; then mv "$tmp" "$B4_CONFIG_FILE" log_info "Removed previous HTTPS configuration" else rm -f "$tmp" fi fi fi } feature_https_remove() { return 0 } register_feature "https" REGISTERED_SERVICES="" register_service() { id="$1" REGISTERED_SERVICES="${REGISTERED_SERVICES} ${id}" } service_call() { func="$1" shift service_dispatch "$B4_SERVICE_TYPE" "$func" "$@" } service_dispatch() { sid="$1" func="$2" shift 2 fn="service_${sid}_${func}" if type "$fn" >/dev/null 2>&1; then "$fn" "$@" else log_warn "Service type '${sid}' does not implement '${func}'" return 1 fi } service_show_crash_log() { _errlog="" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then _errlog=$(jq -r '.system.logging.error_file // empty' "$B4_CONFIG_FILE" 2>/dev/null) fi [ -z "$_errlog" ] && _errlog="/var/log/b4/errors.log" if [ -s "$_errlog" ]; then log_info "Last log entries from $_errlog:" tail -5 "$_errlog" 2>/dev/null | while IFS= read -r _line; do log_info " $_line" done fi } service_entware_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" 2>/dev/null || true if [ -f "${B4_SERVICE_DIR}/rc.func" ]; then _service_entware_install_rcfunc else _service_entware_install_standalone fi chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_ok "Init script created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info " ${B4_SERVICE_DIR}/${B4_SERVICE_NAME} start" log_info " ${B4_SERVICE_DIR}/${B4_SERVICE_NAME} stop" } _service_entware_install_rcfunc() { cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null 2>&1 && PREARGS="nohup" DESC="\$PROCS" PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin kernel_mod_load() { KERNEL=\$(uname -r) for mod in nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq; do modprobe "\$mod" >/dev/null 2>&1 && continue mod_path=\$(find /lib/modules/\$KERNEL -name "\${mod}.ko*" 2>/dev/null | head -1) [ -n "\$mod_path" ] && insmod "\$mod_path" >/dev/null 2>&1 || true done } [ "\$1" = "start" ] || [ "\$1" = "restart" ] && kernel_mod_load . /opt/etc/init.d/rc.func EOF } _service_entware_install_standalone() { cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null 2>&1 && continue mod_path=\$(find /lib/modules/\$KERNEL -name "\${mod}.ko*" 2>/dev/null | head -1) [ -n "\$mod_path" ] && insmod "\$mod_path" >/dev/null 2>&1 || true done } start() { echo "Starting b4..." [ -f "\$PIDFILE" ] && kill -0 \$(cat "\$PIDFILE") 2>/dev/null && echo "Already running" && return 1 kernel_mod_load if which nohup >/dev/null 2>&1; then nohup \$PROG --config \$CONFIG >/dev/null 2>&1 & elif which setsid >/dev/null 2>&1; then setsid \$PROG --config \$CONFIG >/dev/null 2>&1 & else (\$PROG --config \$CONFIG >/dev/null 2>&1 &) fi echo \$! >"\$PIDFILE" sleep 1 if kill -0 \$(cat "\$PIDFILE") 2>/dev/null; then echo "b4 started (PID: \$(cat \$PIDFILE))" else echo "b4 failed to start, check /var/log/b4/errors.log" rm -f "\$PIDFILE" return 1 fi } stop() { echo "Stopping b4..." [ -f "\$PIDFILE" ] && kill \$(cat "\$PIDFILE") 2>/dev/null rm -f "\$PIDFILE" killall b4 2>/dev/null || true echo "b4 stopped" } case "\$1" in start) start ;; stop) stop ;; restart) stop; sleep 1; start ;; *) echo "Usage: \$0 {start|stop|restart}"; exit 1 ;; esac EOF } service_entware_remove() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info "Removed service: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" fi } service_entware_start() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" start 2>/dev/null || { log_warn "Could not start service" return 1 } sleep 2 if pidof b4 >/dev/null 2>&1 || pgrep -x b4 >/dev/null 2>&1; then log_ok "Service started" return 0 fi log_err "Service crashed immediately after start" service_show_crash_log return 1 fi log_warn "Could not start service" return 1 } service_entware_stop() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true fi } register_service "entware" service_none_install() { log_warn "No init system configured — b4 will not start automatically" log_info "Start manually: ${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}" } service_none_remove() { return 0 } service_none_start() { log_warn "No service configured — start b4 manually" return 1 } service_none_stop() { return 0 } register_service "none" service_openrc_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null 2>&1 || true done } EOF chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" rc-update add "${B4_SERVICE_NAME}" default 2>/dev/null || true log_ok "OpenRC service created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info " rc-service ${B4_SERVICE_NAME} start" log_info " rc-service ${B4_SERVICE_NAME} stop" } service_openrc_remove() { rc-update del "${B4_SERVICE_NAME}" default 2>/dev/null || true rc-service "${B4_SERVICE_NAME}" stop 2>/dev/null || true if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info "Removed OpenRC service: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" fi } service_openrc_start() { rc-service "${B4_SERVICE_NAME}" start 2>/dev/null || { log_warn "Could not start service"; return 1; } sleep 2 if pidof b4 >/dev/null 2>&1 || pgrep -x b4 >/dev/null 2>&1; then log_ok "Service started" return 0 fi log_err "Service crashed immediately after start" service_show_crash_log return 1 } service_openrc_stop() { rc-service "${B4_SERVICE_NAME}" stop 2>/dev/null || true } register_service "openrc" service_procd_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null 2>&1 && continue mod_path=\$(find /lib/modules/\$KERNEL -name "\${mod}.ko*" 2>/dev/null | head -1) [ -n "\$mod_path" ] && insmod "\$mod_path" >/dev/null 2>&1 || true done } start_service() { kernel_mod_load procd_open_instance procd_set_param command \$PROG --config \$CONFIG procd_set_param respawn \${respawn_threshold:-3600} \${respawn_timeout:-5} \${respawn_retry:-5} procd_set_param stdout 0 procd_set_param stderr 0 procd_set_param pidfile /var/run/b4.pid procd_close_instance } stop_service() { return 0 } service_triggers() { procd_add_reload_trigger "b4" } EOF chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_ok "Procd init script created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" enable 2>/dev/null || true log_info "Service enabled for boot" } service_procd_remove() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" disable 2>/dev/null || true rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info "Removed procd service: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" fi } service_procd_start() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" restart 2>/dev/null || { log_warn "Could not start service"; return 1; } sleep 2 if pidof b4 >/dev/null 2>&1 || pgrep -x b4 >/dev/null 2>&1; then log_ok "Service started" return 0 fi log_err "Service crashed immediately after start" service_show_crash_log return 1 fi log_warn "Could not start service" return 1 } service_procd_stop() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true fi } register_service "procd" service_systemd_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null || true systemctl disable "${B4_SERVICE_NAME}" 2>/dev/null || true rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" systemctl daemon-reload log_info "Removed systemd service: ${B4_SERVICE_NAME}" } service_systemd_start() { if systemctl restart "${B4_SERVICE_NAME}" 2>/dev/null; then _elapsed=0 while [ "$_elapsed" -lt 10 ]; do sleep 1 _elapsed=$((_elapsed + 1)) if systemctl is-active --quiet "${B4_SERVICE_NAME}" 2>/dev/null; then log_ok "Service started" return 0 fi if systemctl is-failed --quiet "${B4_SERVICE_NAME}" 2>/dev/null; then break fi done log_err "Service failed to start" log_info "Check logs with: journalctl -u ${B4_SERVICE_NAME} --no-pager -n 10" journalctl -u "${B4_SERVICE_NAME}" --no-pager -n 5 2>/dev/null | while IFS= read -r _line; do log_info " $_line" done return 1 fi log_warn "Could not start service" return 1 } service_systemd_stop() { systemctl stop "${B4_SERVICE_NAME}" 2>/dev/null || true } register_service "systemd" service_sysv_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" </dev/null 2>&1 && continue mod_path=\$(find /lib/modules/\$KERNEL -name "\${mod}.ko*" 2>/dev/null | head -1) [ -n "\$mod_path" ] && insmod "\$mod_path" >/dev/null 2>&1 || true done } start() { echo "Starting b4..." [ -f "\$PIDFILE" ] && kill -0 \$(cat "\$PIDFILE") 2>/dev/null && echo "Already running" && return 1 kernel_mod_load if which nohup >/dev/null 2>&1; then nohup \$PROG --config \$CONFIG >/dev/null 2>&1 & elif which setsid >/dev/null 2>&1; then setsid \$PROG --config \$CONFIG >/dev/null 2>&1 & else (\$PROG --config \$CONFIG >/dev/null 2>&1 &) fi echo \$! >"\$PIDFILE" sleep 1 if kill -0 \$(cat "\$PIDFILE") 2>/dev/null; then echo "b4 started (PID: \$(cat \$PIDFILE))" else echo "b4 failed to start, check /var/log/b4/errors.log" rm -f "\$PIDFILE" return 1 fi } stop() { echo "Stopping b4..." [ -f "\$PIDFILE" ] && kill \$(cat "\$PIDFILE") 2>/dev/null rm -f "\$PIDFILE" echo "b4 stopped" } case "\$1" in start) start ;; stop) stop ;; restart) stop; sleep 1; start ;; *) echo "Usage: \$0 {start|stop|restart}"; exit 1 ;; esac EOF chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_ok "Init script created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info " ${B4_SERVICE_DIR}/${B4_SERVICE_NAME} start" log_info " ${B4_SERVICE_DIR}/${B4_SERVICE_NAME} stop" } service_sysv_remove() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_info "Removed init script: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" fi } service_sysv_start() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" start 2>/dev/null || { log_warn "Could not start service"; return 1; } sleep 2 if pidof b4 >/dev/null 2>&1 || pgrep -x b4 >/dev/null 2>&1; then log_ok "Service started" return 0 fi log_err "Service crashed immediately after start" service_show_crash_log return 1 fi log_warn "Could not start service" return 1 } service_sysv_stop() { if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true fi } register_service "sysv" action_install() { version="$1" force_arch="$2" check_root if [ "$QUIET_MODE" -eq 1 ]; then WIZARD_MODE="auto" _user_bin_dir="$B4_BIN_DIR" _user_data_dir="$B4_DATA_DIR" platform_auto_detect platform_call info [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" B4_ARCH="${force_arch:-$(detect_architecture)}" detect_pkg_manager for f in $REGISTERED_FEATURES; do fdefault=$(feature_dispatch "$f" default_enabled) [ "$fdefault" = "yes" ] && ENABLED_FEATURES="${ENABLED_FEATURES} ${f}" done else wizard_start case "$WIZARD_MODE" in auto) wizard_auto_detect ;; manual) wizard_manual_configure ;; esac [ -n "$force_arch" ] && B4_ARCH="$force_arch" wizard_select_features fi echo "" log_header "Installing B4" log_info "Checking dependencies..." platform_call check_deps if [ -z "$version" ]; then log_info "Fetching latest version..." version=$(get_latest_version) fi log_ok "Version: ${version}" log_ok "Architecture: ${B4_ARCH}" ensure_dir "$B4_BIN_DIR" "Binary directory" || exit 1 ensure_dir "$B4_DATA_DIR" "Data directory" || exit 1 setup_temp file_name="${BINARY_NAME}-linux-${B4_ARCH}.tar.gz" download_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version}/${file_name}" archive_path="${TEMP_DIR}/${file_name}" log_info "Downloading b4..." if ! fetch_file "$download_url" "$archive_path"; then log_err "Download failed for architecture: ${B4_ARCH}" exit 1 fi sha_url="${download_url}.sha256" _cs_ret=0 verify_checksum "$archive_path" "$sha_url" || _cs_ret=$? if [ "$_cs_ret" -eq 2 ]; then log_warn "Checksum mismatch — download may be corrupted" if ! confirm "Continue anyway?"; then exit 1 fi fi log_info "Extracting..." cd "$TEMP_DIR" tar -xzf "$archive_path" || { log_err "Failed to extract archive"; exit 1; } rm -f "$archive_path" if [ ! -f "${BINARY_NAME}" ]; then log_err "Binary not found in archive" exit 1 fi stop_b4 rm -f /var/log/b4.log /opt/var/log/b4.log /tmp/log/b4.log 2>/dev/null || true if [ -f "${B4_BIN_DIR}/${BINARY_NAME}" ]; then ts=$(date '+%Y%m%d_%H%M%S') mv "${B4_BIN_DIR}/${BINARY_NAME}" "${B4_BIN_DIR}/${BINARY_NAME}.backup.${ts}" log_info "Existing binary backed up" fi mv "${BINARY_NAME}" "${B4_BIN_DIR}/" 2>/dev/null || cp "${BINARY_NAME}" "${B4_BIN_DIR}/" || { log_err "Failed to install binary to ${B4_BIN_DIR}" exit 1 } chmod +x "${B4_BIN_DIR}/${BINARY_NAME}" _ver_exit=0 sh -c "\"${B4_BIN_DIR}/${BINARY_NAME}\" --version" >/dev/null 2>&1 || _ver_exit=$? if [ "$_ver_exit" -eq 0 ]; then installed_ver=$("${B4_BIN_DIR}/${BINARY_NAME}" --version 2>&1 | head -1) log_ok "Binary installed: ${installed_ver}" rm -f "${B4_BIN_DIR}/${BINARY_NAME}".backup.* 2>/dev/null || true elif [ "$_ver_exit" -gt 128 ] && echo "$B4_ARCH" | grep -q "^mips" && ! echo "$B4_ARCH" | grep -q "softfloat"; then _sf_arch="${B4_ARCH}_softfloat" log_warn "Binary crashed (exit code $_ver_exit) — likely hardfloat/softfloat mismatch" log_info "Retrying with ${_sf_arch}..." _sf_file="${BINARY_NAME}-linux-${_sf_arch}.tar.gz" _sf_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version}/${_sf_file}" _sf_archive="${TEMP_DIR}/${_sf_file}" if fetch_file "$_sf_url" "$_sf_archive"; then cd "$TEMP_DIR" rm -f "${BINARY_NAME}" 2>/dev/null tar -xzf "$_sf_archive" 2>/dev/null && rm -f "$_sf_archive" if [ -f "${BINARY_NAME}" ]; then mv "${BINARY_NAME}" "${B4_BIN_DIR}/" 2>/dev/null || cp "${BINARY_NAME}" "${B4_BIN_DIR}/" chmod +x "${B4_BIN_DIR}/${BINARY_NAME}" if "${B4_BIN_DIR}/${BINARY_NAME}" --version >/dev/null 2>&1; then installed_ver=$("${B4_BIN_DIR}/${BINARY_NAME}" --version 2>&1 | head -1) log_ok "Softfloat binary works: ${installed_ver}" log_info "Tip: use --arch=${_sf_arch} for future installs" B4_ARCH="$_sf_arch" rm -f "${B4_BIN_DIR}/${BINARY_NAME}".backup.* 2>/dev/null || true else log_err "Softfloat binary also failed — manual troubleshooting needed" log_info "Run with --sysinfo for diagnostics, or try --arch= manually" fi else log_err "Failed to extract softfloat binary" fi else log_err "Could not download softfloat variant" log_info "Try reinstalling with: --arch=${_sf_arch}" fi else log_warn "Binary installed but version check failed (exit code: $_ver_exit)" fi log_info "Setting up service..." service_call install if [ -n "$ENABLED_FEATURES" ]; then features_run fi _install_summary "$version" } _install_summary() { version="$1" echo "" log_header "Installation Complete" log_sep log_detail "Version" "$version" log_detail "Binary" "${B4_BIN_DIR}/${BINARY_NAME}" log_detail "Config" "${B4_CONFIG_FILE}" log_detail "Service" "${B4_SERVICE_TYPE}" log_sep if ! echo "$PATH" | grep -q "$B4_BIN_DIR"; then log_warn "$B4_BIN_DIR is not in PATH" log_info "Consider: ln -s ${B4_BIN_DIR}/${BINARY_NAME} /usr/bin/${BINARY_NAME}" fi _show_web_info echo "" log_info "To see all options: ${B4_BIN_DIR}/${BINARY_NAME} --help" echo "" if [ "$QUIET_MODE" -eq 0 ] && [ "$B4_SERVICE_TYPE" != "none" ]; then if is_b4_running; then if confirm "B4 is already running. Restart now?"; then service_call stop || true sleep 1 service_call start || true fi else if confirm "Start B4 service now?"; then service_call start || true fi fi fi echo "" printf "${GREEN}${BOLD} B4 installation finished!${NC}\n" echo "" } _show_web_info() { web_port="7000" protocol="http" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then web_port=$(jq -r '.system.web_server.port // 7000' "$B4_CONFIG_FILE" 2>/dev/null) || true tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) || true [ -n "$tls" ] && protocol="https" fi lan_ip="" if command_exists ip; then lan_ip=$(ip -4 addr show br0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1) [ -z "$lan_ip" ] && lan_ip=$(ip -4 addr 2>/dev/null | grep 'inet 192.168' | head -1 | awk '{print $2}' | cut -d'/' -f1) fi if [ -n "$lan_ip" ]; then echo "" log_info "Web interface: ${protocol}://${lan_ip}:${web_port}" fi } action_remove() { check_root log_header "Removing B4" if [ -z "$B4_PLATFORM" ]; then platform_auto_detect || true if [ -n "$B4_PLATFORM" ]; then platform_call info fi fi _remove_find_config stop_b4 if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then log_info "Removing service..." service_call remove 2>/dev/null || true else for svc in \ /etc/systemd/system/b4.service \ /etc/init.d/b4 \ /opt/etc/init.d/S99b4; do if [ -f "$svc" ]; then rm -f "$svc" log_info "Removed: $svc" fi done command_exists systemctl && systemctl daemon-reload 2>/dev/null || true fi features_remove for dir in /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /tmp/b4; do if [ -f "${dir}/${BINARY_NAME}" ]; then rm -f "${dir}/${BINARY_NAME}" rm -f "${dir}/${BINARY_NAME}".backup.* 2>/dev/null || true log_info "Removed binary from: ${dir}" fi done _remove_config_dirs rm -f /var/run/b4.pid 2>/dev/null || true rm -f /var/log/b4.log /opt/var/log/b4.log /tmp/log/b4.log 2>/dev/null || true rm -rf /var/log/b4 2>/dev/null || true echo "" log_ok "B4 has been removed" echo "" } _remove_find_config() { if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ]; then log_info "Using config: $B4_CONFIG_FILE" return 0 fi for cfg in /etc/b4/b4.json /opt/etc/b4/b4.json /etc/storage/b4/b4.json; do if [ -f "$cfg" ]; then B4_CONFIG_FILE="$cfg" B4_DATA_DIR=$(dirname "$cfg") log_info "Found config: $B4_CONFIG_FILE" return 0 fi done log_warn "No config file found" } _remove_config_dirs() { checked="" for cfg_dir in "$B4_DATA_DIR" /etc/b4 /opt/etc/b4 /etc/storage/b4; do [ -z "$cfg_dir" ] && continue [ -d "$cfg_dir" ] || continue case " $checked " in *" $cfg_dir "*) continue ;; esac checked="${checked} ${cfg_dir}" remaining=$(ls -1 "$cfg_dir" 2>/dev/null) if [ -n "$remaining" ]; then log_info "Remaining files in ${cfg_dir}:" echo "$remaining" | while read -r f; do printf " %s\n" "$f" >&2 done fi if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove config directory ${cfg_dir}?" "n"; then rm -rf "$cfg_dir" log_info "Removed: ${cfg_dir}" else log_info "Keeping: ${cfg_dir}" fi done } action_update() { target_ver="$1" force_arch="$2" check_root log_header "Updating B4" if [ -z "$B4_PLATFORM" ]; then platform_auto_detect || true if [ -n "$B4_PLATFORM" ]; then platform_call info fi fi existing_bin="" for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin; do [ -z "$dir" ] && continue if [ -f "${dir}/${BINARY_NAME}" ]; then existing_bin="${dir}/${BINARY_NAME}" B4_BIN_DIR="$dir" break fi done if [ -z "$existing_bin" ]; then log_err "B4 is not installed. Use install mode instead." exit 1 fi _ver_full=$("$existing_bin" --version 2>&1) || _ver_full="" current_ver=$(echo "$_ver_full" | grep -i "version" | head -1) [ -z "$current_ver" ] && current_ver="unknown" log_info "Current: ${current_ver}" if [ -n "$force_arch" ]; then B4_ARCH="$force_arch" else B4_ARCH=$(detect_architecture) fi if [ -n "$target_ver" ]; then latest_ver="$target_ver" log_info "Target: ${latest_ver}" else log_info "Checking for updates..." latest_ver=$(get_latest_version) log_info "Latest: ${latest_ver}" fi if [ "$current_ver" = "$latest_ver" ] || echo "$current_ver" | grep -Fq "$latest_ver"; then log_ok "Already up to date" return 0 fi if [ "$QUIET_MODE" -eq 0 ]; then if ! confirm "Update to ${latest_ver}?"; then log_info "Update cancelled" return 0 fi fi setup_temp file_name="${BINARY_NAME}-linux-${B4_ARCH}.tar.gz" download_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${latest_ver}/${file_name}" archive_path="${TEMP_DIR}/${file_name}" log_info "Downloading ${latest_ver}..." fetch_file "$download_url" "$archive_path" || { log_err "Download failed"; exit 1; } sha_url="${download_url}.sha256" _cs_ret=0 verify_checksum "$archive_path" "$sha_url" || _cs_ret=$? if [ "$_cs_ret" -eq 2 ]; then log_warn "Checksum mismatch — download may be corrupted" if ! confirm "Continue anyway?"; then exit 1 fi fi cd "$TEMP_DIR" tar -xzf "$archive_path" || { log_err "Extraction failed"; exit 1; } if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then log_info "Stopping service..." service_call stop 2>/dev/null || true sleep 1 else stop_b4 fi ts=$(date '+%Y%m%d_%H%M%S') cp "$existing_bin" "${existing_bin}.backup.${ts}" rm -f "$existing_bin" mv "${TEMP_DIR}/${BINARY_NAME}" "$existing_bin" 2>/dev/null || \ cp "${TEMP_DIR}/${BINARY_NAME}" "$existing_bin" || \ { log_err "Failed to replace binary"; exit 1; } chmod +x "$existing_bin" if "$existing_bin" --version >/dev/null 2>&1; then new_ver=$("$existing_bin" --version 2>&1 | head -1) log_ok "Updated to: ${new_ver}" rm -f "${existing_bin}".backup.* 2>/dev/null || true else log_warn "Updated binary failed version check" fi if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then log_info "Restarting service..." service_call start 2>/dev/null || true fi echo "" log_ok "Update complete" echo "" } action_sysinfo() { log_header "B4 System Diagnostics" log_sep log_detail "Hostname" "$(hostname 2>/dev/null || cat /proc/sys/kernel/hostname 2>/dev/null || echo 'unknown')" log_detail "Kernel" "$(uname -r)" log_detail "Architecture (raw)" "$(uname -m)" log_detail "Architecture (b4)" "$(detect_architecture 2>/dev/null || echo 'unknown')" [ -f /etc/os-release ] && log_detail "Distribution" "$(. /etc/os-release && echo "$PRETTY_NAME")" [ -f /etc/openwrt_release ] && log_detail "OpenWrt" "$(. /etc/openwrt_release && echo "$DISTRIB_DESCRIPTION")" is_lxc_container && log_detail "Container" "${YELLOW}LXC${NC}" cpu_cores="" if [ -f /proc/cpuinfo ]; then cpu_cores=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null) fi [ -n "$cpu_cores" ] && log_detail "CPU cores" "$cpu_cores" _raw_arch=$(uname -m) case "$_raw_arch" in mips*) if [ -f /proc/cpuinfo ]; then _cpu_model=$(grep -i "cpu model" /proc/cpuinfo 2>/dev/null | head -1 | sed 's/.*: *//') [ -n "$_cpu_model" ] && log_detail "CPU model" "$_cpu_model" if grep -qi "nofpu\|no fpu" /proc/cpuinfo 2>/dev/null; then log_detail "FPU" "${YELLOW}not available (softfloat needed)${NC}" elif grep -qi "FPU" /proc/cpuinfo 2>/dev/null; then log_detail "FPU" "${GREEN}available${NC}" fi fi if [ -f /etc/openwrt_release ]; then _owrt_arch=$(sed -n "s/^DISTRIB_ARCH=['\"\`]*\([^'\"\`]*\).*/\1/p" /etc/openwrt_release 2>/dev/null) [ -n "$_owrt_arch" ] && log_detail "OpenWrt arch" "$_owrt_arch" fi if command_exists opkg; then _opkg_arch=$(opkg print-architecture 2>/dev/null | grep -i "mips" | head -1 | awk '{print $2}') [ -n "$_opkg_arch" ] && log_detail "opkg arch" "$_opkg_arch" fi _elf_bin="" for _eb in /bin/sh /bin/busybox /bin/ls; do [ -f "$_eb" ] && _elf_bin="$_eb" && break done if [ -n "$_elf_bin" ]; then _ei_data=$(dd if="$_elf_bin" bs=1 skip=5 count=1 2>/dev/null | _byte_to_dec) case "$_ei_data" in 1) log_detail "ELF endian" "little-endian" ;; 2) log_detail "ELF endian" "big-endian" ;; *) ;; esac fi if is_softfloat; then log_detail "Float ABI" "${YELLOW}soft-float${NC}" else log_detail "Float ABI" "hard-float" fi ;; *) ;; esac if [ -f /proc/meminfo ]; then mem_total=$(awk '/^MemTotal:/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null) mem_avail=$(awk '/^MemAvailable:/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null) [ -z "$mem_avail" ] && mem_avail=$(awk '/^MemFree:/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null) if [ -n "$mem_total" ]; then log_detail "Memory" "${mem_total} MB (Available: ${mem_avail:-?} MB)" fi fi _saved_platform="$B4_PLATFORM" _saved_bin_dir="$B4_BIN_DIR" _saved_data_dir="$B4_DATA_DIR" _saved_config_file="$B4_CONFIG_FILE" _saved_service_type="$B4_SERVICE_TYPE" _saved_service_dir="$B4_SERVICE_DIR" _saved_service_name="$B4_SERVICE_NAME" _saved_pkg_manager="$B4_PKG_MANAGER" platform_auto_detect 2>/dev/null || true if [ -n "$B4_PLATFORM" ]; then pname=$(platform_dispatch "$B4_PLATFORM" name 2>/dev/null) log_detail "Detected platform" "${pname} (${B4_PLATFORM})" platform_call info 2>/dev/null || true log_detail "Binary dir" "${B4_BIN_DIR}" log_detail "Data dir" "${B4_DATA_DIR}" log_detail "Service type" "${B4_SERVICE_TYPE}" fi log_sep found_bin="" _bin_crashed="" for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /tmp/b4; do [ -z "$dir" ] && continue if [ -f "${dir}/${BINARY_NAME}" ] && [ -x "${dir}/${BINARY_NAME}" ]; then _ver_exit=0 _ver_full=$(sh -c "\"${dir}/${BINARY_NAME}\" --version 2>/dev/null" 2>/dev/null) || _ver_exit=$? if [ "$_ver_exit" -gt 128 ] 2>/dev/null; then _bin_crashed="${dir}/${BINARY_NAME}" continue fi if echo "$_ver_full" | grep -qi "b4 version\|bypass\|dpi"; then found_bin="${dir}/${BINARY_NAME}" _ver_out=$(echo "$_ver_full" | grep -i "version" | head -1) break fi fi done if [ -n "$found_bin" ]; then log_detail "Binary" "$found_bin" log_detail "Version" "$_ver_out" if [ -n "$B4_BIN_DIR" ] && [ -n "$_bin_crashed" ]; then log_detail "WARNING" "${RED}${_bin_crashed} crashes (segfault) — wrong architecture?${NC}" fi elif [ -n "$_bin_crashed" ]; then log_detail "Binary" "${_bin_crashed}" _arch_hint="" if command_exists file; then _arch_hint=$(file "$_bin_crashed" 2>/dev/null | sed 's/.*: //') elif command_exists readelf; then _arch_hint=$(readelf -h "$_bin_crashed" 2>/dev/null | awk '/Machine:/ {$1=""; print substr($0,2)}') fi if [ -n "$_arch_hint" ]; then log_detail "Status" "${RED}crashes on startup (segfault)${NC}" log_detail "Binary type" "$_arch_hint" log_detail "System arch" "$(uname -m)" else log_detail "Status" "${RED}crashes on startup (segfault) — wrong architecture?${NC}" fi else log_detail "Binary" "${YELLOW}not found${NC}" fi cfg_file="" for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -z "$cfg" ] && continue [ -f "$cfg" ] && cfg_file="$cfg" && break done [ -n "$cfg_file" ] && log_detail "Config" "$cfg_file" if is_b4_running; then log_detail "Service status" "${GREEN}running${NC}" b4_pid="" for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ] && kill -0 "$(cat "$pf")" 2>/dev/null; then b4_pid=$(cat "$pf") break fi done [ -z "$b4_pid" ] && b4_pid=$(pgrep -x "$BINARY_NAME" 2>/dev/null | head -1) [ -z "$b4_pid" ] && b4_pid=$(pgrep -f "${BINARY_NAME}" 2>/dev/null | head -1) if [ -n "$b4_pid" ]; then if [ -f "/proc/${b4_pid}/status" ]; then mem_kb=$(awk '/^VmRSS:/ {print $2}' "/proc/${b4_pid}/status" 2>/dev/null) if [ -n "$mem_kb" ]; then mem_mb=$(awk "BEGIN {printf \"%.1f\", $mem_kb/1024}") log_detail "Memory usage" "${mem_mb} MB (PID: ${b4_pid})" fi fi if [ -f "/proc/${b4_pid}/stat" ]; then proc_start=$(awk '{print $22}' "/proc/${b4_pid}/stat" 2>/dev/null) clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100) sys_uptime=$(awk '{print int($1)}' /proc/uptime 2>/dev/null) if [ -n "$proc_start" ] && [ -n "$sys_uptime" ] && [ "$clk_tck" -gt 0 ] 2>/dev/null; then proc_secs=$((proc_start / clk_tck)) running_secs=$((sys_uptime - proc_secs)) if [ "$running_secs" -ge 3600 ] 2>/dev/null; then hours=$((running_secs / 3600)) mins=$(((running_secs % 3600) / 60)) log_detail "Uptime" "${hours}h ${mins}m" elif [ "$running_secs" -ge 60 ] 2>/dev/null; then mins=$((running_secs / 60)) log_detail "Uptime" "${mins}m" elif [ "$running_secs" -ge 0 ] 2>/dev/null; then log_detail "Uptime" "${running_secs}s" fi fi fi fi else log_detail "Service status" "${YELLOW}not running${NC}" fi if [ -n "$cfg_file" ] && command_exists jq; then queue_num=$(jq -r '.system.queue_num // empty' "$cfg_file" 2>/dev/null) || true workers=$(jq -r '.system.workers // empty' "$cfg_file" 2>/dev/null) || true geosite=$(jq -r '.system.geo.sitedat_path // empty' "$cfg_file" 2>/dev/null) || true geoip=$(jq -r '.system.geo.ipdat_path // empty' "$cfg_file" 2>/dev/null) || true [ -n "$queue_num" ] && [ "$queue_num" != "null" ] && log_detail "Queue number" "$queue_num" [ -n "$workers" ] && [ "$workers" != "null" ] && log_detail "Worker threads" "$workers" if [ -n "$geosite" ] && [ "$geosite" != "null" ] && [ -f "$geosite" ]; then size=$(ls -lh "$geosite" 2>/dev/null | awk '{print $5}') log_detail "geosite.dat" "${geosite} (${size})" fi if [ -n "$geoip" ] && [ "$geoip" != "null" ] && [ -f "$geoip" ]; then size=$(ls -lh "$geoip" 2>/dev/null | awk '{print $5}') log_detail "geoip.dat" "${geoip} (${size})" fi fi log_sep echo "" log_info "Kernel modules:" for mod in xt_NFQUEUE nfnetlink_queue xt_connbytes xt_multiport nf_conntrack; do if lsmod 2>/dev/null | grep -q "^${mod}"; then printf " ${GREEN}loaded${NC} %s\n" "$mod" >&2 elif _kmod_builtin "$mod"; then printf " ${GREEN}built-in${NC} %s\n" "$mod" >&2 else printf " ${YELLOW}missing${NC} %s ${DIM}(may be built-in)${NC}\n" "$mod" >&2 fi done _nfq_ipt="" if command_exists iptables; then _nfq_ipt="iptables" elif command_exists iptables-legacy; then _nfq_ipt="iptables-legacy" fi if [ -n "$_nfq_ipt" ]; then if $_nfq_ipt -t mangle -C B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null; then $_nfq_ipt -t mangle -D B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null || true fi if $_nfq_ipt -t mangle -N B4_TEST 2>/dev/null; then if $_nfq_ipt -t mangle -A B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null; then printf " ${GREEN} OK${NC} %s\n" "NFQUEUE works (functional test passed)" >&2 $_nfq_ipt -t mangle -D B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null || true else printf " ${RED} FAIL${NC} %s\n" "NFQUEUE not functional" >&2 fi $_nfq_ipt -t mangle -X B4_TEST 2>/dev/null || true fi fi echo "" log_info "Required tools:" _fw_found=0 if command_exists nft; then if nft add table inet _b4_test 2>/dev/null; then nft delete table inet _b4_test 2>/dev/null || true printf " ${GREEN}found${NC} nft ${DIM}(nftables — functional)${NC}\n" >&2 _fw_found=1 else printf " ${YELLOW}found${NC} nft ${DIM}(nftables — ${RED}not functional${NC}${DIM})${NC}\n" >&2 fi fi if command_exists iptables; then _ipt_ver=$(iptables --version 2>/dev/null) if echo "$_ipt_ver" | grep -q "nf_tables"; then printf " ${YELLOW}found${NC} iptables ${DIM}(nft-variant)${NC}\n" >&2 else printf " ${GREEN}found${NC} iptables\n" >&2 fi _fw_found=1 fi if command_exists iptables-legacy; then printf " ${GREEN}found${NC} iptables-legacy\n" >&2 _fw_found=1 fi if [ "$_fw_found" = "0" ]; then printf " ${RED}missing${NC} iptables or nft ${DIM}(firewall required)${NC}\n" >&2 fi for tool in tar; do if command_exists "$tool"; then printf " ${GREEN}found${NC} %s\n" "$tool" >&2 else printf " ${RED}missing${NC} %s ${DIM}(required for install)${NC}\n" >&2 fi done if command_exists curl; then if curl -sI --max-time 5 "https://github.com" >/dev/null 2>&1; then printf " ${GREEN}found${NC} curl ${GREEN}(HTTPS OK)${NC}\n" >&2 else printf " ${YELLOW}found${NC} curl ${RED}(HTTPS failed)${NC}\n" >&2 fi elif command_exists wget; then if wget --spider -q --timeout=5 "https://github.com" 2>/dev/null; then printf " ${GREEN}found${NC} wget ${GREEN}(HTTPS OK)${NC}\n" >&2 elif wget --spider -q --timeout=5 --no-check-certificate "https://github.com" 2>/dev/null; then printf " ${YELLOW}found${NC} wget ${YELLOW}(HTTPS only with --no-check-certificate)${NC}\n" >&2 else printf " ${YELLOW}found${NC} wget ${RED}(HTTPS failed)${NC}\n" >&2 fi else printf " ${RED}missing${NC} curl or wget ${DIM}(required for download)${NC}\n" >&2 fi echo "" log_info "Optional tools:" for tool in jq sha256sum nohup modprobe ipset; do if command_exists "$tool"; then printf " ${GREEN}found${NC} %s\n" "$tool" >&2 else case "$tool" in jq) printf " ${YELLOW}missing${NC} %s ${DIM}(config editing won't work)${NC}\n" "$tool" >&2 ;; sha256sum) printf " ${YELLOW}missing${NC} %s ${DIM}(checksum verify skipped)${NC}\n" "$tool" >&2 ;; nohup) printf " ${YELLOW}missing${NC} %s ${DIM}(service may stop on session close)${NC}\n" "$tool" >&2 ;; modprobe) printf " ${YELLOW}missing${NC} %s ${DIM}(kernel modules loaded via insmod)${NC}\n" "$tool" >&2 ;; ipset) printf " ${YELLOW}missing${NC} %s ${DIM}(needed for routing on iptables systems)${NC}\n" "$tool" >&2 ;; *) ;; esac fi done if command_exists curl && command_exists wget; then printf " ${GREEN}found${NC} wget ${DIM}(fallback downloader)${NC}\n" >&2 elif command_exists wget && ! command_exists curl; then printf " ${YELLOW}missing${NC} curl ${DIM}(wget used as primary)${NC}\n" >&2 elif command_exists curl && ! command_exists wget; then printf " ${DIM} ---${NC} wget ${DIM}(not needed, curl available)${NC}\n" >&2 fi echo "" detect_pkg_manager log_detail "Package manager" "${B4_PKG_MANAGER:-none}" echo "" log_info "Storage:" _sysinfo_shown_devs="" for dir in / /opt /tmp /jffs /mnt/sda1 /etc/storage; do if [ -d "$dir" ]; then _sysinfo_show_storage "$dir" fi done for dir in /mnt/*; do [ -d "$dir" ] || continue case "$dir" in /mnt/sda1) continue ;; *) ;; esac _sysinfo_show_storage "$dir" done echo "" log_sep B4_PLATFORM="$_saved_platform" B4_BIN_DIR="$_saved_bin_dir" B4_DATA_DIR="$_saved_data_dir" B4_CONFIG_FILE="$_saved_config_file" B4_SERVICE_TYPE="$_saved_service_type" B4_SERVICE_DIR="$_saved_service_dir" B4_SERVICE_NAME="$_saved_service_name" B4_PKG_MANAGER="$_saved_pkg_manager" } _sysinfo_show_storage() { _dir="$1" _dev=$(df "$_dir" 2>/dev/null | tail -1 | awk '{print $1}') case "$_sysinfo_shown_devs" in *"|${_dev}|"*) return 0 ;; # already shown esac _sysinfo_shown_devs="${_sysinfo_shown_devs}|${_dev}|" avail=$(df -h "$_dir" 2>/dev/null | tail -1 | awk '{print $4}') writable="rw" [ ! -w "$_dir" ] && writable="ro" printf " %-20s %s available (%s)\n" "$_dir" "${avail:-?}" "$writable" >&2 } main() { ACTION="install" VERSION="" FORCE_ARCH="" for arg in "$@"; do case "$arg" in --remove | --uninstall | -r) ACTION="remove" ;; --update | -u) ACTION="update" ;; --sysinfo | --info | -i) ACTION="sysinfo" ;; --quiet | -q) QUIET_MODE=1 ;; --arch=*) FORCE_ARCH="${arg#*=}" ;; --platform=*) B4_PLATFORM="${arg#*=}" ;; --bin-dir=*) B4_BIN_DIR="${arg#*=}" ;; --data-dir=*) B4_DATA_DIR="${arg#*=}" ;; --help | -h) _show_help exit 0 ;; v* | V*) VERSION="$arg" ;; *) ;; esac done if [ "$QUIET_MODE" -ne 1 ] 2>/dev/null && [ ! -t 0 ] && [ -e /dev/tty ]; then exec