mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-10-20 11:11:39 +00:00
* 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 of50e71425: 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 of0dcc9308: 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: since50e71425the 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.
5256 lines
161 KiB
Bash
Executable file
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
|
|
|