b4/install.sh
Daniel Lavrushin c48fa3eef6
issue-429-432 (#438)
* Enhance nftables rules to include connection tracking direction

- Added rules to track the direction of original and reply packets in the nftables configuration for better handling of TCP traffic on port 443.
- Updated existing rules to incorporate connection tracking direction, ensuring that original and reply packets are processed correctly.
- Adjusted the conditions for packet acceptance and queuing to improve traffic management and performance.
A TCP port filter that covers client source ports makes b4 skip outgoing TCP
Fixes #429
Domain-only sets leave the first segment of Chrome's split ClientHello untouched
Fixes #430
A TCP Fast Open SYN that carries a ClientHello is fragmented like a data segment
Fixes #431
b4's own fakes and fragments count toward the per-connection queue window
Fixes #432

* issue-427-428 (#435)

* feat(config): add TCP port overlap detection with ephemeral ranges

* feat(direction): implement queue direction handling and add tests

* Enhance packet direction handling and flow management in TUN

- Introduced a new `Direction` type in the `engine` package to represent packet directions.
- Updated `firstNLimiter` to return packet direction alongside admission status.
- Enhanced flow counting logic to track the direction of TCP flows (original vs reply).
- Implemented direction-based packet forwarding in the `Engine` to ensure correct routing.
- Added tests to verify the correct handling of packet directions and flow records.
- Refactored existing tests to utilize the new direction handling logic.
- Introduced new utility functions for determining TCP direction based on conntrack rules.

* fix(queue): queue router traffic once, clamp MSS before the queue, give b4's own packets their own window

- Router-local packets were queued twice, at OUTPUT and again at POSTROUTING
  (regression since 1.26.1): b4 now jumps from OUTPUT only when device
  filtering moves the B4 jump to FORWARD.
- nftables: the MSS clamp rules ran after the queue rules, so queued SYN-ACKs
  and router SYNs were never clamped (since 1.39.1); they now lead prerouting,
  output and forward, as on iptables.
- Packets b4 sends into a connection carry a mark bit of their own (0x2000000).
  The OUTPUT CONNMARK save no longer tags those connections with the queue
  mark (regression since 1.71.1): their SYN-ACKs and RSTs reach b4 again, so
  RST protection, RST escalation and TTL learning work on them; their server
  data stays unqueued unless a set uses an incoming mode.
- #432: connections b4 has sent into get the window again on top (2N TCP, 2M
  UDP), so its own fakes and fragments no longer use up the client's packets.
  Diagnostics and the installer compare a flowtable guard with 2*max(N,M).
- TCP to private destinations is accepted unprocessed again (regression since
  1.22.0): inbound connections to port-forwarded LAN servers broke when a set
  matched.
- nftables keeps counting the reply window in original packets; the port
  classifier warning gets its own dashboard event.

* fix(quic): a QUIC drop holds for the whole connection, not only the queue window (#428)

When a set drops or rejects a QUIC connection (udp.mode drop or reject, or a
block set), b4 now keeps it dropped past the first datagrams the queue rules
hand it. Before, a connection whose first datagram had been let through (a
ClientHello split over two Initials, as Chrome sends it, decided on the
second) used up its queue window on the dropped datagrams and the rest
reached the server, so a blocked site could open over QUIC.

- nftables: the flow goes into a timed set (b4_udp_drop4/6, 120 s) dropped
  in b4_chain ahead of the UDP window rule.
- iptables, TUN with xt_connbytes, and alongside the nftables set: b4
  deletes the flow's conntrack entry through ctnetlink, so its later
  datagrams start a fresh entry inside the window and reach b4 again. Every
  delete names a complete tuple and carries a mark guard that turns a
  tuple-less request into a no-op instead of a table flush; a self-test
  decides once whether conntrack netlink is usable.
- TUN without xt_connbytes: the engine's own packet count lets a dropped
  connection through to b4 past its limit.
- System Info shows whether drops hold (nf_conntrack_netlink,
  kmod-nf-conntrack-netlink on OpenWrt, which the installer recommends).

* fix(queue): drop the wider window, hold only real QUIC, keep held flows decidable, docs

- Revert the #432 window extension (2N/2M on connections b4 sent into) and
  the TUN doubling for tracked re-sends: letting the strategies reach client
  data past about 10 KB put the default pastseq fakes inside the stream and
  broke TLS uploads through a default-strategy set (measured 21-26 of 80),
  and the combo delay hit the first request after the handshake. #432 stays
  open until fakes cannot land inside the stream.
- The return for connections b4 sent into matches bit 0x2000000 with the
  queue mark clear, so foreign whole-value marks that carry both bits are not
  taken for b4's.
- A QUIC drop holds a flow only when the datagram is QUIC or belongs to a
  QUIC flow b4 named; other UDP to a learned address is dropped as before.
- nftables: the drop set is written from a background writer over one
  netlink socket, waiting behind another nf_tables transaction is no longer
  a failure, and a settings change empties the drop sets (with conntrack
  netlink) so held connections are decided again.
- RST protection judges the first connection to an address b4 has not
  learned yet by the SYN-ACK it saw before the connection matched a set.
- A queue mark containing bit 0x2000000 is warned about.
- The iptables monitor checks for a TCP queue rule on one listing line;
  System Info lists nf_conntrack_netlink only for iptables; make test-netns
  runs the conntrack netlink tests after the tables tests.
- Docs EN/RU, AI topics and changelog for the queue direction, the marks,
  router traffic, the MSS clamp order, QUIC drops and private destinations.

* fix(queue): direction match on duplication rules, private guard only without an address match, refuse a new queue mark with bit 25

From the Copilot review of #438.

- Duplication rules carry the ORIGINAL direction match wherever the
  kernel has it: B4 (-m conntrack --ctdir ORIGINAL), b4_chain (ct
  direction original) and TUN's duplication steer rules. Without it a
  wide port filter let a reply from a duplication address, such as a
  local service answering a client in that range, be queued at
  POSTROUTING or steered into the TUN and duplicated as client
  traffic, since the hook and the TUN take such packets as original.
  The TUN tests that kept these rules port-only now require the match.
- The private-destination guard in handleTCPPacket applies only when
  no set's IP targets cover the address on that port. A set that lists
  10.0.0.0/8 keeps its strategy, as before this branch; SNI-matched
  and port-only sets still leave port-forwarded LAN servers alone.
- Saving a queue mark with bit 0x2000000 as a new value is refused
  (queue.mark, mark_conflict). A configuration that already holds one
  keeps loading and saving with the warning, so an upgrade does not
  stop the service.
- Docs (EN, RU): capture.md and marks.md drop the duplication
  exemption from the direction text, the zapret note follows (its
  untracked packets are no longer queued toward duplication
  addresses), marks.md lists the refusal, matching.md the exception
  for listed private addresses.

* fix(config): refuse a mark with bit 25 only where NFQUEUE mode newly takes it

From Copilot's review of 50e71425: the save-time refusal also turned
down a TUN configuration's mark such as 0x2008000, although TUN adds
0x10000000 to its packets and never 0x2000000, and loading such a
configuration from disk only warns in NFQUEUE mode. The refusal now
applies when a save leaves NFQUEUE mode with a mark that has bit
0x2000000 and the running configuration did not already run NFQUEUE
mode with that mark, so leaving TUN mode with one is still refused.
marks.md (EN, RU) says NFQUEUE mode.

* docs: what an ASUS connection-mark rewrite does to b4's replies

Measured on an ASUS RT-BE88U with AiProtection on: the firmware writes
its own value into the connection mark of device connections from their
start, and the values of all 24 test connections had bit 15, the
default queue mark. The return for b4's own connections in
B4_PREROUTING then skipped all 235 replies of 6 HTTPS connections and
all 6 answers of DNS queries to an outside resolver. marks.md and the
three AI topics said the server's packets are queued as on any other
connection in that case, which holds only for values without the queue
bit, such as the 0x10C3D seen with QoS on. They now say the firmware
can remove bit 0x2000000, and that a value with every queue-mark bit
hides the connection's replies, the SYN-ACK and resets included.

marks.md (EN, RU) also says that b4 refuses a queue mark with bit
0x2000000 when a save would put it on NFQUEUE mode for the first time,
instead of accepting it with a warning.

* test(ctnl): refuse to flush conntrack when the namespace check cannot prove isolation

From Copilot's review of 35cea3d9: with B4_NETNS_TEST=1 and root, a
failed lookup of /proc/self/ns/net or /proc/1/ns/net skipped the
isolation check, and the tuple-less delete test could then flush the
conntrack table of the namespace it ran in, the host's included.

Failing closed on any failed lookup would refuse make test-netns
itself: inside unshare -Urnm, /proc/1/ns/net answers EACCES. When a
lookup fails, the tests now refuse to run if any interface other than
loopback is up. A fresh namespace passes: the kernel's fallback tunnel
devices (gre0, sit0, tunl0 and the like) are created down. Checked: the
rig passes, and the same rig with a dummy interface up is refused.

* docs(capture): the drop set holds by addresses and ports

From Copilot's review of 0dcc9308: on nftables, a new QUIC connection
that a device opens from the same UDP port to the same server address
and port within the 2 minutes is dropped by the same set element,
whatever name it carries. It takes a domain-only set and a client that
reuses one UDP socket for several connections. Exempting Initials from
the drop set and deleting the element when b4 accepts a replacement
would change the kernel rules and weaken the hold on routers without
nf_conntrack_netlink, so the code stays and internals/capture.md (EN,
RU) states the limit.

* fix(nfq): Match any IP address does not count as listing a private address

From the final review of #438: since 50e71425 the private-destination
guard skipped TCP to a private address whenever any address target of
a set matched it. A set with Match any IP address (0.0.0.0/0, ::/0)
and no source devices matches every private address, so a ClientHello
from the internet to a port-forwarded LAN server got that set's
strategy again, and a name match by another set could learn the LAN
server's address.

The guard now lets TCP to a private address through to processing only
when the matched set's most specific network holding it lies inside
the private ranges (SuffixSet.MatchedIPNet, utils.IsPrivateNet): a set
that lists 10.0.0.0/8 or 192.168.1.50 keeps its strategy, the catch-all
does not.

The test cases cover the catch-all, a private range next to the
catch-all, and assert that an untouched ClientHello is neither logged
nor read, so the "port the set leaves out" case now fails when the
guard moves above the port check (checked with go test -overlay on
both mutations).

Docs (EN, RU): matching.md states the exception and what b4 learns,
dscp.md and routing.md name the private-address exception for learned
destinations, and the dscp.enabled topic follows. The port-forward
changelog bullet drops its last sentence, a caveat on a FIXED line.

* fix(nfq): an overlapping private target on another port no longer hides the set that takes the port

From Copilot's review of dde38ff0: the private-destination guard asked
only about the set the address match picked, after the port check had
cleared it. With set A listing 10.1.2.3/32 on port 8443 and set B
listing 10.0.0.0/8 and the ClientHello's domain on 443, A won the
address match, the port check cleared it, and the guard accepted the
packet before the name match could pick B. Without the guard (main) B
handled it.

SuffixSet.ListsPrivateTCPTarget answers the guard's question directly:
does any set that applies to the device (source devices, IP family)
list the destination through a private network and take the port. If
so, b4 handles the connection as usual; Match any IP address still
lists no private address. It replaces MatchedIPNet.

Tests: the matcher case table (overlap, port, catch-all, source device,
IP family, IPv6) and the handler overlap case; the overlap and
catch-all cases both fail with the guard reverted to "no address
match" (go test -overlay). matching.md (EN, RU) says a set that
applies to the device lists the address, and that b4, not
necessarily that set, then handles the connection as usual.
2026-10-17 18:57:28 +02:00

5256 lines
161 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 installer/ 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
B4_UPDATE_LOG="${B4_UPDATE_LOG:-}"
_log_emit() {
[ -z "$B4_UPDATE_LOG" ] && return
printf '%s [%-4s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S' 2>/dev/null)" "$1" "$2" \
>>"$B4_UPDATE_LOG" 2>/dev/null || true
}
log_info() {
_log_emit "INFO" "$1"
[ "$QUIET_MODE" -eq 1 ] && return
printf "${BLUE}[INFO]${NC} %s\n" "$1" >&2
}
log_ok() {
_log_emit "OK" "$1"
[ "$QUIET_MODE" -eq 1 ] && return
printf "${GREEN}[ OK ]${NC} %s\n" "$1" >&2
}
log_warn() {
_log_emit "WARN" "$1"
[ "$QUIET_MODE" -eq 1 ] && return
printf "${YELLOW}[WARN]${NC} %s\n" "$1" >&2
}
log_err() {
_log_emit "ERR" "$1"
printf "${RED}[ERR ]${NC} %s\n" "$1" >&2
}
log_header() {
_log_emit "----" "$1"
[ "$QUIET_MODE" -eq 1 ] && return
printf "\n${MAGENTA}${BOLD}%s${NC}\n" "$1" >&2
}
log_detail() {
_log_emit "INFO" "$1: $2"
[ "$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
}
B4_KERNEL_MODULES="nfnetlink nf_conntrack nf_conntrack_netlink xt_connbytes xt_NFQUEUE nfnetlink_queue xt_multiport nf_tables nft_queue nft_ct nf_nat nft_masq nft_tproxy nft_socket nf_tproxy_ipv4 nf_tproxy_ipv6 xt_TPROXY xt_socket xt_hashlimit nft_limit"
REPO_OWNER="DanielLavrushin"
REPO_NAME="b4"
BINARY_NAME="b4"
TEMP_DIR="/tmp/b4_install_$$"
WGET_INSECURE=""
CURL_INSECURE=""
B4_MIRRORS="${B4_MIRRORS:-https://proxy.b4core.app https://proxy2.b4core.app}"
B4_SF_BASE="${B4_SF_BASE:-https://downloads.sourceforge.net/project/b4core}"
B4_CONNECT_TIMEOUT="${B4_CONNECT_TIMEOUT:-8}"
B4_STALL_TIMEOUT="${B4_STALL_TIMEOUT:-30}"
B4_MAX_TIME="${B4_MAX_TIME:-600}"
B4_GEO_MAX_TIME="${B4_GEO_MAX_TIME:-3600}"
B4_PROBE_TIMEOUT="${B4_PROBE_TIMEOUT:-6}"
B4_FETCH_MAX_TIME=""
B4_FETCH_TMP=""
B4_FETCH_PID=""
B4_PENDING_FILES=""
B4_BIN_DIR=""
B4_DATA_DIR=""
B4_CONFIG_FILE=""
B4_SERVICE_TYPE=""
B4_SERVICE_DIR=""
B4_SERVICE_NAME=""
B4_PKG_MANAGER=""
B4_PLATFORM=""
B4_SUPPORTED_ARCHS="amd64 arm64 armv7 armv6 armv5 386 mips mipsle mips_softfloat mipsle_softfloat mips64 mips64le loong64 ppc64 ppc64le riscv64 s390x"
command_exists() {
command -v "$1" >/dev/null 2>&1 || which "$1" >/dev/null 2>&1
}
_byte_at() {
_ba_char=$(
dd if="$1" bs=1 skip="$2" count=1 2>/dev/null | tr -d '\000'
echo .
)
_ba_char=${_ba_char%.}
if [ -n "$_ba_char" ]; then
LC_ALL=C printf '%d\n' "'$_ba_char"
return 0
fi
[ "$(dd if="$1" bs=1 skip="$2" count=1 2>/dev/null | wc -c)" -eq 1 ] || return 1
echo 0
}
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}'
}
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
flush_disk
return 0
}
flush_disk() {
command_exists sync || return 0
sync 2>/dev/null || true
return 0
}
TEMP_MIN_KB=20000
setup_temp() {
_tmp_problem="is missing"
for _tmp_base in /tmp "$B4_BIN_DIR" /opt /var/tmp /root "$HOME"; do
[ -n "$_tmp_base" ] && [ -d "$_tmp_base" ] || continue
_tmp_avail=$(get_avail_kb "$_tmp_base")
if ! [ "${_tmp_avail:-0}" -gt "$TEMP_MIN_KB" ] 2>/dev/null; then
if [ "$_tmp_base" = /tmp ]; then
_tmp_problem="is too small (${_tmp_avail:-?}KB free, need ${TEMP_MIN_KB}KB)"
fi
continue
fi
if [ "$_tmp_base" = /tmp ]; then
_tmp_dir="/tmp/b4_install_$$"
else
_tmp_dir="${_tmp_base}/.b4_install_$$"
fi
if mkdir -m 700 "$_tmp_dir" 2>/dev/null; then
TEMP_DIR="$_tmp_dir"
if [ "$_tmp_base" != /tmp ]; then
log_warn "Using ${_tmp_base} for temp files: /tmp ${_tmp_problem}"
fi
return 0
fi
if [ "$_tmp_base" = /tmp ]; then
if [ -e "$_tmp_dir" ] || [ -L "$_tmp_dir" ]; then
_tmp_problem="already has ${_tmp_dir}"
else
_tmp_problem="is not writable"
fi
fi
done
log_err "No usable temp directory: /tmp ${_tmp_problem}"
log_err "No writable fallback directory with ${TEMP_MIN_KB}KB free found."
log_info "Free space or re-run with --bin-dir on external storage."
exit 1
}
pending_add() {
B4_PENDING_FILES="${B4_PENDING_FILES:+${B4_PENDING_FILES}
}$1"
}
pending_drop() {
B4_PENDING_FILES=$(printf '%s\n' "$B4_PENDING_FILES" | grep -vxF -e "$1") || B4_PENDING_FILES=""
}
_pending_cleanup() {
[ -n "$B4_PENDING_FILES" ] || return 0
while IFS= read -r _pc_file; do
[ -z "$_pc_file" ] || rm -f "$_pc_file" 2>/dev/null || true
done <<EOF
$B4_PENDING_FILES
EOF
B4_PENDING_FILES=""
}
cleanup_temp() {
if [ -n "$B4_FETCH_PID" ]; then
kill "$B4_FETCH_PID" 2>/dev/null || true
wait "$B4_FETCH_PID" 2>/dev/null || true
B4_FETCH_PID=""
fi
if [ -n "$B4_FETCH_TMP" ]; then
rm -f "$B4_FETCH_TMP" 2>/dev/null || true
B4_FETCH_TMP=""
fi
_pending_cleanup
rm -rf "$TEMP_DIR" 2>/dev/null || true
}
_on_interrupt() {
cleanup_temp
stty echo 2>/dev/null || true
printf "\n" >&2
log_err "Aborted"
exit 130
}
trap cleanup_temp EXIT
trap _on_interrupt INT TERM HUP
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
}
arch_is_supported() {
for _ais in $B4_SUPPORTED_ARCHS; do
[ "$_ais" = "$1" ] && return 0
done
return 1
}
require_supported_arch() {
if [ -z "$1" ]; then
log_err "Could not determine this machine's architecture"
log_info "Pick one with --arch=<name>. Available: ${B4_SUPPORTED_ARCHS}"
exit 1
fi
if ! arch_is_supported "$1"; then
log_err "No published b4 build for architecture '$1'"
log_info "Pick one with --arch=<name>. Available: ${B4_SUPPORTED_ARCHS}"
exit 1
fi
}
_arch_nearest_supported() {
case "$1" in
mips64_softfloat) echo "mips64" ;;
mips64le_softfloat) echo "mips64le" ;;
*) echo "" ;;
esac
}
detect_architecture() {
_da_arch=$(_detect_architecture_raw) || return 1
if arch_is_supported "$_da_arch"; then
echo "$_da_arch"
return 0
fi
_da_alt=$(_arch_nearest_supported "$_da_arch")
if [ -n "$_da_alt" ]; then
log_warn "No '${_da_arch}' build is published; using '${_da_alt}' instead"
echo "$_da_alt"
return 0
fi
log_err "Detected architecture '${_da_arch}' has no published build"
log_info "Pick one manually with --arch=<name>. Available: ${B4_SUPPORTED_ARCHS}"
return 1
}
_detect_architecture_raw() {
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:[[: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"
return 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
[ "$(_byte_at /bin/sh 5)" = "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=$(_byte_at "$_sf_elf_bin" 4)
_sf_ei_data=$(_byte_at "$_sf_elf_bin" 5)
_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=$(_byte_at "$_sf_elf_bin" "$_sf_check_off")
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
return 1
}
_https_works_unverified() {
if command_exists curl && curl -sI -k --max-time 5 "https://github.com" >/dev/null 2>&1; then
return 0
fi
command_exists wget && wget --spider -q --timeout=5 --no-check-certificate "https://github.com" 2>/dev/null
}
_install_ca_certificates() {
if command_exists opkg; then
opkg update >/dev/null 2>&1 || true
opkg install ca-certificates >/dev/null 2>&1 || true
opkg install ca-bundle >/dev/null 2>&1 || true
opkg install wget-ssl >/dev/null 2>&1 || true
hash -r 2>/dev/null || true
return 0
fi
if command_exists apk; then
apk update >/dev/null 2>&1 || true
apk add ca-certificates >/dev/null 2>&1 || true
apk add wget-ssl >/dev/null 2>&1 || apk add wget >/dev/null 2>&1 || true
hash -r 2>/dev/null || true
return 0
fi
return 1
}
ensure_https_support() {
if check_https_support; then
return 0
fi
log_warn "Verified HTTPS not available - trying to install CA certificates"
if _install_ca_certificates && check_https_support; then
log_ok "CA certificates installed, verified HTTPS now works"
return 0
fi
if ! _https_works_unverified; then
log_err "HTTPS not available. Cannot download from GitHub."
log_info "On Entware/OpenWrt: opkg install wget-ssl ca-certificates"
return 1
fi
log_warn "HTTPS works only WITHOUT certificate verification (CA certs missing)"
log_warn "Downloads could not be authenticated: the archive AND its checksum"
log_warn "would both come over a connection nobody can verify."
if [ "${B4_ALLOW_INSECURE_TLS:-0}" = "1" ]; then
log_warn "B4_ALLOW_INSECURE_TLS=1 - continuing over unverified TLS"
WGET_INSECURE="--no-check-certificate"
CURL_INSECURE="-k"
return 0
fi
if [ "$QUIET_MODE" -eq 1 ] 2>/dev/null; then
log_err "Refusing to download over unverified TLS in quiet mode."
log_info "Install CA certificates, or re-run with B4_ALLOW_INSECURE_TLS=1 to accept the risk."
return 1
fi
if confirm "Continue over UNVERIFIED TLS anyway?" "n"; then
WGET_INSECURE="--no-check-certificate"
CURL_INSECURE="-k"
return 0
fi
log_err "Aborted: no verified HTTPS available."
log_info "On Entware/OpenWrt: opkg install wget-ssl ca-certificates"
return 1
}
MIRROR_OWNERS="DanielLavrushin Loyalsoldier runetfreedom XTLS Flowseal"
mirror_url() {
_mu_base="$1"
_mu_url="$2"
for _mu_owner in $MIRROR_OWNERS; do
case "$_mu_url" in
https://raw.githubusercontent.com/${_mu_owner}/* | \
https://github.com/${_mu_owner}/* | \
https://api.github.com/repos/${_mu_owner}/*)
echo "${_mu_base}/github/${_mu_url}"
return 0
;;
esac
done
echo ""
}
sf_url() {
_su_prefix="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/"
case "$1" in
"${_su_prefix}"*)
echo "${B4_SF_BASE}/${1#"$_su_prefix"}"
;;
*) echo "" ;;
esac
}
_wget_supports() {
wget --help 2>&1 | grep -qF -- "$1"
}
_wget_timeout_opt() {
if _wget_supports "--timeout"; then
echo "--timeout=$1"
elif _wget_supports "-T SEC"; then
echo "-T $1"
fi
}
_wget_once_opt() {
if _wget_supports "--tries"; then
echo "--tries=1"
fi
}
mirror_alive() {
_ma_base="$1"
if command_exists curl; then
curl -sf $CURL_INSECURE --connect-timeout "$B4_CONNECT_TIMEOUT" \
--max-time "$B4_PROBE_TIMEOUT" -o /dev/null \
"${_ma_base}/b4/health" 2>/dev/null && return 0
return 1
fi
if command_exists wget; then
_ma_args="-q $WGET_INSECURE -O /dev/null $(_wget_timeout_opt "$B4_PROBE_TIMEOUT") $(_wget_once_opt)"
wget $_ma_args "${_ma_base}/b4/health" 2>/dev/null && return 0
fi
return 1
}
_wget_guarded() {
_wg_out="$1"
_wg_quiet="$2"
shift 2
if [ "$_wg_quiet" = "1" ]; then
wget "$@" 2>/dev/null &
else
wget "$@" &
fi
_wg_pid=$!
B4_FETCH_PID=$_wg_pid
_wg_max="${B4_FETCH_MAX_TIME:-$B4_MAX_TIME}"
_wg_prev=0
_wg_stall=0
_wg_elapsed=0
_wg_tick=5
_wg_floor=$((1024 * 5))
while kill -0 "$_wg_pid" 2>/dev/null; do
sleep "$_wg_tick"
kill -0 "$_wg_pid" 2>/dev/null || break
_wg_elapsed=$((_wg_elapsed + _wg_tick))
_wg_now=$(wc -c 2>/dev/null <"$_wg_out" | awk '{print $1}')
if [ -z "$_wg_now" ]; then
_wg_now=0
fi
if [ $((_wg_now - _wg_prev)) -lt "$_wg_floor" ]; then
_wg_stall=$((_wg_stall + _wg_tick))
else
_wg_stall=0
fi
_wg_prev=$_wg_now
_wg_over=0
if [ "$_wg_max" -gt 0 ] 2>/dev/null && [ "$_wg_elapsed" -ge "$_wg_max" ]; then
_wg_over=1
fi
if [ "$_wg_stall" -ge "$B4_STALL_TIMEOUT" ] || [ "$_wg_over" -eq 1 ]; then
kill "$_wg_pid" 2>/dev/null || true
wait "$_wg_pid" 2>/dev/null || true
B4_FETCH_PID=""
return 1
fi
done
_wg_rc=0
wait "$_wg_pid" || _wg_rc=$?
B4_FETCH_PID=""
return "$_wg_rc"
}
_do_fetch() {
_df_url="$1"
_df_out="$2"
_df_max="${B4_FETCH_MAX_TIME:-$B4_MAX_TIME}"
_df_progress=0
[ -t 2 ] && [ "$QUIET_MODE" -ne 1 ] && _df_progress=1
if command_exists curl; then
_df_args="-fL $CURL_INSECURE --connect-timeout $B4_CONNECT_TIMEOUT --speed-limit 1024 --speed-time $B4_STALL_TIMEOUT"
if [ "$_df_max" -gt 0 ] 2>/dev/null; then
_df_args="$_df_args --max-time $_df_max"
fi
_df_rc=0
if [ "$_df_progress" -eq 1 ]; then
curl $_df_args --progress-bar -o "$_df_out" "$_df_url" 2>&1 || _df_rc=$?
else
curl -s $_df_args -o "$_df_out" "$_df_url" 2>/dev/null || _df_rc=$?
fi
case "$_df_rc" in
0) return 0 ;;
23) return 2 ;;
18 | 22 | 28 | 56 | 63) return 1 ;;
esac
fi
command_exists wget || return 1
rm -f "$_df_out" 2>/dev/null || true
if [ "$_df_progress" -eq 1 ]; then
_df_wargs="$WGET_INSECURE"
_wget_supports "--show-progress" && _df_wargs="$_df_wargs --show-progress -q"
else
_df_wargs="-q $WGET_INSECURE"
fi
_df_wargs="$_df_wargs $(_wget_timeout_opt "$B4_STALL_TIMEOUT")"
_wget_supports "--connect-timeout" && _df_wargs="$_df_wargs --connect-timeout=$B4_CONNECT_TIMEOUT"
_df_rc=0
_wget_guarded "$_df_out" $((1 - _df_progress)) $_df_wargs -O "$_df_out" "$_df_url" || _df_rc=$?
case "$_df_rc" in
0) return 0 ;;
3) return 2 ;;
esac
return 1
}
_fetch_attempt() {
_fa_url="$1"
_fa_part="$2"
_fa_check="$3"
_fa_orig="$4"
rm -f "$_fa_part" 2>/dev/null || true
_fa_rc=0
_do_fetch "$_fa_url" "$_fa_part" || _fa_rc=$?
if [ "$_fa_rc" -eq 0 ] && [ -s "$_fa_part" ]; then
if [ -z "$_fa_check" ] || "$_fa_check" "$_fa_part" "$_fa_url" "$_fa_orig"; then
return 0
fi
fi
rm -f "$_fa_part" 2>/dev/null || true
[ "$_fa_rc" -eq 2 ] && return 2
return 1
}
fetch_file() {
_ff_src="$1"
_ff_out="$2"
_ff_check="$3"
_ff_part="${_ff_out}.part"
if ! command_exists curl && ! command_exists wget; then
log_err "Neither curl nor wget found"
return 1
fi
B4_FETCH_TMP="$_ff_part"
_ff_rc=0
_fetch_attempt "$_ff_src" "$_ff_part" "$_ff_check" "$_ff_src" || _ff_rc=$?
_ff_announced=0
if [ "$_ff_rc" -eq 1 ]; then
for _ff_base in $B4_MIRRORS; do
_ff_url=$(mirror_url "$_ff_base" "$_ff_src")
[ -z "$_ff_url" ] && continue
mirror_alive "$_ff_base" || continue
if [ "$_ff_announced" -eq 0 ]; then
log_warn "Direct download failed, trying mirrors..."
_ff_announced=1
fi
log_info "Mirror: ${_ff_base}"
_ff_rc=0
_fetch_attempt "$_ff_url" "$_ff_part" "$_ff_check" "$_ff_src" || _ff_rc=$?
[ "$_ff_rc" -eq 1 ] || break
done
fi
if [ "$_ff_rc" -eq 1 ]; then
_ff_sf=$(sf_url "$_ff_src")
if [ -n "$_ff_sf" ]; then
if [ "$_ff_announced" -eq 0 ]; then
log_warn "Direct download failed, trying mirrors..."
_ff_announced=1
fi
log_info "Mirror: SourceForge"
_ff_rc=0
_fetch_attempt "$_ff_sf" "$_ff_part" "$_ff_check" "$_ff_src" || _ff_rc=$?
fi
fi
if [ "$_ff_rc" -eq 0 ]; then
flush_disk
if mv -f "$_ff_part" "$_ff_out" 2>/dev/null; then
B4_FETCH_TMP=""
return 0
fi
log_err "Could not move the download into place: ${_ff_out}"
elif [ "$_ff_rc" -eq 2 ]; then
log_err "Could not write ${_ff_part} (disk full or not writable)"
fi
rm -f "$_ff_part" 2>/dev/null || true
B4_FETCH_TMP=""
log_err "Failed to download: $_ff_src"
return 1
}
_content_length() {
tr -d '\r' | awk '
{ sub(/^[ \t]+/, "") }
/^HTTP\// { v = ""; ok = ($2 ~ /^2/) }
tolower($1) == "content-length:" { v = $2 }
END { if (ok && v ~ /^[0-9]+$/ && v > 0) print v }'
}
remote_size() {
_rs_url="$1"
_rs_max="${2:-25}"
_rs_len=""
if command_exists curl; then
_rs_len=$(curl -sIL $CURL_INSECURE --connect-timeout "$B4_CONNECT_TIMEOUT" \
--max-time "$_rs_max" "$_rs_url" 2>/dev/null | _content_length)
fi
if [ -z "$_rs_len" ] && command_exists wget; then
_rs_args="$WGET_INSECURE $(_wget_timeout_opt "$_rs_max") $(_wget_once_opt)"
_wget_supports "--connect-timeout" && _rs_args="$_rs_args --connect-timeout=$B4_CONNECT_TIMEOUT"
_rs_len=$(wget -S --spider $_rs_args "$_rs_url" 2>&1 | _content_length)
fi
[ -n "$_rs_len" ] || return 1
echo "$_rs_len"
}
_do_fetch_stdout() {
_dfs_url="$1"
if command_exists curl; then
curl -sfL $CURL_INSECURE --connect-timeout "$B4_CONNECT_TIMEOUT" --max-time 25 "$_dfs_url" 2>/dev/null && return 0
fi
if command_exists wget; then
_dfs_args="-qO- $WGET_INSECURE $(_wget_timeout_opt 25) $(_wget_once_opt)"
_wget_supports "--connect-timeout" && _dfs_args="$_dfs_args --connect-timeout=$B4_CONNECT_TIMEOUT"
wget $_dfs_args "$_dfs_url" 2>/dev/null && return 0
fi
return 1
}
fetch_stdout() {
_fs_src="$1"
_fs_out=$(_do_fetch_stdout "$_fs_src") && [ -n "$_fs_out" ] && echo "$_fs_out" && return 0
for _fs_base in $B4_MIRRORS; do
_fs_url=$(mirror_url "$_fs_base" "$_fs_src")
[ -z "$_fs_url" ] && continue
mirror_alive "$_fs_base" || continue
_fs_out=$(_do_fetch_stdout "$_fs_url") && [ -n "$_fs_out" ] && echo "$_fs_out" && return 0
done
return 1
}
_extract_tag_name() {
grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4
}
version_number() {
echo "$1" | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9A-Za-z.-]*' | head -1
}
get_latest_version() {
api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest"
version=$(fetch_stdout "$api_url" | _extract_tag_name)
if [ -z "$version" ]; then
for _glv_base in $B4_MIRRORS; do
mirror_alive "$_glv_base" || continue
version=$(_do_fetch_stdout "${_glv_base}/b4/api/releases/latest" | _extract_tag_name)
[ -n "$version" ] && break
done
fi
if [ -z "$version" ]; then
log_err "Failed to fetch latest version"
exit 1
fi
echo "$version"
}
sha256_of() {
_sh_file="$1"
_sh_out=$(sha256sum "$_sh_file" 2>/dev/null | awk '{print $1}')
if [ -n "$_sh_out" ]; then
echo "$_sh_out"
return 0
fi
_sh_out=$(busybox sha256sum "$_sh_file" 2>/dev/null | awk '{print $1}')
if [ -n "$_sh_out" ]; then
echo "$_sh_out"
return 0
fi
_sh_out=$(openssl dgst -sha256 "$_sh_file" 2>/dev/null | awk '{print $NF}')
if [ -n "$_sh_out" ]; then
echo "$_sh_out"
return 0
fi
return 1
}
verify_checksum() {
file="$1"
checksum_url="$2"
checksum_file="${file}.sha256"
if ! fetch_file "$checksum_url" "$checksum_file"; then
rm -f "$checksum_file"
log_warn "Could not fetch the published SHA256 for this archive"
return 1
fi
expected=$(awk '{print $1}' "$checksum_file")
rm -f "$checksum_file"
if [ -z "$expected" ]; then
log_warn "The published SHA256 for this archive is empty"
return 1
fi
actual=$(sha256_of "$file") || {
log_warn "No working sha256 tool found (tried sha256sum, busybox sha256sum, openssl)"
return 3
}
if [ "$expected" = "$actual" ]; then
if [ -n "$WGET_INSECURE" ]; then
log_warn "SHA256 matches ($actual) but was fetched over UNVERIFIED TLS - authenticity not proven"
else
log_ok "SHA256 verified: $actual"
fi
return 0
fi
log_err "SHA256 mismatch! Expected: $expected Got: $actual"
return 2
}
checksum_gate() {
_cg_ret=0
verify_checksum "$1" "$2" || _cg_ret=$?
[ "$_cg_ret" -eq 0 ] && return 0
if [ "$_cg_ret" -eq 2 ]; then
log_err "SHA256 mismatch: the archive is not the published release"
else
log_err "The archive could not be checked against its published SHA256"
fi
if [ "$QUIET_MODE" -eq 1 ]; then
log_err "Refusing to install an unverified binary unattended"
return 1
fi
confirm "Install it anyway?" "n"
}
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)
_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
}
_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
}
B4_PIDFILES="/var/run/b4.pid /run/b4.pid /opt/var/run/b4.pid /tmp/b4.pid"
_pid_is_b4() {
_pib="$1"
case "$_pib" in
'' | *[!0-9]*) return 1 ;;
esac
kill -0 "$_pib" 2>/dev/null || return 1
[ -d /proc/self ] || return 0
grep -q '^State:[[:space:]]*Z' "/proc/${_pib}/status" 2>/dev/null && return 1
if [ -r "/proc/${_pib}/cmdline" ]; then
case "$(tr '\0' '\n' <"/proc/${_pib}/cmdline" 2>/dev/null | head -1)" in
"$BINARY_NAME" | */"$BINARY_NAME") return 0 ;;
esac
return 1
fi
[ "$(cat "/proc/${_pib}/comm" 2>/dev/null)" = "$BINARY_NAME" ]
}
_b4_pid_candidates() {
for _bpc in $B4_PIDFILES; do
[ -f "$_bpc" ] && tr -d ' \t\r\n' <"$_bpc" 2>/dev/null
echo
done
if command_exists pidof; then
pidof "$BINARY_NAME" 2>/dev/null | tr ' ' '\n'
fi
if command_exists pgrep; then
pgrep -x "$BINARY_NAME" 2>/dev/null || true
fi
_ps_out=$(ps w 2>/dev/null || ps 2>/dev/null) || true
if [ -n "$_ps_out" ]; then
echo "$_ps_out" | grep -v grep | grep -E "[/ ]${BINARY_NAME}( |\$)" | awk '$1 ~ /^[0-9]+$/ {print $1}'
fi
return 0
}
b4_pids() {
_bps_out=""
for _bps_p in $(_b4_pid_candidates 2>/dev/null); do
case " $_bps_out " in
*" $_bps_p "*) continue ;;
esac
_pid_is_b4 "$_bps_p" || continue
_bps_out="${_bps_out} ${_bps_p}"
done
[ -n "$_bps_out" ] || return 1
echo $_bps_out
}
b4_pid() {
_bp_all=$(b4_pids) || return 1
set -- $_bp_all
echo "$1"
}
is_b4_running() {
b4_pids >/dev/null 2>&1
}
wait_for_b4_exit() {
_wbe_limit="${1:-20}"
_wbe_i=0
while is_b4_running; do
[ "$_wbe_i" -lt "$_wbe_limit" ] || return 1
sleep 1
_wbe_i=$((_wbe_i + 1))
[ $((_wbe_i % 5)) -eq 0 ] && log_info "Waiting for b4 to exit (${_wbe_i}s of ${_wbe_limit}s)"
done
return 0
}
stop_b4() {
_sb_pids=$(b4_pids) || return 0
log_info "Stopping running b4 process (PID: ${_sb_pids})..."
for _sb_p in $_sb_pids; do
kill "$_sb_p" 2>/dev/null || true
done
wait_for_b4_exit 18 && return 0
_sb_pids=$(b4_pids) || return 0
log_warn "b4 (PID: ${_sb_pids}) ignored SIGTERM for 18s, sending SIGKILL"
for _sb_p in $_sb_pids; do
kill -9 "$_sb_p" 2>/dev/null || true
done
wait_for_b4_exit 5 && return 0
_sb_pids=$(b4_pids) || return 0
log_err "b4 (PID: ${_sb_pids}) is still running after SIGKILL"
return 1
}
wait_for_new_b4() {
_wfn_old="$1"
_wfn_limit="${2:-15}"
_wfn_i=0
while [ "$_wfn_i" -lt "$_wfn_limit" ]; do
_wfn_new=$(b4_pid) || _wfn_new=""
if [ -n "$_wfn_new" ] && [ "$_wfn_new" != "$_wfn_old" ]; then
echo "$_wfn_new"
return 0
fi
sleep 1
_wfn_i=$((_wfn_i + 1))
done
return 1
}
b4_running_cmdline() {
_pid=$(b4_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
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
}
config_secure_perms() {
[ -n "$B4_CONFIG_FILE" ] || return 0
[ -f "$B4_CONFIG_FILE" ] || return 0
chmod 600 "$B4_CONFIG_FILE" 2>/dev/null || true
}
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
}
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_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
if ! read _INPUT; then
printf "\n" >&2
log_err "Aborted (no more input)"
exit 130
fi
_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"
if [ -n "$_user_bin_dir" ] && ! is_abs_path "$_user_bin_dir"; then
log_err "B4_BIN_DIR must be an absolute path (got: $_user_bin_dir)"
exit 1
fi
if [ -n "$_user_data_dir" ] && ! is_abs_path "$_user_data_dir"; then
log_err "B4_DATA_DIR must be an absolute path (got: $_user_data_dir)"
exit 1
fi
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) || B4_ARCH=""
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
while true; do
read_input "Binary directory [${B4_BIN_DIR}]: " "$B4_BIN_DIR"
if is_abs_path "$_INPUT"; then
B4_BIN_DIR="$_INPUT"
break
fi
log_warn "Binary directory must be an absolute path (got: ${_INPUT:-empty})"
done
while true; do
read_input "Data directory [${B4_DATA_DIR}]: " "$B4_DATA_DIR"
if is_abs_path "$_INPUT"; then
B4_DATA_DIR="$_INPUT"
B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json"
break
fi
log_warn "Data directory must be an absolute path (got: ${_INPUT:-empty})"
done
echo ""
echo " Service types:${REGISTERED_SERVICES}"
_svc_platform_type="$B4_SERVICE_TYPE"
while true; do
read_input "Service type [${B4_SERVICE_TYPE}]: " "$B4_SERVICE_TYPE"
_svc_ok=0
for _svc in $REGISTERED_SERVICES; do
[ "$_svc" = "$_INPUT" ] && _svc_ok=1
done
if [ "$_svc_ok" -eq 1 ]; then
B4_SERVICE_TYPE="$_INPUT"
break
fi
log_warn "Unknown service type '${_INPUT}'. Available:${REGISTERED_SERVICES}"
done
if [ "$B4_SERVICE_TYPE" != "$_svc_platform_type" ]; then
case "$B4_SERVICE_TYPE" in
systemd)
B4_SERVICE_DIR="/etc/systemd/system"
B4_SERVICE_NAME="b4.service"
;;
openrc | procd | sysv)
B4_SERVICE_DIR="/etc/init.d"
B4_SERVICE_NAME="b4"
;;
entware)
B4_SERVICE_DIR="/opt/etc/init.d"
B4_SERVICE_NAME="S99b4"
;;
none)
B4_SERVICE_DIR=""
B4_SERVICE_NAME=""
;;
esac
fi
auto_arch=$(detect_architecture 2>/dev/null) || auto_arch=""
_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_call_optional() {
func="$1"
shift
fn="platform_${B4_PLATFORM}_${func}"
type "$fn" >/dev/null 2>&1 || return 0
"$fn" "$@"
}
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_init() {
_pi_bin_dir="$B4_BIN_DIR"
_pi_data_dir="$B4_DATA_DIR"
platform_auto_detect || return 1
platform_call info
[ -n "$_pi_bin_dir" ] && B4_BIN_DIR="$_pi_bin_dir"
[ -n "$_pi_data_dir" ] && B4_DATA_DIR="$_pi_data_dir"
[ -n "$_pi_data_dir" ] && B4_CONFIG_FILE="${_pi_data_dir}/b4.json"
return 0
}
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 $B4_KERNEL_MODULES; do
_kmod_available "$mod" && continue
modprobe "$mod" 2>/dev/null || true
done
if ! _warn_if_queue_unavailable; then
case "$B4_PKG_MANAGER" in
apt) log_info "Try: apt install linux-modules-extra-$(uname -r) xtables-addons-common" ;;
dnf | yum) log_info "Try: dnf install kernel-modules-extra xtables-addons" ;;
pacman) log_info "Try: pacman -S xtables-addons" ;;
*) ;;
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 command_exists iptables && ! _nft_functional && ! command_exists ipset; then
rec_missing="${rec_missing} ipset"
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 $B4_KERNEL_MODULES; 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 ! _warn_if_queue_unavailable; then
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 'Kernel modules for Netfilter' in the router web UI: System settings > Component options"
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 ipset || rec_missing="${rec_missing} ipset"
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
}
B4_KEENETIC_HOOK="/opt/etc/ndm/netfilter.d/50-b4.sh"
platform_keenetic_install_hooks() {
ensure_dir "$(dirname "$B4_KEENETIC_HOOK")" "NDMS hook directory" || return 1
cat >"$B4_KEENETIC_HOOK" <<'EOF' || return 1
#!/bin/sh
[ "$type" = "iptables" ] || [ "$type" = "ip6tables" ] || exit 0
case "$table" in
mangle | nat | filter | raw) ;;
*) exit 0 ;;
esac
b4_ready() {
name=
while read -r k v; do
case "$k" in
Name:) name=$v ;;
SigCgt:)
[ "$name" = "b4" ] || return 1
case "${#v}:$v" in
16:*[2367abef]?? | 32:*[89abcdef]???) return 0 ;;
esac
return 1
;;
esac
done
return 1
}
for f in /var/run/b4.pid /run/b4.pid /tmp/b4.pid; do
[ -f "$f" ] || continue
pid=
read -r pid 2>/dev/null <"$f"
case "$pid" in
'' | *[!0-9]*) continue ;;
esac
b4_ready 2>/dev/null <"/proc/$pid/status" || continue
kill -USR1 "$pid" 2>/dev/null && exit 0
done
exit 0
EOF
chmod +x "$B4_KEENETIC_HOOK" || return 1
log_ok "NDMS netfilter hook installed: ${B4_KEENETIC_HOOK}"
}
platform_keenetic_remove_hooks() {
[ -f "$B4_KEENETIC_HOOK" ] || return 0
rm -f "$B4_KEENETIC_HOOK" 2>/dev/null || return 1
log_info "Removed NDMS netfilter hook: ${B4_KEENETIC_HOOK}"
}
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 $B4_KERNEL_MODULES; 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 ! _warn_if_queue_unavailable; then
log_info "Check that 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 ipset || rec_missing="${rec_missing} ipset"
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 $B4_KERNEL_MODULES; 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
_warn_if_queue_unavailable || true
}
_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 _nft_functional; 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 ! _kmod_available "nft_tproxy"; then
rec_missing="${rec_missing} kmod-nft-tproxy"
fi
if ! _kmod_available "nft_socket"; then
rec_missing="${rec_missing} kmod-nft-socket"
fi
if ! _kmod_available "nf_conntrack_netlink"; then
rec_missing="${rec_missing} kmod-nf-conntrack-netlink"
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=""
FAILED_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_prepare() {
for f in $ENABLED_FEATURES; do
type "feature_${f}_prepare" >/dev/null 2>&1 || continue
fname=$(feature_dispatch "$f" name)
log_header "Feature: ${fname}"
feature_dispatch "$f" prepare || true
done
}
features_run() {
for f in $ENABLED_FEATURES; do
fname=$(feature_dispatch "$f" name)
_fr_name="$fname"
log_header "Feature: ${_fr_name}"
if ! feature_dispatch "$f" run; then
log_warn "Feature '${_fr_name}' had issues"
FAILED_FEATURES="${FAILED_FEATURES:+${FAILED_FEATURES}, }${_fr_name}"
fi
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
(umask 077 && jq -n \
--arg user "$_auth_user" \
--arg pass "$_auth_pass" \
'{ system: { web_server: { username: $user, password: $pass } } }' \
>"$B4_CONFIG_FILE")
config_secure_perms
else
tmp="${B4_CONFIG_FILE}.tmp"
if (umask 077 && 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"
config_secure_perms
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|16
2|RUNET Freedom|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release|18
3|B4 GeoIP|https://github.com/DanielLavrushin/b4geoip/releases/latest/download|18"
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_prepare() {
_geo_prepare geoip
}
feature_geoip_run() {
_geo_commit geoip
}
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|11
2|RUNET Freedom|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release|74"
feature_geosite_name() {
echo "GeoSite data"
}
feature_geosite_description() {
echo "Download geosite.dat for domain categorization"
}
feature_geosite_default_enabled() {
echo "yes"
}
feature_geosite_prepare() {
_geo_prepare geosite
}
feature_geosite_run() {
_geo_commit geosite
}
feature_geosite_remove() {
_geo_remove_file "sitedat_path" "geosite.dat"
}
register_feature "geosite"
_geo_kind() {
case "$1" in
geosite)
_gk_sources="$GEOSITE_SOURCES"
_gk_default=2
_gk_path_key=sitedat_path
_gk_url_key=sitedat_url
_gk_label=GeoSite
;;
geoip)
_gk_sources="$GEOIP_SOURCES"
_gk_default=3
_gk_path_key=ipdat_path
_gk_url_key=ipdat_url
_gk_label=GeoIP
;;
*) return 1 ;;
esac
_gk_file="${1}.dat"
}
_geo_state_set() {
eval "_GEO_STATE_${1}=\$2"
eval "_GEO_TARGET_${1}=\$3"
eval "_GEO_CFG_${1}=\$4"
eval "_GEO_SRC_${1}=\$5"
}
_geo_state_get() {
eval "_gs_state=\${_GEO_STATE_${1}:-}"
eval "_gs_target=\${_GEO_TARGET_${1}:-}"
eval "_gs_cfg=\${_GEO_CFG_${1}:-}"
eval "_gs_src=\${_GEO_SRC_${1}:-}"
}
_geo_sum_parse() {
_gsp_sum=$(tr -d '\r' | awk 'NR == 1 { print tolower($1) }')
case "$_gsp_sum" in
'' | *[!0-9a-f]*) return 1 ;;
esac
[ "${#_gsp_sum}" -eq 64 ] || return 1
echo "$_gsp_sum"
}
_geodat_header_ok() {
_gh_bytes=""
_gh_at=0
while [ "$_gh_at" -lt 8 ]; do
_gh_byte=$(_byte_at "$1" "$_gh_at") || break
_gh_bytes="$_gh_bytes $_gh_byte"
_gh_at=$((_gh_at + 1))
done
echo "$_gh_bytes" | awk '
{
for (i = 1; i <= NF; i++) b[n++] = $i
if (n < 3 || b[0] != 10) exit 1
for (i = 1; i < n && i <= 5; i++) if (b[i] < 128) break
if (i >= n - 1 || i > 5) exit 1
exit (b[i + 1] == 10) ? 0 : 1
}
END { if (n == 0) exit 1 }'
}
_geodat_verify() {
_gv_file="$1"
_gv_url="$2"
_gv_orig="$3"
_gv_name=$(basename "$_gv_orig")
if ! _geodat_header_ok "$_gv_file"; then
log_warn "The server did not return a geodata file for ${_gv_name} (an error or block page?)"
return 1
fi
_gv_second=0
_gv_want=$(_do_fetch_stdout "${_gv_url}.sha256sum" | _geo_sum_parse) || _gv_want=""
if [ "$_gv_url" != "$_gv_orig" ]; then
if [ -n "$_gv_want" ]; then
_gv_second=1
else
_gv_want=$(fetch_stdout "${_gv_orig}.sha256sum" | _geo_sum_parse) || _gv_want=""
fi
fi
if [ -z "$_gv_want" ]; then
log_warn "No checksum is published for ${_gv_name}, accepting it unverified"
return 0
fi
_gv_have=$(sha256_of "$_gv_file") || {
log_warn "No working sha256 tool found, accepting ${_gv_name} unverified"
return 0
}
[ "$_gv_have" = "$_gv_want" ] && return 0
if [ "$_gv_second" -eq 1 ]; then
_gv_alt=$(fetch_stdout "${_gv_orig}.sha256sum" | _geo_sum_parse) || _gv_alt=""
if [ -n "$_gv_alt" ] && [ "$_gv_alt" != "$_gv_want" ]; then
[ "$_gv_have" = "$_gv_alt" ] && return 0
_gv_want="${_gv_want} or ${_gv_alt}"
fi
fi
log_warn "Checksum mismatch for ${_gv_name}: published ${_gv_want}, got ${_gv_have}"
return 1
}
_geo_kept_notice() {
if [ -f "$2" ]; then
log_err "The current ${1}.dat is kept"
else
log_warn "b4 skips ${1} categories until ${1}.dat is downloaded"
fi
}
_geo_mb() {
awk -v b="$1" 'BEGIN { printf "%.1f MB", b / 1048576 }'
}
_geo_existing_dir() {
_ged_dir="$1"
while [ -n "$_ged_dir" ] && [ "$_ged_dir" != "/" ] && [ ! -d "$_ged_dir" ]; do
_ged_dir=$(dirname "$_ged_dir")
done
echo "${_ged_dir:-/}"
}
_geo_source_field() {
echo "$_gk_sources" | awk -F'|' -v n="$1" -v f="$2" '$1 == n { print $f; exit }'
}
_geo_probe_sizes() {
_gp_sizes="${TEMP_DIR:-/tmp}/geo_sizes_${_gp_kind}_$$"
rm -rf "$_gp_sizes" 2>/dev/null || true
mkdir -p "$_gp_sizes" 2>/dev/null || return 0
echo "$_gk_sources" | {
while IFS='|' read -r _gps_num _gps_name _gps_base _gps_approx; do
[ -n "$_gps_num" ] || continue
(remote_size "${_gps_base}/${_gk_file}" "$B4_PROBE_TIMEOUT" >"${_gp_sizes}/${_gps_num}" 2>/dev/null || true) &
done
wait
}
}
_geo_source_size() {
_gss_live=$(cat "${_gp_sizes}/$1" 2>/dev/null) || _gss_live=""
case "$_gss_live" in
'' | *[!0-9]*) ;;
*)
echo "$_gss_live live"
return 0
;;
esac
_gss_mb=$(_geo_source_field "$1" 4)
case "$_gss_mb" in
'' | *[!0-9]*) ;;
*) echo "$((_gss_mb * 1048576)) approx" ;;
esac
}
_geo_source_bytes() {
_geo_source_size "$1" | cut -d' ' -f1
}
_geo_size_text() {
_gst_size=$(_geo_source_size "$1")
case "$_gst_size" in
*live) _geo_mb "${_gst_size% live}" ;;
*approx) echo "~$(_geo_source_field "$1" 4) MB" ;;
esac
}
_geo_source_label() {
_gsl_label=$(_geo_source_field "$1" 2)
_gsl_size=$(_geo_size_text "$1")
if [ -n "$_gsl_size" ]; then
_gsl_label="${_gsl_label}, ${_gsl_size}"
fi
if [ "$1" = "$_gk_default" ]; then
_gsl_label="${_gsl_label}, recommended"
fi
echo "$_gsl_label"
}
_geo_fits() {
case "$1" in
'' | *[!0-9]*) return 0 ;;
esac
case "$_gp_avail" in
'' | *[!0-9]*) return 0 ;;
esac
[ "$_gp_avail" -ge $((($1 + 1023) / 1024 + 1024)) ]
}
_geo_pick_default() {
_gp_pick="$_gk_default"
_gpd_bytes=$(_geo_source_bytes "$_gk_default")
if _geo_fits "$_gpd_bytes"; then
return 0
fi
_gpd_best=""
_gpd_best_bytes=""
for _gpd_num in $(echo "$_gk_sources" | cut -d'|' -f1); do
_gpd_size=$(_geo_source_bytes "$_gpd_num")
case "$_gpd_size" in
'' | *[!0-9]*) continue ;;
esac
_geo_fits "$_gpd_size" || continue
if [ -z "$_gpd_best" ] || [ "$_gpd_size" -lt "$_gpd_best_bytes" ]; then
_gpd_best="$_gpd_num"
_gpd_best_bytes="$_gpd_size"
fi
done
if [ -z "$_gpd_best" ]; then
return 0
fi
_gp_pick="$_gpd_best"
_gp_note="$(_geo_source_field "$_gk_default" 2) ($(_geo_size_text "$_gk_default")) does not fit in ${_gp_dir} ($(_geo_mb $((_gp_avail * 1024))) free), so the default is $(_geo_source_field "$_gpd_best" 2) ($(_geo_size_text "$_gpd_best"))"
}
_geo_smaller_hint() {
[ -n "$_gp_sizes" ] || return 0
_gp_avail=$(get_avail_kb "$(dirname "$_gp_target")") || _gp_avail=""
for _gsh_num in $(echo "$_gk_sources" | cut -d'|' -f1); do
[ "$_gsh_num" != "$_gp_pick" ] || continue
_gsh_bytes=$(_geo_source_bytes "$_gsh_num")
case "$_gsh_bytes" in
'' | *[!0-9]*) continue ;;
esac
if _geo_fits "$_gsh_bytes"; then
log_info "$(_geo_source_field "$_gsh_num" 2) ($(_geo_size_text "$_gsh_num")) fits in the free space"
fi
done
}
_geo_room_ok() {
_gr_target="$1"
_gr_src="$2"
_gr_bytes="${3:-}"
_gr_dir=$(dirname "$_gr_target")
case "$_gr_bytes" in
-) _gr_bytes="" ;;
'') _gr_bytes=$(remote_size "$_gr_src") || _gr_bytes="" ;;
esac
if [ -z "$_gr_bytes" ] && [ -f "$_gr_target" ]; then
_gr_bytes=$(wc -c 2>/dev/null <"$_gr_target" | awk '{print $1}')
fi
case "$_gr_bytes" in
'' | *[!0-9]*) return 0 ;;
esac
_gr_avail=$(get_avail_kb "$_gr_dir")
case "$_gr_avail" in
'' | *[!0-9]*) return 0 ;;
esac
_gr_need=$(((_gr_bytes + 1023) / 1024 + 1024))
_gr_for=""
_gr_bin="${TEMP_DIR}/${BINARY_NAME}"
if [ -f "$_gr_bin" ] && [ -d "$B4_BIN_DIR" ] &&
same_filesystem "$_gr_dir" "$B4_BIN_DIR" && ! same_filesystem "$TEMP_DIR" "$B4_BIN_DIR"; then
_gr_binkb=$(du -k "$_gr_bin" 2>/dev/null | awk '{print $1}')
case "$_gr_binkb" in
'' | *[!0-9]*) ;;
*)
_gr_need=$((_gr_need + _gr_binkb + BIN_SLACK_KB))
_gr_for=", including room for the new b4 binary"
;;
esac
fi
[ "$_gr_avail" -ge "$_gr_need" ] && return 0
log_err "Not enough space in ${_gr_dir}: ${_gr_avail}KB free, ${_gr_need}KB needed${_gr_for}"
return 1
}
_geo_prepare() {
_gp_kind="$1"
_geo_kind "$_gp_kind" || return 1
_geo_state_set "$_gp_kind" failed "" "" ""
_gp_dir="$B4_DATA_DIR"
_gp_current=""
if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then
_gp_existing=$(jq -r ".system.geo.${_gk_path_key} // empty" "$B4_CONFIG_FILE" 2>/dev/null) || true
if [ -n "$_gp_existing" ] && [ "$_gp_existing" != "null" ]; then
if is_abs_path "$_gp_existing"; then
_gp_dir=$(dirname "$_gp_existing")
log_info "Found existing ${_gp_kind} path: $_gp_dir"
else
log_warn "Ignoring non-absolute ${_gp_kind} path in config: $_gp_existing"
fi
fi
_gp_current=$(jq -r ".system.geo.${_gk_url_key} // empty" "$B4_CONFIG_FILE" 2>/dev/null) || true
[ "$_gp_current" != "null" ] || _gp_current=""
fi
_gp_pick=""
_gp_custom=""
if [ -n "$_gp_current" ]; then
_gp_pick=$(echo "$_gk_sources" | awk -F'|' -v u="$_gp_current" -v f="$_gk_file" '$3 "/" f == u { print $1; exit }')
if [ -z "$_gp_pick" ]; then
_gp_custom=$(($(echo "$_gk_sources" | grep -c '|') + 1))
_gp_pick="$_gp_custom"
fi
fi
_gp_sizes=""
_gp_avail=""
_gp_note=""
if [ "$QUIET_MODE" -ne 1 ] || [ -z "$_gp_pick" ]; then
log_info "Checking the size of each ${_gk_label} source..."
_geo_probe_sizes
_gp_avail=$(get_avail_kb "$(_geo_existing_dir "$_gp_dir")") || _gp_avail=""
fi
if [ -z "$_gp_pick" ]; then
_geo_pick_default
if [ -n "$_gp_note" ] && [ "$QUIET_MODE" -eq 1 ]; then
log_warn "$_gp_note"
fi
fi
if [ "$QUIET_MODE" -ne 1 ]; then
log_sep
echo ""
echo " Available ${_gp_kind} sources:"
for _gp_num in $(echo "$_gk_sources" | cut -d'|' -f1); do
_gp_line=$(_geo_source_label "$_gp_num")
if [ -n "$_gp_current" ] && [ "$_gp_num" = "$_gp_pick" ]; then
_gp_line="${_gp_line}, in use"
fi
printf " ${BOLD}%s${NC}) %s\n" "$_gp_num" "$_gp_line"
done
if [ -n "$_gp_custom" ]; then
printf " ${BOLD}%s${NC}) %s\n" "$_gp_custom" "Source in use: ${_gp_current}"
fi
echo ""
if [ -n "$_gp_note" ]; then
log_warn "$_gp_note"
fi
read_input "Select source [${_gp_pick}]: " "$_gp_pick"
if [ -n "$_gp_custom" ] && [ "$_INPUT" = "$_gp_custom" ]; then
_gp_pick="$_gp_custom"
elif [ -n "$(_geo_source_field "$_INPUT" 3)" ]; then
_gp_pick="$_INPUT"
else
log_warn "Invalid selection, using default"
fi
fi
_gp_known=""
if [ -n "$_gp_custom" ] && [ "$_gp_pick" = "$_gp_custom" ]; then
_gp_src="$_gp_current"
else
_gp_src="$(_geo_source_field "$_gp_pick" 3)/${_gk_file}"
case "$(_geo_source_size "$_gp_pick")" in
*live) _gp_known=$(_geo_source_bytes "$_gp_pick") ;;
*) [ -z "$_gp_sizes" ] || _gp_known="-" ;;
esac
fi
if [ "$QUIET_MODE" -ne 1 ]; then
while true; do
read_input "Save directory [${_gp_dir}]: " "$_gp_dir"
if is_abs_path "$_INPUT"; then
_gp_dir="$_INPUT"
break
fi
log_warn "Save directory must be an absolute path (got: ${_INPUT:-empty})"
done
fi
if ! is_abs_path "$_gp_dir"; then
log_err "${_gk_label} save directory must be an absolute path (got: ${_gp_dir:-empty})"
return 1
fi
ensure_dir "$_gp_dir" "${_gk_label} directory" || return 1
_gp_cfg="${_gp_dir}/${_gk_file}"
_gp_target="$_gp_cfg"
if [ -L "$_gp_target" ]; then
_gp_real=$(readlink -f "$_gp_target" 2>/dev/null) || _gp_real=""
[ -n "$_gp_real" ] && _gp_target="$_gp_real"
fi
_gp_new="${_gp_target}.new"
rm -f "$_gp_new" "${_gp_new}.part" 2>/dev/null || true
_gp_want=$(fetch_stdout "${_gp_src}.sha256sum" | _geo_sum_parse) || _gp_want=""
if [ -n "$_gp_want" ] && [ -f "$_gp_target" ]; then
_gp_have=$(sha256_of "$_gp_target") || _gp_have=""
if [ "$_gp_have" = "$_gp_want" ]; then
log_ok "${_gk_file} is up to date"
_geo_state_set "$_gp_kind" fresh "$_gp_target" "$_gp_cfg" "$_gp_src"
return 0
fi
fi
if ! _geo_room_ok "$_gp_target" "$_gp_src" "$_gp_known"; then
_geo_smaller_hint
_geo_kept_notice "$_gp_kind" "$_gp_target"
return 1
fi
log_info "Downloading ${_gk_file}..."
pending_add "$_gp_new"
B4_FETCH_MAX_TIME="$B4_GEO_MAX_TIME"
_gp_rc=0
fetch_file "$_gp_src" "$_gp_new" _geodat_verify || _gp_rc=$?
B4_FETCH_MAX_TIME=""
if [ "$_gp_rc" -ne 0 ]; then
rm -f "$_gp_new" 2>/dev/null || true
pending_drop "$_gp_new"
log_err "Failed to download ${_gk_file}"
_geo_kept_notice "$_gp_kind" "$_gp_target"
return 1
fi
log_ok "${_gk_file} downloaded"
_geo_state_set "$_gp_kind" pending "$_gp_target" "$_gp_cfg" "$_gp_src"
return 0
}
_geo_commit() {
_gc_kind="$1"
_geo_kind "$_gc_kind" || return 1
_geo_state_get "$_gc_kind"
if [ -z "$_gs_state" ]; then
_geo_prepare "$_gc_kind" || true
_geo_state_get "$_gc_kind"
fi
case "$_gs_state" in
pending)
_gc_new="${_gs_target}.new"
if ! mv -f "$_gc_new" "$_gs_target" 2>/dev/null; then
rm -f "$_gc_new" 2>/dev/null || true
pending_drop "$_gc_new"
log_err "Could not replace ${_gs_target}"
_geo_kept_notice "$_gc_kind" "$_gs_target"
_geo_state_set "$_gc_kind" failed "" "" ""
return 1
fi
flush_disk
pending_drop "$_gc_new"
log_ok "${_gk_file} installed to $(dirname "$_gs_cfg")"
_geo_state_set "$_gc_kind" fresh "$_gs_target" "$_gs_cfg" "$_gs_src"
;;
fresh) ;;
*) return 1 ;;
esac
_geo_update_config "$_gk_path_key" "$_gs_cfg" "$_gk_url_key" "$_gs_src"
}
_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
(umask 077 && jq -n \
--arg pv "$path_val" \
--arg uv "$url_val" \
"{ system: { geo: { ${path_key}: \$pv, ${url_key}: \$uv } } }" \
>"$B4_CONFIG_FILE")
config_secure_perms
log_ok "Created config with ${path_key}"
return 0
fi
tmp="${B4_CONFIG_FILE}.tmp"
if (umask 077 && 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"
config_secure_perms
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 > System > 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
(umask 077 && jq -n \
--arg cert "$cert_path" \
--arg key "$key_path" \
'{ system: { web_server: { tls_cert: $cert, tls_key: $key } } }' \
>"$B4_CONFIG_FILE")
config_secure_perms
else
tmp="${B4_CONFIG_FILE}.tmp"
if (umask 077 && 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"
config_secure_perms
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 (umask 077 && 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"
config_secure_perms
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_init_gen() {
_sig_fn="service_${1}_init_gen"
if type "$_sig_fn" >/dev/null 2>&1; then
"$_sig_fn"
else
echo 2
fi
}
service_stop_b4() {
if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then
service_call stop || true
wait_for_b4_exit 20 && return 0
log_info "b4 is still running after the service stop, stopping it directly"
fi
stop_b4
}
service_verify_started() {
_svs_old="$1"
if _svs_new=$(wait_for_new_b4 "$_svs_old" "${2:-15}"); then
log_ok "Service started (PID: ${_svs_new})"
return 0
fi
log_err "Service failed to start"
service_show_crash_log
return 1
}
service_show_crash_log() {
_logdir=""
if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then
if [ "$(jq -r '(.system.logging // {}) | has("directory")' "$B4_CONFIG_FILE" 2>/dev/null)" = "true" ]; then
_logdir=$(jq -r '.system.logging.directory // ""' "$B4_CONFIG_FILE" 2>/dev/null)
[ -z "$_logdir" ] && return 0
else
_ef=$(jq -r '.system.logging.error_file // empty' "$B4_CONFIG_FILE" 2>/dev/null)
[ -n "$_ef" ] && _logdir=$(dirname "$_ef")
fi
fi
[ -z "$_logdir" ] && _logdir="/var/log/b4"
_errlog="${_logdir}/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
return 0
fi
log_info "No entries in ${_errlog}."
if [ "$B4_SERVICE_TYPE" = "systemd" ]; then
log_info "Check: journalctl -u ${B4_SERVICE_NAME:-b4} --no-pager -n 30"
fi
log_info "Or run it in the foreground: ${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}"
}
_service_write_standalone_init() {
_swsi_path="$1"
_swsi_pidfile="$2"
cat >"$_swsi_path" <<EOF || return 1
#!/bin/sh
B4_INIT_GEN=2
PROG="${B4_BIN_DIR}/${BINARY_NAME}"
CONFIG="${B4_CONFIG_FILE}"
PIDFILE="${_swsi_pidfile}"
export PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
mkdir -p "\$(dirname "\$PIDFILE")" 2>/dev/null || PIDFILE="/var/run/b4.pid"
kernel_mod_load() {
KERNEL=\$(uname -r)
for mod in $B4_KERNEL_MODULES; 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
}
b4_is_b4() {
case "\$1" in
'' | *[!0-9]*) return 1 ;;
esac
kill -0 "\$1" 2>/dev/null || return 1
[ -d /proc/self ] || return 0
grep -q '^State:[[:space:]]*Z' "/proc/\$1/status" 2>/dev/null && return 1
case "\$(tr '\0' '\n' <"/proc/\$1/cmdline" 2>/dev/null | head -1)" in
${BINARY_NAME} | */${BINARY_NAME}) return 0 ;;
esac
return 1
}
b4_pids() {
_out=""
for _q in \$(cat "\$PIDFILE" 2>/dev/null) \$(pidof ${BINARY_NAME} 2>/dev/null) \$(pgrep -x ${BINARY_NAME} 2>/dev/null); do
case " \$_out " in
*" \$_q "*) continue ;;
esac
b4_is_b4 "\$_q" && _out="\$_out \$_q"
done
[ -n "\$_out" ] || return 1
echo \$_out
}
b4_running() {
b4_pids >/dev/null 2>&1
}
start() {
echo "Starting b4..."
if b4_running; then
echo "Already running (PID: \$(b4_pids))"
return 1
fi
kernel_mod_load
_started=""
if which nohup >/dev/null 2>&1; then
nohup \$PROG --config \$CONFIG >/dev/null 2>&1 &
_started=\$!
elif which setsid >/dev/null 2>&1; then
setsid \$PROG --config \$CONFIG >/dev/null 2>&1 &
_started=\$!
else
(\$PROG --config \$CONFIG >/dev/null 2>&1 &)
fi
[ -n "\$_started" ] && echo "\$_started" >"\$PIDFILE"
sleep 2
if b4_running; then
echo "b4 started (PID: \$(b4_pids))"
return 0
fi
rm -f "\$PIDFILE"
echo "b4 failed to start, check /var/log/b4/errors.log"
return 1
}
stop() {
echo "Stopping b4..."
_pids=\$(b4_pids) || {
rm -f "\$PIDFILE"
echo "b4 is not running"
return 0
}
for _q in \$_pids; do
kill "\$_q" 2>/dev/null
done
_i=0
while [ "\$_i" -lt 20 ]; do
sleep 1
b4_running || {
rm -f "\$PIDFILE"
echo "b4 stopped"
return 0
}
_i=\$((_i + 1))
done
_pids=\$(b4_pids) || {
rm -f "\$PIDFILE"
echo "b4 stopped"
return 0
}
echo "b4 (PID: \$_pids) did not exit, sending SIGKILL"
for _q in \$_pids; do
kill -9 "\$_q" 2>/dev/null
done
sleep 1
if b4_running; then
echo "b4 is still running"
return 1
fi
rm -f "\$PIDFILE"
echo "b4 stopped"
}
case "\$1" in
start) start ;;
stop) stop ;;
restart) stop && start ;;
status)
if b4_running; then
echo "b4 is running (PID: \$(b4_pids))"
else
echo "b4 is not running"
exit 3
fi
;;
*)
echo "Usage: \$0 {start|stop|restart|status}"
exit 1
;;
esac
EOF
}
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 || return 1
else
_service_write_standalone_init "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" /opt/var/run/b4.pid || return 1
fi
chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" || return 1
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 || return 1
#!/bin/sh
# B4 DPI Bypass Service - Entware
B4_INIT_GEN=2
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 $B4_KERNEL_MODULES; 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_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() {
_init="${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
if [ ! -f "$_init" ]; then
log_warn "Could not start service"
return 1
fi
_old=$(b4_pid) || _old=""
"$_init" restart 2>/dev/null || {
log_warn "Could not start service"
return 1
}
service_verify_started "$_old"
}
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_init_gen() {
echo 3
}
service_openrc_install() {
ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1
cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" <<EOF || return 1
#!/sbin/openrc-run
name="b4"
description="B4 DPI Bypass Service"
B4_INIT_GEN=$(service_openrc_init_gen)
command="${B4_BIN_DIR}/${BINARY_NAME}"
command_args="--config ${B4_CONFIG_FILE}"
command_background=true
pidfile="/run/b4.pid"
retry="TERM/20/KILL/5"
output_log="/dev/null"
error_log="/dev/null"
export PATH="\${PATH}:/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
depend() {
need net
}
start_pre() {
# Load kernel modules
for mod in $B4_KERNEL_MODULES; do
modprobe "\$mod" >/dev/null 2>&1 || true
done
}
EOF
chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" || return 1
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() {
_old=$(b4_pid) || _old=""
rc-service "${B4_SERVICE_NAME}" restart 2>/dev/null || {
log_warn "Could not start service"
return 1
}
service_verify_started "$_old"
}
service_openrc_stop() {
rc-service "${B4_SERVICE_NAME}" stop 2>/dev/null || true
is_b4_running || return 0
log_info "OpenRC does not own the running b4, stopping it directly"
stop_b4
}
register_service "openrc"
service_procd_install() {
ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1
cat >"${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" <<EOF || return 1
#!/bin/sh /etc/rc.common
# B4 DPI Bypass Service (procd)
START=99
STOP=10
USE_PROCD=1
B4_INIT_GEN=2
PROG="${B4_BIN_DIR}/${BINARY_NAME}"
CONFIG="${B4_CONFIG_FILE}"
export 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 $B4_KERNEL_MODULES; 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 env PATH="\$PATH"
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 term_timeout 20
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "b4"
}
EOF
chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" || return 1
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() {
_init="${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
if [ ! -f "$_init" ]; then
log_warn "Could not start service"
return 1
fi
_old=$(b4_pid) || _old=""
"$_init" restart 2>/dev/null || {
log_warn "Could not start service"
return 1
}
service_verify_started "$_old"
}
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 || return 1
[Unit]
Description=B4 DPI Bypass Service
After=network.target
[Service]
Type=simple
User=root
Environment=PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
[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() {
systemctl reset-failed "${B4_SERVICE_NAME}" 2>/dev/null || true
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
_service_write_standalone_init "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" /var/run/b4.pid || {
log_err "Cannot write init script: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
return 1
}
chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" || return 1
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() {
_init="${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
if [ ! -f "$_init" ]; then
log_warn "Could not start service"
return 1
fi
_old=$(b4_pid) || _old=""
"$_init" restart 2>/dev/null || {
log_warn "Could not start service"
return 1
}
service_verify_started "$_old"
}
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"
if [ -n "$_user_bin_dir" ] && ! is_abs_path "$_user_bin_dir"; then
log_err "B4_BIN_DIR must be an absolute path (got: $_user_bin_dir)"
exit 1
fi
if [ -n "$_user_data_dir" ] && ! is_abs_path "$_user_data_dir"; then
log_err "B4_DATA_DIR must be an absolute path (got: $_user_data_dir)"
exit 1
fi
platform_init
if [ -n "$force_arch" ]; then
B4_ARCH="$force_arch"
else
B4_ARCH=$(detect_architecture) || B4_ARCH=""
fi
require_supported_arch "$B4_ARCH"
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"
require_supported_arch "$B4_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
checksum_gate "$archive_path" "${download_url}.sha256" || exit 1
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
if [ -n "$ENABLED_FEATURES" ]; then
features_prepare
fi
B4_WAS_RUNNING=0
is_b4_running && B4_WAS_RUNNING=1
service_stop_b4 || {
log_err "b4 is still running; refusing to replace the binary underneath it"
exit 1
}
rm -f /var/log/b4.log /opt/var/log/b4.log /tmp/log/b4.log 2>/dev/null || true
ensure_bin_space "$B4_BIN_DIR" "${TEMP_DIR}/${BINARY_NAME}" || exit 1
ts=$(date '+%Y%m%d_%H%M%S')
backup_bin="${B4_BIN_DIR}/${BINARY_NAME}.backup.${ts}"
if [ -f "${B4_BIN_DIR}/${BINARY_NAME}" ]; then
log_info "Existing binary set aside for rollback"
fi
stash_binary "${B4_BIN_DIR}/${BINARY_NAME}" "$backup_bin" || {
log_err "Could not move the existing binary aside"
exit 1
}
_newbin="${B4_BIN_DIR}/${BINARY_NAME}.new.$$"
rm -f "$_newbin" 2>/dev/null || true
_swap_failed=0
if ! { mv "${BINARY_NAME}" "$_newbin" 2>/dev/null || cp "${BINARY_NAME}" "$_newbin"; } ||
! chmod +x "$_newbin" ||
! mv -f "$_newbin" "${B4_BIN_DIR}/${BINARY_NAME}"; then
_swap_failed=1
fi
if [ "$_swap_failed" -eq 1 ]; then
rm -f "$_newbin" 2>/dev/null || true
log_err "Failed to install binary to ${B4_BIN_DIR}"
restore_binary "${B4_BIN_DIR}/${BINARY_NAME}" "$backup_bin" && log_warn "Rolled back to the previous version"
exit 1
fi
flush_disk
_ver_exit=0
_bin_verified=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}"
_bin_verified=1
rm -f "$backup_bin" 2>/dev/null || true
elif [ "$_ver_exit" -gt 128 ] && arch_is_supported "${B4_ARCH}_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}"
_sf_ok=0
if ! fetch_file "$_sf_url" "$_sf_archive"; then
log_err "Could not download softfloat variant"
log_info "Try reinstalling with: --arch=${_sf_arch}"
elif ! checksum_gate "$_sf_archive" "${_sf_url}.sha256"; then
rm -f "$_sf_archive" 2>/dev/null || true
else
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
_newbin="${B4_BIN_DIR}/${BINARY_NAME}.new.$$"
if mv "${BINARY_NAME}" "$_newbin" 2>/dev/null || cp "${BINARY_NAME}" "$_newbin"; then
chmod +x "$_newbin"
mv -f "$_newbin" "${B4_BIN_DIR}/${BINARY_NAME}" || rm -f "$_newbin"
flush_disk
fi
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"
_sf_ok=1
_bin_verified=1
rm -f "$backup_bin" 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
fi
if [ "$_sf_ok" -eq 0 ] && restore_binary "${B4_BIN_DIR}/${BINARY_NAME}" "$backup_bin"; then
log_warn "Rolled back to the previously installed version"
fi
else
log_warn "Binary installed but version check failed (exit code: $_ver_exit)"
if restore_binary "${B4_BIN_DIR}/${BINARY_NAME}" "$backup_bin"; then
log_warn "Rolled back to the previously installed version"
fi
fi
log_info "Setting up service..."
_svc_failed=0
service_call install || {
log_err "Service setup failed - b4 will not start automatically"
_svc_failed=1
}
if [ "$_bin_verified" -eq 1 ]; then
platform_call_optional install_hooks || log_warn "Platform hooks could not be installed"
else
platform_call_optional remove_hooks || true
fi
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 [ "$B4_SERVICE_TYPE" != "none" ]; then
if [ "$QUIET_MODE" -eq 1 ]; then
service_call start || log_warn "Could not start the b4 service"
elif is_b4_running; then
if confirm "B4 is already running. Restart now?"; then
service_call start || true
fi
elif [ "${B4_WAS_RUNNING:-0}" -eq 1 ]; then
if confirm "B4 was running before the update. Start it again?"; then
service_call start || true
fi
else
if confirm "Start B4 service now?"; then
service_call start || true
fi
fi
fi
echo ""
if [ -n "$FAILED_FEATURES" ]; then
log_err "Not completed: ${FAILED_FEATURES} (see the messages above)"
echo ""
fi
if [ "${_svc_failed:-0}" -eq 1 ]; then
log_err "B4 is installed, but its service was not set up - start it by hand:"
log_err " ${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}"
echo ""
return 1
fi
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"
platform_init || true
_remove_find_config
service_stop_b4 || {
log_err "b4 is still running and could not be stopped"
log_info "Stop it by hand and re-run, or its firewall rules will be left behind."
exit 1
}
_removed_any=0
_remove_netfilter_state
platform_call_optional remove_hooks || true
if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then
if [ -n "$B4_SERVICE_DIR" ] && [ -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" ]; then
_removed_any=1
fi
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"
_removed_any=1
fi
done
command_exists systemctl && systemctl daemon-reload 2>/dev/null || true
fi
features_remove
for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /jffs/b4 /ssd/b4 /tmp/b4; do
[ -z "$dir" ] && continue
if [ -f "${dir}/${BINARY_NAME}" ]; then
rm -f "${dir}/${BINARY_NAME}"
rm -f "${dir}/${BINARY_NAME}".backup.* 2>/dev/null || true
rm -f "${dir}/${BINARY_NAME}".new.* 2>/dev/null || true
log_info "Removed binary from: ${dir}"
_removed_any=1
fi
done
_stray=$(command -v "$BINARY_NAME" 2>/dev/null || true)
if [ -n "$_stray" ] && [ -f "$_stray" ]; then
log_warn "A b4 binary is still on PATH at ${_stray} - remove it by hand"
fi
_remove_config_dirs
rm -f /var/run/b4.pid /run/b4.pid /opt/var/run/b4.pid /tmp/b4.pid 2>/dev/null || true
rm -f /var/run/b4-tun.state /run/b4-tun.state /tmp/b4_sysctl_snapshot.json 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 ""
if [ "$_removed_any" -eq 1 ]; then
log_ok "B4 has been removed"
else
log_warn "Nothing to remove: no b4 binary or service was found"
fi
echo ""
}
_remove_netfilter_state() {
_rns_bin=""
for _rns_dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /jffs/b4 /ssd/b4 /tmp/b4; do
[ -n "$_rns_dir" ] && [ -x "${_rns_dir}/${BINARY_NAME}" ] || continue
_rns_bin="${_rns_dir}/${BINARY_NAME}"
break
done
if [ -n "$_rns_bin" ]; then
log_info "Clearing firewall and routing state..."
if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ]; then
_rns_out=$("$_rns_bin" --clear-tables --config "$B4_CONFIG_FILE" 2>&1) && return 0
else
_rns_out=$("$_rns_bin" --clear-tables 2>&1) && return 0
fi
log_warn "${_rns_bin} --clear-tables failed"
if [ -n "$_rns_out" ]; then
echo "$_rns_out" | tail -n 5 | while read -r _rns_line; do
printf " %s\n" "$_rns_line" >&2
done
fi
log_warn "Removing the known nftables tables directly"
fi
command_exists nft || return 0
for _rns_t in "inet b4_mangle" "inet b4_route" "inet b4_dscp" "ip b4_nat" "ip b4_dnsnat" "ip6 b4_dnsnat6"; do
nft delete table ${_rns_t} 2>/dev/null || true
done
}
_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
}
LEGACY_SERVICE_LOGS="/var/log/b4.log /opt/var/log/b4.log /tmp/log/b4.log"
installed_service_type() {
_svc="$1"
if grep -qF "[Service]" "$_svc" 2>/dev/null || grep -qF "ExecStart=" "$_svc" 2>/dev/null; then
echo "systemd"
elif grep -q "USE_PROCD\|procd_open_instance" "$_svc" 2>/dev/null; then
echo "procd"
elif grep -q "openrc-run" "$_svc" 2>/dev/null; then
echo "openrc"
elif grep -q "rc\.func\|/opt/var/run/b4\.pid" "$_svc" 2>/dev/null; then
echo "entware"
else
echo "sysv"
fi
}
installed_init_gen() {
_iig=$(sed -n 's/^B4_INIT_GEN=\([0-9][0-9]*\).*/\1/p' "$1" 2>/dev/null | head -1)
echo "${_iig:-0}"
}
_recover_service_paths() {
_rsp_svc="$1"
[ -f "$_rsp_svc" ] || return 1
_rsp_cfg=$(sed -n 's/^CONFIG="\([^"]*\)".*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
[ -z "$_rsp_cfg" ] && _rsp_cfg=$(sed -n 's/^ARGS="--config=\([^"]*\)".*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
[ -z "$_rsp_cfg" ] && _rsp_cfg=$(sed -n 's/.*--config[ =]"\([^"]*\)".*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
[ -z "$_rsp_cfg" ] && _rsp_cfg=$(sed -n 's/.*--config[ =]\([^" ]*\).*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
_rsp_prog=$(sed -n 's/^PROG="\([^"]*\)".*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
[ -z "$_rsp_prog" ] && _rsp_prog=$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
[ -z "$_rsp_prog" ] && _rsp_prog=$(sed -n 's/^command="\([^"]*\)".*/\1/p' "$_rsp_svc" 2>/dev/null | head -1)
case "$_rsp_cfg" in
/*)
case "$_rsp_prog" in
/*) B4_BIN_DIR=$(dirname "$_rsp_prog") ;;
esac
if [ "$_rsp_cfg" != "$B4_CONFIG_FILE" ]; then
log_info "Keeping the config path the installed service uses: ${_rsp_cfg}"
B4_CONFIG_FILE="$_rsp_cfg"
fi
return 0
;;
esac
return 1
}
refresh_legacy_service_script() {
_svc_refreshed=0
[ -z "$B4_SERVICE_DIR" ] && return 0
[ -z "$B4_SERVICE_NAME" ] && return 0
_svc="${B4_SERVICE_DIR}/${B4_SERVICE_NAME}"
[ -f "$_svc" ] || return 0
_svc_type=$(installed_service_type "$_svc")
_legacy_log=0
grep -q "b4\.log" "$_svc" 2>/dev/null && _legacy_log=1
_outdated=0
_installed_gen=$(installed_init_gen "$_svc")
_wanted_gen=$(service_init_gen "$_svc_type")
if grep -q "B4 DPI Bypass Service" "$_svc" 2>/dev/null && [ "$_installed_gen" -lt "$_wanted_gen" ]; then
_outdated=1
fi
[ "$_legacy_log" -eq 1 ] || [ "$_outdated" -eq 1 ] || return 0
if [ "$_svc_type" = "systemd" ]; then
if [ "$_legacy_log" -eq 1 ]; then
log_warn "Systemd unit ${_svc} sends b4 output to a legacy log file"
log_info "Leaving the unit as it is, b4 did not write it"
fi
return 0
fi
if [ "$_legacy_log" -eq 1 ]; then
log_warn "Init script logs b4 output to a legacy file that is never rotated"
else
log_info "Installed ${_svc_type} service script is generation ${_installed_gen}, current is ${_wanted_gen}"
fi
log_info "Refreshing ${_svc_type} service script: ${_svc}"
if _recover_service_paths "$_svc" && service_dispatch "$_svc_type" install >/dev/null 2>&1; then
log_ok "Service script refreshed"
_svc_refreshed=1
elif [ "$_legacy_log" -eq 1 ]; then
log_warn "Could not regenerate the service script safely, patching the redirect in place"
for _legacy in $LEGACY_SERVICE_LOGS; do
_esc=$(echo "$_legacy" | sed 's#\.#\\.#g')
sed -i "s#>${_esc} 2>&1#>/dev/null 2>\&1#g" "$_svc" 2>/dev/null || true
sed -i "s#\"${_esc}\"#\"/dev/null\"#g" "$_svc" 2>/dev/null || true
sed -i "s#${_esc}#/var/log/b4/errors.log#g" "$_svc" 2>/dev/null || true
done
_svc_refreshed=1
else
log_warn "Could not regenerate the service script safely, keeping the installed one"
fi
for _legacy in $LEGACY_SERVICE_LOGS; do
if [ -f "$_legacy" ]; then
_sz=$(du -sk "$_legacy" 2>/dev/null | awk '{print $1}')
[ -n "$_sz" ] && log_info "Removing stale log ${_legacy} (${_sz} KB)"
rm -f "$_legacy" 2>/dev/null || true
fi
done
}
_refresh_installed_service() {
refresh_legacy_service_script
[ "$_svc_refreshed" -eq 1 ] || return 0
if ! is_b4_running; then
log_info "b4 is not running, the refreshed service script applies at its next start"
return 0
fi
saved_cmdline=$(b4_running_cmdline 2>/dev/null || true)
if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then
log_info "Stopping service (${B4_SERVICE_TYPE}) so it comes back under the refreshed script..."
fi
service_stop_b4 || {
log_warn "Could not stop the running b4 process, restart it by hand to apply the refreshed script"
return 0
}
_update_restart_b4
}
_update_restart_b4() {
if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then
log_info "Restarting service (${B4_SERVICE_TYPE})..."
service_call start 2>/dev/null || true
fi
if is_b4_running; then
log_ok "b4 is running"
elif [ "$B4_SERVICE_TYPE" = "systemd" ] || [ "$B4_SERVICE_TYPE" = "procd" ]; then
log_err "b4 did not come back up under ${B4_SERVICE_TYPE}"
service_show_crash_log
elif [ -n "$saved_cmdline" ]; then
log_info "Service manager did not restart b4, relaunching directly"
if relaunch_b4 "$saved_cmdline"; then
log_ok "b4 relaunched"
else
log_warn "Failed to relaunch b4, start it manually:"
log_warn " ${saved_cmdline}"
fi
else
log_warn "b4 is not running, start it manually:"
log_warn " ${existing_bin} --config ${B4_CONFIG_FILE}"
fi
}
action_update() {
target_ver="$1"
force_arch="$2"
check_root
log_header "Updating B4"
platform_init || true
existing_bin=""
if [ -n "$B4_EXISTING_BIN" ] && [ -f "$B4_EXISTING_BIN" ]; then
existing_bin="$B4_EXISTING_BIN"
B4_BIN_DIR=$(dirname "$B4_EXISTING_BIN")
fi
if [ -z "$existing_bin" ]; then
for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /jffs/b4 /tmp/b4 /ssd/b4; do
[ -z "$dir" ] && continue
if [ -f "${dir}/${BINARY_NAME}" ]; then
existing_bin="${dir}/${BINARY_NAME}"
B4_BIN_DIR="$dir"
break
fi
done
fi
if [ -z "$existing_bin" ]; then
_path_bin=$(command -v "$BINARY_NAME" 2>/dev/null || true)
if [ -n "$_path_bin" ] && [ -f "$_path_bin" ]; then
existing_bin="$_path_bin"
B4_BIN_DIR=$(dirname "$_path_bin")
fi
fi
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) || B4_ARCH=""
fi
require_supported_arch "$B4_ARCH"
if [ -n "$B4_LOCAL_ARCHIVE" ]; then
if [ -n "$target_ver" ]; then
log_warn "Ignoring a supplied archive: ${target_ver} was asked for by name"
B4_LOCAL_ARCHIVE=""
elif [ ! -f "$B4_LOCAL_ARCHIVE" ]; then
log_warn "Ignoring a stale archive path left by an earlier run: ${B4_LOCAL_ARCHIVE}"
B4_LOCAL_ARCHIVE=""
fi
fi
if [ -n "$B4_LOCAL_ARCHIVE" ]; then
latest_ver=""
log_info "Source: ${B4_LOCAL_ARCHIVE}"
elif [ -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 [ -z "$B4_LOCAL_ARCHIVE" ]; then
_cur_num=$(version_number "$current_ver")
if [ -n "$_cur_num" ] && [ "$_cur_num" = "$(version_number "$latest_ver")" ]; then
log_ok "Already up to date"
_refresh_installed_service
return 0
fi
fi
if [ "$QUIET_MODE" -eq 0 ]; then
if [ -n "$B4_LOCAL_ARCHIVE" ]; then
_confirm_msg="Install the supplied archive?"
else
_confirm_msg="Update to ${latest_ver}?"
fi
if ! confirm "$_confirm_msg"; then
log_info "Update cancelled"
return 0
fi
fi
setup_temp
file_name="${BINARY_NAME}-linux-${B4_ARCH}.tar.gz"
if [ -n "$B4_LOCAL_ARCHIVE" ]; then
case "$B4_LOCAL_ARCHIVE" in
/*) ;;
*)
log_err "Archive path must be absolute: ${B4_LOCAL_ARCHIVE}"
exit 1
;;
esac
if [ ! -f "$B4_LOCAL_ARCHIVE" ]; then
log_err "Archive not found: ${B4_LOCAL_ARCHIVE}"
exit 1
fi
archive_path="$B4_LOCAL_ARCHIVE"
log_info "Installing from a supplied archive, no download"
else
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
}
checksum_gate "$archive_path" "${download_url}.sha256" || exit 1
fi
cd "$TEMP_DIR"
tar -xzf "$archive_path" || {
log_err "Extraction failed"
exit 1
}
if [ ! -f "${TEMP_DIR}/${BINARY_NAME}" ]; then
log_err "Binary not found in archive"
exit 1
fi
if [ -n "$B4_LOCAL_ARCHIVE" ] && [ "$B4_LOCAL_ARCHIVE_OWNED" = "1" ]; then
rm -f "$B4_LOCAL_ARCHIVE" 2>/dev/null || true
fi
bin_dir=$(dirname "$existing_bin")
ensure_bin_space "$bin_dir" "${TEMP_DIR}/${BINARY_NAME}" || exit 1
saved_cmdline=$(b4_running_cmdline 2>/dev/null || true)
[ -n "$saved_cmdline" ] && log_info "Running command line: ${saved_cmdline}"
if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then
log_info "Stopping service (${B4_SERVICE_TYPE})..."
fi
service_stop_b4 || {
log_err "Could not stop the running b4 process"
log_info "Replacing the binary underneath a live b4 risks a half-written"
log_info "file being exec'd by a respawn. Stop it by hand and re-run."
exit 1
}
_newbin="${existing_bin}.new.$$"
rm -f "${existing_bin}".new.* 2>/dev/null || true
if ! { mv "${TEMP_DIR}/${BINARY_NAME}" "$_newbin" 2>/dev/null || cp "${TEMP_DIR}/${BINARY_NAME}" "$_newbin"; } ||
! chmod +x "$_newbin"; then
rm -f "$_newbin" 2>/dev/null || true
log_err "Failed to stage the new binary in ${bin_dir}"
_update_restart_b4
exit 1
fi
ts=$(date '+%Y%m%d_%H%M%S')
backup_bin="${existing_bin}.backup.${ts}"
stash_binary "$existing_bin" "$backup_bin" || {
rm -f "$_newbin" 2>/dev/null || true
log_err "Could not move the current binary aside"
_update_restart_b4
exit 1
}
update_failed=0
mv -f "$_newbin" "$existing_bin" || {
rm -f "$_newbin" 2>/dev/null || true
log_err "Failed to replace binary"
update_failed=1
}
if [ "$update_failed" -eq 0 ]; then
flush_disk
fi
if [ "$update_failed" -eq 0 ] && "$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 "$backup_bin" 2>/dev/null || true
else
if [ "$update_failed" -eq 0 ]; then
log_warn "Updated binary failed version check"
fi
update_failed=1
if restore_binary "$existing_bin" "$backup_bin"; then
log_ok "Rolled back to the previous version"
elif [ -f "$existing_bin" ]; then
log_warn "No backup to roll back to, keeping the new binary"
else
log_err "No working binary in ${bin_dir}, reinstall b4 manually"
fi
fi
refresh_legacy_service_script
if [ "$update_failed" -eq 0 ]; then
platform_call_optional install_hooks || log_warn "Platform hooks could not be installed"
else
platform_call_optional remove_hooks || true
fi
_update_restart_b4
echo ""
if [ "$update_failed" -eq 1 ]; then
log_warn "Update did not complete, previous version is still in place"
echo ""
return 1
fi
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=$(_byte_at "$_elf_bin" 5)
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}"
_si_pid=$(b4_pid) || _si_pid=""
if [ -n "$_si_pid" ]; then
if [ -f "/proc/${_si_pid}/status" ]; then
mem_kb=$(awk '/^VmRSS:/ {print $2}' "/proc/${_si_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: ${_si_pid})"
fi
fi
if [ -f "/proc/${_si_pid}/stat" ]; then
proc_start=$(awk '{print $22}' "/proc/${_si_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
_si_tun_mode="no"
if [ -n "$cfg_file" ] && command_exists jq; then
queue_mode=$(jq -r '.queue.mode // empty' "$cfg_file" 2>/dev/null) || true
queue_num=$(jq -r '.queue.start_num // empty' "$cfg_file" 2>/dev/null) || true
workers=$(jq -r '.queue.threads // 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
if [ -z "$queue_mode" ] || [ "$queue_mode" = "null" ]; then
queue_mode="nfqueue (default)"
fi
if [ -z "$queue_num" ] || [ "$queue_num" = "null" ]; then
queue_num="537 (default)"
fi
if [ -z "$workers" ] || [ "$workers" = "null" ]; then
workers="4 (default)"
fi
case "$queue_mode" in
tun*) _si_tun_mode="yes" ;;
*) ;;
esac
log_detail "Queue mode" "$queue_mode"
log_detail "Queue number" "$queue_num"
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
[ -n "$B4_PKG_MANAGER" ] || detect_pkg_manager
_si_backend=$(_firewall_backend)
_si_queue_ok="no"
_si_counters_ok="no"
case "$_si_backend" in
nftables)
_nft_queue_works && _si_queue_ok="yes"
_nft_ct_counters_work && _si_counters_ok="yes"
;;
iptables | iptables-legacy)
_ipt_nfqueue_works "$_si_backend" && _si_queue_ok="yes"
_ipt_connbytes_works "$_si_backend" && _si_counters_ok="yes"
;;
*) ;;
esac
echo ""
log_detail "Firewall backend" "$_si_backend"
log_info "Kernel modules:"
for mod in $(_queue_modules_for_backend "$_si_backend"); do
if lsmod 2>/dev/null | grep -q "^${mod}[[:space:]]"; 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
if [ "$_si_backend" = "none" ]; then
printf " ${RED} FAIL${NC} %s\n" "No working firewall backend (neither nft nor iptables) - b4 cannot install its rules" >&2
elif [ "$_si_queue_ok" = "yes" ]; then
printf " ${GREEN} OK${NC} %s\n" "Packet queue works (${_si_backend} functional test passed)" >&2
elif [ "$_si_tun_mode" = "yes" ]; then
printf " ${YELLOW} WARN${NC} %s\n" "Packet queue not functional on ${_si_backend}, but b4 is configured for TUN mode, which does not use it" >&2
else
printf " ${RED} FAIL${NC} %s\n" "Packet queue not functional on ${_si_backend} - b4 cannot start in NFQUEUE mode" >&2
case "$B4_PKG_MANAGER" in
apk) printf " ${DIM} Try: apk add %s${NC}\n" "$(_queue_pkgs_for_backend "$_si_backend")" >&2 ;;
opkg) printf " ${DIM} Try: opkg install %s${NC}\n" "$(_queue_pkgs_for_backend "$_si_backend")" >&2 ;;
*) printf " ${DIM} Needs these kernel modules: %s${NC}\n" "$(_queue_modules_for_backend "$_si_backend")" >&2 ;;
esac
printf " ${DIM} Or switch b4 to TUN mode (queue.mode = \"tun\"), which needs no NFQUEUE${NC}\n" >&2
fi
if [ "$_si_backend" != "none" ]; then
if [ "$_si_counters_ok" = "yes" ]; then
printf " ${GREEN} OK${NC} %s\n" "Connection packet counters work (b4 queues only the first packets of a connection)" >&2
else
printf " ${YELLOW} WARN${NC} %s\n" "Connection packet counters unavailable - b4 inspects every packet on the configured ports" >&2
fi
fi
_flow_offload=""
_flow_guard=0
if command_exists nft; then
_nft_ruleset=$(nft list ruleset 2>/dev/null)
if echo "$_nft_ruleset" | grep -q "flow add @\|flow offload @"; then
if echo "$_nft_ruleset" | grep -qE "flags[[:space:]]+offload"; then
_flow_offload="hardware"
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_nft_ruleset" | _nft_flow_guard)
fi
fi
if [ -z "$_flow_offload" ]; then
for _fb in iptables iptables-legacy; do
command_exists "$_fb" || continue
_ipt_filter=$($_fb -t filter -S 2>/dev/null)
if echo "$_ipt_filter" | grep -q "FLOWOFFLOAD"; then
if echo "$_ipt_filter" | grep -q -- "--hw"; then
_flow_offload="hardware"
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_ipt_filter" | _ipt_flow_guard)
break
fi
done
fi
_flow_window=$(_b4_queue_window)
if [ -z "$_flow_offload" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading off (b4 can intercept traffic)" >&2
elif [ "$_flow_guard" -gt "$_flow_window" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading active (${_flow_offload}), held off until packet ${_flow_guard} of a connection - b4 still sees the start of every connection" >&2
if [ "$(_b4_duplicate_sets)" != "0" ]; then
printf " ${YELLOW} WARN${NC} %s\n" "A routing set has TCP duplication enabled, which needs every packet of the connection - turn flow offloading off or disable duplication" >&2
fi
elif [ "$_flow_guard" -gt 0 ]; then
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}), held off only until packet ${_flow_guard} - b4 reads the first ${_flow_window} packets of a connection, so it loses the tail" >&2
printf " ${DIM} Raise the threshold above ${_flow_window} in /usr/share/firewall4/templates/ruleset.uc, then: fw4 restart${NC}\n" >&2
else
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}) - bypasses b4; disable it for b4 to work" >&2
printf " ${DIM} Or hold it off until b4 has seen the start of a connection: in /usr/share/firewall4/templates/ruleset.uc${NC}\n" >&2
printf " ${DIM} replace 'flow offload @ft' with 'ct original packets ge 40 flow offload @ft', then: fw4 restart${NC}\n" >&2
fi
echo ""
log_info "Required tools:"
_fw_found=0
if command_exists nft; then
if _nft_functional; then
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" "$tool" >&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#*=}"
if ! arch_is_supported "$FORCE_ARCH"; then
printf 'ERROR: unknown architecture: %s\nAvailable: %s\n' "$FORCE_ARCH" "$B4_SUPPORTED_ARCHS" >&2
exit 1
fi
;;
--platform=*)
B4_PLATFORM="${arg#*=}"
;;
--bin-dir=*)
B4_BIN_DIR="${arg#*=}"
;;
--data-dir=*)
_dd="${arg#*=}"
if ! is_abs_path "$_dd"; then
printf 'ERROR: --data-dir must be an absolute path (got: %s)\n' "${_dd:-empty}" >&2
exit 1
fi
B4_DATA_DIR="$_dd"
;;
--help | -h)
_show_help
exit 0
;;
v* | V*)
VERSION="$arg"
;;
-*)
printf 'ERROR: unknown option: %s\n' "$arg" >&2
printf 'Options take the form --name=value (not --name value).\n' >&2
printf 'Run with --help for the full list.\n' >&2
exit 1
;;
*) ;;
esac
done
if [ "$QUIET_MODE" -ne 1 ] 2>/dev/null && [ ! -t 0 ]; then
if (exec </dev/tty) 2>/dev/null; then
exec </dev/tty
elif [ "$ACTION" = "remove" ]; then
log_err "No terminal available for the removal prompts; re-run with --quiet to remove without asking"
exit 1
else
log_warn "No terminal available - continuing non-interactively with defaults"
QUIET_MODE=1
fi
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 " B4_ALLOW_INSECURE_TLS=1 Download over unverified TLS when no CA certificates work"
echo " B4_GEO_MAX_TIME Time limit for one geodata download in seconds (default 3600, 0 = none)"
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