From 0c549400109d65420466f53cb21f221dff44fe3b Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 09:05:18 +0100 Subject: [PATCH 1/6] fix(tor-shield): Multiple bug fixes for control socket and startup - Fix subshell bug in get_circuits (pipe loses JSON state) - Add has_control flag to status for frontend awareness - Fix UseBridges without bridge lines causing Tor to fail - Fix hidden service directory ownership (tor:tor) - Change log output from file to syslog - Fix run directory ownership and permissions (700) - Add CookieAuthentication for control socket auth - Use socat instead of nc (BusyBox lacks Unix socket support) - Add socat as package dependency - Optimize duplicate curl calls in status check - Use fallback IP services for real_ip detection Co-Authored-By: Claude Opus 4.5 --- .../root/usr/libexec/rpcd/luci.tor-shield | 100 +++++++++++++----- package/secubox/secubox-app-tor/Makefile | 2 +- .../files/etc/init.d/tor-shield | 25 +++-- 3 files changed, 96 insertions(+), 31 deletions(-) diff --git a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield index 2960ae7d..3a14c369 100644 --- a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield +++ b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield @@ -7,22 +7,47 @@ . /usr/share/libubox/jshn.sh CONFIG="tor-shield" -TOR_CONTROL="/var/run/tor/control" +TOR_RUN="/var/run/tor" +TOR_CONTROL="$TOR_RUN/control" TOR_DATA="/var/lib/tor" -# Send command to Tor control socket +# Send command to Tor control socket with cookie auth tor_control() { if [ ! -S "$TOR_CONTROL" ]; then return 1 fi - echo -e "$1" | nc -U "$TOR_CONTROL" 2>/dev/null + + # Read cookie for authentication + local cookie="" + local cookie_file="$TOR_RUN/control_auth_cookie" + if [ -f "$cookie_file" ]; then + cookie=$(hexdump -e '32/1 "%02x"' "$cookie_file" 2>/dev/null) + fi + + # Use socat for Unix socket (BusyBox nc doesn't support -U) + # Send AUTHENTICATE first, then the actual command + { + [ -n "$cookie" ] && echo "AUTHENTICATE $cookie" + echo "$1" + } | socat - UNIX-CONNECT:"$TOR_CONTROL" 2>/dev/null } -# Check if Tor is running +# Check if Tor is running (via tor-shield, not just any tor process) is_running() { + # Check for tor-shield's pid file first + if [ -f "$TOR_RUN/tor.pid" ]; then + local pid=$(cat "$TOR_RUN/tor.pid" 2>/dev/null) + [ -n "$pid" ] && [ -d "/proc/$pid" ] && return 0 + fi + # Fallback to checking any tor process pgrep tor >/dev/null 2>&1 } +# Check if tor-shield control socket is available +has_control() { + [ -S "$TOR_CONTROL" ] +} + # Get bootstrap percentage get_bootstrap() { local status=$(tor_control "GETINFO status/bootstrap-phase") @@ -49,35 +74,47 @@ get_status() { # Running state if is_running; then json_add_boolean "running" 1 + json_add_boolean "has_control" "$(has_control && echo 1 || echo 0)" - # Bootstrap percentage - local bootstrap=$(get_bootstrap) + # Bootstrap percentage (only if control socket available) + local bootstrap=0 + if has_control; then + bootstrap=$(get_bootstrap) + fi json_add_int "bootstrap" "${bootstrap:-0}" # Get exit IP if bootstrapped if [ "$bootstrap" -ge 100 ]; then local socks_port config_get socks_port socks port '9050' - local exit_ip=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IP' 2>/dev/null) + # Single curl call for both IP and IsTor + local tor_check=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null) + local exit_ip=$(echo "$tor_check" | jsonfilter -e '@.IP' 2>/dev/null) json_add_string "exit_ip" "${exit_ip:-unknown}" # Check if using Tor - local is_tor=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IsTor' 2>/dev/null) + local is_tor=$(echo "$tor_check" | jsonfilter -e '@.IsTor' 2>/dev/null) json_add_boolean "is_tor" "$([ "$is_tor" = "true" ] && echo 1 || echo 0)" - # Get circuit count - local circuits=$(tor_control "GETINFO circuit-status" | grep -c "BUILT" 2>/dev/null) - json_add_int "circuit_count" "${circuits:-0}" + # Get circuit count (only if control available) + if has_control; then + local circuits=$(tor_control "GETINFO circuit-status" | grep -c "BUILT" 2>/dev/null) + json_add_int "circuit_count" "${circuits:-0}" - # Get bandwidth - local bw_read=$(tor_control "GETINFO traffic/read" | grep "250" | awk '{print $2}') - local bw_written=$(tor_control "GETINFO traffic/written" | grep "250" | awk '{print $2}') - json_add_int "bytes_read" "${bw_read:-0}" - json_add_int "bytes_written" "${bw_written:-0}" + # Get bandwidth + local bw_read=$(tor_control "GETINFO traffic/read" | grep "250" | awk '{print $2}') + local bw_written=$(tor_control "GETINFO traffic/written" | grep "250" | awk '{print $2}') + json_add_int "bytes_read" "${bw_read:-0}" + json_add_int "bytes_written" "${bw_written:-0}" + else + json_add_int "circuit_count" 0 + json_add_int "bytes_read" 0 + json_add_int "bytes_written" 0 + fi fi # Uptime from pid file - local pidfile="/var/run/tor/tor.pid" + local pidfile="$TOR_RUN/tor.pid" if [ -f "$pidfile" ]; then local pid=$(cat "$pidfile") if [ -d "/proc/$pid" ]; then @@ -88,6 +125,7 @@ get_status() { fi else json_add_boolean "running" 0 + json_add_boolean "has_control" 0 json_add_int "bootstrap" 0 fi @@ -98,9 +136,11 @@ get_status() { json_add_boolean "bridges_enabled" "$bridges_enabled" json_add_string "bridge_type" "$bridge_type" - # Get real IP - local real_ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null) - json_add_string "real_ip" "${real_ip:-unknown}" + # Get real IP (try multiple services as fallback) + local real_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null) + [ -z "$real_ip" ] && real_ip=$(curl -s --max-time 5 https://ifconfig.me/ip 2>/dev/null) + [ -z "$real_ip" ] && real_ip="unknown" + json_add_string "real_ip" "$real_ip" json_dump } @@ -172,9 +212,18 @@ get_circuits() { return fi - local circuits=$(tor_control "GETINFO circuit-status") + # Check if control socket exists + if [ ! -S "$TOR_CONTROL" ]; then + json_close_array + json_dump + return + fi - echo "$circuits" | grep "BUILT" | while read line; do + # Use temp file to avoid subshell issue with pipe + local TMP_CIRCUITS="/tmp/tor_circuits_$$" + tor_control "GETINFO circuit-status" | grep "BUILT" > "$TMP_CIRCUITS" 2>/dev/null + + while read line; do local id=$(echo "$line" | awk '{print $1}') local status=$(echo "$line" | awk '{print $2}') local path=$(echo "$line" | awk '{print $3}') @@ -189,7 +238,8 @@ get_circuits() { # Parse path into nodes json_add_array "nodes" - local IFS=',' + local OLD_IFS="$IFS" + IFS=',' for node in $path; do local fingerprint=$(echo "$node" | cut -d'~' -f1 | tr -d '$') local name=$(echo "$node" | cut -d'~' -f2) @@ -199,12 +249,14 @@ get_circuits() { json_add_string "name" "${name:-$fingerprint}" json_close_object done + IFS="$OLD_IFS" json_close_array json_close_object fi - done + done < "$TMP_CIRCUITS" + rm -f "$TMP_CIRCUITS" json_close_array json_dump } diff --git a/package/secubox/secubox-app-tor/Makefile b/package/secubox/secubox-app-tor/Makefile index 96e437ad..f801dc72 100644 --- a/package/secubox/secubox-app-tor/Makefile +++ b/package/secubox/secubox-app-tor/Makefile @@ -22,7 +22,7 @@ define Package/secubox-app-tor PKGARCH:=all SUBMENU:=SecuBox Apps TITLE:=SecuBox Tor Shield - DEPENDS:=+tor +tor-geoip +iptables +curl +jsonfilter + DEPENDS:=+tor +tor-geoip +iptables +curl +jsonfilter +socat endef define Package/secubox-app-tor/description diff --git a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield index 48b1a99c..7fc04611 100644 --- a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield +++ b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield @@ -37,7 +37,8 @@ generate_torrc() { config_get strict_nodes security strict_nodes '0' mkdir -p "$TOR_RUN" "$TOR_DATA" - chmod 700 "$TOR_DATA" + chown tor:tor "$TOR_RUN" "$TOR_DATA" + chmod 700 "$TOR_RUN" "$TOR_DATA" cat > "$TORRC" << EOF # SecuBox Tor Shield - Auto-generated config @@ -46,9 +47,12 @@ generate_torrc() { User tor DataDirectory $TOR_DATA PidFile $TOR_RUN/tor.pid -Log notice file /var/log/tor.log +Log notice syslog ControlSocket $TOR_RUN/control ControlSocketsGroupWritable 1 +CookieAuthentication 1 +CookieAuthFile $TOR_RUN/control_auth_cookie +CookieAuthFileGroupReadable 1 # SOCKS proxy SocksPort $socks_addr:$socks_port @@ -79,15 +83,23 @@ VirtualAddrNetworkIPv4 10.192.0.0/10 EOF fi - # Bridge configuration + # Bridge configuration - only enable if bridges are configured if [ "$bridges_enabled" = "1" ]; then - cat >> "$TORRC" << EOF + # Count bridge lines first + BRIDGE_COUNT=0 + count_bridge() { BRIDGE_COUNT=$((BRIDGE_COUNT + 1)); } + config_list_foreach bridges bridge_lines count_bridge + + # Only add UseBridges if there are actual bridges configured + if [ "$BRIDGE_COUNT" -gt 0 ]; then + cat >> "$TORRC" << EOF # Bridge mode UseBridges 1 EOF - # Add bridge lines from config - config_list_foreach bridges bridge_lines add_bridge_line + # Add bridge lines from config + config_list_foreach bridges bridge_lines add_bridge_line + fi fi # Exit node restrictions @@ -130,6 +142,7 @@ add_hidden_service() { local hs_dir="$TOR_DATA/hidden_service_$name" mkdir -p "$hs_dir" + chown tor:tor "$hs_dir" chmod 700 "$hs_dir" cat >> "$TORRC" << EOF From 134736a0e90076ff1f35d82c9b02302b88dfa814 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 09:13:21 +0100 Subject: [PATCH 2/6] fix(tor-shield): Handle RPC expect unwrapping in circuits API The RPC expect clause unwraps the response, so circuits data may be an array directly rather than an object with circuits property. Co-Authored-By: Claude Opus 4.5 --- .../htdocs/luci-static/resources/tor-shield/api.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js index be2fe776..db0c26e5 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js @@ -203,9 +203,13 @@ return baseclass.extend({ callCircuits(), callBandwidth() ]).then(function(results) { + // Handle RPC expect unwrapping - results[1] may be array or object + var circuitsData = results[1] || []; + var circuits = Array.isArray(circuitsData) ? circuitsData : (circuitsData.circuits || []); + return { status: results[0] || {}, - circuits: (results[1] || {}).circuits || [], + circuits: circuits, bandwidth: results[2] || {} }; }); From 5400e34e1ffadbdd39790107f10a7f9772934b9c Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 09:17:19 +0100 Subject: [PATCH 3/6] fix(tor-shield): Handle RPC expect unwrapping in hidden services view Same issue as circuits - RPC expect clause unwraps response to array. Co-Authored-By: Claude Opus 4.5 --- .../luci-static/resources/view/tor-shield/hidden-services.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js index 27dcb654..630dd026 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js @@ -176,7 +176,8 @@ return view.extend({ render: function(data) { var self = this; - var services = data.services || []; + // Handle RPC expect unwrapping - data may be array or object + var services = Array.isArray(data) ? data : (data.services || []); var view = E('div', { 'class': 'tor-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }), From 787fe3864ef6a9ac5145af3c1ee998a7492fc062 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 09:59:07 +0100 Subject: [PATCH 4/6] fix(tor-shield): Fix json_init bug and permission issues RPCD backend: - Fix critical json_init bug: variables must be extracted BEFORE json_init() which wipes the loaded JSON. Affected functions: save_settings, do_enable, set_bridges, add/remove_hidden_service - Fix process detection: use pgrep instead of pid file - Fix uptime calculation: get PID from pgrep, not pid file - Fix RPC expect unwrapping in getDashboardData for presets Init script: - Remove PidFile directive (procd manages the process) - Clean up stale files before starting to avoid permission issues - Set proper ownership on torrc after generation - Fix iptables chain creation to handle "already exists" gracefully - Remove from OUTPUT chain before attempting chain deletion Co-Authored-By: Claude Opus 4.5 --- .../luci-static/resources/tor-shield/api.js | 6 +- .../root/usr/libexec/rpcd/luci.tor-shield | 56 +++++++++++-------- .../files/etc/init.d/tor-shield | 19 +++++-- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js index db0c26e5..2f4b6a1d 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js @@ -188,9 +188,13 @@ return baseclass.extend({ callPresets(), callBandwidth() ]).then(function(results) { + // Handle RPC expect unwrapping - results may be array or object + var presetsData = results[1] || []; + var presets = Array.isArray(presetsData) ? presetsData : (presetsData.presets || []); + return { status: results[0] || {}, - presets: (results[1] || {}).presets || [], + presets: presets, bandwidth: results[2] || {} }; }); diff --git a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield index 3a14c369..2c49bbd3 100644 --- a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield +++ b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield @@ -32,15 +32,10 @@ tor_control() { } | socat - UNIX-CONNECT:"$TOR_CONTROL" 2>/dev/null } -# Check if Tor is running (via tor-shield, not just any tor process) +# Check if Tor is running is_running() { - # Check for tor-shield's pid file first - if [ -f "$TOR_RUN/tor.pid" ]; then - local pid=$(cat "$TOR_RUN/tor.pid" 2>/dev/null) - [ -n "$pid" ] && [ -d "/proc/$pid" ] && return 0 - fi - # Fallback to checking any tor process - pgrep tor >/dev/null 2>&1 + # Check for tor process (procd manages the process, no pid file) + pgrep -f "/usr/sbin/tor" >/dev/null 2>&1 } # Check if tor-shield control socket is available @@ -113,15 +108,12 @@ get_status() { fi fi - # Uptime from pid file - local pidfile="$TOR_RUN/tor.pid" - if [ -f "$pidfile" ]; then - local pid=$(cat "$pidfile") - if [ -d "/proc/$pid" ]; then - local start_time=$(stat -c %Y "/proc/$pid" 2>/dev/null) - local now=$(date +%s) - json_add_int "uptime" "$((now - start_time))" - fi + # Uptime from process start time + local pid=$(pgrep -f "/usr/sbin/tor" | head -1) + if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then + local start_time=$(stat -c %Y "/proc/$pid" 2>/dev/null) + local now=$(date +%s) + json_add_int "uptime" "$((now - start_time))" fi else json_add_boolean "running" 0 @@ -149,12 +141,15 @@ get_status() { do_enable() { read input json_load "$input" + + # Get values from input BEFORE json_init + local preset json_get_var preset preset - - json_init - [ -z "$preset" ] && preset="anonymous" + # Now initialize output JSON + json_init + # Load preset configuration config_load "$CONFIG" @@ -383,10 +378,14 @@ add_hidden_service_json() { add_hidden_service() { read input json_load "$input" + + # Get values from input BEFORE json_init + local name local_port virtual_port json_get_var name name json_get_var local_port local_port json_get_var virtual_port virtual_port + # Now initialize output JSON json_init if [ -z "$name" ]; then @@ -420,8 +419,12 @@ add_hidden_service() { remove_hidden_service() { read input json_load "$input" + + # Get values from input BEFORE json_init + local name json_get_var name name + # Now initialize output JSON json_init if [ -z "$name" ]; then @@ -587,9 +590,13 @@ add_bridge_json() { set_bridges() { read input json_load "$input" + + # Get values from input BEFORE json_init + local enabled type json_get_var enabled enabled json_get_var type type + # Now initialize output JSON json_init [ -n "$enabled" ] && uci set tor-shield.bridges.enabled="$enabled" @@ -664,9 +671,9 @@ save_settings() { read input json_load "$input" - json_init - - # Get values from input + # Get values from input BEFORE json_init (which wipes loaded JSON) + local mode dns_over_tor kill_switch socks_port trans_port dns_port + local exit_nodes exclude_exit strict_nodes json_get_var mode mode json_get_var dns_over_tor dns_over_tor json_get_var kill_switch kill_switch @@ -677,6 +684,9 @@ save_settings() { json_get_var exclude_exit exclude_exit_nodes json_get_var strict_nodes strict_nodes + # Now initialize output JSON + json_init + # Apply settings [ -n "$mode" ] && uci set tor-shield.main.mode="$mode" [ -n "$dns_over_tor" ] && uci set tor-shield.main.dns_over_tor="$dns_over_tor" diff --git a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield index 7fc04611..a7e112b0 100644 --- a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield +++ b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield @@ -37,6 +37,11 @@ generate_torrc() { config_get strict_nodes security strict_nodes '0' mkdir -p "$TOR_RUN" "$TOR_DATA" + + # Clean up stale files that may have wrong ownership + rm -f "$TOR_RUN/tor.pid" "$TOR_RUN/control" "$TOR_RUN/control_auth_cookie" 2>/dev/null + rm -f "$TOR_DATA/lock" 2>/dev/null + chown tor:tor "$TOR_RUN" "$TOR_DATA" chmod 700 "$TOR_RUN" "$TOR_DATA" @@ -46,7 +51,6 @@ generate_torrc() { User tor DataDirectory $TOR_DATA -PidFile $TOR_RUN/tor.pid Log notice syslog ControlSocket $TOR_RUN/control ControlSocketsGroupWritable 1 @@ -123,6 +127,9 @@ EOF if [ -f /usr/share/tor/geoip6 ]; then echo "GeoIPv6File /usr/share/tor/geoip6" >> "$TORRC" fi + + # Ensure torrc is readable by tor user + chown tor:tor "$TORRC" } add_bridge_line() { @@ -166,6 +173,10 @@ setup_iptables() { # Get Tor user ID local tor_uid=$(id -u tor 2>/dev/null || echo "tor") + # Remove from OUTPUT chain first (to allow chain deletion) + iptables -t nat -D OUTPUT -j TOR_SHIELD 2>/dev/null + iptables -t filter -D OUTPUT -j TOR_SHIELD 2>/dev/null + # Clear existing Tor rules iptables -t nat -F TOR_SHIELD 2>/dev/null iptables -t nat -X TOR_SHIELD 2>/dev/null @@ -174,9 +185,9 @@ setup_iptables() { [ "$mode" = "transparent" ] || return 0 - # Create chains - iptables -t nat -N TOR_SHIELD - iptables -t filter -N TOR_SHIELD + # Create chains (ignore "already exists" errors) + iptables -t nat -N TOR_SHIELD 2>/dev/null || true + iptables -t filter -N TOR_SHIELD 2>/dev/null || true # Exclude Tor traffic iptables -t nat -A TOR_SHIELD -m owner --uid-owner $tor_uid -j RETURN From 785ba9eb4c24858eb98cbec60b4b6660cbd6457e Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 10:45:25 +0100 Subject: [PATCH 5/6] fix(wireguard): Handle RPC expect unwrapping in API and views The RPC expect clause unwraps responses - when `expect: { peers: [] }` is used, the response `{peers: [...]}` gets unwrapped to just `[...]`. Fixed: - api.js: getAllData and getMonitoringData now handle both array and object formats for peers, interfaces, and rates - overview.js: render and polling functions now safely unwrap data that may be array or nested object Co-Authored-By: Claude Opus 4.5 --- .../view/wireguard-dashboard/overview.js | 14 ++++++++++---- .../resources/wireguard-dashboard/api.js | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js index 1242ef91..df9f4c0a 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js @@ -222,8 +222,11 @@ return view.extend({ return api.getAllData().then(L.bind(function(data) { var status = data.status || {}; - var interfaces = (data.interfaces || {}).interfaces || []; - var peers = (data.peers || {}).peers || []; + // Handle RPC expect unwrapping - may be array or object + var interfacesData = data.interfaces || []; + var peersData = data.peers || []; + var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []); + var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []); this.updateStats(status); this.updatePeers(peers); @@ -240,8 +243,11 @@ return view.extend({ render: function(data) { var self = this; var status = data.status || {}; - var interfaces = (data.interfaces || {}).interfaces || []; - var peers = (data.peers || {}).peers || []; + // Handle RPC expect unwrapping - may be array or object + var interfacesData = data.interfaces || []; + var peersData = data.peers || []; + var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []); + var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []); // Store peer descriptions this.peerDescriptions = data.descriptions || {}; diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js index 8b00d2d2..2fa4d950 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js @@ -178,10 +178,14 @@ return baseclass.extend({ callGetTraffic(), callPeerDescriptions() ]).then(function(results) { + // Handle RPC expect unwrapping - results may be array or object + var peersData = results[1] || []; + var interfacesData = results[2] || []; + return { status: results[0] || {}, - peers: results[1] || { peers: [] }, - interfaces: results[2] || { interfaces: [] }, + peers: Array.isArray(peersData) ? peersData : (peersData.peers || []), + interfaces: Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []), traffic: results[3] || {}, descriptions: (results[4] || {}).descriptions || {} }; @@ -196,10 +200,14 @@ return baseclass.extend({ callBandwidthRates(), callPeerDescriptions() ]).then(function(results) { + // Handle RPC expect unwrapping - results may be array or object + var peersData = results[1] || []; + var ratesData = results[2] || []; + return { status: results[0] || {}, - peers: results[1] || { peers: [] }, - rates: (results[2] || {}).rates || [], + peers: Array.isArray(peersData) ? peersData : (peersData.peers || []), + rates: Array.isArray(ratesData) ? ratesData : (ratesData.rates || []), descriptions: (results[3] || {}).descriptions || {} }; }); From fed7bd43c1ac88a3aff66cd1b8c0f2a9cece53ee Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 11:42:29 +0100 Subject: [PATCH 6/6] fix(haproxy): Combine fullchain + key for HAProxy certificates HAProxy requires certificate files to contain both the fullchain (cert + intermediate CA) and the private key concatenated together. Changes: - haproxyctl: Fix cert_add to create combined .pem files - haproxy-sync-certs: New script to sync ACME certs to HAProxy format - haproxy.sh: ACME deploy hook for HAProxy - init.d: Sync certs before starting HAProxy - Makefile: Install new scripts, add cron job for cert sync This fixes the "No Private Key found" error when HAProxy tries to load certificates that only contain the fullchain without the key. Co-Authored-By: Claude Opus 4.5 --- package/secubox/secubox-app-haproxy/Makefile | 19 +++++- .../files/etc/init.d/haproxy | 3 + .../files/usr/lib/acme/deploy/haproxy.sh | 59 +++++++++++++++++++ .../files/usr/sbin/haproxy-sync-certs | 47 +++++++++++++++ .../files/usr/sbin/haproxyctl | 7 ++- 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh create mode 100644 package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs diff --git a/package/secubox/secubox-app-haproxy/Makefile b/package/secubox/secubox-app-haproxy/Makefile index 6c3b2045..78a65596 100644 --- a/package/secubox/secubox-app-haproxy/Makefile +++ b/package/secubox/secubox-app-haproxy/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-haproxy PKG_VERSION:=1.0.0 -PKG_RELEASE:=13 +PKG_RELEASE:=14 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=MIT @@ -50,11 +50,28 @@ define Package/secubox-app-haproxy/install $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_BIN) ./files/usr/sbin/haproxyctl $(1)/usr/sbin/haproxyctl + $(INSTALL_BIN) ./files/usr/sbin/haproxy-sync-certs $(1)/usr/sbin/haproxy-sync-certs + + $(INSTALL_DIR) $(1)/usr/lib/acme/deploy + $(INSTALL_BIN) ./files/usr/lib/acme/deploy/haproxy.sh $(1)/usr/lib/acme/deploy/haproxy.sh $(INSTALL_DIR) $(1)/usr/share/haproxy/templates $(INSTALL_DATA) ./files/usr/share/haproxy/templates/* $(1)/usr/share/haproxy/templates/ $(INSTALL_DIR) $(1)/usr/share/haproxy/certs + + # Add cron job for certificate sync after ACME renewals + $(INSTALL_DIR) $(1)/etc/cron.d + echo "# Sync ACME certs to HAProxy after renewals" > $(1)/etc/cron.d/haproxy-certs + echo "15 3 * * * root /usr/sbin/haproxy-sync-certs >/dev/null 2>&1" >> $(1)/etc/cron.d/haproxy-certs +endef + +define Package/secubox-app-haproxy/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +# Sync existing ACME certificates on install +/usr/sbin/haproxy-sync-certs 2>/dev/null || true +exit 0 endef $(eval $(call BuildPackage,secubox-app-haproxy)) diff --git a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy index 94cd476c..6bed2b38 100644 --- a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy +++ b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy @@ -16,6 +16,9 @@ start_service() { [ "$enabled" = "1" ] || return 0 + # Sync ACME certificates to HAProxy format before starting + /usr/sbin/haproxy-sync-certs 2>/dev/null || true + procd_open_instance procd_set_param command "$PROG" service-run procd_set_param respawn 3600 5 0 diff --git a/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh b/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh new file mode 100644 index 00000000..eba5b5b2 --- /dev/null +++ b/package/secubox/secubox-app-haproxy/files/usr/lib/acme/deploy/haproxy.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# ACME deploy hook for HAProxy +# Combines fullchain + private key into single .pem file +# Usage: Called by acme.sh after certificate issuance/renewal + +HAPROXY_CERTS_DIR="/srv/haproxy/certs" + +# acme.sh passes these environment variables: +# DOMAIN - the domain name +# CERT_PATH - path to the domain certificate +# KEY_PATH - path to the domain private key +# CA_PATH - path to the intermediate CA certificate +# FULLCHAIN_PATH - path to the full chain certificate +# CERT_KEY_PATH - same as KEY_PATH + +deploy() { + local domain="$1" + local key_path="$2" + local cert_path="$3" + local ca_path="$4" + local fullchain_path="$5" + + [ -z "$domain" ] && { echo "Error: domain required"; return 1; } + + mkdir -p "$HAPROXY_CERTS_DIR" + + # Use fullchain if available, otherwise use cert + ca + local combined_cert="" + if [ -n "$fullchain_path" ] && [ -f "$fullchain_path" ]; then + combined_cert="$fullchain_path" + elif [ -n "$cert_path" ] && [ -f "$cert_path" ]; then + combined_cert="$cert_path" + else + echo "Error: No certificate file found for $domain" + return 1 + fi + + if [ -z "$key_path" ] || [ ! -f "$key_path" ]; then + echo "Error: No key file found for $domain" + return 1 + fi + + # Combine fullchain + private key for HAProxy + echo "Deploying certificate for $domain to HAProxy..." + cat "$combined_cert" "$key_path" > "$HAPROXY_CERTS_DIR/$domain.pem" + chmod 600 "$HAPROXY_CERTS_DIR/$domain.pem" + + echo "Certificate deployed: $HAPROXY_CERTS_DIR/$domain.pem" + + # Reload HAProxy if running + if [ -x /etc/init.d/haproxy ]; then + /etc/init.d/haproxy reload 2>/dev/null || true + fi + + return 0 +} + +# Entry point for acme.sh deploy hook +deploy "$Le_Domain" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs new file mode 100644 index 00000000..994321a5 --- /dev/null +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs @@ -0,0 +1,47 @@ +#!/bin/sh +# Sync ACME certificates to HAProxy format +# Combines fullchain + private key into .pem files +# Called by ACME renewal or manually via haproxyctl + +ACME_DIR="/etc/acme" +HAPROXY_CERTS_DIR="/srv/haproxy/certs" + +log_info() { echo "[haproxy-sync-certs] $*"; logger -t haproxy-sync-certs "$*"; } +log_error() { echo "[haproxy-sync-certs] ERROR: $*" >&2; logger -t haproxy-sync-certs -p err "$*"; } + +mkdir -p "$HAPROXY_CERTS_DIR" + +# Find all ACME certificates and deploy them +for domain_dir in "$ACME_DIR"/*/; do + [ -d "$domain_dir" ] || continue + + # Skip non-domain directories + case "$(basename "$domain_dir")" in + ca|*.ecc) continue ;; + esac + + domain=$(basename "$domain_dir") + fullchain="$domain_dir/fullchain.cer" + key="$domain_dir/${domain}.key" + + # Try alternate paths + [ -f "$fullchain" ] || fullchain="$domain_dir/fullchain.pem" + [ -f "$key" ] || key="$domain_dir/privkey.pem" + [ -f "$key" ] || key="$domain_dir/${domain}.key" + + if [ -f "$fullchain" ] && [ -f "$key" ]; then + log_info "Syncing certificate for $domain" + cat "$fullchain" "$key" > "$HAPROXY_CERTS_DIR/$domain.pem" + chmod 600 "$HAPROXY_CERTS_DIR/$domain.pem" + else + log_error "Missing cert or key for $domain (fullchain=$fullchain, key=$key)" + fi +done + +log_info "Certificate sync complete" + +# Reload HAProxy if running +if pgrep -x haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then + log_info "Reloading HAProxy..." + /etc/init.d/haproxy reload 2>/dev/null || true +fi diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index c72ce7dd..a94ee7b6 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -630,8 +630,13 @@ cmd_cert_add() { --home "$LE_WORKING_DIR" \ --cert-file "$CERTS_PATH/$domain.crt" \ --key-file "$CERTS_PATH/$domain.key" \ - --fullchain-file "$CERTS_PATH/$domain.pem" \ + --fullchain-file "$CERTS_PATH/$domain.fullchain.pem" \ --reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true + + # HAProxy needs combined file: fullchain + private key + log_info "Creating combined PEM for HAProxy..." + cat "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.key" > "$CERTS_PATH/$domain.pem" + chmod 600 "$CERTS_PATH/$domain.pem" fi # Restart HAProxy if it was running