From cfb89528fa2da3e52f0c030b6b4b450ee5fb0ebf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Oct 2026 12:40:08 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=88=20Sync=202026-10-28=2012:40:08?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geoview/Makefile | 4 +- luci-app-amlogic/Makefile | 4 +- .../resources/view/amlogic/backup.js | 44 +- .../share/rpcd/acl.d/luci-app-amlogic.json | 3 +- .../root/usr/share/rpcd/ucode/luci.amlogic | 24 +- luci-app-openvpn-plus/Makefile | 2 +- .../root/usr/libexec/luci-app-openvpn-plus | 2079 +++++++++++------ .../resources/view/vpn/openvpn-keygen.js | 1 + luci-app-passwall/Makefile | 2 +- .../luasrc/passwall/util_hysteria2.lua | 2 + .../luasrc/passwall/util_xray.lua | 2 + luci-app-passwall2/Makefile | 2 +- .../luasrc/controller/passwall2.lua | 6 +- luci-app-passwall2/luasrc/passwall2/api.lua | 11 +- .../luasrc/passwall2/util_xray.lua | 2 + .../view/passwall2/socks_auto_switch/btn.htm | 4 +- .../resources/view/network/wireless.js | 25 +- natflow/Makefile | 4 +- open-app-filter/Makefile | 2 +- open-app-filter/files/oaf_version | 2 +- 20 files changed, 1459 insertions(+), 766 deletions(-) diff --git a/geoview/Makefile b/geoview/Makefile index 91bfbcbce..5e038ad50 100644 --- a/geoview/Makefile +++ b/geoview/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=geoview -PKG_VERSION:=0.2.8 -PKG_RELEASE:=8 +PKG_VERSION:=0.2.9 +PKG_RELEASE:=9 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)? diff --git a/luci-app-amlogic/Makefile b/luci-app-amlogic/Makefile index 92b579013..0803ecb19 100644 --- a/luci-app-amlogic/Makefile +++ b/luci-app-amlogic/Makefile @@ -22,11 +22,11 @@ PKG_NAME:=luci-app-amlogic # DD = day (01 = 1st) # NN = daily release sequence (01 = first release of the day, 02 = second, ...) # Example below: 26.10.0101 = October 1, 2026, 1st release of the day -PKG_VERSION:=26.10.2501 +PKG_VERSION:=26.10.2801 # PKG_RELEASE marks the LuCI variant; onekey-install.sh relies on it (do NOT bump) # 1 = Lua version (lua branch) → tag: 26.10.0101 # 2 = JS version (main branch) → tag: 26.10.0101-js -PKG_RELEASE:=26 +PKG_RELEASE:=27 PKG_LICENSE:=GPL-2.0 License PKG_MAINTAINER:=ophub diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js index 62df5ed8e..64a6dda3d 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js @@ -13,9 +13,10 @@ 'require fs'; 'require view.amlogic.shared as amlogicShared'; -// Generate one openwrt_config.tar.gz on the server and return its path so the -// frontend can stream it back via fs.read_direct. -const callBackup = rpc.declare({ object: 'luci.amlogic', method: 'backup_create' }); +// Generate one openwrt_config.tar.gz on the server (background) so the +// frontend can stream it back via fs.read_direct once backup_status is done. +const callBackup = rpc.declare({ object: 'luci.amlogic', method: 'backup_create' }); +const callBackupStatus = rpc.declare({ object: 'luci.amlogic', method: 'backup_status' }); // List existing snapshot names (e.g. etc-000 / etc-001 / etc-202401...). const callSnapList = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_list', expect: { names: [] } }); @@ -69,17 +70,11 @@ return view.extend({ const btn = ev.currentTarget; btn.disabled = true; btn.value = _('Generating...'); - // Step 1: ask the server to pack /etc into openwrt_config.tar.gz. - return callBackup().then(function (r) { - if (!r || !r.ok) { - downloadStatus.textContent = _("Couldn't open file:") + ' ' + (r ? r.path : ''); - return; - } - // Switch to success color before streaming the blob - downloadStatus.className = 'amlogic-status-ok'; - downloadStatus.textContent = _('The file Will download automatically.') + ' ' + r.path; - // Fetch the file blob via fs.read_direct and trigger browser download. - return fs.read_direct(r.path, 'blob').then(function (blob) { + downloadStatus.className = 'amlogic-status-err'; + downloadStatus.textContent = ''; + + function downloadFile(path) { + return fs.read_direct(path, 'blob').then(function (blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -89,6 +84,27 @@ return view.extend({ document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 10000); }); + } + + function pollStatus(deadline) { + return callBackupStatus().then(function (s) { + if (s && s.status === 'done' && s.size > 0) { + downloadStatus.className = 'amlogic-status-ok'; + downloadStatus.textContent = _('The file Will download automatically.') + ' ' + s.path; + return downloadFile(s.path); + } + if (s && s.status === 'failed') + throw new Error(_("Couldn't open file:") + ' ' + (s.path || '')); + if (Date.now() > deadline) + throw new Error(_('Backup generation timed out.')); + return new Promise(function (resolve) { setTimeout(resolve, 1500); }) + .then(function () { return pollStatus(deadline); }); + }); + } + + // Step 1: kick off the background pack job, then poll until done. + return callBackup().then(function () { + return pollStatus(Date.now() + 300000); }).catch(function (e) { downloadStatus.textContent = _('Create upload file error.') + ' ' + (e && e.message ? e.message : ''); diff --git a/luci-app-amlogic/root/usr/share/rpcd/acl.d/luci-app-amlogic.json b/luci-app-amlogic/root/usr/share/rpcd/acl.d/luci-app-amlogic.json index d89c6995b..c0e52cdbe 100644 --- a/luci-app-amlogic/root/usr/share/rpcd/acl.d/luci-app-amlogic.json +++ b/luci-app-amlogic/root/usr/share/rpcd/acl.d/luci-app-amlogic.json @@ -14,7 +14,8 @@ "list_uploads", "read_log_tail", "read_log_full", - "cpu_policies" + "cpu_policies", + "backup_status" ] }, "uci": [ "amlogic" ], diff --git a/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic b/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic index 6bd303ff8..ecae8aaa2 100644 --- a/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic +++ b/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic @@ -18,6 +18,7 @@ const LOG_PLUGIN = LOG_DIR + '/amlogic_check_plugin.log'; const LOG_KERNEL = LOG_DIR + '/amlogic_check_kernel.log'; const LOG_FIRMWARE = LOG_DIR + '/amlogic_check_firmware.log'; const LOG_RESCUE = LOG_DIR + '/amlogic_check_rescue.log'; +const BACKUP_STATUS = LOG_DIR + '/amlogic_backup_status.log'; const UPLOAD_TMP = '/tmp/upload'; const PKG_NAME = 'luci-app-amlogic'; const BACKUP_LIST = '/etc/amlogic_backup_list.conf'; @@ -681,13 +682,29 @@ function m_install_upload(req) { return { error: 'unsupported file type' }; } +// Kick off openwrt-backup -b in the background; the resulting tarball can be +// fetched via fs.read_direct once backup_status reports done. Packing /etc +// (docker, openclash rules, etc.) often exceeds the ~20s ubus RPC timeout, so +// this must not block the rpcd thread. function m_backup_create() { ensure_dirs(); shcall('chmod +x /usr/sbin/openwrt-backup'); - shcall('/usr/sbin/openwrt-backup -b > ' + LOG_FILE + ' && sync'); + writefile(BACKUP_STATUS, 'running\n'); + shbg('/usr/sbin/openwrt-backup -b > ' + LOG_FILE + ' 2>&1 && sync && ' + + '[ -f /.reserved/openwrt_config.tar.gz ] && ' + + 'echo done > ' + BACKUP_STATUS + ' || echo failed > ' + BACKUP_STATUS); + return { code: 0 }; +} + +function m_backup_status() { + const t = trim(readfile(BACKUP_STATUS) ?? ''); const path = '/.reserved/openwrt_config.tar.gz'; - const ok = access(path); - return { ok: ok ? true : false, path }; + const st = stat(path); + return { + status: t != '' ? t : 'idle', + path, + size: (st && st.type == 'file') ? st.size : 0 + }; } function m_save_backup_list(req) { @@ -778,6 +795,7 @@ const methods = { delete_upload: { args: { name: '' }, call: m_delete_upload }, install_upload: { args: { name: '' }, call: m_install_upload }, backup_create: { call: m_backup_create }, + backup_status: { call: m_backup_status }, save_backup_list:{ args: { content: '' }, call: m_save_backup_list }, prime_backup_list:{ call: m_prime_backup_list }, reload_cpu: { call: m_reload_cpu }, diff --git a/luci-app-openvpn-plus/Makefile b/luci-app-openvpn-plus/Makefile index 1f03366f9..cdbb39c60 100755 --- a/luci-app-openvpn-plus/Makefile +++ b/luci-app-openvpn-plus/Makefile @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openvpn-plus PKG_VERSION:=1.0.0 -PKG_RELEASE:=7 +PKG_RELEASE:=8 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Manfred Jaider diff --git a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus index 96492edbf..6dc87834e 100755 --- a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus +++ b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus @@ -11,164 +11,250 @@ # Get the live hardware device name behind the logical 'lan' interface getl3dev() { + # no argument (no shift) local dump device + local fallback="br-lan" - # Step 1: Fetch network interface status from ubus + # Get the network interface status from ubus dump=$(ubus call network.interface.lan status 2>/dev/null) if [ -z "$dump" ]; then - # Fallback if ubus fails - echo "br-lan" + printf "%s\n" "$fallback" return 1 fi - # Step 2: Try to get l3_device, then fallback to standard device - device=$(echo "$dump" | jsonfilter -e '@.l3_device' 2>/dev/null) - [ -z "$device" ] && device=$(echo "$dump" | jsonfilter -e '@.device' 2>/dev/null) + # Try to get l3_device. If it is empty, try to get standard device. + device=$(jsonfilter -s "$dump" -e '@.l3_device' 2>/dev/null) + if [ -z "$device" ]; then + device=$(jsonfilter -s "$dump" -e '@.device' 2>/dev/null) + fi - # Step 3: Return the found device or the default fallback if [ -n "$device" ]; then - echo "$device" + printf "%s\n" "$device" return 0 else - echo "br-lan" + printf "%s\n" "$fallback" return 1 fi } # Get mac address of lan interface using the dynamic device name getmac() { + # no argument (no shift) local device mac - # Step 1: Get the dynamic device name (e.g. br-lan, eth0, lan0) + # Get the dynamic device name (e.g. br-lan) device=$(getl3dev) - # Step 2: Fetch the live MAC address of that specific device - mac=$(ubus call network.device status "{\"name\":\"${device}\"}" 2>/dev/null | jsonfilter -e '@.macaddr' 2>/dev/null) + # Get the live MAC address of that specific device. + mac=$(ubus call network.device status 2>/dev/null | jsonfilter -e "@[\"$device\"].macaddr" 2>/dev/null) - # Step 3: Return the MAC or the default fallback address if [ -n "$mac" ]; then - echo "$mac" + printf "%s\n" "$mac" return 0 else - echo "00:00:00:00:00:00" + printf "%s\n" "00:00:00:00:00:00" return 1 fi } -# Get the active LAN IP with mask +# Get the active LAN IP with mask (optional parameter: cidr) lanip() { shift # Remove "lanip" from the arguments list - local cidr="$1" - # Try to extract the first IPv4 address and subnet mask from the JSON data - local dump ip mask - dump=$(ubus call network.interface.lan status 2>/dev/null) - ip=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) - mask=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].mask' 2>/dev/null) - - # If no IPv4 address exists, try to get the first IPv6 address and prefix length - if [ -z "$ip" ]; then - ip=$(echo "$dump" | jsonfilter -e '@["ipv6-address"][0].address' 2>/dev/null) - mask=$(echo "$dump" | jsonfilter -e '@["ipv6-address"][0].mask' 2>/dev/null) + # Check if the number of parameters is valid (must be 0 or 1) + if [ "$#" -gt 1 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 fi + # Default to empty string if no argument is passed + local cidr="${1:-}" + local dump ip mask + + # Fetch network interface status from ubus + dump=$(ubus call network.interface.lan status 2>/dev/null) + if [ -z "$dump" ]; then + printf "%s\n" "ERROR: Failed to get net status." >&2 + return 1 + fi + + # Try to get the first IPv4 address and mask + ip=$(jsonfilter -s "$dump" -e '@["ipv4-address"][0].address' 2>/dev/null) + mask=$(jsonfilter -s "$dump" -e '@["ipv4-address"][0].mask' 2>/dev/null) + + # If no IPv4 exists, try to get the first IPv6 address and mask + if [ -z "$ip" ]; then + ip=$(jsonfilter -s "$dump" -e '@["ipv6-address"][0].address' 2>/dev/null) + mask=$(jsonfilter -s "$dump" -e '@["ipv6-address"][0].mask' 2>/dev/null) + fi + + # Check if we found an IP address if [ -z "$ip" ]; then - # Error if [ "$cidr" = "cidr" ]; then - echo "0.0.0.0/0" + printf "%s\n" "0.0.0.0/0" else - echo "0.0.0.0" + printf "%s\n" "0.0.0.0" fi return 1 else - # Success - if [ "$cidr" = "cidr" ]; then - echo "${ip}/${mask}" + # Return the IP. If cidr is requested and mask exists, add the mask. + if [ "$cidr" = "cidr" ] && [ -n "$mask" ]; then + printf "%s\n" "${ip}/${mask}" else - echo "$ip" + printf "%s\n" "$ip" fi return 0 fi } +# Helper to validate interface, instance or device names (security sandbox) +internal_checkname() { + # internal function (no shift) + local name="$1" + case "$name" in + "") + # Block empty inputs instantly + return 1 + ;; + *..*|*/*) + # Block path traversal (..) and directory slashes + return 1 + ;; + *[!a-zA-Z0-9._=+\@\~-]*) + # Block any character that is NOT inside our expanded safe white-box allowlist + return 1 + ;; + esac + return 0 +} + # Get the transfer statistics of a network interface getnetstat() { shift # Remove "getnetstat" from the arguments list + + # Check if exactly one argument is passed + if [ "$#" -ne 1 ]; then + printf "%s\n" "0|0|0|0|false|unknown" + return 1 + fi + local interface="$1" + # Validate interface name against invalid characters + if ! internal_checkname "$interface"; then + printf "%s\n" "0|0|0|0|false|invalid" + return 1 + fi + # Check if network interface statistics directory exists if [ ! -d "/sys/class/net/$interface/statistics" ]; then - echo "0|0|0|0|false|$interface" + printf "%s\n" "0|0|0|0|false|$interface" return 0 fi # Read statistics directly from sysfs files local rxb txb rxp txp - rxb=$(cat /sys/class/net/"$interface"/statistics/rx_bytes 2>/dev/null || echo 0) - txb=$(cat /sys/class/net/"$interface"/statistics/tx_bytes 2>/dev/null || echo 0) - rxp=$(cat /sys/class/net/"$interface"/statistics/rx_packets 2>/dev/null || echo 0) - txp=$(cat /sys/class/net/"$interface"/statistics/tx_packets 2>/dev/null || echo 0) + rxb=$(cat "/sys/class/net/$interface/statistics/rx_bytes" 2>/dev/null || printf "0") + txb=$(cat "/sys/class/net/$interface/statistics/tx_bytes" 2>/dev/null || printf "0") + rxp=$(cat "/sys/class/net/$interface/statistics/rx_packets" 2>/dev/null || printf "0") + txp=$(cat "/sys/class/net/$interface/statistics/tx_packets" 2>/dev/null || printf "0") - echo "$rxb|$txb|$rxp|$txp|true|$interface" + printf "%s\n" "$rxb|$txb|$rxp|$txp|true|$interface" return 0 } # Evaluates system hardware to determine the highest-performing cipher suite anchor bestcrypto() { - # no arguments (no shift) - if grep -E -q 'aes|pmull' /proc/cpuinfo; then - echo "AES" + # no argument (no shift) + # Check for hardware encryption support (AES or PMULL) + if grep -qE 'aes|pmull' /proc/cpuinfo 2>/dev/null; then + printf "%s\n" "AES" else - echo "CHACHA" + printf "%s\n" "CHACHA" fi return 0 } +# Central security helper to validate file paths +internal_checkpath() { + # internal (no shift) + local path="$1" + + # Centralized portable security gate using 100% consistent case matching + # Strictly NO double quotes used anywhere in the patterns! + case "$path" in + "") + # Block empty inputs instantly + return 1 + ;; + *..*|*\\*) + # Block path traversal (..) and backslashes + return 1 + ;; + *\;*|*\&*|*\|*|*\$*|*"\`"*|*"´"*|*\<*|*\>*|*\@*|*\ *) + # Block dangerous shell injection characters and spaces. + # Characters are safely escaped with a backslash to protect the shell syntax. + return 1 + ;; + *$'\t'*|$'\n'*|$'\r'*) + # Block control characters like tabs, newlines, and carriage returns natively + return 1 + ;; + esac + + # Step 2: Block wildcards (* and ?) using the verified ash-safe string replacement trick. + # Kept outside because '*' and '?' are native case wildcard operators. + local clean_wildcards="${path//\*/}" + clean_wildcards="${clean_wildcards//\?/}" + if [ "$path" != "$clean_wildcards" ]; then + return 1 + fi + + return 0 +} + # readstatus only allows access to: /proc/uptime, /var/run/openvpn.*.status, /tmp/run/openvpn.*.status, and /etc/openvpn/luci/*.conf readstatus() { shift # Remove "readstatus" from the arguments list if [ "$#" -ne 1 ]; then - echo "ERROR: Invalid number of parameters." >&2 + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi local requested_path="$1" - # 1. Critical Guard: Block empty inputs or directory traversal hacks (..) - if [ -z "$requested_path" ] || echo "$requested_path" | grep -q '\.\.'; then - echo "ERROR: Structural security barrier rejection." >&2 + # Check path contains invalid characters + if ! internal_checkpath "$requested_path"; then + printf "%s\n" "ERROR: Invalid characters in path." >&2 return 1 fi - # 2. Fix: If the path does not start with a slash, add it dynamically + # Ensure the path starts with a slash case "$requested_path" in /*) ;; *) requested_path="/$requested_path" ;; esac - # 3. Strict White-Box Pattern Validation matching the openwrt/luci ACL expectations + # Strict White-Box Pattern Validation (Exact match only) case "$requested_path" in - /proc/uptime) - ;; - /var/run/openvpn.*.status) - ;; - /tmp/run/openvpn.*.status) - ;; + /proc/uptime|\ + /var/run/openvpn.*.status|\ + /tmp/run/openvpn.*.status|\ /etc/openvpn/luci/*.conf) + # Authorized paths ;; *) - echo "ERROR: Access denied. Protected path." >&2 + printf "%s\n" "ERROR: Access denied. Protected path." >&2 return 1 ;; esac - # 4. Output the file content securely - if [ -e "$requested_path" ]; then + # Step 4: Output the file content securely + if [ -f "$requested_path" ]; then cat "$requested_path" return 0 else - # Return an explicit empty line for valid but empty status files - echo "" + printf "\n" return 0 fi } @@ -177,61 +263,63 @@ readstatus() { internal_securepath() { # internal (no shift) local target_path="$1" - local block_access="$2" # Optional: "write" to block read-only folders + local block_access="${2:-}" # Optional: "write" to block read-only folders - # 1. Critical Guard: Block empty inputs or directory traversal hacks (..) - if [ -z "$target_path" ] || echo "$target_path" | grep -q '\.\.'; then + # Step 1: Run the character check. If it fails, internal_checkpath blocks it. + if ! internal_checkpath "$target_path"; then + printf "%s\n" "ERROR: Invalid characters in path." >&2 return 1 fi - # 2. Fix: If the path does not start with a slash, add it dynamically + # Step 2: Ensure path starts with a slash case "$target_path" in /*) ;; *) target_path="/$target_path" ;; esac - # 3. Check if the absolute path is inside the allowed sandbox folders + # Step 3: Check sandbox folders case "$target_path" in /proc/uptime) - # Strictly read-only /proc/uptime - [ "$block_access" = "write" ] && return 1 - echo "$target_path" + if [ "$block_access" = "write" ]; then + printf "%s\n" "ERROR: Access denied. Write protected path." >&2 + return 1 + fi return 0 ;; /proc/*) - # Strictly read-only PID checks! - # Validate that the path contains ONLY digits after /proc/ (e.g., /proc/1234) - [ "$block_access" = "write" ] && return 1 - if echo "$target_path" | grep -q -E '^/proc/[0-9]+$'; then - echo "$target_path" - return 0 + if [ "$block_access" = "write" ]; then + printf "%s\n" "ERROR: Access denied. Write protected path." >&2 + return 1 fi - return 1 + local pid_part="${target_path#/proc/}" + case "$pid_part" in + *[!0-9]*|"") + printf "%s\n" "ERROR: Access denied. Invalid PID path." >&2 + return 1 + ;; + *) return 0 ;; + esac ;; - /var/run/openvpn.keygen.lock|/var/run/fw4.state|/var/run/openvpn.*.status) - # Read-only files! Block if write access is requested. - [ "$block_access" = "write" ] && return 1 - echo "$target_path" + /var/run/openvpn.keygen.lock|\ + /var/run/fw4.state|\ + /var/run/openvpn.*.status|\ + /tmp/openvpn.keygen*|\ + /tmp/run/openvpn.*.status) + if [ "$block_access" = "write" ]; then + printf "%s\n" "ERROR: Access denied. Write protected path." >&2 + return 1 + fi return 0 ;; - /tmp/openvpn.keygen*|/tmp/run/openvpn.*.status) - # Read-only files! Block if write access is requested. - [ "$block_access" = "write" ] && return 1 - echo "$target_path" - return 0 - ;; - /etc/config/openvpn|/etc/config/openvpn-def) - # Isolated Config Files: Full read and write access for configuration - echo "$target_path" - return 0 - ;; - /etc/openvpn/luci/*|/etc/openvpn/keys/*) - # Isolated App Storage: Full read and write access for certificates and profiles - echo "$target_path" + /etc/config/openvpn|\ + /etc/config/openvpn-def|\ + /etc/openvpn/luci/*|\ + /etc/openvpn/keys/*) + # Full read and write access return 0 ;; *) - # Any other file, system folder, or unlisted path is strictly blocked + printf "%s\n" "ERROR: Access denied. Protected path." >&2 return 1 ;; esac @@ -241,22 +329,29 @@ internal_securepath() { readfile() { shift # Remove "readfile" from the arguments list if [ "$#" -ne 1 ]; then - echo "ERROR: Invalid number of parameters." >&2 + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi - # Get the validated absolute path from internal_securepath - local filepath - filepath=$(internal_securepath "$1" "read") - if [ -z "$filepath" ]; then - echo "ERROR: Access denied. Protected path." >&2 + local filepath="$1" + + # Run central path verification (It handles all errors automatically) + if ! internal_securepath "$filepath" "read"; then return 1 fi - if [ -e "$filepath" ]; then + + # Ensure path has a leading slash for the file read + case "$filepath" in + /*) ;; + *) filepath="/$filepath" ;; + esac + + # Output the file content + if [ -f "$filepath" ]; then cat "$filepath" return 0 else - echo "" + printf "\n" return 0 fi } @@ -265,31 +360,40 @@ readfile() { writefile() { shift # Remove "writefile" from the arguments list if [ "$#" -ne 2 ]; then - echo "ERROR: Invalid number of parameters." + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi - # Get the validated absolute path from securepath with write permission check - local filepath - filepath=$(internal_securepath "$1" "write") + local filepath="$1" local content="$2" - if [ -z "$filepath" ]; then - echo "ERROR: Access denied. Protected path." + + # Run central path verification (It handles all errors automatically) + if ! internal_securepath "$filepath" "write"; then return 1 fi + # Ensure path has a leading slash for directory and file operations + case "$filepath" in + /*) ;; + *) filepath="/$filepath" ;; + esac + # Ensure the target folder exists before writing files into it - mkdir -p "$(dirname "$filepath")" + local dirpath + dirpath=$(dirname "$filepath") + if [ ! -d "$dirpath" ]; then + mkdir -p "$dirpath" + chmod 755 "$dirpath" + fi - # Restrict umask inline to block race conditions without subshell (prevents LuCI/rpcd process blocking and premature termination) - local old_umask - old_umask=$(umask) - umask 077 - printf '%s' "$content" > "$filepath" - umask "$old_umask" + # Write content securely using an isolated subshell sandbox. + # The subshell automatically resets the umask when it closes. + ( + umask 077 + printf '%s' "$content" > "$filepath" + chmod 600 "$filepath" + ) - # Restrict permissions permanently so only root can access it - chmod 600 "$filepath" return 0 } @@ -297,19 +401,24 @@ writefile() { removefile() { shift # Remove "removefile" from the arguments list if [ "$#" -ne 1 ]; then - echo "ERROR: Invalid number of parameters." >&2 + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi - # Get the validated absolute path from securepath with write permission check - local filepath - filepath=$(internal_securepath "$1" "write") - if [ -z "$filepath" ]; then - echo "ERROR: Access denied. Protected path." >&2 + local filepath="$1" + + # Run central path verification (It handles all errors automatically) + if ! internal_securepath "$filepath" "write"; then return 1 fi - # Delete the file safely without generating errors if it does not exist + # Ensure path has a leading slash for the file remove operation + case "$filepath" in + /*) ;; + *) filepath="/$filepath" ;; + esac + + # Delete the file (No error if file not exist) rm -f "$filepath" return 0 } @@ -318,18 +427,23 @@ removefile() { statfile() { shift # Remove "statfile" from the arguments list if [ "$#" -ne 1 ]; then - echo "ERROR: Invalid number of parameters." >&2 + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi - # Get the validated absolute path from securepath - local filepath - filepath=$(internal_securepath "$1" "read") - if [ -z "$filepath" ]; then - echo "ERROR: Access denied. Protected path." >&2 + local filepath="$1" + + # Run central path verification (It handles all errors automatically) + if ! internal_securepath "$filepath" "read"; then return 1 fi + # Ensure path has a leading slash for the file remove operation + case "$filepath" in + /*) ;; + *) filepath="/$filepath" ;; + esac + # Check if something exists at the path (file, directory or symlink) if [ -e "$filepath" ] || [ -L "$filepath" ]; then local file_type="file" @@ -339,35 +453,54 @@ statfile() { [ -L "$filepath" ] && file_type="symlink" if [ "$file_type" = "file" ]; then - # wc -c returns "123 /path/to/file", we use awk to extract just the number - file_size=$(wc -c "$filepath" 2>/dev/null | awk '{print $1}') + # Using '< "$filepath"' makes wc print ONLY the number + file_size=$(wc -c < "$filepath" 2>/dev/null) + # Cut off everything after the first space (removes path or padding) + file_size="${file_size%% *}" [ -z "$file_size" ] && file_size=0 fi - # Get modification time + # Get modification time safely local mtime - mtime=$(date -r "$filepath" +%s 2>/dev/null || echo 0) + mtime=$(date -r "$filepath" +%s 2>/dev/null || printf "0") - # Print simulated JSON string - echo "{\"type\":\"$file_type\",\"size\":$file_size,\"mtime\":$mtime}" + # Print simulated JSON string securely using printf + printf '{"type":"%s","size":%s,"mtime":%s}\n' "$file_type" "$file_size" "$mtime" return 0 fi + + # Return an empty JSON object if file does not exist + printf '%s\n' '{"type":"none","size":0,"mtime":0}' return 1 } # OpenVPN version ovpnver() { - # no arguments (no shift) + # Check if the openvpn binary exists and is executable + if ! command -v openvpn >/dev/null 2>&1; then + printf "%s\n" "ERROR: OpenVPN binary not found." >&2 + return 1 + fi openvpn --version - return 0; + return 0 } # Central validator for sleep seconds parameter internal_validatesleep() { - # internal (no shift) + # internal function (no shift) local sec="$1" - # Check if empty, contains non-digits, is less than 1, or greater than 60 - if [ -z "$sec" ] || ! echo "$sec" | grep -q '^[0-9]\+$' || [ "$sec" -lt 1 ] || [ "$sec" -gt 60 ]; then + case "$sec" in + "") + # Block empty inputs instantly + return 1 + ;; + *[!0-9]*) + # Block if it contains anything that is NOT a number (0-9) + return 1 + ;; + esac + # Check the limits (1-60). + if [ "$sec" -lt 1 ] || [ "$sec" -gt 60 ]; then return 1 fi return 0 @@ -376,98 +509,144 @@ internal_validatesleep() { # OpenVPN self healing start engine ovpnstart() { shift # Remove "ovpnstart" from the arguments list - local inst="$1" - local sleepsec="$2" + if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + + local instance="$1" + local sleepsec="${2:-}" + local all_running="1" + + # Check instance name is valid + if [ -n "$instance" ]; then + if ! internal_checkname "$instance"; then + printf "%s\n" "ERROR: Invalid character format in instance name." >&2 + return 1 + fi + fi # Execute optional validated sleep delay before doing any work if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi - if [ -z "$(echo "$inst" | tr -d ' ')" ]; then - inst="" - fi - - local all_running="1" - - if [ -n "$inst" ] ; then + if [ -n "$instance" ]; then # A) SINGLE INSTANCE MODE: Check if this specific instance is running - if ! ps | grep -v "grep" | grep -q "openvpn(${inst})"; then + if ! ps -w | grep -v "grep" | grep -F -q "openvpn(${instance})"; then all_running="0" fi else # B) GLOBAL MODE: Check all enabled profiles in the configuration - uci -q show openvpn | grep "=openvpn" | while read -r line; do - local check_inst - check_inst="${line##*.}" - check_inst="${check_inst%%=*}" + # Get list of instances (instance1 instance2 ...) + local openvpn_sections + openvpn_sections=$(uci -q show openvpn | grep "=openvpn" | awk -F'[.=]' '{print $2}' 2>/dev/null) - if [ "$(uci -q get openvpn."${check_inst}".enabled)" = "1" ]; then - if ! ps | grep -v "grep" | grep -q "openvpn(${check_inst})"; then - exit 1 + local check_inst + for check_inst in $openvpn_sections; do + [ -z "$check_inst" ] && continue + + # Ensure the extracted configuration profile name is safe + if internal_checkname "$check_inst"; then + if [ "$(uci -q get openvpn."${check_inst}".enabled)" = "1" ]; then + if ! ps -w | grep -v "grep" | grep -F -q "openvpn(${check_inst})"; then + all_running="0" + break + fi fi fi done - - if [ "$?" -ne 0 ]; then - all_running="0" - fi fi + # Self-healing trigger if any required instance is down if [ "$all_running" = "0" ]; then - /etc/init.d/openvpn stop "$inst" >/dev/null 2>&1 + + # Sequential hard reset sequence (tested and failsafe workaround). + # Required because frozen processes often ignore a single restart command. + /etc/init.d/openvpn stop "$instance" >/dev/null 2>&1 sleep 1 - /etc/init.d/openvpn restart "$inst" >/dev/null 2>&1 + /etc/init.d/openvpn restart "$instance" >/dev/null 2>&1 sleep 1 - /etc/init.d/openvpn reload "$inst" >/dev/null 2>&1 + /etc/init.d/openvpn reload "$instance" >/dev/null 2>&1 fi + + return 0 } # OpenVPN self healing restart engine ovpnrestart() { shift # Remove "ovpnrestart" from the arguments list - local inst="$1" - local sleepsec="$2" + if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + + local instance="$1" + local sleepsec="${2:-}" + + # Check instance name is valid + if [ -n "$instance" ]; then + if ! internal_checkname "$instance"; then + printf "%s\n" "ERROR: Invalid character format in instance name." >&2 + return 1 + fi + fi # Execute optional validated sleep delay before doing any work if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi - if [ -z "$(echo "$inst" | tr -d ' ')" ]; then - inst="" - fi - - /etc/init.d/openvpn stop "$inst" >/dev/null 2>&1 + # Sequential hard reset sequence (tested and failsafe workaround). + # Required because frozen processes often ignore a single restart command. + /etc/init.d/openvpn stop "$instance" >/dev/null 2>&1 sleep 1 - /etc/init.d/openvpn restart "$inst" >/dev/null 2>&1 + /etc/init.d/openvpn restart "$instance" >/dev/null 2>&1 sleep 1 - /etc/init.d/openvpn reload "$inst" >/dev/null 2>&1 + /etc/init.d/openvpn reload "$instance" >/dev/null 2>&1 } # OpenVPN force stop engine ovpnstop() { shift # Remove "ovpnstop" from the arguments list - local inst="$1" - local sleepsec="$2" + if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + + local instance="$1" + local sleepsec="${2:-}" + + # Check instance name is valid + if [ -n "$instance" ]; then + if ! internal_checkname "$instance"; then + printf "%s\n" "ERROR: Invalid character format in instance name." >&2 + return 1 + fi + fi # Execute optional validated sleep delay before doing any work if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi - if [ -z "$(echo "$inst" | tr -d ' ')" ]; then - inst="" - fi - - /etc/init.d/openvpn stop "$inst" >/dev/null 2>&1 + # Sequential hard stop sequence (tested and failsafe workaround). + # Twice executed to ensure frozen processes are completely terminated. + /etc/init.d/openvpn stop "$instance" >/dev/null 2>&1 sleep 1 - /etc/init.d/openvpn stop "$inst" >/dev/null 2>&1 + /etc/init.d/openvpn stop "$instance" >/dev/null 2>&1 + + return 0 } # firewall stop/start/restart engine firewallservice () { shift # Remove "firewallservice" from the arguments list + if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + local action_cmd="$1" local sleepsec="$2" @@ -479,6 +658,9 @@ firewallservice () { # 2. Call the standard OpenWrt system firewall controller safely if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ] || [ "$action_cmd" = "reload" ]; then /etc/init.d/firewall "$action_cmd" >/dev/null 2>&1 + else + printf "%s\n" "ERROR: Invalid firewall action command." >&2 + return 1 fi return 0 @@ -487,60 +669,116 @@ firewallservice () { # Secure helper to read and change firewall rules from LuCI without restrictions firewallnft() { shift # Remove "firewallnft" from the arguments list - local chain="$1" - # Check if the chain name is empty or contains bad characters (only letters, numbers, and underscores are allowed) - if [ -z "$chain" ] || ! echo "$chain" | grep -Eq '^[A-Za-z0-9_]+$'; then + if [ "$#" -ne 1 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 return 1 fi + + local chain="$1" + + # Validate that the nft binary is available on the system + if ! command -v nft >/dev/null 2>&1; then + printf "%s\n" "ERROR: nftables binary not found." >&2 + return 1 + fi + + # Check if the chain name is empty or contains bad characters + if ! internal_checkname "$chain"; then + printf "%s\n" "ERROR: Invalid character format in chain name." >&2 + return 1 + fi + + # Get the specific firewall chain from nftables /usr/sbin/nft list chain inet fw4 "$chain" 2>&1 + return 0 } # Secure helper to calculate cryptographic hashes using openssl dgst (openssldgst -sha256 test) openssldgst() { shift # Remove "openssldgst" from the arguments list - # printf '%s' "test" | /usr/bin/openssl dgst "-sha256" -r 2>&1 (result: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08) - printf '%s' "$2" | /usr/bin/openssl dgst "$1" -r 2>&1 + if [ "$#" -ne 2 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + + local algo="$1" + local content="$2" + + # Validate that the openssl binary is available on the system + if ! command -v openssl >/dev/null 2>&1; then + printf "%s\n" "ERROR: openssl binary not found." >&2 + return 1 + fi + + # Check for allowed hash algorithms + case "$algo" in + -sha256|-sha512|-sha384|-sha1|-md5) + # Authorized crypto algorithm + ;; + *) + printf "%s\n" "ERROR: Unauthorized hash algorithm." >&2 + return 1 + ;; + esac + + # Securely pipe the content to openssl dgst and remove the " *stdin" text + local hash_result + hash_result=$(printf '%s' "$content" | /usr/bin/openssl dgst "$algo" -r 2>&1) + + # Output only the clean hex string + printf "%s\n" "${hash_result%% *}" return 0 } # Manage temporary download links for mobile profiles [create|delete] symlink() { shift # Remove "symlink" from the arguments list + if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + local ovpn_instance="$1" local action="$2" - local raw_display_name="$3" + local raw_display_name="${3:-}" + + # Check if instance name is valid + if ! internal_checkname "$ovpn_instance"; then + printf "%s\n" "ERROR: Invalid character format in instance name." >&2 + return 1 + fi + local export_static_path="/etc/openvpn/luci/${ovpn_instance}.ovpn" local lan_ip="" local link_filename="" local ram_link_dir="/tmp/luci-ovpn-plus" local www_link_dir="/www/ovpn-downloads" - # Check if instance name is provided and safe (alphanumeric only). - if [ -z "$ovpn_instance" ] || ! echo "$ovpn_instance" | grep -q '^[A-Za-z0-9]\+$'; then - echo "ERROR: Invalid instance name." - return 1 - fi - - # Clean the display name. Only letters, numbers, hyphens, and underscores are allowed. + # Check if display name is valid if [ -n "$raw_display_name" ]; then - link_filename=$(echo "$raw_display_name" | tr -cd 'A-Za-z0-9_-') - fi - - # Fallback if the cleaned name is empty. - if [ -z "$link_filename" ]; then - link_filename="${ovpn_instance}" + if ! internal_checkname "$raw_display_name"; then + printf "%s\n" "ERROR: Invalid character format in display name." >&2 + return 1 + fi + link_filename="$raw_display_name" + else + link_filename="$ovpn_instance" fi case "$action" in create) if [ ! -f "$export_static_path" ]; then - echo "ERROR: Profile file missing at $export_static_path" + printf "%s\n" "ERROR: Profile file missing." >&2 return 1 fi - # Create a 9-byte random secret token as hex + # Create a 9-byte random secret token as hex safely local secret_token secret_token=$(dd if=/dev/urandom bs=9 count=1 2>/dev/null | hexdump -e '"%02x"') + if [ -z "$secret_token" ]; then + printf "%s\n" "ERROR: Failed to generate secure token." >&2 + return 1 + fi local session_dir="${ram_link_dir}/ovpn-tmp-${secret_token}-${link_filename}" mkdir -p "$session_dir" @@ -550,29 +788,33 @@ symlink() { cp -f "$export_static_path" "$token_protected_file" 2>/dev/null chmod 644 "$token_protected_file" - # Create webserver shortcut to the RAM folder + # Create webserver shortcut to the RAM folder securely if [ ! -L "$www_link_dir" ] && [ ! -d "$www_link_dir" ]; then ln -s "$ram_link_dir" "$www_link_dir" fi if [ -f "$token_protected_file" ]; then - lan_ip=$(lanip) - if [ -z "$lan_ip" ] || [ "$lan_ip" = "0.0.0.0" ]; then - lan_ip="localhost" + # Get lan ip with internal lanip function + lan_ip=$(lanip "lanip") + [ -z "$lan_ip" ] && lan_ip="localhost" + # If the IP contains a colon (:), it is an IPv6 address and needs brackets [] - elif echo "$lan_ip" | grep -q ':'; then - lan_ip="[${lan_ip}]" - fi - echo "SUCCESS: http://${lan_ip}/ovpn-downloads/ovpn-tmp-${secret_token}-${link_filename}/${link_filename}.ovpn" + case "$lan_ip" in + *:*) + lan_ip="[${lan_ip}]" + ;; + esac + printf "SUCCESS: http://%s/ovpn-downloads/ovpn-tmp-%s-%s/%s.ovpn\n" "$lan_ip" "$secret_token" "$link_filename" "$link_filename" else - echo "ERROR: Failed to create web export file." + printf "%s\n" "ERROR: Failed to create web export file." >&2 return 1 fi ;; + delete) - # Safe deletion inside RAM directory only - if [ -n "$link_filename" ] && [ -d "$ram_link_dir" ]; then - rm -rf "${ram_link_dir}"/ovpn-tmp-*-"${link_filename}" + # Safe deletion inside RAM directory only. Absolute path structure guaranteed. + if [ -d "$ram_link_dir" ]; then + rm -rf "${ram_link_dir}/ovpn-tmp-*-${link_filename}" # Remove RAM base directory and the web symlink if it was the last file rmdir "$ram_link_dir" 2>/dev/null @@ -580,107 +822,204 @@ symlink() { rm -f "$www_link_dir" fi - echo "SUCCESS: Link directory and static profile deleted from RAM." + printf "%s\n" "SUCCESS: Link directory and static profile deleted from RAM." else - echo "ERROR: Invalid deletion request." + printf "%s\n" "ERROR: Invalid deletion request." >&2 return 1 fi ;; + *) - echo "Usage: /usr/libexec/luci-app-openvpn-plus symlink [instance_id] [create|delete] [display_name]" + printf "%s\n" "ERROR: Invalid action command." >&2 return 1 ;; esac return 0 } +# Helper to prevent routing loops for IPv4 and IPv6 subnets +internal_checkloopback() { + # internal (no shift) + local target_route="$1" # e.g. "iroute 192.168.20.0 255.255.255.0" + local target_ip="" + + # Extract the IP or network address safely from the directive + case "$target_route" in + iroute\ *) + target_ip=$(printf "%s" "$target_route" | awk '{print $2}') + ;; + iroute-ipv6\ *) + target_ip=$(printf "%s" "$target_route" | awk '{print $2}') + # Strip the CIDR prefix for the route test if present (e.g. 2001:db8::/64 -> 2001:db8::) + target_ip="${target_ip%%/*}" + ;; + *) + # Not a routing directive + return 0 + ;; + esac + + [ -z "$target_ip" ] && return 0 + + # Get the dynamic dynamic local LAN device name (e.g. br-lan or eth0) + local lan_dev + lan_dev=$(getl3dev) + [ -z "$lan_dev" ] && lan_dev="br-lan" # Safe fallback + + # Ask the Linux Kernel how it would route to this target + local route_check + case "$target_ip" in + *:*) + # IPv6 route check + route_check=$(ip -6 route get "$target_ip" 2>/dev/null) + ;; + *) + # IPv4 route check + route_check=$(ip route get "$target_ip" 2>/dev/null) + ;; + esac + + # Check kernel response + case "$route_check" in + local\ *) + # LOOP DETECTED: Target matches the router's own interface IP directly! + return 1 + ;; + *via*) + # SAFE: The route goes to an external gateway + return 0 + ;; + *"dev $lan_dev"*) + # LOOP DETECTED: The target is directly inside our own local LAN subnet (no 'via')! + return 1 + ;; + esac + + # Network is safe to route + return 0 +} + # Validates connecting clients and applies dynamic routing topologies iroute() { shift # Remove "iroute" from the arguments list + if [ "$#" -lt 1 ]; then + logger -t openvpn-luci "iroute() Connection rejected: Missing target file argument from OpenVPN daemon." + return 1 + fi - # EXTERNAL ENVIRONMENT VARIABLES (Provided dynamically by OpenVPN Daemon): - # $common_name - Common Name (CN) of the connecting client certificate - # $trusted_ip - Real public WAN IP address of the remote client - # $proto - Protocol used by the client connection (tcp/udp) - local common_name="${common_name}" - local trusted_ip="${trusted_ip}" - local proto="${proto}" + # We always use the first parameter as the target file, exactly as OpenVPN expects + local tmp_file="$1" - case $proto in - *tcp*) proto="tcp" ;; - esac + # Strict character check on the path to prevent system manipulation + if ! internal_checkpath "$tmp_file"; then + logger -t openvpn-luci "iroute() Connection rejected: Untrusted target file path format." + return 1 + fi - # Anti-Loop Protection - if [ -n "$trusted_ip" ]; then - # Check all locally assigned interfaces for an exact match with the peer source IP - if ip addr show | grep -q -E "inet (6 )?$trusted_ip/"; then - logger -t openvpn-luci "Connection rejected: A loopback loop was detected while establishing a connection using the local router IP '$trusted_ip'." - return 1 - fi + local common_name="${common_name:-}" + if [ -z "$common_name" ]; then + logger -t openvpn-luci "iroute() Connection rejected: Missing common_name." + return 1 fi local i=1 local matched=0 - local var_cn val_cn - local var_rt val_rt + local val_cn val_rt - while [ -n "$(eval echo \$CLIENT_CNAME_$i 2>/dev/null)" ]; do - var_cn="CLIENT_CNAME_$i" - eval "val_cn=\$$var_cn" 2>/dev/null + while true; do + # Extract the value cleanly from the internal shell memory list + val_cn=$(set | grep "^CLIENT_CNAME_$i=" | cut -d'=' -f2-) + [ -z "$val_cn" ] && break - if [ -n "$val_cn" ] && [ "$val_cn" = "$common_name" ] 2>/dev/null; then - var_rt="CLIENT_ROUTE_$i" - eval "val_rt=\$$var_rt" 2>/dev/null + # Robust dual-quoting cleanup (supports both ' and " styles) + val_cn="${val_cn#\'}"; val_cn="${val_cn#\"}" + val_cn="${val_cn%\'}"; val_cn="${val_cn%\"}" - if [ -n "$val_rt" ] 2>/dev/null; then - # Write internal route (e.g. iroute 192.168.20.0 ...) - echo "$val_rt" > "$1" - matched=1 - return 0 + if [ "$val_cn" = "$common_name" ]; then + val_rt=$(set | grep "^CLIENT_ROUTE_$i=" | cut -d'=' -f2-) + + # Apply the same robust cleanup for the routing string + val_rt="${val_rt#\'}"; val_rt="${val_rt#\"}" + val_rt="${val_rt%\'}"; val_rt="${val_rt%\"}" + + if [ -n "$val_rt" ]; then + case "$val_rt" in + iroute\ *|iroute-ipv6\ *) + # Run our precise IPv4/IPv6 subnet loopback validation helper + if ! internal_checkloopback "$val_rt"; then + logger -t openvpn-luci "iroute() Connection rejected: Routing loop hazard! Client route '$val_rt' overlaps with local networks." + return 1 + fi + + # Write the routing directive into the file provided by OpenVPN + printf "%s\n" "$val_rt" > "$tmp_file" + matched=1 + logger -t openvpn-luci "iroute() iroute() Authorized: client '$common_name' -> route '$val_rt'" + break + ;; + *) + logger -t openvpn-luci "iroute() Security alert: invalid prefix in CLIENT_ROUTE_$i for '$common_name'." + return 1 + ;; + esac fi fi i=$((i+1)) done if [ "$matched" -eq 0 ]; then - logger -t openvpn-luci "Connection rejected: missing CLIENT_CNAME or CLIENT_ROUTE parameters for '$common_name'." + logger -t openvpn-luci "iroute() Connection rejected: Missing matching route configuration for '$common_name'." return 1 fi + return 0 } # Clean any string from spaces, tabs, newlines, http(s)://, paths, ports and URL brackets cleanipdns() { shift # Remove "cleanipdns" from the arguments list - local input="$1" - - # 1. Delete spaces, tabs, and carriage returns - input=$(echo "$input" | tr -d ' \t\r\n') - - # 2. Strip http:// or https:// from the beginning (Case-Insensitive) - input=$(echo "$input" | sed -E 's|^https?://||I') - - # 3. Strip everything from the first remaining slash onwards (removes paths/folders) - input=$(echo "$input" | sed -E 's|/.*||') - - # 4. PERFECT ORDER PORT FILTER: RFC-compliant separation based on your discovery - if echo "$input" | grep -q "\]"; then - # Case A: String contains IPv6 brackets -> Cut everything instantly after the closing bracket ']' - input=$(echo "$input" | sed -E 's|(\]).*|\1|') - elif echo "$input" | grep -q -E "\.[0-9a-zA-Z:-]+:[0-9]+$|^[0-9.]+: [0-9]+$"; then - # Case B: Verified IPv4 or Domain WITH a trailing port (contains dots and ends on :numbers) - input=$(echo "$input" | sed -E 's|:[0-9]+$||') - else - # Case C: Pure raw IPv6 address without brackets -> Strictly DO NOT touch the end of the address! - : + if [ "$#" -ne 1 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 fi - # 5. Remove brackets [ and ] used for IPv6 URL notation - input=$(echo "$input" | tr -d '[]') + local input="$1" - # Output the final perfectly cleaned domain, IPv4 or IPv6 string + # Step 1: Delete spaces, tabs, and carriage returns natively in RAM + input="${input//[[:space:]]/}" + input="${input//[$'\r'$'\n']/}" + + # Step 2: Strip http:// or https:// from the beginning safely + case "$input" in + [Hh][Tt][Tt][Pp][Ss]://*) input="${input#*://}" ;; + [Hh][Tt][Tt][Pp]://*) input="${input#*://}" ;; + esac + + # Step 3: Strip everything from the first remaining slash onwards + input="${input%%/*}" + + # Step 4: RFC-compliant separation of ports and brackets using bulletproof case matching + case "$input" in + *]* ) + # Case A: String contains IPv6 brackets -> Cut everything after closing bracket + input="${input%%]*}]" + ;; + *.*:[0-9]* ) + # Case B: Verified IPv4 or Domain with trailing port (Must contain a real DOT and end on :numbers) + input="${input%:[0-9]*}" + ;; + * ) + # Case C: Pure raw IPv6 address or clean host -> Do not touch! + ;; + esac + + # Step 5: Remove brackets [ and ] used for IPv6 URL notation natively + input="${input//[/}" + input="${input//]/}" + + # Step 6: Output the final perfectly cleaned string if [ -n "$input" ]; then - echo "$input" + printf "%s\n" "$input" return 0 fi return 1 @@ -689,31 +1028,109 @@ cleanipdns() { # Validate and clean any given IPv4 or IPv6 address string checkip() { shift # Remove "checkip" from the arguments list - local ip="$1" - - # Delete all spaces, tabs and hidden lines from the text - ip=$(echo "$ip" | tr -d ' \t\r\n') - [ -z "$ip" ] && return 1 - - local ipv4_regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$" - # The exact working combination from successful shell test! - local ipv6_regex="^([0-9a-fA-F]{1,4}:|:){1,7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,7}$" - - # Case 1: Check if the text is a valid IPv4 address (numbers 0 to 255) - if echo "$ip" | grep -E -q "$ipv4_regex"; then - echo "$ip" | awk -F. '$1<=255 && $2<=255 && $3<=255 && $4<=255 {print $0}' | grep -q "." && { - echo "$ip" - return 0 - } + if [ "$#" -ne 1 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 fi - # Case 2: Check if the text matches the verified IPv6 regex - if echo "$ip" | grep -E -q "$ipv6_regex"; then - if echo "$ip" | grep -q ":"; then - # Change all hex letters to lowercase for safe matching later - echo "$ip" | tr 'A-Z' 'a-z' - return 0 - fi + local raw_input="$1" + + # Pre-clean the input string using cleanipdns + local ip + ip=$(cleanipdns "cleanipdns" "$raw_input" 2>/dev/null) + [ -z "$ip" ] && return 1 + + # Safe protocol separation using consistent POSIX case matching + case "$ip" in + *:*) + # IPv6 Verification + case "$ip" in + *[!0-9a-fA-F:]*) return 1 ;; + esac + + # Ask the Linux Kernel if this is a mathematically valid IPv6 address + if ip -6 route get "$ip" >/dev/null 2>&1; then + # Success: Convert hex to lowercase cleanly via awk + printf "%s\n" "$ip" | awk '{print tolower($0)}' + return 0 + fi + ;; + *) + # IPv4 Verification + local ipv4_regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$" + if printf "%s" "$ip" | grep -E -q "$ipv4_regex"; then + local o1 o2 o3 o4 rem + o1="${ip%%.*}"; rem="${ip#*.}" + o2="${rem%%.*}"; rem="${rem#*.}" + o3="${rem%%.*}"; o4="${rem#*.}" + + if [ "$o1" -le 255 ] && [ "$o2" -le 255 ] && [ "$o3" -le 255 ] && [ "$o4" -le 255 ]; then + printf "%s\n" "$ip" + return 0 + fi + fi + ;; + esac + + return 1 +} + +# Helper to perform secure DNS lookup with dynamic Dual-Stack priority +internal_nslookup() { + # internal function (no shift) + local domain="$1" + local mode_arg="${2:-}" + [ -z "$domain" ] && return 1 + + # Check optional ipv6first argument + local prefer_v6="false" + if [ "$mode_arg" = "ipv6first" ]; then + prefer_v6="true" + fi + + # Intelligent Dual-Stack parser + local awk_parser=' + NR > 2 && /Address/ { + sub(/^Address[ 0-9]*:[ \t]*/, "", $0); + sub(/^[ \t]*/, "", $0); + clean_ip = tolower($1); + + if (prefer_v6 == "true") { + # IPv6-First Logic + if (clean_ip ~ /:/) { + print clean_ip; + v6_found = 1; + exit; + } else if (clean_ip ~ /\./ && fallback_v4 == "") { + fallback_v4 = clean_ip; + } + } else { + # IPv4-First Logic (Standard) + if (clean_ip ~ /\./) { + print clean_ip; + v4_found = 1; + exit; + } else if (clean_ip ~ /:/ && fallback_v6 == "") { + fallback_v6 = clean_ip; + } + } + } + END { + if (prefer_v6 == "true") { + if (v6_found != 1 && fallback_v4 != "") print fallback_v4; + } else { + if (v4_found != 1 && fallback_v6 != "") print fallback_v6; + } + } + ' + + # Execute the lookup and pass the result to awk parser + local lookup_result + lookup_result=$(nslookup -timeout=5 "$domain" 2>/dev/null | awk -v prefer_v6="$prefer_v6" "$awk_parser") + + if [ -n "$lookup_result" ]; then + printf "%s\n" "$lookup_result" + return 0 fi return 1 @@ -722,54 +1139,43 @@ checkip() { # Resolve domain name to IP or validate raw IPv4/IPv6 input resolveip() { shift # Remove "resolveip" from the arguments list + if [ "$#" -ne 1 ]; then + printf "%s\n" "ERROR: Invalid number of parameters." >&2 + return 1 + fi + local target="$1" - # Use cleanipdns to fix the input string first - target=$(cleanipdns "cleanipdns" "$target") + # Pre-clean the input string using cleanipdns + target=$(cleanipdns "cleanipdns" "$target" 2>/dev/null) [ -z "$target" ] && return 1 local resolved_ip="" - local is_domain=false - # 1. Check if the input is a domain name or a raw IP address case "$target" in - *":"*) - # If it contains a colon, it must be an IPv6. - # If it has characters outside of hex (a-f), it's an invalid domain/input. - case "$target" in - *[!0-9a-fA-F:]*) is_domain=true ;; - esac + *:*) + # Pure raw IPv6 address typed directly into the field + resolved_ip="$target" + ;; + *[a-zA-Z]*) + # Contains letters without colons. This is a domain name. + # We delegate the entire DNS engine to our internal helper function + resolved_ip=$(internal_nslookup "$target") ;; *) - # If it contains no colon, it is either IPv4 or a Domain. - # If it contains any letters (a-z), it is a domain name. - case "$target" in - *[a-zA-Z]*) is_domain=true ;; - esac + # Contains only numbers and dots. This is a raw IPv4 address. + resolved_ip="$target" ;; esac - # 2. Resolve domains or pass raw IPs through - if [ "$is_domain" = true ]; then - # OpenWrt safe: We delete the first 2 lines (local dns server info). - # The IPv6 regex safely handles all compressed and mixed-case IPv6 formats from nslookup. - resolved_ip=$(nslookup -timeout=5 "$target" 2>/dev/null | \ - sed '1,2d' | \ - grep "Address" | \ - sed -E 's/Address:[[:space:]]*|Address[[:space:]]*//I' | \ - grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}|(([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:(:[0-9a-fA-F]{1,4}){1,7}|::)" | \ - head -n 1) - else - # It is a raw IPv4 or IPv6 address typed directly into the field - resolved_ip="$target" - fi + [ -z "$resolved_ip" ] && return 1 - # 3. Mathematically validate the final IP string structure with checkip engine - resolved_ip=$(checkip "checkip" "$resolved_ip") + # validate the resolved IP string + resolved_ip=$(checkip "checkip" "$resolved_ip" 2>/dev/null) if [ -z "$resolved_ip" ]; then return 1 else - echo "$resolved_ip" + printf "%s\n" "$resolved_ip" return 0 fi } @@ -777,111 +1183,89 @@ resolveip() { # Check if TCP or UDP port is accessible from outside on Double NAT checkport() { shift # Remove "checkport" from the arguments list + if [ "$#" -lt 4 ] || [ "$#" -gt 5 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi local public_host="$1" # Public WAN IP or DDNS domain name - local ext_port="$2" # External Port on WAN main modem (e.g. 58493) - local int_port="$3" # Internal Port on LAN OpenVPN router (e.g. 58493) + local ext_port="$2" # External Port on WAN main modem + local int_port="$3" # Internal Port on LAN OpenVPN router local proto="$4" # Protocol "udp" or "tcp" - local debug="$5" # "-debug" parameter shows firewall counter + local debug="${5:-}" # Optional "-debug" parameter local usage="\n\nUsage: luci-app-openvpn-plus checkport [-debug]\n\nExample: luci-app-openvpn-plus checkport mydomain.net 443 444 tcp -debug\n\n" - # check parameters + # Pre-clean the public host string instantly using our cleanipdns helper + public_host=$(cleanipdns "cleanipdns" "$public_host" 2>/dev/null) + + # Validate parameters format using strict POSIX case matching if [ -z "$public_host" ] || [ -z "$ext_port" ] || [ -z "$int_port" ] || [ -z "$proto" ]; then - printf "ERROR: Missing parameters.%b" "$usage" + printf "ERROR: Missing parameters.%b" "$usage" >&2 return 1 fi - case "$public_host:$ext_port:$int_port" in - *[\ \ ]*) - printf "ERROR: public_host or ports contain spaces.%b" "$usage" + + case "$ext_port:$int_port" in + *[!0-9]*:*) + printf "ERROR: ext_port is not a valid number.%b" "$usage" >&2 return 1 ;; - *:*[!0-9]*:*) - printf "ERROR: ext_port is not a valid number.%b" "$usage" - return 1 - ;; - *:*:*[!0-9]*) - printf "ERROR: int_port is not a valid number.%b" "$usage" + *:*[!0-9]*) + printf "ERROR: int_port is not a valid number.%b" "$usage" >&2 return 1 ;; esac + if [ "$proto" != "udp" ] && [ "$proto" != "tcp" ]; then - printf "ERROR: udp or tcp missing.%b" "$usage" + printf "ERROR: udp or tcp missing.%b" "$usage" >&2 return 1 fi + local debug_mode=false if [ "$debug" = "-debug" ]; then debug_mode=true elif [ -n "$debug" ]; then - printf "ERROR: Invalid optional parameter '%s'.%b" "$debug" "$usage" + printf "ERROR: Invalid optional parameter '%s'.%b" "$debug" "$usage" >&2 return 1 fi - # check and resolve ip from hostname + # Check and resolve IP from hostname safely local resolved_ip - resolved_ip=$(resolveip "resolveip" "$public_host") + resolved_ip=$(resolveip "resolveip" "$public_host" 2>/dev/null) if [ -z "$resolved_ip" ]; then - printf "ERROR: Invalid public hostname or ip: '%s'.%b" "$public_host" "$usage" + printf "ERROR: Invalid public hostname or ip: '%s'.%b" "$public_host" "$usage" >&2 return 1 fi + + # Portable Dual-Stack selection using our atomic case engine local ipv="ipv4" local ip="ip" - if echo "$resolved_ip" | grep -q ":"; then - ipv="ipv6" - ip="ip6" - fi + case "$resolved_ip" in + *:*) + ipv="ipv6" + ip="ip6" + ;; + esac - # Create id for firewall table + # Create unique tracking ID for the firewall table local checkport_id="luci_app_openvpn_plus_checkport_${ext_port}_${int_port}" # 1. Open the firewall tracking chain matching the target protocol exactly if [ "$debug_mode" = true ]; then - nft add chain inet fw4 "$checkport_id" - - if [ "$proto" = "tcp" ]; then - # a) if incoming port is valid on this ip/host, count it and drop (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" tcp dport "$int_port" counter drop - - # b) Catch EVERYTHING else from this IP on TCP layer, forcing a drop regardless of TCP flags/states (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto tcp drop - else - # a) if incoming port is valid on this ip/host, count it and drop (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" udp dport "$int_port" counter drop - - # b) Catch EVERYTHING else from this IP on UDP layer (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto udp drop - fi - - # Insert the firewall tracking chain in the input rule at first position + nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" "$proto" dport "$int_port" counter drop + nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto "$proto" drop nft insert rule inet fw4 input jump "$checkport_id" # Debug: firewall table content on start - echo "--- DEBUG: FIREWALL FILTER TABLE - ON START CHECK" + printf "%s\n" "--- DEBUG: FIREWALL FILTER TABLE - ON START CHECK" nft -a list chain inet fw4 "$checkport_id" 2>/dev/null - echo "---" - + printf "%s\n" "---" else - - nft add chain inet fw4 "$checkport_id" 2>/dev/null - - if [ "$proto" = "tcp" ]; then - # a) if incoming port is valid on this ip/host, count it and drop (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" tcp dport "$int_port" counter drop 2>/dev/null - - # b) Catch EVERYTHING else from this IP on TCP layer, forcing a drop regardless of TCP flags/states (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto tcp drop 2>/dev/null - else - # a) if incoming port is valid on this ip/host, count it and drop (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" udp dport "$int_port" counter drop 2>/dev/null - - # b) Catch EVERYTHING else from this IP on UDP layer (IPv4 or IPv6) - nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto udp drop 2>/dev/null - fi - - # Insert the firewall tracking chain in the input rule at first position - nft insert rule inet fw4 input jump "$checkport_id" 2>/dev/null - + nft add chain inet fw4 "$checkport_id" >/dev/null 2>&1 + nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" "$proto" dport "$int_port" counter drop >/dev/null 2>&1 + nft add rule inet fw4 "$checkport_id" meta nfproto "$ipv" "$ip" saddr "$resolved_ip" meta l4proto "$proto" drop >/dev/null 2>&1 + nft insert rule inet fw4 input jump "$checkport_id" >/dev/null 2>&1 fi # Force refresh by querying the chain @@ -897,7 +1281,7 @@ checkport() { nslookup -port="$ext_port" portcheck.local "$resolved_ip" >/dev/null 2>&1 fi - # 3. Read the nftables chain and verify if packets hit the internal counter of rule a) + # 3. Read the nftables chain and verify if packets hit the internal counter local packet_count packet_count=$(nft list chain inet fw4 "$checkport_id" 2>/dev/null | grep "dport $int_port" | grep -oE "counter packets [0-9]+" | awk '{print $3}') [ -z "$packet_count" ] && packet_count=0 @@ -907,31 +1291,29 @@ checkport() { scan_res="PORT_OPEN" fi - echo "$scan_res" + printf "%s\n" "$scan_res" - # 4. Clean up ALL old ghost firewall rules to keep the router fast and safe + # 4. Clean up ALL old ghost firewall rules cleanly and safely + local handle_ids handle if [ "$debug_mode" = true ]; then - # Debug: firewall table content on end - echo "--- DEBUG: FIREWALL FILTER TABLE - ON END CHECK" + printf "%s\n" "--- DEBUG: FIREWALL FILTER TABLE - ON END CHECK" nft -a list chain inet fw4 "$checkport_id" 2>/dev/null - echo "---" + printf "%s\n" "---" - # 4. Clean up ALL old ghost firewall rules to keep the router fast and safe - local handle_ids handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') for handle in $handle_ids; do + [ -z "$handle" ] && continue nft delete rule inet fw4 input handle "$handle" done nft delete chain inet fw4 "$checkport_id" - else - local handle_ids handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') for handle in $handle_ids; do - nft delete rule inet fw4 input handle "$handle" 2>/dev/null + [ -z "$handle" ] && continue + nft delete rule inet fw4 input handle "$handle" >/dev/null 2>&1 done - nft delete chain inet fw4 "$checkport_id" 2>/dev/null + nft delete chain inet fw4 "$checkport_id" >/dev/null 2>&1 fi if [ "$scan_res" = "PORT_OPEN" ]; then @@ -944,39 +1326,47 @@ checkport() { # Check if the main WAN router has a working static route to a remote net pointing to this VPN router checksroute() { shift # Remove the word "checksroute" from the arguments list + if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi local wan_router_ip="$1" # The main WAN gateway IP (e.g. 192.168.50.1) local remote_test_net="$2" # The remote subnet base (e.g. 192.168.60.0/24) - local debug="$3" # "-debug" parameter shows firewall counter + local debug="${3:-}" # Optional "-debug" parameter local usage="\n\nUsage: luci-app-openvpn-plus checksroute [-debug]\n\nExample: luci-app-openvpn-plus checksroute 192.168.50.1 192.168.60.0/24 -debug\n\n" - # check parameters - if [ -z "$remote_test_net" ] || [ -z "$wan_router_ip" ]; then - printf "ERROR: Missing parameters.%b" "$usage" + if [ -z "$wan_router_ip" ] || [ -z "$remote_test_net" ]; then + printf "ERROR: Missing parameters.%b" "$usage" >&2 return 1 fi - # Clean trailing slashes, prefixes or spaces - local clean_netip clean_wanip - clean_netip=$(echo "$remote_test_net" | cut -d'/' -f1 | tr -d ' ') - clean_wanip=$(echo "$wan_router_ip" | cut -d'/' -f1 | tr -d ' ') - # Check if IPs contain only good characters (numbers, letters a-f, dots, colons) - case "${clean_netip}:${clean_wanip}" in - *[!0-9a-fA-F.:]*) - printf "ERROR: Invalid characters in IP address.%b" "$usage" - return 1 - ;; + # Clean and validate both IP's (strips /24 CIDR and checks limits) + local clean_wanip clean_netip + clean_wanip=$(checkip "checkip" "$wan_router_ip" 2>/dev/null) + clean_netip=$(checkip "checkip" "$remote_test_net" 2>/dev/null) + + if [ -z "$clean_wanip" ] || [ -z "$clean_netip" ]; then + printf "ERROR: Invalid public gateway or subnet IP address.%b" "$usage" >&2 + return 1 + fi + + # Check IPv4 / IPv6 + local wan_is_v6=false + local net_is_v6=false + + case "$clean_wanip" in + *:*) wan_is_v6=true ;; esac - # DETECT IP MIXES: Check if one is IPv4 and the other is IPv6 - local net_is_v6=false - local wan_is_v6=false - echo "$clean_netip" | grep -q ":" && net_is_v6=true - echo "$clean_wanip" | grep -q ":" && wan_is_v6=true + case "$clean_netip" in + *:*) net_is_v6=true ;; + esac + # Check if mixed IPv4 / IPv6 if [ "$net_is_v6" != "$wan_is_v6" ]; then - printf "ERROR: Mixed IP protocols detected (IPv4 and IPv6 cannot be mixed).%b" "$usage" + printf "ERROR: Mixed IP protocols detected (IPv4 and IPv6 cannot be mixed).%b" "$usage" >&2 return 1 fi @@ -984,87 +1374,91 @@ checksroute() { if [ "$debug" = "-debug" ]; then debug_mode=true elif [ -n "$debug" ]; then - printf "ERROR: Invalid optional parameter '%s'.%b" "$debug" "$usage" + printf "ERROR: Invalid optional parameter '%s'.%b" "$debug" "$usage" >&2 return 1 fi - # Change dots (.) and colons (:) to underscores (_) to make the IDs + # Generate unique filesystem-safe IDs local netid wanid - netid=$(echo "$clean_netip" | tr '.:' '__') - wanid=$(echo "$clean_wanip" | tr '.:' '__') + netid="${clean_netip//./_}"; netid="${netid//:/_}" + wanid="${clean_wanip//./_}"; wanid="${wanid//:/_}" - # create id for firewall table + # Create tracking ID for the firewall nat table local checksroute_id="luci_app_openvpn_plus_checksroute_${netid}__${wanid}" local test_ip="" local ping_cmd="ping" - # Structure test_ip based on the actual network type (now verified matching) - if [ "$net_is_v6" = true ]; then - # --- IPv6 --- + # Structure test_ip based on the actual network type verified natively + if [ "$net_is_v6" = "true" ]; then ping_cmd="ping6" - # Remove any trailing colons first - local base_v6 - base_v6=$(echo "$clean_netip" | sed 's/:*$//') + # Remove any trailing colons from the cleaned IPv6 address natively + local base_v6="$clean_netip" + while case "$base_v6" in *:) true;; *) false;; esac; do + base_v6="${base_v6%?}" + done - # If the subnet ends with a double colon (e.g., fd00:1::) - if echo "$clean_netip" | grep -q "::$"; then - test_ip="${base_v6}::254" - else - # If it is a full IP or notation without trailing "::" (e.g., fd00:1:2:3:4:5:6:0) - test_ip=$(echo "$base_v6" | awk -F: '{OFS=":"; $NF="254"; print}') - fi + # If the subnet notation is compact (ends with a double colon like fd00:1::) + case "$clean_netip" in + *::) + test_ip="${base_v6}::254" + ;; + *) + # Replace the last hex block with 254 safely using awk + test_ip=$(printf "%s" "$base_v6" | awk -F: '{OFS=":"; $NF="254"; print}') + ;; + esac else - # --- IPv4 --- ping_cmd="ping" - test_ip=$(echo "$clean_netip" | awk -F. '{print $1"."$2"."$3".254"}') + # Structure the IPv4 probe target (e.g. 192.168.60.254) natively in RAM + test_ip="${clean_netip%.*}.254" fi # 1. Create a minimal temporary NAT rule if [ "$debug_mode" = true ]; then nft add chain inet fw4 "$checksroute_id" "{ type nat hook postrouting priority srcnat; }" - if [ "$wan_is_v6" = true ]; then + if [ "$wan_is_v6" = "true" ]; then nft add rule inet fw4 "$checksroute_id" ip6 daddr "$clean_wanip" counter snat to "$test_ip" else nft add rule inet fw4 "$checksroute_id" ip daddr "$clean_wanip" counter snat to "$test_ip" fi - echo "--- DEBUG: FIREWALL FILTER TABLE - ON START CHECK" + # Debug: firewall table content on start + printf "%s\n" "--- DEBUG: FIREWALL FILTER TABLE - ON START CHECK" nft -a list chain inet fw4 "$checksroute_id" 2>/dev/null - echo "---" + printf "%s\n" "---" else - nft add chain inet fw4 "$checksroute_id" "{ type nat hook postrouting priority srcnat; }" 2>/dev/null - if [ "$wan_is_v6" = true ]; then - nft add rule inet fw4 "$checksroute_id" ip6 daddr "$clean_wanip" counter snat to "$test_ip" 2>/dev/null + nft add chain inet fw4 "$checksroute_id" "{ type nat hook postrouting priority srcnat; }" >/dev/null 2>&1 + if [ "$wan_is_v6" = "true" ]; then + nft add rule inet fw4 "$checksroute_id" ip6 daddr "$clean_wanip" counter snat to "$test_ip" >/dev/null 2>&1 else - nft add rule inet fw4 "$checksroute_id" ip daddr "$clean_wanip" counter snat to "$test_ip" 2>/dev/null + nft add rule inet fw4 "$checksroute_id" ip daddr "$clean_wanip" counter snat to "$test_ip" >/dev/null 2>&1 fi - # Force refresh by querying the chain - nft -a list chain inet fw4 "$checkport_id" >/dev/null 2>&1 + # Force refresh by querying the dynamic chain + nft -a list chain inet fw4 "$checksroute_id" >/dev/null 2>&1 fi # 2. Fire the probe ping to the WAN router and evaluate the exit code + local route_res=1 if $ping_cmd -c 1 -W 1 "$clean_wanip" >/dev/null 2>&1; then - echo "ROUTE_OPEN" - local route_res=0 + printf "%s\n" "ROUTE_OPEN" + route_res=0 else - echo "ROUTE_CLOSED" - local route_res=1 + printf "%s\n" "ROUTE_CLOSED" + route_res=1 fi - # Debug: firewall table content on end if [ "$debug_mode" = true ]; then - echo "--- DEBUG: FIREWALL FILTER TABLE - ON END CHECK" + # Debug: firewall table content on end + printf "%s\n" "--- DEBUG: FIREWALL FILTER TABLE - ON END CHECK" nft -a list chain inet fw4 "$checksroute_id" 2>/dev/null - echo "---" + printf "%s\n" "---" - # Clean up (Errors visible) nft delete chain inet fw4 "$checksroute_id" else - # Clean up (Errors hidden) - nft delete chain inet fw4 "$checksroute_id" 2>/dev/null + nft delete chain inet fw4 "$checksroute_id" >/dev/null 2>&1 fi return $route_res @@ -1073,25 +1467,31 @@ checksroute() { # Check a DDNS domain name or validate a raw public WAN IP address, and verify if it is online checkddns() { shift # Remove the command word from the arguments list + if [ "$#" -ne 1 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi + local target="$1" + + # Resolve and clean the ip/domain using resolveip local resolved_ip - resolved_ip=$(resolveip "resolveip" "$target") + resolved_ip=$(resolveip "resolveip" "$target" 2>/dev/null) [ -z "$resolved_ip" ] && return 1 - # Live connection verification via native OpenWrt core ping utilities + # Get IPv4 / IPv6 ping cmd + local ping_cmd="ping" + case "$resolved_ip" in + *:*) + ping_cmd="ping6" + ;; + esac + + # Live connection verification via dynamic ping command # Tuning: If the first ping succeeds, we return immediately. Second ping acts as a backup. - if echo "$resolved_ip" | grep -q ":"; then - # Target is IPv6 - if ping6 -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1 || ping6 -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1; then - echo "$resolved_ip" - return 0 - fi - else - # Target is IPv4 - if ping -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1 || ping -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1; then - echo "$resolved_ip" - return 0 - fi + if $ping_cmd -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1 || $ping_cmd -c 1 -W 2 "$resolved_ip" >/dev/null 2>&1; then + printf "%s\n" "$resolved_ip" + return 0 fi return 1 @@ -1100,24 +1500,42 @@ checkddns() { # Detect the real public WAN IP (IPv4 or IPv6) publicip() { # no arguments (no shift) - local resolved_ip="" local valid_ip="" - # Stage 1: Google Anycast TXT reflection (Flexible pattern extraction captures both IPv4 and IPv6 inside quotes) - resolved_ip=$(nslookup -timeout=5 -type=txt o-o.myaddr.l.google.com ns1.google.com 2>/dev/null | grep -E -o '"[^"]+"' | tr -d '"' | head -n 1) + # Stage 1: Google Anycast DNS TXT reflection (Primary public IP discovery) + # NR > 2 skips the resolver server info block lines on top. + local google_parser=' + NR > 2 && /text/ { + if (match($0, /"[^"]+"/)) { + clean_txt = substr($0, RSTART + 1, RLENGTH - 2); + print tolower(clean_txt); + exit; + } + } + ' + resolved_ip=$(nslookup -timeout=5 -type=txt o-o.myaddr.l.google.com ns1.google.com 2>/dev/null | awk "$google_parser") - # Stage 2: Cisco OpenDNS fallback (Flexible pattern extraction captures both IPv4 and IPv6 structures) + # Stage 2: Cisco OpenDNS fallback (Triggers only if Google Anycast failed) if [ -z "$resolved_ip" ]; then - resolved_ip=$(nslookup -timeout=5 myip.opendns.com resolver1.opendns.com 2>/dev/null | grep -A 1 "Name:" | grep "Address" | sed 's/Address:[[:space:]]*//I' | grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9a-f]{1,4}:|:){1,7}[0-9a-f]{1,4}" | head -n 1) + # NR > 2 perfectly skips the resolver server info block lines on top. + local cisco_parser=' + NR > 2 && /Address/ { + sub(/^Address[ 0-9]*:[ \t]*/, "", $0); + sub(/^[ \t]*/, "", $0); + print tolower($1); + exit; + } + ' + resolved_ip=$(nslookup -timeout=5 myip.opendns.com resolver1.opendns.com 2>/dev/null | awk "$cisco_parser") fi - # check the returned - valid_ip=$(checkip "checkip" "$resolved_ip") + # Validate the final IP + valid_ip=$(checkip "checkip" "$resolved_ip" 2>/dev/null) - # Output the final validated IP address or return empty string on complete failure + # Output the final validated IP address if [ -n "$valid_ip" ]; then - echo "$valid_ip" + printf "%s\n" "$valid_ip" return 0 fi @@ -1127,128 +1545,153 @@ publicip() { # Sends the update URL to the DDNS provider, checks the response for errors, and returns a clean status message. wgetddns() { shift # Remove the command word from the arguments list + if [ "$#" -ne 2 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi + local url="$1" local domain="$2" if [ -z "$url" ]; then - echo "ERROR: Missing URL parameter for ${domain}." + printf "ERROR: Missing URL parameter for %s.\n" "$domain" >&2 return 1 fi + # Block dangerous Path-Traversal attacks inside the URL (Standard update URLs never contain directory hopping sequences /../) + case "$url" in + *..*) + printf "ERROR: Security sandbox violation. Invalid hopping path detected in URL for %s.\n" "$domain" >&2 + return 1 + ;; + esac + + # Sanitize the display domain name to prevent text-layout injections + if ! internal_checkname "$domain"; then + domain="unknown_domain" + fi + local clean_res="" - local wget_output - local wget_status=1 + local wget_output="" local wget_status=1 local attempt=1 local max_attempts=3 - while [ $attempt -le $max_attempts ]; do - wget_output=$(wget -T 10 --user-agent="'Mozilla/5.0'" -qO- "$url" 2>&1) - wget_status=$? - # ok - break + # Run remote network update with retry loop + while [ "$attempt" -le "$max_attempts" ]; do + wget_output=$(wget -T 10 --user-agent="Mozilla/5.0" -qO- "$url" 2>&1) + wget_status="$?" + + # HTTP Success - break instantly if [ "$wget_status" -eq 0 ]; then break fi - # error - try again - if [ $attempt -lt $max_attempts ]; then - echo "Wget failed (code $wget_status) for ${domain}. Retry $attempt/$max_attempts in 2 seconds..." + # Network or Server Error - retry after delay + if [ "$attempt" -lt "$max_attempts" ]; then + printf "Wget failed (code %s) for %s. Retry %s/%s in 2 seconds...\n" "$wget_status" "$domain" "$attempt" "$max_attempts" sleep 2 fi attempt=$((attempt + 1)) done + # Parse HTTP Error codes if [ "$wget_status" -ne 0 ]; then - # -i ignores case, -E matches specific HTTP error strings from wget stderr - if echo "$wget_output" | grep -q -i -E "401|unauthorized"; then - clean_res="badauth" - elif echo "$wget_output" | grep -q -i -E "404|not found"; then - clean_res="notfound" - elif echo "$wget_output" | grep -q -i -E "403|forbidden"; then - clean_res="blocked" - elif echo "$wget_output" | grep -q -i -E "429|too many requests"; then - clean_res="abuse" - elif echo "$wget_output" | grep -q -i -E "400|bad request"; then - clean_res="badrequest" - elif echo "$wget_output" | grep -q -i -E "502|bad gateway"; then - clean_res="dnserr" - elif echo "$wget_output" | grep -q -i -E "500|503|server error"; then - clean_res="server_error" - else - echo "Update network connection failed for ${domain} (wget code: $wget_status)." - return 1 - fi + case "$wget_output" in + *401*|*[Uu]nauthorized*) clean_res="badauth" ;; + *404*|*[Nn]ot*[Ff]ound*) clean_res="notfound" ;; + *403*|*[Ff]orbidden*) clean_res="blocked" ;; + *429*|*[Tt]oo*[Mm]any*) clean_res="abuse" ;; + *400*|*[Bb]ad*[Rr]equest*) clean_res="badrequest" ;; + *502*|*[Bb]ad*[Gg]ateway*) clean_res="dnserr" ;; + *500*|*503*|*[Ss]erver*[Ee]rror*) clean_res="server_error" ;; + *) + case "$wget_status" in + 4) clean_res="dnserr" ;; + 5) clean_res="badauth" ;; + 8) clean_res="server_error" ;; + *) + printf "Update network connection failed for %s (wget code: %s).\n" "$domain" "$wget_status" + return 1 + ;; + esac + ;; + esac else - clean_res=$(echo "$wget_output" | tr -d ' \t\r\n' | head -c 50) + # Clean the API response string + local tmp_output="${wget_output//[[:space:]]/}" + tmp_output="${tmp_output//[$'\r'$'\n']/}" + clean_res=$(printf "%s" "$tmp_output" | awk '{print substr($0, 1, 50)}') fi - local nice_output + local nice_output="" if [ "$wget_status" -ne 0 ]; then nice_output="HTTP Error ($clean_res)" else - nice_output=$(echo "$wget_output" | tr -d '\r\n' | head -c 60) + local tmp_nice="${wget_output//[$'\r'$'\n']/}" + nice_output=$(printf "%s" "$tmp_nice" | awk '{print substr($0, 1, 60)}') fi - # --- THE UNIVERSAL DDNS ENGINE (DuckDNS, IPv64, dynv6, Dyn, Dynu, No-IP & FreeDNS) --- + # --- Step 4: THE UNIVERSAL DDNS ENGINE (DuckDNS, IPv64, dynv6, Dyn, Dynu, No-IP & FreeDNS) --- case "$clean_res" in *DOCTYPE*|*html*|*xml*) - echo "Error: Provider returned a web page instead of API text for ${domain}. Please check your Custom Update URL!" + printf "Error: Provider returned a web page instead of API text for %s. Please check your Custom Update URL!\n" "$domain" return 1 ;; *good*|*nochg*|ok|OK|*success*|*[Uu]pdated*|*[Nn]o[Cc]hange*|*[Uu]nchanged*|*[Tt]rue*|200|*[Aa]ccepted*|*[Cc]hanged*|*[Rr]enewed*|*[Rr]efresh*|*[Mm]odified*|*[Dd]one*) - echo "Update successful for ${domain}. Provider response: $nice_output" + printf "Update successful for %s. Provider response: %s\n" "$domain" "$nice_output" return 0 ;; *badauth*|*badkey*|*invalid*|*[Aa]uthentication*|*[Ii]nvalid[Tt]oken*) - echo "Warning: Provider rejected credentials for ${domain}. Response: $nice_output. Check your token or username!" + printf "Warning: Provider rejected credentials for %s. Response: %s. Check your token or username!\n" "$domain" "$nice_output" return 1 ;; *unknown*) - echo "Error: Unknown or badly formatted request sent to ${domain}. Please check your Wizard inputs!" + printf "Error: Unknown or badly formatted request sent to %s. Please check your Wizard inputs!\n" "$domain" return 1 ;; *badrequest*) - echo "Error: Invalid parameters sent to ${domain}. Please verify your domain syntax!" + printf "Error: Invalid parameters sent to %s. Please verify your domain syntax!\n" "$domain" return 1 ;; *badagent*) - echo "Error: Client blocked by provider for ${domain}. Your User-Agent string is invalid!" + printf "Error: Client blocked by provider for %s. Your User-Agent string is invalid!\n" "$domain" return 1 ;; *nohost*|*notfqdn*|*notfound*|*[Uu]nableto[Ll]ocate*) - echo "Warning: Provider domain, hostname, or format parameters not found for ${domain}. Check your setup!" + printf "Warning: Provider domain, hostname, or format parameters not found for %s. Check your setup!\n" "$domain" return 1 ;; *numhost*) - echo "Error: Too many hostnames specified or DNS configuration is ambiguous on provider side for ${domain}!" + printf "Error: Too many hostnames specified or DNS configuration is ambiguous on provider side for %s!\n" "$domain" return 1 ;; *badip*|*[Aa]ddress[Mm]issing*) - echo "Error: Provider rejected the IP address for ${domain}. Invalid IP address format detected!" + printf "Error: Provider rejected the IP address for %s. Invalid IP address format detected!\n" "$domain" return 1 ;; *noipv6*) - echo "Error: IPv6 update failed for ${domain}. IPv6 records are not supported under this profile!" + printf "Error: IPv6 update failed for %s. IPv6 records are not supported under this profile!\n" "$domain" return 1 ;; *dnserr*|*dns_error*) - echo "Error: Server-side DNS update failed on provider infrastructure for ${domain}. Response: $nice_output" + printf "Error: DDNS provider or network connection not reachable for %s. Response: %s\n" "$domain" "$nice_output" return 1 ;; *!donator*) - echo "Error: Feature not allowed for ${domain}. You are using premium features on a free account!" + printf "Error: Feature not allowed for %s. You are using premium features on a free account!\n" "$domain" return 1 ;; *abuse*|*blocked*|*disver*) - echo "Error: Update blocked due to abuse or rate limit for ${domain}. Response: $nice_output" + printf "Error: Update blocked due to abuse or rate limit for %s. Response: %s\n" "$domain" "$nice_output" return 1 ;; *servererror*|*server_error*|*[Ee]mergency[Ss]ervices*) - echo "Error: Internal server error on provider side for ${domain}. Response: $nice_output" + printf "Error: Internal server error on provider side for %s. Response: %s\n" "$domain" "$nice_output" return 1 ;; *) - echo "Update finished for ${domain}. Raw response: $nice_output" + printf "Update finished for %s. Raw response: %s\n" "$domain" "$nice_output" return 0 ;; esac @@ -1256,41 +1699,67 @@ wgetddns() { # Autonomous background DDNS checker for openvpn server instances (called from '/etc/hotplug.d/openvpn/35-luci-app-openvpn-plus-lifecycle' on openvpn server instance start) ddnsservice() { - shift # Remove "ddnsservice" string modifier from the system arguments list + shift # Remove "ddnsservice" string modifier from the arguments list + if [ "$#" -ne 3 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi local domain="$1" local url="$2" local instance_name="$3" - local standard_check=60 local cool_down_period=3600 # 1 hour security window lock - local status_file="/var/run/openvpn-ddns-${instance_name}.status" - if [ -z "$url" ] || [ -z "$domain" ]; then - return 0 + # Check parameter exist + if [ -z "$domain" ] || [ -z "$url" ] || [ -z "$instance_name" ]; then + printf "ERROR: Missing parameters.\n" >&2 + return 1 fi - logger -t openvpn-luci "DDNS Service: Background DDNS checker initiated for ${instance_name}." + # Check instance name + if ! internal_checkname "$instance_name"; then + printf "ERROR: Invalid character format in instance name.\n" >&2 + return 1 + fi + + # Block dangerous Path-Traversal attacks inside the URL (Standard update URLs never contain directory hopping sequences *..*) + case "$url" in + *..*) + printf "ERROR: Security sandbox violation. Invalid hopping path detected in URL for %s.\n" "$domain" >&2 + return 1 + ;; + esac + + logger -t openvpn-luci "DDNS Service: Background DDNS checker initiated for ${instance_name} (${domain})." # Protect process lifecycle: disable exit-on-error during network drops set +e local current_router_ip="" local last_resolved_ip="" + local current_uptime last_update_uptime last_url seconds_passed fresh_uptime wgetddns_response + # Keep-Alive loop for DDNS check while true; do + + # Get WAN IP (already verified and cleaned) current_router_ip=$(publicip) - # Resolve active live DNS bindings via nslookup pipelines - if echo "$current_router_ip" | grep -q ":"; then - last_resolved_ip=$(nslookup -timeout=5 -type=aaaa "$domain" 2>/dev/null | grep -A 1 "Name:" | grep "Address" | sed 's/Address:[[:space:]]*//I' | grep -E -o "([0-9a-f]{1,4}:|:){1,7}[0-9a-f]{1,4}" | head -n 1) - else - last_resolved_ip=$(nslookup -timeout=5 -type=a "$domain" 2>/dev/null | grep -A 1 "Name:" | grep "Address" | sed 's/Address:[[:space:]]*//I' | grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}" | head -n 1) - fi + # Get Dual-Stack DNS query priority + local dns_priority="" + case "$current_router_ip" in + *:*) + dns_priority="ipv6first" + ;; + esac - current_router_ip=$(checkip "checkip" "$current_router_ip") - last_resolved_ip=$(checkip "checkip" "$last_resolved_ip") + # Resolve DNS binding matching the WAN Dual-Stack priority + last_resolved_ip=$(internal_nslookup "$domain" "$dns_priority") + + # Validate resolved DNS IP + last_resolved_ip=$(checkip "checkip" "$last_resolved_ip" 2>/dev/null) if [ -z "$current_router_ip" ] || [ -z "$last_resolved_ip" ]; then sleep "$standard_check" @@ -1299,45 +1768,41 @@ ddnsservice() { # Process evaluation logic if [ "$current_router_ip" != "$last_resolved_ip" ]; then - local current_uptime current_uptime=$(awk '{print int($1)}' /proc/uptime) - local last_update_uptime=0 - local last_url="" + last_update_uptime=0 + last_url="" if [ -f "$status_file" ]; then last_update_uptime=$(sed -n '1p' "$status_file" | tr -d '[:space:]' | grep -E -o "^[0-9]+$") - last_url=$(sed -n '2p' "$status_file" | tr -d '\r\n') # FIXED: Retains string variables structure layout + last_url=$(sed -n '2p' "$status_file" | tr -d '\r\n') [ -z "$last_update_uptime" ] && last_update_uptime=0 fi - local seconds_passed=$((current_uptime - last_update_uptime)) + seconds_passed=$((current_uptime - last_update_uptime)) # Evaluate core synchronization conditions if [ "$last_update_uptime" -eq 0 ] || [ "$seconds_passed" -ge "$cool_down_period" ] || [ "$seconds_passed" -lt 0 ] || [ "$url" != "$last_url" ]; then if [ -n "$last_url" ] && [ "$url" != "$last_url" ]; then - logger -t openvpn-luci "DDNS Service: URL change detected for ${instance_name}. Forcing immediate update." + logger -t openvpn-luci "DDNS Service: URL change detected for ${instance_name} (${domain}). Forcing immediate update." else - logger -t openvpn-luci "DDNS Service: Different IP found for ${instance_name}. WAN: ${current_router_ip} vs DNS: ${last_resolved_ip}. Sending update URL." + logger -t openvpn-luci "DDNS Service: Different IP found for ${instance_name} (${domain}). WAN: ${current_router_ip} vs DNS: ${last_resolved_ip}. Sending update URL." fi - local wgetddns_response - wgetddns_response=$(wgetddns "wgetddns" "$url" "$instance_name") + # Trigger the ddns update engine + wgetddns_response=$(wgetddns "wgetddns" "$url" "$domain") + logger -t openvpn-luci "DDNS Service: Update DDNS for ${instance_name}: ${wgetddns_response}" - logger -t openvpn-luci "DDNS Service: ${wgetddns_response}" - - local fresh_uptime fresh_uptime=$(awk '{print int($1)}' /proc/uptime) - echo "$fresh_uptime" > "$status_file" - echo "$url" >> "$status_file" + printf "%s\n" "$fresh_uptime" > "$status_file" + printf "%s\n" "$url" >> "$status_file" fi else - # Flash-safe evaluation window prevents loop locks on identical states + # Flash-safe evaluation prevents loop locks on identical states if [ -f "$status_file" ]; then - local current_uptime last_update_uptime current_uptime=$(awk '{print int($1)}' /proc/uptime) last_update_uptime=$(sed -n '1p' "$status_file" | tr -d '[:space:]' | grep -E -o "^[0-9]+$") [ -z "$last_update_uptime" ] && last_update_uptime=0 - local seconds_passed=$((current_uptime - last_update_uptime)) + seconds_passed=$((current_uptime - last_update_uptime)) if [ "$seconds_passed" -ge "$cool_down_period" ] || [ "$seconds_passed" -lt 0 ]; then rm -f "$status_file" @@ -1347,31 +1812,38 @@ ddnsservice() { sleep "$standard_check" done + return 0 } -# This function dynamically check and sort if necessary sorts all openvpn sections +# This function dynamically checks and sorts all openvpn sections if necessary ucisortovpn() { # no arguments (no shift) - # 1. Get the current raw list of instances from the file + # Step 1: Extract all section names using a single fast awk parser + local sections_parser=' + /=openvpn/ { + split($1, path, "."); + if (path[2] != "") print path[2]; + } + ' local sections - sections=$(uci -q show openvpn | grep "=openvpn" | cut -d. -f2 | cut -d= -f1) + sections=$(uci -q show openvpn | awk "$sections_parser") [ -z "$sections" ] && return 0 local previous_num=-1 - local need_reorder=false + local need_reorder="false" + local section_name current_num # 2. Check if a smaller number comes after a bigger number for section_name in $sections; do - # Extract only the digits from the name (e.g., "instance2" -> "2") - local current_num - current_num=$(echo "$section_name" | grep -oE "[0-9]+") + # Strip everything that is NOT a number + current_num="${section_name//[!0-9]/}" [ -z "$current_num" ] && continue - # If the current number is smaller than the last one, we must reorder! + # If a smaller number comes after a bigger number, we must reorder! if [ "$current_num" -lt "$previous_num" ]; then - need_reorder=true + need_reorder="true" break fi previous_num="$current_num" @@ -1380,20 +1852,26 @@ ucisortovpn() { # 3. Run the reorder process only if necessary if [ "$need_reorder" = "true" ]; then local index=0 - - # Get the naturally sorted list (instance1, instance2) + local sort_parser=' + { + orig = $0; + gsub(/[^0-9]/, "", $0); + print $0, orig; + } + ' local sorted_sections - sorted_sections=$(echo "$sections" | sort -V) + sorted_sections=$(printf "%s\n" "$sections" | awk "$sort_parser" | sort -n | awk '{print $2}') for section_name in $sorted_sections; do [ -z "$section_name" ] && continue - - # Execute reorder for every section (e.g uci reorder openvpn.instance1=0) - uci reorder "openvpn.${section_name}=${index}" 2>/dev/null - index=$((index + 1)) + # Secure validation of the dynamic section name before execution + if internal_checkname "$section_name"; then + uci reorder "openvpn.${section_name}=${index}" 2>/dev/null + index=$((index + 1)) + fi done - # commit to save + # Commit to save configuration changes permanently uci commit openvpn 2>/dev/null fi @@ -1402,7 +1880,7 @@ ucisortovpn() { # Delete configuration and key files of deleted VPN instances cleanup() { - shift # Remove "cleanup" from the arguments list + # no argument (no shift) local conf_file local instname local conf_dir="/etc/openvpn/luci" @@ -1419,6 +1897,12 @@ cleanup() { instname="${conf_file##*/}" instname="${instname%.conf}" + # CRITICAL SECURITY GUARD: Validate the instance name before any file operation. + # This completely neutralizes dangerous path traversal attacks via rm -rf! + if ! internal_checkname "$instname"; then + continue + fi + # Step 3: If the UCI section does not exist, delete files and crypto keys if ! uci -q get openvpn."$instname" >/dev/null; then rm -f "$conf_file" @@ -1437,6 +1921,9 @@ cleanup() { # Delete the static .ovpn file for this specific deleted instance rm -f "$conf_dir/${instname}.ovpn" + # remove ddns status file for this instance + rm -f "/var/run/openvpn-ddns-${instname}.status" + # Clean up the specific RAM files for this deleted instance if [ -d "$ram_link_dir" ]; then rm -rf "${ram_link_dir}"/ovpn-tmp-*-*"${instname}" @@ -1461,103 +1948,111 @@ cleanup() { # Validate and extract cryptographic metadata for LuCI keymeta() { shift # Remove "keymeta" from the arguments list - local filename="$1" - local absolute_path="/etc/openvpn/keys/$filename" - local b64_payload="" - - # Hard validation guard against directory traversal attacks - if [ -z "$filename" ] || echo "$filename" | grep -q '\.\.'; then - echo "ERROR: Invalid or restricted filename frame context." + if [ "$#" -ne 1 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 return 1 fi + local filename="$1" + + # Check filename (this completely neutralizes dangerous path traversal or execution injections) + if [ -z "$filename" ] || ! internal_checkname "$filename"; then + printf "ERROR: Invalid filename argument.\n" >&2 + return 1 + fi + + local absolute_path="/etc/openvpn/keys/$filename" + local b64_payload="" + local cert_text="" + local hex_lines_count=0 + if [ -s "$absolute_path" ]; then # Case A: Check for valid Certificate (CA, Server or Client Certificate) if grep -q "BEGIN CERTIFICATE" "$absolute_path"; then - local cert_text cert_text=$(openssl x509 -in "$absolute_path" -noout -text 2>/dev/null) - if echo "$cert_text" | grep -q "CA:TRUE"; then + if printf "%s" "$cert_text" | grep -q "CA:TRUE"; then if grep -q "BEGIN PRIVATE KEY" "$absolute_path" || grep -q "BEGIN RSA PRIVATE KEY" "$absolute_path" || grep -q "BEGIN EC PRIVATE KEY" "$absolute_path"; then - echo "TYPE: CERTIFICATE AUTHORITY (CA:TRUE) - SECRET-KEY-CA BUNDLED" + printf "%s\n" "TYPE: CERTIFICATE AUTHORITY (CA:TRUE) - SECRET-KEY-CA BUNDLED" else - echo "TYPE: CERTIFICATE AUTHORITY (CA:TRUE)" + printf "%s\n" "TYPE: CERTIFICATE AUTHORITY (CA:TRUE)" fi else - echo "TYPE: STANDARD CERTIFICATE" + printf "%s\n" "TYPE: STANDARD CERTIFICATE" fi - echo "$cert_text" | grep -E "Subject:|Not After|Public-Key|ASN1 OID|[Nn][Ii][Ss][Tt]|[Cc]urve" | sed 's/^[[:space:]]*//' + printf "%s" "$cert_text" | grep -E "Subject:|Not After|Public-Key|ASN1 OID|[Nn][Ii][Ss][Tt]|[Cc]urve" | awk '{sub(/^[ \t]*/, ""); print}' # Case B: Check for standalone Private Keys (RSA or ECC) elif grep -q "BEGIN PRIVATE KEY" "$absolute_path" || grep -q "BEGIN RSA PRIVATE KEY" "$absolute_path" || grep -q "BEGIN EC PRIVATE KEY" "$absolute_path"; then - echo "TYPE: PRIVATE-KEY" + printf "%s\n" "TYPE: PRIVATE-KEY" openssl pkey -in "$absolute_path" -noout -text 2>/dev/null | \ - grep -E "Private-Key|ASN1 OID|[Nn][Ii][Ss][Tt]|[Cc]urve" | sed 's/^[[:space:]]*//' + grep -E "Private-Key|ASN1 OID|[Nn][Ii][Ss][Tt]|[Cc]urve" | awk '{sub(/^[ \t]*/, ""); print}' # Case C: Check for standalone Diffie-Hellman Parameter matrices elif grep -q "DH PARAMETERS" "$absolute_path"; then openssl pkeyparam -in "$absolute_path" -noout -text 2>/dev/null | \ - grep -E "DH Parameters|bit" | sed 's/^[[:space:]]*//' + grep -E "DH Parameters|bit" | awk '{sub(/^[ \t]*/, ""); print}' # Case D: Modern OpenVPN Static & Asymmetric V2 Key Validator elif grep -q "BEGIN OpenVPN" "$absolute_path"; then if grep -q "BEGIN OpenVPN Static key V1" "$absolute_path"; then # Old Hex V1 Format check (16 lines of hex data) - local hex_lines_count hex_lines_count=$(grep -v "BEGIN" "$absolute_path" | grep -v "END" | tr -d '\r' | grep -E -c "^[0-9a-fA-F]{32}$") if [ "$hex_lines_count" -eq 16 ] && grep -q "END OpenVPN Static key V1" "$absolute_path"; then - echo "WARNING: Symmetric Static Secret (2048 Bit) - TLS-CRYPT V1 Unsupported" + printf "%s\n" "WARNING: Symmetric Static Secret (2048 Bit) - TLS-CRYPT V1 Unsupported" else - echo "ERROR: OpenVPN static key V1 validation failed (Structural format corruption)." + printf "%s\n" "ERROR: OpenVPN static key V1 validation failed (Structural format corruption)." fi elif grep -q "BEGIN OpenVPN tls-crypt-v2 server key" "$absolute_path"; then # Modern Asymmetric V2 Base64 extraction and integrity test b64_payload=$(grep -v "BEGIN" "$absolute_path" | grep -v "END" | tr -d '\r\n') - # OpenSSL base64 pipe checks if decode throws errors (corrupted characters) - if echo "$b64_payload" | openssl base64 -d >/dev/null 2>&1 && grep -q "END OpenVPN tls-crypt-v2 server key" "$absolute_path"; then - echo "TYPE: TLSv2-Server-Key - TLS-CRYPT V2 SERVER MASTER KEY Validated" + if printf "%s" "$b64_payload" | openssl base64 -d >/dev/null 2>&1 && grep -q "END OpenVPN tls-crypt-v2 server key" "$absolute_path"; then + printf "%s\n" "TYPE: TLSv2-Server-Key - TLS-CRYPT V2 SERVER MASTER KEY Validated" else - echo "ERROR: OpenVPN tls-crypt-v2 server key validation failed (Base64 encoding or structural format corruption)." + printf "%s\n" "ERROR: OpenVPN tls-crypt-v2 server key validation failed (Base64 encoding or structural format corruption)." fi elif grep -q "BEGIN OpenVPN tls-crypt-v2 client key" "$absolute_path"; then # Modern Asymmetric V2 Base64 extraction and integrity test b64_payload=$(grep -v "BEGIN" "$absolute_path" | grep -v "END" | tr -d '\r\n') - if echo "$b64_payload" | openssl base64 -d >/dev/null 2>&1 && grep -q "END OpenVPN tls-crypt-v2 client key" "$absolute_path"; then - echo "TYPE: TLSv2-Client-Key - TLS-CRYPT V2 CLIENT KEY Validated" + if printf "%s" "$b64_payload" | openssl base64 -d >/dev/null 2>&1 && grep -q "END OpenVPN tls-crypt-v2 client key" "$absolute_path"; then + printf "%s\n" "TYPE: TLSv2-Client-Key - TLS-CRYPT V2 CLIENT KEY Validated" else - echo "ERROR: OpenVPN tls-crypt-v2 client key validation failed (Base64 encoding or structural format corruption)." + printf "%s\n" "ERROR: OpenVPN tls-crypt-v2 client key validation failed (Base64 encoding or structural format corruption)." fi else # Fallback for generic old static keys if grep -q "END OpenVPN Static key" "$absolute_path"; then - echo "WARNING: Symmetric Static Secret (2048 Bit) - STATIC KEY Unsupported" + printf "%s\n" "WARNING: Symmetric Static Secret (2048 Bit) - STATIC KEY Unsupported" else - echo "ERROR: OpenVPN static key validation failed (Unknown layout or structural corruption)." + printf "%s\n" "ERROR: OpenVPN static key validation failed (Unknown layout or structural corruption)." fi fi else - echo "INVALID: Custom Raw Data Structure" + printf "%s\n" "INVALID: Custom Raw Data Structure" fi else - echo "ERROR: File is empty or missing." + printf "ERROR: File is empty or missing.\n" >&2 + return 1 fi + return 0 } # Remove keygen_worker temporary keys keygenclean() { - shift # Remove "keygenclean" from the arguments list + # no argument (no shift) rm -f /tmp/openvpn.keygen.* return 0 } # Create default fallback certificates and keys using the router name initkeys() { - shift # Remove "initkeys" from the arguments list + # no argument (no shift) + local key_dir="/etc/openvpn/keys" local lock_file="/var/run/openvpn.keygen.lock" local missing=0 @@ -1573,7 +2068,7 @@ initkeys() { local server_default_tls2_key="$key_dir/server_default.tls2.key" local client_default_tls2_key="$key_dir/client_default.tls2.key" - # Step 1: Check if all default files exist on the disk (Including new client-cert nodes) + # Step 1: Check if all default files exist on the disk [ -f "$ca_default_crt" ] || missing=1 [ -f "$server_default_crt" ] || missing=1 [ -f "$server_default_key" ] || missing=1 @@ -1607,21 +2102,21 @@ initkeys() { if [ -z "$router_name" ]; then router_name=$(uci -q get system.@system.hostname); fi if [ -z "$router_name" ]; then router_name="OpenWrt"; fi - # Clean the router name string from bad characters and spaces - # Using octal codes \042 (") \047 (') and \134 (\) to avoid single-quote escaping - router_name=$(echo "$router_name" | tr -d '\042\047\134' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + # Clean the router name string natively in RAM (No echo, tr, or sed process forks!) + router_name="${router_name//[\"\'\\]/}" + router_name="${router_name//[[:space:]]/}" - # Create temporary extension configuration files in RAM + # Create temporary extension configuration files in RAM safely using printf local ext_server_tmp="/tmp/openvpn.initkeys.server.ext.$$" local ext_client_tmp="/tmp/openvpn.initkeys.client.ext.$$" - echo "basicConstraints=CA:FALSE" > "$ext_server_tmp" - echo "keyUsage=digitalSignature,keyEncipherment" >> "$ext_server_tmp" - echo "extendedKeyUsage=serverAuth" >> "$ext_server_tmp" + printf "%s\n" "basicConstraints=CA:FALSE" > "$ext_server_tmp" + printf "%s\n" "keyUsage=digitalSignature,keyEncipherment" >> "$ext_server_tmp" + printf "%s\n" "extendedKeyUsage=serverAuth" >> "$ext_server_tmp" - echo "basicConstraints=CA:FALSE" > "$ext_client_tmp" - echo "keyUsage=digitalSignature" >> "$ext_client_tmp" - echo "extendedKeyUsage=clientAuth" >> "$ext_client_tmp" + printf "%s\n" "basicConstraints=CA:FALSE" > "$ext_client_tmp" + printf "%s\n" "keyUsage=digitalSignature" >> "$ext_client_tmp" + printf "%s\n" "extendedKeyUsage=clientAuth" >> "$ext_client_tmp" # Step 4: Generate all crypto parts in a fast waterfall chain (Strictly separated roles) @@ -1707,6 +2202,19 @@ initkeys() { # Asynchronous key and certificate generator keygen() { shift # Remove "keygen" from the arguments list + if [ "$#" -lt 1 ] || [ "$#" -gt 6 ]; then + printf "ERROR: Invalid number of parameters. Expected between 1 and 6 arguments.\n" >&2 + return 1 + fi + # Check all provided parameters using internal_checkname + local check_param + for check_param in "$@"; do + if [ -n "$check_param" ] && ! internal_checkname "$check_param"; then + printf "ERROR: Security sandbox violation. Invalid character format in arguments.\n" >&2 + return 1 + fi + done + local type="${1:-unknown}" local instance="${2:-default}" local bits="${3:-2048}" @@ -1744,7 +2252,7 @@ keygen() { ca_engine="-newkey rsa:$bits" if [ "$bits" = "ec" ]; then rm -f "$ec_param_tmp" - echo "[CMD] openssl ecparam -name prime256v1 -out $ec_param_tmp" >> "$log_file" + printf "%s\n" "[CMD] openssl ecparam -name prime256v1 -out $ec_param_tmp" >> "$log_file" openssl ecparam -name prime256v1 -out "$ec_param_tmp" 2>> "$log_file" chmod 600 "$ec_param_tmp" ca_engine="-newkey ec:$ec_param_tmp" @@ -1753,15 +2261,15 @@ keygen() { if [ -n "$cname" ]; then target_cn="$cname"; fi ( set +e - echo "LOG: Invoking openssl engine for self-signed CA generation..." >> "$log_file" - echo "[CMD] openssl req -new -x509 -nodes -days $days $ca_engine -subj \"/CN=$target_cn\" -keyout $ca_key_buffer -out $ca_cert_buffer $custom" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for self-signed CA generation..." >> "$log_file" + printf "%s\n" "[CMD] openssl req -new -x509 -nodes -days $days $ca_engine -subj \"/CN=$target_cn\" -keyout $ca_key_buffer -out $ca_cert_buffer $custom" >> "$log_file" openssl req -new -x509 -nodes -days "$days" $ca_engine -subj "/CN=$target_cn" -keyout "$ca_key_buffer" -out "$ca_cert_buffer" $custom >/dev/null 2>> "$log_file" if [ -f "$ca_key_buffer" ] && [ -s "$ca_key_buffer" ] && [ -f "$ca_cert_buffer" ] && [ -s "$ca_cert_buffer" ]; then cat "$ca_cert_buffer" >> "$ca_key_buffer" chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Cryptographic CA asset generation returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Cryptographic CA asset generation returned structural failure." >> "$log_file" fi rm -f "$ca_cert_buffer" "/tmp/openvpn.keygen.dh.tmp" set -e @@ -1773,19 +2281,19 @@ keygen() { ( set +e if [ "$bits" = "ec" ]; then - echo "LOG: Invoking openssl engine for ECC private key generation..." >> "$log_file" - echo "[CMD] openssl ecparam -name prime256v1 -genkey -out $active_out $custom" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for ECC private key generation..." >> "$log_file" + printf "%s\n" "[CMD] openssl ecparam -name prime256v1 -genkey -out $active_out $custom" >> "$log_file" openssl ecparam -name prime256v1 -genkey -out "$active_out" $custom 2>> "$log_file" else - echo "LOG: Invoking openssl engine for RSA private key generation..." >> "$log_file" - echo "[CMD] openssl genrsa -out $active_out $custom $bits" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for RSA private key generation..." >> "$log_file" + printf "%s\n" "[CMD] openssl genrsa -out $active_out $custom $bits" >> "$log_file" openssl genrsa -out "$active_out" $custom "$bits" 2>> "$log_file" fi if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Server private key buffer generation returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Server private key buffer generation returned structural failure." >> "$log_file" fi set -e ) & @@ -1797,19 +2305,19 @@ keygen() { set +e echo "LOG: Validating local Certificate Authority access..." >> "$log_file" if [ ! -s "$out_ca_file" ] || [ ! -s "$out_server_key_file" ]; then - echo "ERROR: Required multi-stage RAM staging nodes are missing or empty!" >> "$log_file" + printf "%s\n" "ERROR: Required multi-stage RAM staging nodes are missing or empty!" >> "$log_file" else local target_cn="OpenVPN-Server-$instance" if [ -n "$cname" ]; then target_cn="$cname"; fi # Create a tiny temporary extension file in RAM local ext_tmp="/tmp/openvpn.keygen.ext.$$" - echo "basicConstraints=CA:FALSE" > "$ext_tmp" - echo "keyUsage=digitalSignature,keyEncipherment" >> "$ext_tmp" - echo "extendedKeyUsage=serverAuth" >> "$ext_tmp" + printf "%s\n" "basicConstraints=CA:FALSE" > "$ext_tmp" + printf "%s\n" "keyUsage=digitalSignature,keyEncipherment" >> "$ext_tmp" + printf "%s\n" "extendedKeyUsage=serverAuth" >> "$ext_tmp" - echo "LOG: Invoking openssl engine for dynamic certificate signing..." >> "$log_file" - echo "[CMD] openssl req -new -nodes -key $out_server_key_file -subj \"/CN=$target_cn\" | openssl x509 -req -days $days -CA $out_ca_file -CAkey $out_ca_file -set_serial $$ -extfile $ext_tmp -out $active_out $custom" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for dynamic certificate signing..." >> "$log_file" + printf "%s\n" "[CMD] openssl req -new -nodes -key $out_server_key_file -subj \"/CN=$target_cn\" | openssl x509 -req -days $days -CA $out_ca_file -CAkey $out_ca_file -set_serial $$ -extfile $ext_tmp -out $active_out $custom" >> "$log_file" openssl req -new -nodes -key "$out_server_key_file" -subj "/CN=$target_cn" 2>/dev/null |\ openssl x509 -req -days "$days" -CA "$out_ca_file" -CAkey "$out_ca_file" -set_serial "$$" \ @@ -1821,9 +2329,9 @@ keygen() { if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Server certificate dynamic signing pipeline returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Server certificate dynamic signing pipeline returned structural failure." >> "$log_file" fi fi set -e @@ -1836,19 +2344,19 @@ keygen() { ( set +e if [ "$bits" = "ec" ]; then - echo "LOG: Invoking openssl engine for client ECC private key generation..." >> "$log_file" - echo "[CMD] openssl ecparam -name prime256v1 -genkey -out $active_out $custom" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for client ECC private key generation..." >> "$log_file" + printf "%s\n" "[CMD] openssl ecparam -name prime256v1 -genkey -out $active_out $custom" >> "$log_file" openssl ecparam -name prime256v1 -genkey -out "$active_out" $custom 2>> "$log_file" else - echo "LOG: Invoking openssl engine for client RSA private key generation..." >> "$log_file" - echo "[CMD] openssl genrsa -out $active_out $custom $bits" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for client RSA private key generation..." >> "$log_file" + printf "%s\n" "[CMD] openssl genrsa -out $active_out $custom $bits" >> "$log_file" openssl genrsa -out "$active_out" $custom "$bits" 2>> "$log_file" fi if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Client private key buffer generation returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Client private key buffer generation returned structural failure." >> "$log_file" fi set -e ) & @@ -1858,23 +2366,23 @@ keygen() { rm -f "$active_out" ( set +e - echo "LOG: Validating local Certificate Authority access..." >> "$log_file" + printf "%s\n" "LOG: Validating local Certificate Authority access..." >> "$log_file" local real_ca_file="/etc/openvpn/keys/ca_${instance}.crt" local real_ca_key="/etc/openvpn/keys/ca_${instance}.crt" if [ ! -s "$real_ca_file" ] || [ ! -s "$out_client_key_file" ]; then - echo "ERROR: Required CA assets (/etc/openvpn/keys/ca_${instance}.crt) or client keys are missing!" >> "$log_file" + printf "%s\n" "ERROR: Required CA assets (/etc/openvpn/keys/ca_${instance}.crt) or client keys are missing!" >> "$log_file" else local target_cn="OpenVPN-Client-$instance" if [ -n "$cname" ]; then target_cn="$cname"; fi # Create a tiny temporary extension file in RAM local ext_tmp="/tmp/openvpn.keygen.client.ext.$$" - echo "basicConstraints=CA:FALSE" > "$ext_tmp" - echo "keyUsage=digitalSignature" >> "$ext_tmp" - echo "extendedKeyUsage=clientAuth" >> "$ext_tmp" + printf "%s\n" "basicConstraints=CA:FALSE" > "$ext_tmp" + printf "%s\n" "keyUsage=digitalSignature" >> "$ext_tmp" + printf "%s\n" "extendedKeyUsage=clientAuth" >> "$ext_tmp" - echo "LOG: Invoking openssl engine for client certificate signing..." >> "$log_file" - echo "[CMD] openssl req -new -nodes -key '$out_client_key_file' -subj '/CN=$target_cn' | openssl x509 -req -days '$days' -CA '$real_ca_file' -CAkey '$real_ca_key' -set_serial '$$' -extfile $ext_tmp -out '$active_out' $custom" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for client certificate signing..." >> "$log_file" + printf "%s\n" "[CMD] openssl req -new -nodes -key '$out_client_key_file' -subj '/CN=$target_cn' | openssl x509 -req -days '$days' -CA '$real_ca_file' -CAkey '$real_ca_key' -set_serial '$$' -extfile $ext_tmp -out '$active_out' $custom" >> "$log_file" openssl req -new -nodes -key "$out_client_key_file" -subj "/CN=$target_cn" 2>/dev/null |\ openssl x509 -req -days "$days" -CA "$real_ca_file" -CAkey "$real_ca_key" -set_serial "$$" \ @@ -1886,9 +2394,9 @@ keygen() { if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Client certificate dynamic signing pipeline returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Client certificate dynamic signing pipeline returned structural failure." >> "$log_file" fi fi set -e @@ -1900,14 +2408,14 @@ keygen() { rm -f "$active_out" ( set +e - echo "LOG: Invoking openssl engine for fast Diffie-Hellman parameters..." >> "$log_file" - echo "[CMD] openssl dhparam -dsaparam $custom -out '$active_out' '$bits'" >> "$log_file" + printf "%s\n" "LOG: Invoking openssl engine for fast Diffie-Hellman parameters..." >> "$log_file" + printf "%s\n" "[CMD] openssl dhparam -dsaparam $custom -out '$active_out' '$bits'" >> "$log_file" openssl dhparam -dsaparam $custom -out "$active_out" "$bits" >> "$log_file" 2>&1 if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Diffie-Hellman cryptographic prime generation returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Diffie-Hellman cryptographic prime generation returned structural failure." >> "$log_file" fi set -e ) & @@ -1917,14 +2425,14 @@ keygen() { rm -f "$active_out" ( set +e - echo "LOG: Invoking openvpn engine for asymmetric tls-crypt-v2 master server key generation..." >> "$log_file" - echo "[CMD] openvpn --genkey tls-crypt-v2-server '$active_out'" >> "$log_file" + printf "%s\n" "LOG: Invoking openvpn engine for asymmetric tls-crypt-v2 master server key generation..." >> "$log_file" + printf "%s\n" "[CMD] openvpn --genkey tls-crypt-v2-server '$active_out'" >> "$log_file" openvpn --genkey tls-crypt-v2-server "$active_out" 2>> "$log_file" if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Asymmetric v2 master server key engine tokens returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Asymmetric v2 master server key engine tokens returned structural failure." >> "$log_file" fi set -e ) & @@ -1938,22 +2446,22 @@ keygen() { local server_tls2_key="/etc/openvpn/keys/server_${instance}.tls2.key" ( set +e - echo "LOG: Invoking openvpn engine for asymmetric tls-crypt-v2 client key generation..." >> "$log_file" + printf "%s\n" "LOG: Invoking openvpn engine for asymmetric tls-crypt-v2 client key generation..." >> "$log_file" local target_tls_name_base64 target_tls_name_base64=$(printf '%s' "$target_tls_name" | openssl enc -base64) - echo "[CMD] openvpn --tls-crypt-v2 '$server_tls2_key' --genkey tls-crypt-v2-client '$active_out' '$target_tls_name_base64'" >> "$log_file" + printf "%s\n" "[CMD] openvpn --tls-crypt-v2 '$server_tls2_key' --genkey tls-crypt-v2-client '$active_out' '$target_tls_name_base64'" >> "$log_file" openvpn --tls-crypt-v2 "$server_tls2_key" --genkey tls-crypt-v2-client "$active_out" "$target_tls_name_base64" 2>> "$log_file" if [ -f "$active_out" ] && [ -s "$active_out" ]; then chmod 600 "$active_out" - echo "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" + printf "%s\n" "LOG: WORKFLOW_SUCCESSFUL" >> "$log_file" else - echo "ERROR: Asymmetric v2 client key derivation engine tokens returned structural failure." >> "$log_file" + printf "%s\n" "ERROR: Asymmetric v2 client key derivation engine tokens returned structural failure." >> "$log_file" fi set -e ) & ;; *) - echo "ERROR: Invalid cryptographic profile requested." >> "$log_file" + printf "%s\n" "ERROR: Invalid cryptographic profile requested." >> "$log_file" return 1 ;; esac @@ -1962,68 +2470,152 @@ keygen() { # async worker with start-stop-daemon +# Asynchronous ovpn service controller wrapper ovpnservice_async() { shift # Remove "ovpnservice_async" from the arguments list + if [ "$#" -ne 2 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi local action_cmd="$1" - local inst="$2" + local instance="$2" local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" - if [ -z "$(echo "$inst" | tr -d ' ')" ]; then - inst="" - fi - if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then - # 1. Clean up any stale or previous ghost workers instantly - start-stop-daemon -K -q -p "$pid_file" 2>/dev/null - rm -f "$pid_file" - # 2. Start the target command via worker wrapper to handle absolute lifecycle traps - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnservice_worker "$action_cmd" "$inst" + # Check instance name + if ! internal_checkname "$instance"; then + printf "ERROR: Security sandbox violation. Invalid character format in parameter instance name.\n" >&2 + return 1 fi + + # Check service command + case "$action_cmd" in + start|stop|restart) + # 1. Clean up any stale or previous ghost workers instantly + start-stop-daemon -K -q -p "$pid_file" 2>/dev/null + rm -f "$pid_file" + + # 2. Start the target command via worker wrapper to handle absolute lifecycle traps + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnservice_worker "$action_cmd" "$instance" + ;; + *) + printf "ERROR: Invalid service command '%s'. Only start, stop, or restart are permitted.\n" "$action_cmd" >&2 + return 1 + ;; + esac return 0 } +# Asynchronous ovpn service background worker engine ovpnservice_worker() { shift # Remove "ovpnservice_worker" from the arguments list - local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" - local cmd="$1" - local inst="$2" - trap 'rm -f "$pid_file"' EXIT INT TERM - if [ "$cmd" = "start" ]; then - ovpnstart "ovpnstart" "$inst" 3 - elif [ "$cmd" = "restart" ]; then - ovpnrestart "ovpnrestart" "$inst" 3 - elif [ "$cmd" = "stop" ]; then - ovpnstop "ovpnstop" "$inst" 3 + if [ "$#" -ne 2 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 fi + + local cmd="$1" + local instance="$2" + local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" + + # Check instance name + if ! internal_checkname "$instance"; then + printf "ERROR: Security sandbox violation. Invalid character format in parameter instance name.\n" >&2 + return 1 + fi + + # Set clean traps for absolute process lifecycle cleanup + trap 'rm -f "$pid_file"' EXIT INT TERM + + # Check service command + case "$cmd" in + start) + ovpnstart "ovpnstart" "$instance" 3 + ;; + restart) + ovpnrestart "ovpnrestart" "$instance" 3 + ;; + stop) + ovpnstop "ovpnstop" "$instance" 3 + ;; + *) + printf "ERROR: Unknown command '%s' inside background worker.\n" "$cmd" >&2 + rm -f "$pid_file" + trap - EXIT INT TERM + return 1 + ;; + esac + + # Final cleanup and removal of the system process lock rm -f "$pid_file" + trap - EXIT INT TERM return 0 } +# Asynchronous firewall service controller wrapper firewallservice_async() { shift # Remove "firewallservice_async" from the arguments list + if [ "$#" -ne 1 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi + local action_cmd="$1" local pid_file="/var/run/luci-app-openvpn-plus-firewall-service.pid" - if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then - # 1. Clean up any stale or previous ghost workers instantly - start-stop-daemon -K -q -p "$pid_file" 2>/dev/null - rm -f "$pid_file" - # 2. Start the worker directly via the main script frame context - # Inside the worker, we use a trap to cleanly remove the pid_file upon exit - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallservice_worker "$action_cmd" - fi + # Check service command + case "$action_cmd" in + start|stop|restart) + # 1. Clean up any stale or previous ghost workers instantly + start-stop-daemon -K -q -p "$pid_file" 2>/dev/null + rm -f "$pid_file" + + # 2. Start the target command via worker wrapper to handle absolute lifecycle traps + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallservice_worker "$action_cmd" + ;; + *) + printf "ERROR: Invalid firewall command '%s'. Only start, stop, or restart are permitted.\n" "$action_cmd" >&2 + return 1 + ;; + esac + return 0 } +# Asynchronous firewall service background worker engine firewallservice_worker() { shift # Remove "firewallservice_worker" from the arguments list + if [ "$#" -ne 1 ]; then + printf "ERROR: Invalid number of parameters.\n" >&2 + return 1 + fi + local cmd="$1" local pid_file="/var/run/luci-app-openvpn-plus-firewall-service.pid" + + # Set clean traps for absolute process lifecycle cleanup trap 'rm -f "$pid_file"' EXIT INT TERM - firewallservice "firewallservice" "$cmd" 3 + + # Check service command + case "$cmd" in + start|stop|restart) + # Invoke the core firewall engine + firewallservice "firewallservice" "$cmd" 3 + ;; + *) + printf "ERROR: Unknown command '%s' inside background firewall worker.\n" "$cmd" >&2 + rm -f "$pid_file" + trap - EXIT INT TERM + return 1 + ;; + esac + + # Final cleanup and removal of the system process lock rm -f "$pid_file" + trap - EXIT INT TERM return 0 } +# Asynchronous initkey controller wrapper initkeys_async() { - shift # Remove "initkeys_async" from the arguments list + # no argument (no shift) local pid_file="/var/run/openvpn.initkeys.pid" # 1. Check if the key generator is running if start-stop-daemon -K -q -p "$pid_file" -t 2>/dev/null; then @@ -2033,8 +2625,9 @@ initkeys_async() { start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- "initkeys_worker" return 0 } +# Asynchronous initkey service background worker engine initkeys_worker() { - shift # Remove "initkeys_worker" from the arguments list + # no argument (no shift) local pid_file="/var/run/openvpn.initkeys.pid" # 3. Secure the PID file lifecycle instantly with a trap framework trap 'rm -f "$pid_file"' EXIT INT TERM @@ -2044,37 +2637,77 @@ initkeys_worker() { return 0 } +# Asynchronous keygen service controller wrapper keygen_async() { - shift # Remove "keygen" from the arguments list - local type="$1" - local instance="$2" - if [ -z "$instance" ] || ! echo "$instance" | grep -Eq '^[A-Za-z0-9_-]+$'; then - echo "ERROR: Invalid instance name." >&2 + shift # Remove "keygen_async" from the arguments list + if [ "$#" -lt 1 ] || [ "$#" -gt 6 ]; then + printf "ERROR: Invalid number of parameters. Expected between 1 and 6 arguments.\n" >&2 return 1 fi + # Check all provided parameters using internal_checkname + local check_param + for check_param in "$@"; do + if [ -n "$check_param" ] && ! internal_checkname "$check_param"; then + printf "ERROR: Security sandbox violation. Invalid character format in keygen parameters.\n" >&2 + return 1 + fi + done + + # Get and check instance is not empty + local type="${1:-}" + local instance="${2:-}" + if [ -z "$instance" ]; then + printf "ERROR: Missing required instance parameter.\n" >&2 + return 1 + fi + local pid_file="/var/run/openvpn.keygen-${instance}.pid" - # 1. Start the background keygen worker and create the PID file automatically - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- "keygen_worker" "$instance" "$@" + + # Start the background keygen worker and create the PID file automatically. We pass the entire secure argument stream ("$@") + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- keygen_worker "$@" + return 0 } +# Asynchronous service background keygen worker engine keygen_worker() { shift # Remove "keygen_worker" from the arguments list - local instance="$1" - if [ -z "$instance" ] || ! echo "$instance" | grep -Eq '^[A-Za-z0-9_-]+$'; then + if [ "$#" -lt 1 ] || [ "$#" -gt 6 ]; then return 1 fi + + # Check all provided parameters using internal_checkname + local check_param + for check_param in "$@"; do + if [ -n "$check_param" ] && ! internal_checkname "$check_param"; then + return 1 + fi + done + + # Get and check instance is not empty + local type="${1:-}" + local instance="${2:-}" + if [ -z "$instance" ]; then + return 1 + fi + local pid_file="/var/run/openvpn.keygen-${instance}.pid" - shift # Remove "instance" from the arguments list - # 3. Secure the dynamic PID node tracking with a strict trap guard + + # Set clean traps for absolute process lifecycle cleanup trap 'rm -f "$pid_file"' EXIT INT TERM - # 4. Run the certificate engine pipeline with corrected original parameters ($@: type="$1", instance="$2") + + # Run the certificate engine pipeline with verified arguments keygen "keygen" "$@" + local exit_code=$? + + # Final cleanup and removal of the system process lock rm -f "$pid_file" - return 0 + trap - EXIT INT TERM + + return $exit_code } usage() { - local args="{getmac|lanip|getnetstat|bestcrypto|readstatus|readfile|writefile|removefile|statfile|ovpnver|ovpnstart|ovpnrestart|ovpnstop|ovpnservice|firewallservice|firewallnft|openssldgst|symlink|iroute|cleanipdns|checkip|resolveip|checkport|checksroute|checkddns|publicip|wgetddns|ddnsservice|ucisortovpn|cleanup|keymeta|keygenclean|initkeys|keygen} [parameters]" + local args="{getl3dev|getmac|lanip|getnetstat|bestcrypto|readstatus|readfile|writefile|removefile|statfile|ovpnver|ovpnstart|ovpnrestart|ovpnstop|ovpnservice|firewallservice|firewallnft|openssldgst|symlink|iroute|cleanipdns|checkip|resolveip|checkport|checksroute|checkddns|publicip|wgetddns|ddnsservice|ucisortovpn|cleanup|keymeta|keygenclean|initkeys|keygen} [parameters]" # Check if the user gave an argument if [ -z "$1" ]; then echo "Usage: $0 $args" @@ -2115,7 +2748,7 @@ case "$1" in wgetddns) wgetddns "$@" ;; ddnsservice) ddnsservice "$@" ;; ucisortovpn) ucisortovpn ;; - cleanup) cleanup "$@" ;; + cleanup) cleanup ;; keymeta) keymeta "$@" ;; keygenclean) keygenclean ;; @@ -2124,8 +2757,8 @@ case "$1" in firewallservice_worker) firewallservice_worker "$@" ;; ovpnservice) ovpnservice_async "$@" ;; ovpnservice_worker) ovpnservice_worker "$@" ;; - initkeys) initkeys_async "$@" ;; - initkeys_worker) initkeys_worker "$@" ;; + initkeys) initkeys_async ;; + initkeys_worker) initkeys_worker ;; keygen) keygen_async "$@" ;; keygen_worker) keygen_worker "$@" ;; diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js index 8a5636024..58387eba5 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js @@ -853,6 +853,7 @@ const openAutomatedPostKeyGenModal = async function (newInstanceItem, appData, w yesBtn.style.display = 'none'; noBtn.style.display = 'none'; cancelBtn.style.display = 'none'; + backupBtn.style.display = 'none'; promptMessageNode.style.display = 'none'; wizardHelperLayoutBlock.style.display = 'none'; progressTextArea.style.display = 'block'; diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 38e7cbbc7..69c2e4165 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.10.22 -PKG_RELEASE:=351 +PKG_RELEASE:=352 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua index 51924690d..82b79e79c 100644 --- a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua +++ b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua @@ -145,7 +145,9 @@ function gen_config(var) }, quic = { initStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, + maxStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, initConnReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, + maxConnReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, maxIdleTimeout = (function(t) t = tonumber(tostring(t or "30"):match("^%d+")) return (t and t >= 4 and t <= 120) and t .. "s" or "30s" diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index f7e9619ce..0d8f7d959 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -393,7 +393,9 @@ function gen_outbound(flag, node, tag, proxy_table) end)(node.hysteria2_keep_alive_period), disablePathMTUDiscovery = node.hysteria2_disable_mtu_discovery == "1", initStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, + maxStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, initConnectionReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, + maxConnectionReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, } end if fragment and fragment_table and ({raw=1, ws=1, httpupgrade=1, grpc=1, xhttp=1})[TP] then diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 594d1fda9..45c2d0534 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.10.20 -PKG_RELEASE:=152 +PKG_RELEASE:=153 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua index fdd15dde5..308f9c5a3 100644 --- a/luci-app-passwall2/luasrc/controller/passwall2.lua +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -248,12 +248,12 @@ function socks_autoswitch_add_node() uci_set(id, "enable_autoswitch", "1") local new_list = uci_get(id, "autoswitch_backup_node") or {} for i = #new_list, 1, -1 do - if (uci_get(new_list[i], "remarks") or ""):find(key) then + if (uci_get(new_list[i], "remarks") or ""):find(key, 1, true) then table.remove(new_list, i) end end for k, e in ipairs(api.get_valid_nodes()) do - if e.node_type == "normal" and e["remark"]:find(key) then + if e.node_type == "normal" and e["remark"]:find(key, 1, true) then table.insert(new_list, e.id) end end @@ -270,7 +270,7 @@ function socks_autoswitch_remove_node() uci_set(id, "enable_autoswitch", "1") local new_list = uci_get(id, "autoswitch_backup_node") or {} for i = #new_list, 1, -1 do - if (uci_get(new_list[i], "remarks") or ""):find(key) then + if (uci_get(new_list[i], "remarks") or ""):find(key, 1, true) then table.remove(new_list, i) end end diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index 33c93b57d..439a5defd 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -1518,11 +1518,6 @@ function to_move(app_name,file) } end - local flag = sys.call('busybox pgrep -af "passwall2/.*'.. app_name ..'" >/dev/null') - if flag == 0 then - sys.call("/etc/init.d/passwall2 stop") - end - local old_app_size = 0 if fs.access(app_path) then old_app_size = get_file_space(app_path) @@ -1538,6 +1533,12 @@ function to_move(app_name,file) end end + -- Complete preflight before stopping an otherwise working proxy. + local flag = sys.call('busybox pgrep -af "passwall2/.*'.. app_name ..'" >/dev/null') + if flag == 0 then + sys.call("/etc/init.d/passwall2 stop") + end + result = exec("/bin/mv", { "-f", bin_path, app_path }, nil, command_timeout) == 0 sys.call(cmd_rm_tmp) diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 45fb1e816..f400785bc 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -402,7 +402,9 @@ function gen_outbound(flag, node, tag, proxy_table) end)(node.hysteria2_keep_alive_period), disablePathMTUDiscovery = node.hysteria2_disable_mtu_discovery == "1", initStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, + maxStreamReceiveWindow = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)*1048576 or nil, initConnectionReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, + maxConnectionReceiveWindow = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)*1048576 or nil, } end if fragment and fragment_table and ({raw=1, ws=1, httpupgrade=1, grpc=1, xhttp=1})[TP] then diff --git a/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/btn.htm b/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/btn.htm index 657cc85a0..c4cb010df 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/btn.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/btn.htm @@ -9,13 +9,13 @@ local api = map.api function add_node_by_key() { var key = prompt("<%:Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase.%>", ""); if (key) { - window.location.href = '<%=api.url("socks_autoswitch_add_node")%>' + "?id=" + socks_id + "&key=" + key; + window.location.href = '<%=api.url("socks_autoswitch_add_node")%>' + "?id=" + encodeURIComponent(socks_id) + "&key=" + encodeURIComponent(key); } } function remove_node_by_key() { var key = prompt("<%:Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase.%>", ""); if (key) { - window.location.href = '<%=api.url("socks_autoswitch_remove_node")%>' + "?id=" + socks_id + "&key=" + key; + window.location.href = '<%=api.url("socks_autoswitch_remove_node")%>' + "?id=" + encodeURIComponent(socks_id) + "&key=" + encodeURIComponent(key); } } //]]> diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js index 4401e2aa2..ee7a4a53c 100644 --- a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -30,6 +30,12 @@ const callIwinfoDevices = rpc.declare({ method: 'devices', expect: { devices: [] } }); +const callIwinfoScanCompat = rpc.declare({ + object: 'iwinfo', + method: 'scan', + params: [ 'device' ], + expect: { results: [] } +}); const callSystemBoard = rpc.declare({ object: 'system', method: 'board', @@ -1271,7 +1277,6 @@ function parseIwPhyMaxWidthMap(stdout) { const widthHint = rawLine.match(/Supported Channel Width:\s*(.+)$/); if (widthHint) { - let maxWidth = 20; const widths = []; widthHint[1].replace(/\b(20|40|80|160|320)\s*MHz\b/g, function(_, mhz) { @@ -1282,9 +1287,13 @@ function parseIwPhyMaxWidthMap(stdout) { widths.push(160); if (widths.length) - maxWidth = Math.max.apply(Math, widths); + mapping[currentPhy] = Math.max(mapping[currentPhy] || 0, Math.max.apply(Math, widths)); + continue; + } - mapping[currentPhy] = Math.max(mapping[currentPhy] || 0, maxWidth); + const heWidth = rawLine.match(/HE(?:\s+(?:RX|TX) MCS and NSS set\s+|)(160|80\+80)(?:\s*MHz|\/5GHz)/); + if (heWidth) { + mapping[currentPhy] = Math.max(mapping[currentPhy] || 0, 160); continue; } @@ -4587,7 +4596,15 @@ return view.extend({ }; s.handleScanRefresh = function(radioDev, scanCache, table, stop) { - return radioDev.getScanList().then(L.bind(function(results) { + const radio = radioDev.getName(); + const scan = isQcaWifiHwtype(uci.get('wireless', radio, 'type')) + ? loadIwinfoResolver().then(function(resolver) { + const target = resolver.aliasMap[radio]; + return target ? callIwinfoScanCompat(target) : []; + }) + : radioDev.getScanList(); + + return scan.then(L.bind(function(results) { const rows = []; for (let r of results) diff --git a/natflow/Makefile b/natflow/Makefile index dc4e61d5b..9f67c41ea 100644 --- a/natflow/Makefile +++ b/natflow/Makefile @@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk PKG_NAME:=natflow PKG_VERSION:=20260930 -PKG_RELEASE:=97 +PKG_RELEASE:=98 PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=551cda85b5b3f8f57d6c43bad271015a773d1c31 +PKG_SOURCE_VERSION:=95a6e9208c7e7ef6ef75f2f4e6193fdcc72d764c PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_MAINTAINER:=Chen Minqiang PKG_LICENSE:=GPL-2.0 diff --git a/open-app-filter/Makefile b/open-app-filter/Makefile index 622f28bb5..b07826136 100644 --- a/open-app-filter/Makefile +++ b/open-app-filter/Makefile @@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=appfilter PKG_VERSION:=7.0.2 -PKG_RELEASE:=5 +PKG_RELEASE:=6 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) include $(INCLUDE_DIR)/package.mk diff --git a/open-app-filter/files/oaf_version b/open-app-filter/files/oaf_version index 9fe9ff9d9..a8907c025 100644 --- a/open-app-filter/files/oaf_version +++ b/open-app-filter/files/oaf_version @@ -1 +1 @@ -7.0.1 +7.0.2