mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-04-28 03:20:35 +00:00
3360 lines
105 KiB
Bash
Executable file
3360 lines
105 KiB
Bash
Executable file
#!/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' </proc/1/environ 2>/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
|
|
}
|
|
|
|
_nft_functional() {
|
|
command_exists nft || return 1
|
|
nft list ruleset >/dev/null 2>&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/<CTID>.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
|
|
|
|
if ! _kmod_available "xt_connbytes" && ! _nft_functional; then
|
|
log_warn "xt_connbytes kernel module not available — b4 will fail to start on iptables"
|
|
log_info "Enable it via router web UI: System settings > Component options"
|
|
log_info " look for 'Netfilter kernel modules' / 'xtables-addons' / 'Connection tracking extensions'"
|
|
log_info "Or try (Entware, kernel must match):"
|
|
log_info " opkg update && opkg install kmod-ipt-conntrack-extra"
|
|
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 ! _nft_functional; then
|
|
if ! command_exists ipset; then
|
|
rec_missing="${rec_missing} ipset"
|
|
if [ "$B4_PKG_MANAGER" = "opkg" ]; then
|
|
_kmod_available "ip_set" || rec_missing="${rec_missing} kmod-ipt-ipset"
|
|
fi
|
|
fi
|
|
if ! _kmod_available "xt_connbytes" && [ "$B4_PKG_MANAGER" = "opkg" ]; then
|
|
rec_missing="${rec_missing} kmod-ipt-conntrack-extra iptables-mod-conntrack-extra"
|
|
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 = <your 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}" <<EOF
|
|
#!/bin/sh
|
|
# B4 DPI Bypass Service — Entware
|
|
|
|
ENABLED=yes
|
|
PROCS=b4
|
|
ARGS="--config=${B4_CONFIG_FILE}"
|
|
PREARGS=""
|
|
which nohup >/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}" <<EOF
|
|
#!/bin/sh
|
|
# B4 DPI Bypass Service — Entware standalone
|
|
PROG="${B4_BIN_DIR}/${BINARY_NAME}"
|
|
CONFIG="${B4_CONFIG_FILE}"
|
|
PIDFILE="/opt/var/run/b4.pid"
|
|
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
|
|
}
|
|
|
|
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}" <<EOF
|
|
#!/sbin/openrc-run
|
|
|
|
name="b4"
|
|
description="B4 DPI Bypass Service"
|
|
|
|
command="${B4_BIN_DIR}/${BINARY_NAME}"
|
|
command_args="--config ${B4_CONFIG_FILE}"
|
|
command_background=true
|
|
pidfile="/run/b4.pid"
|
|
|
|
output_log="/dev/null"
|
|
error_log="/dev/null"
|
|
|
|
depend() {
|
|
need net
|
|
}
|
|
|
|
start_pre() {
|
|
# Load kernel modules
|
|
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 || 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}" <<EOF
|
|
#!/bin/sh /etc/rc.common
|
|
# B4 DPI Bypass Service (procd)
|
|
|
|
START=99
|
|
STOP=10
|
|
USE_PROCD=1
|
|
|
|
PROG="${B4_BIN_DIR}/${BINARY_NAME}"
|
|
CONFIG="${B4_CONFIG_FILE}"
|
|
|
|
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
|
|
}
|
|
|
|
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}" <<EOF
|
|
[Unit]
|
|
Description=B4 DPI Bypass Service
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
ExecStart=${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
TimeoutStopSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable "${B4_SERVICE_NAME}" 2>/dev/null || true
|
|
log_ok "Systemd service created and enabled: ${B4_SERVICE_NAME}"
|
|
}
|
|
|
|
service_systemd_remove() {
|
|
systemctl stop "${B4_SERVICE_NAME}" 2>/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}" <<EOF
|
|
#!/bin/sh
|
|
# B4 DPI Bypass Service
|
|
PROG="${B4_BIN_DIR}/${BINARY_NAME}"
|
|
CONFIG="${B4_CONFIG_FILE}"
|
|
PIDFILE="/var/run/b4.pid"
|
|
|
|
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
|
|
}
|
|
|
|
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}"
|
|
|
|
if command -v update-rc.d >/dev/null 2>&1; then
|
|
update-rc.d "${B4_SERVICE_NAME}" defaults 2>/dev/null || true
|
|
elif command -v chkconfig >/dev/null 2>&1; then
|
|
chkconfig --add "${B4_SERVICE_NAME}" 2>/dev/null || true
|
|
chkconfig "${B4_SERVICE_NAME}" on 2>/dev/null || true
|
|
fi
|
|
|
|
log_ok "Init script created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
|
|
}
|
|
|
|
service_sysv_remove() {
|
|
if [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then
|
|
"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" stop 2>/dev/null || true
|
|
if command -v update-rc.d >/dev/null 2>&1; then
|
|
update-rc.d -f "${B4_SERVICE_NAME}" remove 2>/dev/null || true
|
|
elif command -v chkconfig >/dev/null 2>&1; then
|
|
chkconfig --del "${B4_SERVICE_NAME}" 2>/dev/null || true
|
|
fi
|
|
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=<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)
|
|
if _nft_functional; then
|
|
printf " ${YELLOW}missing${NC} %s ${DIM}(needed for routing on iptables systems)${NC}\n" "$tool" >&2
|
|
elif command_exists iptables || command_exists iptables-legacy; then
|
|
printf " ${RED}missing${NC} %s ${DIM}(required — iptables backend in use, install ipset)${NC}\n" "$tool" >&2
|
|
else
|
|
printf " ${YELLOW}missing${NC} %s ${DIM}(no firewall backend detected — backend availability unclear)${NC}\n" "$tool" >&2
|
|
fi
|
|
;;
|
|
*) ;;
|
|
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 </dev/tty
|
|
fi
|
|
|
|
case "$ACTION" in
|
|
install) action_install "$VERSION" "$FORCE_ARCH" ;;
|
|
remove) action_remove ;;
|
|
update) action_update "$VERSION" "$FORCE_ARCH" ;;
|
|
sysinfo) action_sysinfo ;;
|
|
*) ;;
|
|
esac
|
|
}
|
|
|
|
_show_help() {
|
|
echo "B4 Universal Installer"
|
|
echo ""
|
|
echo "Usage: $0 [OPTIONS] [VERSION]"
|
|
echo ""
|
|
echo "Actions:"
|
|
echo " (default) Install b4 (interactive wizard)"
|
|
echo " --update, -u Update b4 to latest version"
|
|
echo " --remove, -r Uninstall b4"
|
|
echo " --sysinfo, -i Show system diagnostics"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --arch=ARCH Force architecture (skip detection)"
|
|
echo " --platform=ID Force platform (skip detection)"
|
|
echo " --bin-dir=DIR Override binary directory"
|
|
echo " --data-dir=DIR Override data/config directory"
|
|
echo " --quiet, -q Non-interactive mode with defaults"
|
|
echo " --help, -h Show this help"
|
|
echo ""
|
|
echo "Environment overrides:"
|
|
echo " B4_PLATFORM Platform ID (generic_linux, openwrt, merlinwrt, ...)"
|
|
echo " B4_BIN_DIR Binary install directory"
|
|
echo " B4_DATA_DIR Data/config directory"
|
|
echo " B4_PKG_MANAGER Package manager (apt, dnf, pacman, opkg, ...)"
|
|
echo ""
|
|
echo "Architectures:"
|
|
echo " amd64, 386, arm64, armv5, armv6, armv7,"
|
|
echo " mips, mipsle, mips_softfloat, mipsle_softfloat,"
|
|
echo " mips64, mips64le, loong64, ppc64, ppc64le, riscv64, s390x"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 Interactive install"
|
|
echo " $0 v1.4.0 Install specific version"
|
|
echo " $0 --arch=mipsle_softfloat Force architecture"
|
|
echo " $0 --platform=openwrt Force platform"
|
|
echo " $0 --quiet Non-interactive with defaults"
|
|
echo " $0 --update Update to latest"
|
|
echo " $0 --remove Uninstall"
|
|
echo " $0 --sysinfo Show diagnostics"
|
|
}
|
|
|
|
main "$@"
|
|
exit 0
|
|
|