mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-08-16 04:23:49 +00:00
887 lines
27 KiB
Bash
887 lines
27 KiB
Bash
#!/bin/sh
|
|
# Core utility functions
|
|
|
|
# --- Configuration ---
|
|
REPO_OWNER="DanielLavrushin"
|
|
REPO_NAME="b4"
|
|
BINARY_NAME="b4"
|
|
TEMP_DIR="/tmp/b4_install_$$"
|
|
WGET_INSECURE=""
|
|
PROXY_BASE_URL="https://proxy.lavrush.in/github"
|
|
|
|
# --- Runtime state (set by platform/wizard) ---
|
|
B4_BIN_DIR=""
|
|
B4_DATA_DIR=""
|
|
B4_CONFIG_FILE=""
|
|
B4_SERVICE_TYPE=""
|
|
B4_SERVICE_DIR=""
|
|
B4_SERVICE_NAME=""
|
|
B4_PKG_MANAGER=""
|
|
B4_PLATFORM=""
|
|
|
|
# --- Command existence check (works on BusyBox/minimal shells) ---
|
|
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"
|
|
}
|
|
|
|
# --- Root check ---
|
|
check_root() {
|
|
if [ "$(id -u 2>/dev/null)" = "0" ]; then
|
|
return 0
|
|
fi
|
|
if [ "$USER" = "root" ]; then
|
|
return 0
|
|
fi
|
|
# Fallback: try writing to /etc
|
|
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
|
|
}
|
|
|
|
# --- Filesystem helpers ---
|
|
get_avail_kb() {
|
|
_path="$1"
|
|
# Return available space in KB
|
|
df -Pk "$_path" 2>/dev/null | awk 'NR==2 {print $4}'
|
|
}
|
|
|
|
get_fs_device() {
|
|
df -Pk "$1" 2>/dev/null | awk 'NR==2 {print $1}'
|
|
}
|
|
|
|
same_filesystem() {
|
|
_sfs_a=$(get_fs_device "$1")
|
|
_sfs_b=$(get_fs_device "$2")
|
|
[ -n "$_sfs_a" ] && [ "$_sfs_a" = "$_sfs_b" ]
|
|
}
|
|
|
|
BIN_SLACK_KB=512
|
|
|
|
ensure_bin_space() {
|
|
_ebs_dir="$1"
|
|
_ebs_src="$2"
|
|
|
|
[ -d "$_ebs_dir" ] || return 0
|
|
[ -f "$_ebs_src" ] || return 0
|
|
same_filesystem "$(dirname "$_ebs_src")" "$_ebs_dir" && return 0
|
|
|
|
_ebs_need=$(du -k "$_ebs_src" 2>/dev/null | awk '{print $1}')
|
|
[ -n "$_ebs_need" ] || return 0
|
|
_ebs_need=$((_ebs_need + BIN_SLACK_KB))
|
|
|
|
_ebs_avail=$(get_avail_kb "$_ebs_dir")
|
|
[ -n "$_ebs_avail" ] || return 0
|
|
|
|
if [ "$_ebs_avail" -lt "$_ebs_need" ] 2>/dev/null; then
|
|
log_err "Not enough disk space in ${_ebs_dir}: ${_ebs_avail}KB free, need ${_ebs_need}KB"
|
|
log_info "Free space there, or re-run with --bin-dir on external storage."
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
stash_binary() {
|
|
_sb_bin="$1"
|
|
_sb_backup="$2"
|
|
|
|
rm -f "${_sb_bin}".backup.* 2>/dev/null || true
|
|
[ -f "$_sb_bin" ] || return 0
|
|
mv "$_sb_bin" "$_sb_backup" 2>/dev/null || return 1
|
|
return 0
|
|
}
|
|
|
|
restore_binary() {
|
|
_rb_bin="$1"
|
|
_rb_backup="$2"
|
|
|
|
[ -n "$_rb_backup" ] && [ -f "$_rb_backup" ] || return 1
|
|
rm -f "$_rb_bin" 2>/dev/null || true
|
|
mv "$_rb_backup" "$_rb_bin" 2>/dev/null || return 1
|
|
chmod +x "$_rb_bin" 2>/dev/null || true
|
|
return 0
|
|
}
|
|
|
|
# --- Temp directory management ---
|
|
# Required free space: ~20MB (archive + extracted binary simultaneously)
|
|
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
|
|
|
|
# --- Package manager detection ---
|
|
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
|
|
}
|
|
|
|
# --- Architecture detection ---
|
|
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)
|
|
# Check for full ARMv7 VFP support, otherwise use armv5 for safety
|
|
if [ -f /proc/cpuinfo ] &&
|
|
grep -qE "(vfpv[3-9])" /proc/cpuinfo 2>/dev/null &&
|
|
grep -qE "CPU architecture:[[:space:]]*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:[[:space:]]*7" /proc/cpuinfo 2>/dev/null; then
|
|
echo "armv7"
|
|
elif grep -qE "CPU architecture:[[:space:]]*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
|
|
# ELF header byte 6 (index 5): 1=little-endian, 2=big-endian
|
|
[ "$(dd if=/bin/sh bs=1 skip=5 count=1 2>/dev/null | _byte_to_dec)" = "1" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
is_softfloat() {
|
|
# On OpenWrt, DISTRIB_ARCH is the most reliable indicator
|
|
# Convention: mips_24kc / mips_74kc = soft-float, mips_24kf = hard-float ('f' = FPU)
|
|
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
|
|
# CPU model ending in 'f' (e.g. 24kf, 74kf) = hard-float
|
|
if echo "$_sf_owrt_arch" | grep -qE '_[a-z]*[0-9]+k?f$'; then
|
|
return 1
|
|
fi
|
|
# MIPS without 'f' suffix = soft-float on OpenWrt
|
|
case "$_sf_owrt_arch" in
|
|
mips_* | mipsel_* | mips64_* | mips64el_*) return 0 ;;
|
|
esac
|
|
fi
|
|
fi
|
|
# On OpenWrt/Entware, check opkg architecture
|
|
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
|
|
# Same convention: CPU model with 'f' suffix = hard-float
|
|
if echo "$_sf_opkg_arch" | grep -qiE "mips(el|64|64el)?_[a-z]*[0-9]+k?f( |$)"; then
|
|
return 1
|
|
fi
|
|
# MIPS in opkg without explicit hard-float = soft-float
|
|
echo "$_sf_opkg_arch" | grep -qi "mips" && return 0
|
|
fi
|
|
# Check /proc/cpuinfo for soft-float indicators
|
|
if [ -f /proc/cpuinfo ]; then
|
|
grep -qi "nofpu\|no fpu\|soft.float" /proc/cpuinfo 2>/dev/null && return 0
|
|
fi
|
|
# Check ELF header for MIPS soft-float flag (EF_MIPS_SOFT_FLOAT = 0x800)
|
|
_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)
|
|
# e_flags offset: 36 for 32-bit ELF, 48 for 64-bit ELF
|
|
_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
|
|
# EF_MIPS_SOFT_FLOAT = 0x800 (bit 11)
|
|
# In little-endian e_flags: bit 11 is in byte at offset+1, bit 3
|
|
# In big-endian e_flags: bit 11 is in byte at offset+2, bit 3
|
|
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
|
|
# Fallback: check via file or readelf if available
|
|
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
|
|
}
|
|
|
|
# --- HTTPS support ---
|
|
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
|
|
# Try with --no-check-certificate
|
|
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
|
|
}
|
|
|
|
# --- Download helpers ---
|
|
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
|
|
|
|
# Try direct
|
|
if _do_fetch "$url" "$output"; then return 0; fi
|
|
|
|
# Try proxy fallback
|
|
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 fallback
|
|
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
|
|
}
|
|
|
|
# --- GitHub release helpers ---
|
|
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
|
|
}
|
|
|
|
# --- Container detection ---
|
|
is_lxc_container() {
|
|
# Check /proc/1/environ for container=lxc
|
|
if [ -f /proc/1/environ ]; then
|
|
tr '\0' '\n' </proc/1/environ 2>/dev/null | grep -q '^container=lxc' && return 0
|
|
fi
|
|
# Fallback: check systemd container detection
|
|
[ -f /run/systemd/container ] && grep -q "lxc" /run/systemd/container 2>/dev/null && return 0
|
|
return 1
|
|
}
|
|
|
|
# --- Kernel module helpers ---
|
|
# Check if a kernel module is built-in (compiled into kernel, not loadable)
|
|
_kmod_builtin() {
|
|
_mod="$1"
|
|
_kver=$(uname -r)
|
|
_f="/lib/modules/${_kver}/modules.builtin"
|
|
[ -f "$_f" ] && grep -q "/${_mod}\.ko" "$_f" 2>/dev/null && return 0
|
|
_f="/lib/modules/${_kver}/modules.builtin.modinfo"
|
|
[ -f "$_f" ] && grep -q "${_mod}\.[a-z]" "$_f" 2>/dev/null && return 0
|
|
[ -d "/sys/module/${_mod}" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Check if a kernel module is available (loaded OR built-in)
|
|
_kmod_available() {
|
|
lsmod 2>/dev/null | grep -q "^${1}[[:space:]]" && return 0
|
|
_kmod_builtin "$1" && return 0
|
|
return 1
|
|
}
|
|
|
|
_nft_functional() {
|
|
command_exists nft || return 1
|
|
if [ -z "$B4_NFT_FUNCTIONAL" ]; then
|
|
if nft add table inet _b4_test >/dev/null 2>&1; then
|
|
nft delete table inet _b4_test >/dev/null 2>&1
|
|
B4_NFT_FUNCTIONAL="yes"
|
|
else
|
|
B4_NFT_FUNCTIONAL="no"
|
|
fi
|
|
fi
|
|
[ "$B4_NFT_FUNCTIONAL" = "yes" ]
|
|
}
|
|
|
|
_firewall_backend() {
|
|
if _nft_functional; then
|
|
echo "nftables"
|
|
elif command_exists iptables; then
|
|
if iptables --version 2>/dev/null | grep -q "nf_tables" && command_exists iptables-legacy; then
|
|
echo "iptables-legacy"
|
|
else
|
|
echo "iptables"
|
|
fi
|
|
elif command_exists iptables-legacy; then
|
|
echo "iptables-legacy"
|
|
else
|
|
echo "none"
|
|
fi
|
|
}
|
|
|
|
_nft_probe_rule() {
|
|
_pt="_b4_probe_$$"
|
|
nft delete table inet "$_pt" >/dev/null 2>&1
|
|
nft add table inet "$_pt" >/dev/null 2>&1 || return 1
|
|
_prc=1
|
|
if nft add chain inet "$_pt" test >/dev/null 2>&1; then
|
|
nft add rule inet "$_pt" test $1 >/dev/null 2>&1 && _prc=0
|
|
fi
|
|
nft delete table inet "$_pt" >/dev/null 2>&1
|
|
return $_prc
|
|
}
|
|
|
|
_nft_queue_works() {
|
|
_nft_functional || return 1
|
|
_nft_probe_rule "counter queue num 0 bypass"
|
|
}
|
|
|
|
_nft_ct_counters_work() {
|
|
_nft_functional || return 1
|
|
_nft_probe_rule "ct original packets < 20 counter accept"
|
|
}
|
|
|
|
_ipt_probe_rule() {
|
|
_ib="$1"
|
|
_itable="$2"
|
|
shift 2
|
|
command_exists "$_ib" || return 1
|
|
_ipc="B4_PROBE_$$"
|
|
$_ib -t "$_itable" -F "$_ipc" >/dev/null 2>&1
|
|
$_ib -t "$_itable" -X "$_ipc" >/dev/null 2>&1
|
|
$_ib -t "$_itable" -N "$_ipc" >/dev/null 2>&1 || return 1
|
|
_prc=1
|
|
$_ib -t "$_itable" -A "$_ipc" "$@" >/dev/null 2>&1 && _prc=0
|
|
$_ib -t "$_itable" -F "$_ipc" >/dev/null 2>&1
|
|
$_ib -t "$_itable" -X "$_ipc" >/dev/null 2>&1
|
|
return $_prc
|
|
}
|
|
|
|
_ipt_nfqueue_works() {
|
|
_ipt_probe_rule "$1" mangle -j NFQUEUE --queue-num 0 --queue-bypass
|
|
}
|
|
|
|
_ipt_connbytes_works() {
|
|
_ipt_probe_rule "$1" filter -p tcp -m connbytes --connbytes-dir original \
|
|
--connbytes-mode packets --connbytes 0:10 -j ACCEPT
|
|
}
|
|
|
|
_nft_flow_guard() {
|
|
awk '
|
|
/flow add @|flow offload @/ {
|
|
g = 0
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i == "ct" && $(i + 1) == "original" && $(i + 2) == "packets") {
|
|
op = $(i + 3)
|
|
val = $(i + 4) + 0
|
|
if (op == ">=" || op == "ge") g = val
|
|
else if (op == ">" || op == "gt") g = val + 1
|
|
}
|
|
}
|
|
if (!seen || g < min) min = g
|
|
seen = 1
|
|
}
|
|
END { print (seen ? min : 0) }
|
|
'
|
|
}
|
|
|
|
_ipt_flow_guard() {
|
|
awk '
|
|
/FLOWOFFLOAD/ {
|
|
g = 0
|
|
if (index($0, "--connbytes-dir original") && index($0, "--connbytes-mode packets") && !index($0, "! --connbytes ")) {
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i == "--connbytes") {
|
|
split($(i + 1), b, ":")
|
|
g = b[1] + 0
|
|
}
|
|
}
|
|
}
|
|
if (!seen || g < min) min = g
|
|
seen = 1
|
|
}
|
|
END { print (seen ? min : 0) }
|
|
'
|
|
}
|
|
|
|
_b4_queue_window() {
|
|
_qw_tcp=19
|
|
_qw_udp=8
|
|
if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then
|
|
_qw_tcp=$(jq -r '.queue.tcp_conn_bytes_limit // 19' "$B4_CONFIG_FILE" 2>/dev/null || echo 19)
|
|
_qw_udp=$(jq -r '.queue.udp_conn_bytes_limit // 8' "$B4_CONFIG_FILE" 2>/dev/null || echo 8)
|
|
fi
|
|
case "$_qw_tcp" in '' | *[!0-9]*) _qw_tcp=19 ;; esac
|
|
case "$_qw_udp" in '' | *[!0-9]*) _qw_udp=8 ;; esac
|
|
if [ "$_qw_udp" -gt "$_qw_tcp" ]; then
|
|
echo "$_qw_udp"
|
|
else
|
|
echo "$_qw_tcp"
|
|
fi
|
|
}
|
|
|
|
_b4_duplicate_sets() {
|
|
if [ -z "$B4_CONFIG_FILE" ] || [ ! -f "$B4_CONFIG_FILE" ] || ! command_exists jq; then
|
|
echo 0
|
|
return 0
|
|
fi
|
|
_ds=$(jq -r '[.sets[]? | select(.enabled == true) | select(.tcp.duplicate.enabled == true)] | length' "$B4_CONFIG_FILE" 2>/dev/null || echo 0)
|
|
case "$_ds" in '' | *[!0-9]*) _ds=0 ;; esac
|
|
echo "$_ds"
|
|
}
|
|
|
|
_queue_functional() {
|
|
case "$1" in
|
|
nftables) _nft_queue_works ;;
|
|
iptables | iptables-legacy) _ipt_nfqueue_works "$1" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
_queue_modules_for_backend() {
|
|
case "$1" in
|
|
nftables) echo "nft_queue nfnetlink_queue nft_ct nf_conntrack" ;;
|
|
*) echo "xt_NFQUEUE nfnetlink_queue xt_connbytes xt_multiport nf_conntrack" ;;
|
|
esac
|
|
}
|
|
|
|
_queue_pkgs_for_backend() {
|
|
if [ "$B4_PKG_MANAGER" = "apk" ]; then
|
|
case "$1" in
|
|
nftables) echo "kmod-nft-queue kmod-nft-nat kmod-nft-compat" ;;
|
|
*) echo "kmod-nft-compat kmod-nft-queue" ;;
|
|
esac
|
|
return 0
|
|
fi
|
|
case "$1" in
|
|
nftables) echo "kmod-nft-queue kmod-nfnetlink-queue kmod-nft-conntrack" ;;
|
|
*) echo "kmod-nfnetlink-queue kmod-ipt-nfqueue iptables-mod-nfqueue kmod-ipt-conntrack-extra iptables-mod-conntrack-extra" ;;
|
|
esac
|
|
}
|
|
|
|
_warn_if_queue_unavailable() {
|
|
_wq_backend=$(_firewall_backend)
|
|
if [ "$_wq_backend" = "none" ]; then
|
|
log_warn "No working firewall backend found (neither nft nor iptables) - b4 cannot install its rules"
|
|
return 1
|
|
fi
|
|
if _queue_functional "$_wq_backend"; then
|
|
return 0
|
|
fi
|
|
|
|
log_warn "The kernel cannot queue packets to b4 on the ${_wq_backend} backend - b4 will not start in NFQUEUE mode"
|
|
case "$B4_PKG_MANAGER" in
|
|
apk) log_info "Try: apk add $(_queue_pkgs_for_backend "$_wq_backend")" ;;
|
|
opkg) log_info "Try: opkg install $(_queue_pkgs_for_backend "$_wq_backend")" ;;
|
|
*) log_info "Load the queue modules: $(_queue_modules_for_backend "$_wq_backend")" ;;
|
|
esac
|
|
log_info "Or switch b4 to TUN mode (queue.mode = \"tun\"), which does not need NFQUEUE"
|
|
return 1
|
|
}
|
|
|
|
# --- Process management ---
|
|
is_b4_running() {
|
|
# Check PID files first (most reliable)
|
|
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
|
|
# Try pgrep -x (exact process name match — won't match scripts containing "b4")
|
|
if command_exists pgrep; then
|
|
pgrep -x "$BINARY_NAME" >/dev/null 2>&1 && return 0
|
|
fi
|
|
# Fallback: check ps for the actual b4 binary (not scripts mentioning b4)
|
|
# Match paths like /usr/bin/b4 or standalone "b4" command, exclude our own PID
|
|
_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..."
|
|
# Try PID file first
|
|
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
|
|
# Then try pkill -x (exact name match)
|
|
if command_exists pkill; then
|
|
pkill -x "$BINARY_NAME" 2>/dev/null || true
|
|
fi
|
|
sleep 2
|
|
}
|
|
|
|
b4_running_cmdline() {
|
|
_pid=""
|
|
if command_exists pgrep; then
|
|
_pid=$(pgrep -x "$BINARY_NAME" 2>/dev/null | head -1)
|
|
fi
|
|
[ -z "$_pid" ] && return 1
|
|
if [ -r "/proc/${_pid}/cmdline" ]; then
|
|
tr '\0' ' ' <"/proc/${_pid}/cmdline" 2>/dev/null | sed 's/ *$//'
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
relaunch_b4() {
|
|
_cmd="$1"
|
|
[ -z "$_cmd" ] && return 1
|
|
# Re-exec the captured argv words directly — no shell re-parse of the
|
|
# cmdline (avoids interpreting ; $ ` etc.). Disable globbing so a literal
|
|
# * in an arg isn't expanded, then restore the caller's noglob state.
|
|
case "$-" in
|
|
*f*) _had_noglob=1 ;;
|
|
*) _had_noglob=0 ;;
|
|
esac
|
|
set -f
|
|
if command_exists setsid; then
|
|
setsid $_cmd >/dev/null 2>&1 &
|
|
else
|
|
nohup $_cmd >/dev/null 2>&1 &
|
|
fi
|
|
if [ "$_had_noglob" = 0 ]; then set +f; fi
|
|
sleep 2
|
|
is_b4_running
|
|
}
|
|
|
|
# --- Directory helpers ---
|
|
is_writable_dir() {
|
|
dir="$1"
|
|
[ -d "$dir" ] && [ -w "$dir" ] && return 0
|
|
# Try to create and test
|
|
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
|
|
}
|
|
|
|
is_abs_path() {
|
|
case "$1" in
|
|
/*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
require_abs_path() {
|
|
if ! is_abs_path "$1"; then
|
|
log_err "${2:-Path} must be an absolute path (got: ${1:-empty})"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# --- Check if user wants to exit ---
|
|
check_exit() {
|
|
case "$1" in
|
|
[eEqQ] | exit | EXIT | quit | QUIT)
|
|
echo ""
|
|
log_info "Aborted by user."
|
|
exit 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- Read user input (works even when stdin is piped) ---
|
|
# Uses global _INPUT to avoid subshell issues with exit
|
|
_INPUT=""
|
|
read_input() {
|
|
prompt="$1"
|
|
default="$2"
|
|
# In quiet mode, always use default without prompting
|
|
if [ "$QUIET_MODE" -eq 1 ] 2>/dev/null; then
|
|
_INPUT="$default"
|
|
return 0
|
|
fi
|
|
printf "${CYAN}%b${NC}" "$prompt" >&2
|
|
read _INPUT || _INPUT="$default"
|
|
# Strip carriage returns (some terminals/SSH clients send \r)
|
|
_INPUT=$(printf '%s' "$_INPUT" | tr -d '\r')
|
|
check_exit "$_INPUT"
|
|
[ -z "$_INPUT" ] && _INPUT="$default"
|
|
return 0
|
|
}
|
|
|
|
# --- Yes/No prompt ---
|
|
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
|
|
}
|