⛄ Sync 2026-11-01 05:43:29

This commit is contained in:
github-actions[bot] committed 2026-11-01 05:43:29 +08:00
1 parent fae68afc83
commit d1e51134a0
156 files changed
+3632 -1994

No files matched your search

+3 -3
View File
@@ -5,10 +5,10 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=dae PKG_NAME:=dae
PKG_VERSION:=2026.10.18 PKG_VERSION:=2026.10.30
PKG_RELEASE:=52 PKG_RELEASE:=53
PKG_SOURCE:=dae-src-2026.10.18-32ed09f0d784.tar.gz PKG_SOURCE:=dae-src-2026.10.30-55c59e68c059.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip PKG_HASH:=skip
+3 -3
View File
@@ -5,10 +5,10 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=daed PKG_NAME:=daed
PKG_VERSION:=2026.10.18 PKG_VERSION:=2026.10.30
PKG_RELEASE:=64 PKG_RELEASE:=65
PKG_SOURCE:=daed-src-2026.10.18-97c371b47eb5.tar.gz PKG_SOURCE:=daed-src-2026.10.30-1850c01d032c.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip PKG_HASH:=skip
+1 -1
View File
@@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dockerd PKG_NAME:=dockerd
PKG_FLAGS:=hold PKG_FLAGS:=hold
PKG_VERSION:=27.3.1 PKG_VERSION:=27.3.1
PKG_RELEASE:=6 PKG_RELEASE:=8
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
+47 -3
View File
@@ -223,6 +223,8 @@ start_service() {
procd_set_param command /usr/bin/dockerd --config-file="${DOCKERD_CONF}" procd_set_param command /usr/bin/dockerd --config-file="${DOCKERD_CONF}"
fi fi
procd_set_param limits nofile="${nofile} ${nofile}" procd_set_param limits nofile="${nofile} ${nofile}"
# Give dockerd the time to stop the containers before it gets killed
procd_set_param term_timeout 300
procd_close_instance procd_close_instance
} }
@@ -286,8 +288,50 @@ iptables_add_blocking_rule() {
config_list_foreach "${cfg}" blocked_interfaces handle_iptables_rule "${device}" "${extra_iptables_args}" config_list_foreach "${cfg}" blocked_interfaces handle_iptables_rule "${device}" "${extra_iptables_args}"
} }
# Read procd's notifications until one contains the given text, or none comes for the given seconds
procd_notification() {
local line
while read -r -t "${2}" line <&3; do
case "${line}" in
*"${1}"*) return 0 ;;
esac
done
return 1
}
stop_service() { stop_service() {
if /etc/init.d/dockerd running; then local i=0
service_stop "/usr/bin/dockerd"
fi procd_running dockerd || return 0
# Subscribe to procd's notifications before it stops dockerd, to learn when it is done.
# The subscriber leaves procd's lock (fd 1000) to this script, which releases it on exit.
STOP_EVENTS="/var/run/dockerd.stop.$$"
mkfifo "${STOP_EVENTS}" || return 0
ubus -t 330 subscribe service > "${STOP_EVENTS}" 1000>&- &
STOP_SUBSCRIBER=$!
exec 3< "${STOP_EVENTS}"
# The subscription takes effect a moment later, and a stop notified before it would be missed:
# wait until procd passes on an event sent to itself
while [ "${i}" -lt 10 ]; do
ubus call service event '{ "type": "dockerd.stopping", "data": {} }'
if procd_notification '"type":"dockerd.stopping"' 1; then
STOP_SUBSCRIBED=1
return 0
fi
i=$((i + 1))
done
}
service_stopped() {
[ -n "${STOP_EVENTS}" ] || return 0
# Wait for dockerd, which exits after stopping the containers, so that a shutdown unmounts the data root after them
[ -n "${STOP_SUBSCRIBED}" ] && procd_notification '"service.stop": {"service":"dockerd"}' 330
exec 3<&-
# A restart ignores SIGTERM while it stops dockerd, and so does the subscriber it started
kill -KILL "${STOP_SUBSCRIBER}" 2> /dev/null
rm -f "${STOP_EVENTS}"
} }
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gately PKG_NAME:=gately
PKG_VERSION:=0.6.1.1 PKG_VERSION:=0.6.2
PKG_RELEASE:=19 PKG_RELEASE:=20
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gowebdav PKG_NAME:=gowebdav
PKG_VERSION:=5.17.2 PKG_VERSION:=5.17.3
PKG_RELEASE:=27 PKG_RELEASE:=28
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=grpcurl PKG_NAME:=grpcurl
PKG_VERSION:=1.9.4 PKG_VERSION:=1.9.4
PKG_RELEASE:=8 PKG_RELEASE:=9
PKG_SOURCE:=v$(PKG_VERSION).tar.gz PKG_SOURCE:=v$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/fullstorydev/grpcurl/archive/refs/tags/ PKG_SOURCE_URL:=https://github.com/fullstorydev/grpcurl/archive/refs/tags/
@@ -32,7 +32,7 @@ GO_PKG_LDFLAGS:=-s -w
GO_PKG_TAGS:=master GO_PKG_TAGS:=master
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk include ../golang/golang-package.mk
define Package/grpcurl define Package/grpcurl
TITLE:=grpcurl is a command-line tool that lets you interact with gRPC servers TITLE:=grpcurl is a command-line tool that lets you interact with gRPC servers
+4 -4
View File
@@ -10,12 +10,12 @@ KAIPLUS_ARCH_x86_64:=amd64
KAIPLUS_ARCH_aarch64:=arm64 KAIPLUS_ARCH_aarch64:=arm64
KAIPLUS_ARCH:=$(KAIPLUS_ARCH_$(ARCH)) KAIPLUS_ARCH:=$(KAIPLUS_ARCH_$(ARCH))
KAIPLUS_HASH_x86_64:=5588a5e1b7874bc5e631ab8ff1288226f567a84d3115808f22352d142c40f5d6 KAIPLUS_HASH_x86_64:=9e28ac8f2ec7b59dfa546b5ed58375397dfda3cd95d35b776fe7f252acb8878f
KAIPLUS_HASH_aarch64:=5be29e4715a1181937c0156a6954738ce96fde2f338a1b03cd32c68b00e61524 KAIPLUS_HASH_aarch64:=af045078037244a4fd80e21ef5a3ba3a9391918adc4a7791fda5ae777386c122
PKG_NAME:=kaiplus PKG_NAME:=kaiplus
PKG_VERSION:=1.0.9 PKG_VERSION:=1.0.10
PKG_RELEASE:=15 PKG_RELEASE:=16
KAIPLUS_URL_FILE:=kaiplus-linux-$(KAIPLUS_ARCH) KAIPLUS_URL_FILE:=kaiplus-linux-$(KAIPLUS_ARCH)
KAIPLUS_DOWNLOAD:=$(KAIPLUS_URL_FILE)-$(KAIPLUS_HASH_$(ARCH)) KAIPLUS_DOWNLOAD:=$(KAIPLUS_URL_FILE)-$(KAIPLUS_HASH_$(ARCH))
@@ -130,13 +130,14 @@ return view.extend({
o = s.option(form.RichListValue, "offloading_type", _("Flow offloading type")); o = s.option(form.RichListValue, "offloading_type", _("Flow offloading type"));
o.value('0', _("None")); o.value('0', _("None"));
o.value('1', _("Software flow offloading"), _('Software based offloading for routing/NAT.')); if (L.hasSystemFeature('offloading'))
o.value('1', _("Software flow offloading"), _('Software based offloading for routing/NAT.'));
if (L.hasSystemFeature('offloading_hw')) if (L.hasSystemFeature('offloading_hw'))
o.value('2', _("Hardware flow offloading"), _('Hardware based offloading for routing with/without NAT.') + ' ' + _(' Requires hardware NAT support.')); o.value('2', _("Hardware flow offloading"), _('Hardware based offloading for routing with/without NAT.') + ' ' + _(' Requires hardware NAT support.'));
if (hasMediatekHNAT) if (hasMediatekHNAT)
o.value('3', "Mediatek HNAT", "基于Mediatek原厂闭源驱动的HNAT卸载"); o.value('3', _('MediaTek HNAT'), _('Hardware NAT offloading using the MediaTek vendor driver.'));
if (hasQualcommECM) if (hasQualcommECM)
o.value('4', 'Qualcomm NSS ECM 加速', 'ECM Auto 模式,自动选择加速引擎。'); o.value('4', _('Qualcomm NSS ECM acceleration'), _('Automatically select the acceleration engine in ECM Auto mode.'));
o.optional = false; o.optional = false;
o.load = function (section_id) { o.load = function (section_id) {
const flow_offloading = uci.get('firewall', section_id, 'flow_offloading'); const flow_offloading = uci.get('firewall', section_id, 'flow_offloading');
@@ -146,7 +147,7 @@ return view.extend({
return '4'; return '4';
if (hasMediatekHNAT && flow_offloading === '3') if (hasMediatekHNAT && flow_offloading === '3')
return '3'; return '3';
return (flow_offloading === '1') return (flow_offloading === '1' && L.hasSystemFeature('offloading'))
? ((flow_offloading_hw === '1' && has_hw_offloading) ? '2' : '1') ? ((flow_offloading_hw === '1' && has_hw_offloading) ? '2' : '1')
: '0'; : '0';
}; };
+16
View File
@@ -1587,3 +1587,19 @@ msgstr "Shell 脚本语法错误。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/custom.js #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/custom.js
msgid "Unable to restart the firewall." msgid "Unable to restart the firewall."
msgstr "无法重启防火墙。" msgstr "无法重启防火墙。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "MediaTek HNAT"
msgstr "MediaTek HNAT"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Hardware NAT offloading using the MediaTek vendor driver."
msgstr "使用 MediaTek 原厂驱动的硬件 NAT 卸载。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Qualcomm NSS ECM acceleration"
msgstr "Qualcomm NSS ECM 加速"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Automatically select the acceleration engine in ECM Auto mode."
msgstr "在 ECM Auto 模式下自动选择加速引擎。"
+16
View File
@@ -1573,3 +1573,19 @@ msgstr "Shell 指令碼語法錯誤。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/custom.js #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/custom.js
msgid "Unable to restart the firewall." msgid "Unable to restart the firewall."
msgstr "無法重新啟動防火牆。" msgstr "無法重新啟動防火牆。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "MediaTek HNAT"
msgstr "MediaTek HNAT"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Hardware NAT offloading using the MediaTek vendor driver."
msgstr "使用 MediaTek 原廠驅動的硬體 NAT 卸載。"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Qualcomm NSS ECM acceleration"
msgstr "Qualcomm NSS ECM 加速"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Automatically select the acceleration engine in ECM Auto mode."
msgstr "在 ECM Auto 模式下自動選擇加速引擎。"
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.378 PKG_VERSION:=0.1.383
PKG_RELEASE:=54 PKG_RELEASE:=55
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openvpn-plus PKG_NAME:=luci-app-openvpn-plus
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Manfred Jaider <info@masmbit.com> PKG_MAINTAINER:=Manfred Jaider <info@masmbit.com>
+9 -8
View File
@@ -51,15 +51,15 @@ Even though the traffic has to pass through **Double-NAT** and simple port forwa
## Screenshots ## Screenshots
| 01 - Server/Client View | 02 - Status View | | Server/Client View | Status View |
| :---: | :---: | | :---: | :---: |
| <img width="1200" alt="01" src="https://github.com/user-attachments/assets/b5f13132-0414-4490-9c80-9a361e1bda40" /> | <img width="1200" alt="02" src="https://github.com/user-attachments/assets/7635f2bd-4839-45e4-9804-d01ce38bde7b" /> | ![Server/Client View](screenshots/server.png) | ![Status View](screenshots/status-dark.png) |
| <img width="2398" alt="03" src="https://github.com/user-attachments/assets/0170376e-3dfe-4ca3-8f03-d450cc7ed0e4" /> | <img width="2430" alt="15" src="https://github.com/user-attachments/assets/1710419c-4f14-4594-a3a4-d8147ec1fe50" /> | ![Client View](screenshots/client.png) | ![QR Export](screenshots/qr-client-export.png) |
| <img width="1796" alt="09" src="https://github.com/user-attachments/assets/1c77aefe-197d-4e64-ab87-898ff50faab1" /> | <img width="1798" alt="10" src="https://github.com/user-attachments/assets/f37365a6-4090-443b-89fe-05a5cd92e714" /> | ![Wizard Mobile Start](screenshots/wizard-mobile-server-01.png) | ![Wizard Mobile Tunnel](screenshots/wizard-mobile-server-02.png) |
| <img width="1798" alt="13" src="https://github.com/user-attachments/assets/147250df-079c-4f77-9c5c-00819b4b495c" /> | <img width="1800" alt="11" src="https://github.com/user-attachments/assets/814bbeb0-a0b8-4b38-93b8-03a978d75692" /> | ![Wizard Mobile IP Setup](screenshots/wizard-mobile-server-03.png) | ![Wizard DDNS Setup](screenshots/wizard-mobile-server-04.png) |
| <img width="1798" alt="12" src="https://github.com/user-attachments/assets/57726ec2-c18d-4531-96ec-0d58a818648a" /> | <img width="2384" alt="04" src="https://github.com/user-attachments/assets/8470fa8c-b4a4-4369-b9c5-bfbd8e27526d" /> | ![Wizard Site-To-Site Start](screenshots/wizard-main-office-01.png) | ![Wizard Site-To-Site Routing](screenshots/wizard-main-office-02.png) |
| <img width="2382" alt="05" src="https://github.com/user-attachments/assets/1ef91c41-675c-40d8-a9da-644370ac2fdf" /> | <img width="2394" alt="06" src="https://github.com/user-attachments/assets/95fbdf13-142f-4f67-9117-15eef266e304" /> | ![Wizard Port Forwarding](screenshots/port-forward.png) | ![Wizard Add Instance](screenshots/add-instance.png) |
| ![Wizard Key View](screenshots/keyview.png) | ![Wizard KeyGen](screenshots/keygen.png)
## Configuration Layout ## Configuration Layout
@@ -84,6 +84,7 @@ luci-app-openvpn-plus/
│ │ └── luci-app-openvpn-plus.po │ │ └── luci-app-openvpn-plus.po
│ ├── zh-cn/ │ ├── zh-cn/
│ │ └── luci-app-openvpn-plus.po │ │ └── luci-app-openvpn-plus.po
├── screenshots/
└── root/ └── root/
├── etc/ ├── etc/
│ ├── config/ │ ├── config/
@@ -730,6 +730,33 @@ openssldgst() {
return 0 return 0
} }
# Clean up empty symlink folder
internal_cleansymlink() {
# internal function (no shift)
local ram_link_dir="${1}"
local www_link_dir="${2}"
# Remove the temporary RAM directory ONLY if it is completely empty
[ -d "${ram_link_dir}" ] && rmdir "${ram_link_dir}" 2>/dev/null
# Smart Web-Folder Cleanup: Check if files other than index.html exist
if [ -d "${www_link_dir}" ]; then
local has_profiles="0"
local check_link
# If a real symlink or folder matching the pattern exists, has_profiles becomes 1
for check_link in "${www_link_dir}"/ovpn-tmp-*-*; do
if [ -e "${check_link}" ] || [ -L "${check_link}" ]; then
has_profiles="1"
break
fi
done
# Remove the web folder if no active profile links are left
if [ "${has_profiles}" = "0" ]; then
rm -rf "${www_link_dir}"
fi
fi
}
# Manage temporary download links for mobile profiles [create|delete] # Manage temporary download links for mobile profiles [create|delete]
symlink() { symlink() {
shift # Remove "symlink" from the arguments list shift # Remove "symlink" from the arguments list
@@ -834,25 +861,8 @@ symlink() {
# Remove the temporary session files for this instance from RAM # Remove the temporary session files for this instance from RAM
rm -rf "${ram_link_dir}"/ovpn-tmp-*-"${ovpn_instance}" rm -rf "${ram_link_dir}"/ovpn-tmp-*-"${ovpn_instance}"
# Remove RAM base directory if it has become completely empty # Clean up empty symlink folder
rmdir "${ram_link_dir}" 2>/dev/null internal_cleansymlink "${ram_link_dir}" "${www_link_dir}"
# Smart Web-Folder Cleanup: Check if files other than index.html exist
if [ -d "${www_link_dir}" ]; then
local has_profiles="0"
local check_link
# If a real symlink or folder matching the pattern exists, has_profiles becomes 1
for check_link in "${www_link_dir}"/ovpn-tmp-*-*; do
if [ -e "${check_link}" ] || [ -L "${check_link}" ]; then
has_profiles="1"
break
fi
done
# Remove the web folder if no active profile links are left
if [ "${has_profiles}" = "0" ]; then
rm -rf "${www_link_dir}"
fi
fi
printf "%s\n" "SUCCESS: Link directory and static profile deleted from RAM." printf "%s\n" "SUCCESS: Link directory and static profile deleted from RAM."
else else
@@ -869,7 +879,6 @@ symlink() {
return 0 return 0
} }
# Helper to prevent routing loops for IPv4 and IPv6 subnets # Helper to prevent routing loops for IPv4 and IPv6 subnets
internal_checkloopback() { internal_checkloopback() {
# internal (no shift) # internal (no shift)
@@ -1944,7 +1953,6 @@ internal_cleaninst() {
if internal_checkname "${instname}"; then if internal_checkname "${instname}"; then
# Safely remove files, dead symlinks or directories # Safely remove files, dead symlinks or directories
rm -rf "${f}" rm -rf "${f}"
logger -t openvpn-luci "Cleanup: Removed orphaned item '${f}'."
fi fi
;; ;;
esac esac
@@ -1988,20 +1996,7 @@ cleanup() {
# Step 3: Remove orphan symlink files # Step 3: Remove orphan symlink files
internal_cleaninst "${active_list}" "${www_link_dir}/ovpn-tmp-*-*" internal_cleaninst "${active_list}" "${www_link_dir}/ovpn-tmp-*-*"
internal_cleaninst "${active_list}" "${ram_link_dir}/ovpn-tmp-*-*" internal_cleaninst "${active_list}" "${ram_link_dir}/ovpn-tmp-*-*"
internal_cleansymlink "${ram_link_dir}" "${www_link_dir}"
# Remove the temporary RAM directory ONLY if it is completely empty
[ -d "${ram_link_dir}" ] && rmdir "${ram_link_dir}" 2>/dev/null
# Smart Web-Folder Cleanup: Check if files other than index.html exist
if [ -d "${www_link_dir}" ]; then
local found_profiles
found_profiles="${www_link_dir}/ovpn-tmp-"*-*
# If the text still has a '*', no active profiles were found
if [ "${found_profiles}" = "${www_link_dir}/ovpn-tmp-*-*" ]; then
rm -rf "${www_link_dir}"
fi
fi
return 0 return 0
} }
@@ -774,10 +774,11 @@ const openAutomatedPostKeyGenModal = async function (newInstanceItem, appData, w
if (optionalShowBtnCancel !== true) { if (optionalShowBtnCancel !== true) {
styleHide = ' display:none !important;'; styleHide = ' display:none !important;';
} }
const backupHide = (wizardParams && wizardParams.isServerBackup) ? '' : styleHide;
const yesBtn = E('button', { 'class': 'btn cbi-button cbi-button-action important', 'style': 'margin-right:10px;' }, TXT.KEY.generate_unique_keys); const yesBtn = E('button', { 'class': 'btn cbi-button cbi-button-action important', 'style': 'margin-right:10px;' }, TXT.KEY.generate_unique_keys);
const noBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-right:10px;' }, TXT.KEY.clone_default_keys); const noBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-right:10px;' }, TXT.KEY.clone_default_keys);
const backupBtn = E('button', { 'class': 'btn cbi-button cbi-button-positive important', 'style': 'margin-right:10px;' }, TXT.BTN.backup); const backupBtn = E('button', { 'class': 'btn cbi-button cbi-button-positive important', 'style': 'margin-right:10px;' + backupHide }, TXT.BTN.backup);
const cancelBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-right:10px;' + styleHide }, TXT.BTN.cancel); const cancelBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-right:10px;' + styleHide }, TXT.BTN.cancel);
const closeBtn = E('button', { 'class': 'cbi-button cbi-button-action important', 'style': 'display:none;' }, TXT.BTN.close); const closeBtn = E('button', { 'class': 'cbi-button cbi-button-action important', 'style': 'display:none;' }, TXT.BTN.close);
@@ -904,7 +905,14 @@ const openAutomatedPostKeyGenModal = async function (newInstanceItem, appData, w
callbacks.uciSaveApplyRestart(instance_id); callbacks.uciSaveApplyRestart(instance_id);
}); });
L.ui.showModal(TXT.KEY.add_server_instance + ' (' + instance_id + ')', [ let displayName;
if (cnName) {
displayName = ': ' + cnName;
} else {
displayName = ' (' + instance_id + ')';
}
L.ui.showModal(TXT.KEY.add_server_instance + displayName, [
E('div', { 'class': 'cbi-map' }, [ E('div', { 'class': 'cbi-map' }, [
E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-section' }, [
promptMessageNode, promptMessageNode,
@@ -392,54 +392,10 @@ const getValidCommonName = function (name) {
return cleanName; return cleanName;
}; };
/**
* Validate and clean any given IPv4 or IPv6 address string
*/
const checkIPAddress = function (rawIp) {
if (!rawIp) {
return null;
}
// Delete all spaces, tabs and hidden lines from the text (tr -d ' \t\r\n')
const ip = rawIp.replace(/[\s\t\r\n]/g, '');
if (ip === '') {
return null;
}
const ipv4Regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
// FIXED: Standard compliant IPv6 pattern that correctly handles "::" and blocks ":::"
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}$|^:(?::[0-9a-fA-F]{1,4}){1,7}$|^::$/;
// Case 1: Check if the text is a valid IPv4 address (numbers 0 to 255)
if (ipv4Regex.test(ip)) {
const segments = ip.split('.');
const octet1 = parseInt(segments[0], 10);
const octet2 = parseInt(segments[1], 10);
const octet3 = parseInt(segments[2], 10);
const octet4 = parseInt(segments[3], 10);
// Match the awk rule: $1<=255 && $2<=255 && $3<=255 && $4<=255
if (octet1 <= 255 && octet2 <= 255 && octet3 <= 255 && octet4 <= 255) {
return { ip: ip, isV6: false };
}
}
// Case 2: Check if the text matches the verified IPv6 regex
if (ipv6Regex.test(ip)) {
if (ip.indexOf(':') !== -1) {
// Change all hex letters to lowercase for safe matching later (tr 'A-Z' 'a-z')
return { ip: ip.toLowerCase(), isV6: true };
}
}
return null;
};
/** /**
* Generates an isolated, self-validating Site-to-Site network inputs row layout block * Generates an isolated, self-validating Site-to-Site network inputs row layout block
*/ */
const renderSubnetInputs = function () { const renderSubnetInputs = function (checkIpCallback) {
// Create the 3 input fields with simple, easy text placeholders // Create the 3 input fields with simple, easy text placeholders
const cnInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.INFO.remote_office_name, 'style': 'width:32%; font-family:monospace; margin-right:1%;' }); const cnInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.INFO.remote_office_name, 'style': 'width:32%; font-family:monospace; margin-right:1%;' });
const subnetInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.INFO.lan_ip + ' (192.168.2.0)', 'style': 'width:32%; font-family:monospace; margin-right:1%;' }); const subnetInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.INFO.lan_ip + ' (192.168.2.0)', 'style': 'width:32%; font-family:monospace; margin-right:1%;' });
@@ -503,7 +459,7 @@ const renderSubnetInputs = function () {
} }
// 2. Validate IP Address using your verified template logic // 2. Validate IP Address using your verified template logic
const ipResult = checkIPAddress(rawSub); const ipResult = (typeof checkIpCallback === 'function') ? checkIpCallback(rawSub) : null
if (!ipResult) { if (!ipResult) {
subnetInput.classList.add('cbi-input-invalid'); subnetInput.classList.add('cbi-input-invalid');
hasValidationError = true; hasValidationError = true;
@@ -1111,7 +1067,6 @@ const showMessage = function (text, type, box) {
box.style.setProperty('display', 'block', 'important'); box.style.setProperty('display', 'block', 'important');
}; };
const hideMessage = function (box) { const hideMessage = function (box) {
box.style.setProperty('display', 'none', 'important'); box.style.setProperty('display', 'none', 'important');
} }
@@ -1411,7 +1366,7 @@ const renderOptionSiteServer = function (portForwardingAlert, wizardData) {
}; };
// 4. Load the input fields row component // 4. Load the input fields row component
const networkInputComponent = renderSubnetInputs(); const networkInputComponent = renderSubnetInputs(wizardData.networkCallbacks.checkIp);
// 5. Create the "Add" button // 5. Create the "Add" button
const addEntryBtn = E('button', { const addEntryBtn = E('button', {
@@ -572,27 +572,71 @@ const sha256sum = async function (textToHash) {
return 'ERROR'; return 'ERROR';
}; };
/**
* Validate and clean any given IPv4 or IPv6 address string safely
*/
const checkIp = function (rawIp) {
if (!rawIp) {
return null;
}
// Delete all spaces, tabs and hidden lines from the text (tr -d ' \t\r\n')
const ip = rawIp.replace(/[\s\t\r\n]/g, '').trim();
if (ip === '') {
return null;
}
const ipv4Regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
const ipv6Regex = /^([\da-fA-F]{1,4}:|:){1,7}([\da-fA-F]{1,4}|:)$/;
// Case 1: Check if the text is a valid IPv4 address (numbers 0 to 255)
if (ipv4Regex.test(ip)) {
const segments = ip.split('.');
if (parseInt(segments[0], 10) <= 255 &&
parseInt(segments, 10) <= 255 &&
parseInt(segments, 10) <= 255 &&
parseInt(segments, 10) <= 255) {
return { ip: ip, isV6: false };
}
}
// Case 2: Check if the text is a syntactically valid IPv6 address. Extra check for triple colons (:::) to keep the regex small
if (ipv6Regex.test(ip) && ip.indexOf(':') !== -1 && ip.indexOf(':::') === -1) {
return { ip: ip.toLowerCase(), isV6: true };
}
return null;
};
/** /**
* Global helper function to determine the fallback IP and mask from browser or UCI config. * Global helper function to determine the fallback IP and mask from browser or UCI config.
*/ */
const getHostnameFallback = function (returnMask) { const getHostnameFallback = function (returnMask) {
// Basic regex patterns to check for valid IPv4 or IPv6 layouts
const ipv4Pattern = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/;
const ipv6Pattern = /^(([0-9a-fA-F]{1,4}:){1,7}|:)(:[0-9a-fA-F]{1,4}){1,7}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
// Get the address string from the browser URL bar and remove IPv6 brackets // Get the address string from the browser URL bar and remove IPv6 brackets
let ip = (window.location.hostname || "").trim().replace(/^\[|\]$/g, ''); let browserIp = (window.location.hostname || "").trim().replace(/^\[|\]$/g, '');
let ip = '';
let mask = '255.255.255.0'; let mask = '255.255.255.0';
// If browser URL is text (like openwrt.lan), fallback to configured UCI IPs // Use our solid validation function to check the browser IP
if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) { let checked = checkIp(browserIp);
ip = L.uci.get('network', 'lan', 'ipaddr') || L.uci.get('network', 'lan', 'ip6addr') || '192.168.1.1';
if (checked) {
ip = checked.ip;
} else {
// If browser URL is a hostname (like openwrt.lan), fallback to configured UCI IPs
let uciIp = L.uci.get('network', 'lan', 'ipaddr') || L.uci.get('network', 'lan', 'ip6addr') || '192.168.1.1';
// Remove any trailing subnet mask from UCI strings (like /64) safely
ip = uciIp.split('/')[0].trim();
// Re-validate the UCI fallback IP to know its family (v4 or v6)
checked = checkIp(ip);
} }
// Set the matching subnet mask format based on the detected IP family // Set the matching subnet mask format based on the detected IP family
if (ipv4Pattern.test(ip)) { if (checked && checked.isV6 === false) {
mask = L.uci.get('network', 'lan', 'netmask') || OPENVPN.IPv4.MASK24; mask = L.uci.get('network', 'lan', 'netmask') || OPENVPN.IPv4.MASK24;
} else if (ipv6Pattern.test(ip)) { } else if (checked && checked.isV6 === true) {
mask = L.uci.get('network', 'lan', 'ip6assign') || '64'; mask = L.uci.get('network', 'lan', 'ip6assign') || '64';
} }
@@ -965,7 +1009,7 @@ const checkNetworkStructure = function (appData) {
gatewayIp = gw4; gatewayIp = gw4;
} }
// Check WAN IPv4 addresses for Double NAT and extract WAN net-boundaries securely [sh] // Check WAN IPv4 addresses for Double NAT and extract WAN net-boundaries securely
const ipaddrs = net.getIPAddrs ? net.getIPAddrs() : []; const ipaddrs = net.getIPAddrs ? net.getIPAddrs() : [];
let deviceMask = (net.getNetmask && net.getNetmask()) ? net.getNetmask() : (net.data && net.data.netmask) ? net.data.netmask : null; let deviceMask = (net.getNetmask && net.getNetmask()) ? net.getNetmask() : (net.data && net.data.netmask) ? net.data.netmask : null;
if (deviceMask === null || deviceMask === undefined || deviceMask === '') { if (deviceMask === null || deviceMask === undefined || deviceMask === '') {
@@ -977,7 +1021,7 @@ const checkNetworkStructure = function (appData) {
const wanIpStr = String(rawWanIp); const wanIpStr = String(rawWanIp);
const ip = wanIpStr.split('/')[0]; const ip = wanIpStr.split('/')[0];
// If the WAN IP is private, we have a Double NAT [sh] // If the WAN IP is private, we have a Double NAT
if (privateIpRegex.test(ip)) { if (privateIpRegex.test(ip)) {
isDoubleNat = true; isDoubleNat = true;
} }
@@ -1004,7 +1048,7 @@ const checkNetworkStructure = function (appData) {
gatewayIp = OPENVPN.IPv4.ZERO; gatewayIp = OPENVPN.IPv4.ZERO;
} }
// Set values to global object (Fixed: changed 'gateway' to 'gatewayIp') // Set values to global object
networkStructure.doubleNat = isDoubleNat; networkStructure.doubleNat = isDoubleNat;
networkStructure.apMode = isApMode; networkStructure.apMode = isApMode;
networkStructure.gateway = gatewayIp; networkStructure.gateway = gatewayIp;
@@ -1038,6 +1082,7 @@ const checkNetworkStructure = function (appData) {
*/ */
const networkCallbacks = ({ const networkCallbacks = ({
sha256sum: sha256sum, sha256sum: sha256sum,
checkIp: checkIp,
getLanIp: getLanIp, getLanIp: getLanIp,
queryPublicIp: queryPublicIp, queryPublicIp: queryPublicIp,
getDdnsOrPublicIp: getDdnsOrPublicIp, getDdnsOrPublicIp: getDdnsOrPublicIp,
@@ -2785,7 +2830,7 @@ const openSiteToSiteExportModal = function (instance_id, nextId, serverAssets, a
selectDropdown.appendChild(E('option', { 'disabled': true }, ['- - -'])); selectDropdown.appendChild(E('option', { 'disabled': true }, ['- - -']));
selectDropdown.appendChild(E('option', { 'value': 'backup_server_profile' }, [ICON.BACKUP + TXT.MSG.backup_server_profile + ': ' + displayId])); selectDropdown.appendChild(E('option', { 'value': 'backup_server_profile' }, [ICON.BACKUP + TXT.MSG.backup_server_profile + ': ' + displayId]));
const subnetInputs = appData.wizardClass.renderSubnetInputs(); const subnetInputs = appData.wizardClass.renderSubnetInputs(networkCallbacks.checkIp);
// Creates a hidden yellowish warning message box for network conflicts or duplicate names // Creates a hidden yellowish warning message box for network conflicts or duplicate names
const errorDisplayNode = E('div', { const errorDisplayNode = E('div', {
@@ -4789,7 +4834,7 @@ const generate_s2s_nft_file = async function (deviceName, localSubnet, remoteSub
let lanDevice = 'br-lan'; let lanDevice = 'br-lan';
const res = await luci_app_openvpn_plus([CFG.LIBEXEC.getl3dev]); const res = await luci_app_openvpn_plus([CFG.LIBEXEC.getl3dev]);
if (res && res !='' && !res.includes("ERROR:")) { if (res && res != '' && !res.includes("ERROR:")) {
lanDevice = res; lanDevice = res;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.29 PKG_VERSION:=26.10.29
PKG_RELEASE:=359 PKG_RELEASE:=360
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -963,17 +963,17 @@ function read_rulelist()
end end
local backup_files = { local backup_files = {
"/etc/config/passwall", "/etc/config/passwall",
"/etc/config/passwall_server", "/etc/config/passwall_server",
"/etc/passwall/rules/block_host", "/etc/passwall/rules/block_host",
"/etc/passwall/rules/block_ip", "/etc/passwall/rules/block_ip",
"/etc/passwall/rules/direct_host", "/etc/passwall/rules/direct_host",
"/etc/passwall/rules/direct_ip", "/etc/passwall/rules/direct_ip",
"/etc/passwall/rules/proxy_host", "/etc/passwall/rules/proxy_host",
"/etc/passwall/rules/proxy_ip", "/etc/passwall/rules/proxy_ip",
"/etc/passwall/rules/lanlist_ipv4", "/etc/passwall/rules/lanlist_ipv4",
"/etc/passwall/rules/lanlist_ipv6", "/etc/passwall/rules/lanlist_ipv6",
"/etc/passwall/rules/domains_excluded" "/etc/passwall/rules/domains_excluded"
} }
function create_backup() function create_backup()
@@ -119,7 +119,7 @@ o.rawhtml = true
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
return string.format([[ return string.format([[
<input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribeAll()" value="%s" />]], <input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribeAll()" value="%s" />]],
translate("Manual subscription All")) translate("Manual subscription All"))
end end
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>") s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
@@ -68,9 +68,9 @@ o.validate = port_validate
---- UDP No Redir Ports ---- UDP No Redir Ports
o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports"), o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports"),
"<font color='red'>" .. translate( "<font color='red'>" .. translate(
"Fill in the ports you don't want to be forwarded by the agent, with the highest priority.") .. "Fill in the ports you don't want to be forwarded by the agent, with the highest priority.") ..
"</font>") "</font>")
o.default = "disable" o.default = "disable"
o:value("disable", translate("No patterns are used")) o:value("disable", translate("No patterns are used"))
o:value("1:65535", translate("All")) o:value("1:65535", translate("All"))
@@ -18,7 +18,7 @@ s.type_name = type_name
s.option_prefix = "socks_" s.option_prefix = "socks_"
api.set_type_cbi(s) api.set_type_cbi(s)
o = s:option(ListValue, "del_protocol", " ") --始终隐藏,用于删除 protocol o = s:option(ListValue, "del_protocol", " ") --始终隐藏,用于删除 protocol
o:depends({ __hide = "1" }) o:depends({ __hide = "1" })
o.rewrite_option = "protocol" o.rewrite_option = "protocol"
@@ -2178,7 +2178,7 @@ function gen_config(var)
local remote_dns_server = api.clone(remote_server) local remote_dns_server = api.clone(remote_server)
remote_dns_server.tag = value.shunt_tag remote_dns_server.tag = value.shunt_tag
local is_local = (remote_server.address and api.is_local_ip(remote_server.address)) or local is_local = (remote_server.address and api.is_local_ip(remote_server.address)) or
(remote_server.server and api.is_local_ip(remote_server.server)) --dns为本地ip,不走代理 (remote_server.server and api.is_local_ip(remote_server.server)) --dns为本地ip,不走代理
remote_dns_server.detour = is_local and "direct" or value.outboundTag remote_dns_server.detour = is_local and "direct" or value.outboundTag
table.insert(dns.servers, remote_dns_server) table.insert(dns.servers, remote_dns_server)
dns_rule.server = remote_dns_server.tag dns_rule.server = remote_dns_server.tag
@@ -175,7 +175,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -169,7 +169,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -125,7 +125,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -137,7 +137,7 @@ local dropdown_data = {
</select> </select>
</div> </div>
<!-- 隐藏 input(保存实际配置值) --> <!-- 隐藏 input(保存实际配置值) -->
<input type="text" id="<%=cbid%>" name="<%=cbid%>" value="<%=current_key%>" data-update="change" style="display:none !important;" /> <input type="text" id="<%=cbid%>" name="<%=cbid%>" value="<%=current_key%>" data-update="change" style="display:none !important;" />
<!-- 模拟 ListValue 控件外观 --> <!-- 模拟 ListValue 控件外观 -->
<div class="cbi-input-value cbi-input-select v-dropdown-display" id="<%=cbid%>.display" tabindex="0"> <div class="cbi-input-value cbi-input-select v-dropdown-display" id="<%=cbid%>.display" tabindex="0">
<span id="<%=cbid%>.label" class="v-dropdown-label" title="<%=pcdata(current_label)%>"> <span id="<%=cbid%>.label" class="v-dropdown-label" title="<%=pcdata(current_label)%>">
@@ -164,7 +164,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -5,11 +5,11 @@ local appname = map.config
local sid = "@global[0]" local sid = "@global[0]"
-%> -%>
<style> <style>
#cbi-<%=appname%>-socks .td.cbi-value-field > div { #cbi-<%=appname%>-socks .td.cbi-value-field > div {
white-space: nowrap; white-space: nowrap;
} }
@media (max-width: 1152px) { @media (max-width: 1152px) {
#cbi-<%=appname%>-socks .td.cbi-value-field > div { #cbi-<%=appname%>-socks .td.cbi-value-field > div {
white-space: normal; white-space: normal;
} }
} }
@@ -187,7 +187,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
//<![CDATA[ //<![CDATA[
var imgs = document.getElementsByTagName('img'); var imgs = document.getElementsByTagName('img');
@@ -133,7 +133,7 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
var checkBoxList = []; var checkBoxList = [];
var len = domList.length; var len = domList.length;
while(len--) { while(len--) {
var dom = domList[len];   var dom = domList[len];
if(dom.type == 'checkbox' && dom.checked) { if(dom.type == 'checkbox' && dom.checked) {
checkBoxList.push(dom.name); checkBoxList.push(dom.name);
} }
@@ -162,7 +162,7 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
var checkBoxList = []; var checkBoxList = [];
var len = domList.length; var len = domList.length;
while(len--) { while(len--) {
var dom = domList[len];   var dom = domList[len];
if(dom.type == 'checkbox' && dom.checked) { if(dom.type == 'checkbox' && dom.checked) {
if(type == 'geoip' && (dom.name === 'chnroute' || dom.name === 'chnroute6')) { if(type == 'geoip' && (dom.name === 'chnroute' || dom.name === 'chnroute6')) {
checkBoxList.push(dom.name); checkBoxList.push(dom.name);
@@ -10,7 +10,7 @@ EEOF
if [ -e "/etc/config/ucitrack" ]; then if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF uci -q batch <<-EOF
delete ucitrack.@passwall[-1] delete ucitrack.@passwall[-1]
add ucitrack passwall add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall set ucitrack.@passwall[-1].init=passwall
commit ucitrack commit ucitrack
@@ -1096,7 +1096,7 @@ start_dns() {
local ACL_RULE_DNSMASQ=0 local ACL_RULE_DNSMASQ=0
for acl_section in $(uci show ${CONFIG} | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do for acl_section in $(uci show ${CONFIG} | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
if [ "$(config_n_get $acl_section enabled)" = "1" ] && \ if [ "$(config_n_get $acl_section enabled)" = "1" ] && \
[ "$(config_n_get $acl_section dns_shunt)" = "dnsmasq" ]; then [ "$(config_n_get $acl_section dns_shunt)" = "dnsmasq" ]; then
ACL_RULE_DNSMASQ=1 ACL_RULE_DNSMASQ=1
break break
fi fi
@@ -172,9 +172,7 @@ local auto_config_lines = {}
-- 直连规则显式覆盖远程兜底的缓存、IPv6、测速和双栈优选设置。 -- 直连规则显式覆盖远程兜底的缓存、IPv6、测速和双栈优选设置。
local DIRECT_RULE_OPTIONS = " -enable-cache -address -6 -speed-check-mode ping,tcp:80,tcp:443 -response-mode first-ping -d yes" local DIRECT_RULE_OPTIONS = " -enable-cache -address -6 -speed-check-mode ping,tcp:80,tcp:443 -response-mode first-ping -d yes"
--设置默认 DNS 分组(兜底组),全局代理模式仍使用远程组。 --设置默认 DNS 分组(兜底组),全局代理模式仍使用远程组。
local DEFAULT_DNS_GROUP = (USE_DEFAULT_DNS == "direct" and "direct") or local DEFAULT_DNS_GROUP = (USE_DEFAULT_DNS == "direct" and "direct") or (USE_DEFAULT_DNS == "remote" and "proxy") or (USE_DEFAULT_DNS == "auto" and AUTO_GROUP)
(USE_DEFAULT_DNS == "remote" and "proxy") or
(USE_DEFAULT_DNS == "auto" and AUTO_GROUP)
local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0") and 1 local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0") and 1
if only_global == 1 then if only_global == 1 then
DEFAULT_DNS_GROUP = "proxy" DEFAULT_DNS_GROUP = "proxy"
@@ -197,20 +197,20 @@ insert_nftset() {
RS = "[ \t\n\r]+" RS = "[ \t\n\r]+"
ORS = "" ORS = ""
} }
$0 != "" { $0 != "" {
if (!first) { if (!first) {
printf "add element %s %s { \n", t, n printf "add element %s %s { \n", t, n
first = 1 first = 1
} else { } else {
print ",\n" print ",\n"
} }
printf " %s", $0 printf " %s", $0
} }
END { END {
if (first) { if (first) {
print "\n }\n" print "\n }\n"
} }
}' | nft -f - }' | nft -f -
} }
gen_nftset() { gen_nftset() {
@@ -1502,7 +1502,7 @@ add_firewall_rule() {
del_firewall_rule() { del_firewall_rule() {
for nft in "dstnat" "srcnat" "nat_output" "mangle_prerouting" "mangle_output"; do for nft in "dstnat" "srcnat" "nat_output" "mangle_prerouting" "mangle_output"; do
local handles=$(nft -a list chain $NFTABLE_NAME ${nft} 2>/dev/null | grep -E "PSW_" | awk -F '# handle ' '{print$2}') local handles=$(nft -a list chain $NFTABLE_NAME ${nft} 2>/dev/null | grep -E "PSW_" | awk -F '# handle ' '{print$2}')
for handle in $handles; do for handle in $handles; do
nft delete rule $NFTABLE_NAME ${nft} handle ${handle} 2>/dev/null nft delete rule $NFTABLE_NAME ${nft} handle ${handle} 2>/dev/null
done done
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.29 PKG_VERSION:=26.10.29
PKG_RELEASE:=160 PKG_RELEASE:=161
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -114,7 +114,7 @@ o.rawhtml = true
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
return string.format([[ return string.format([[
<input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribeAll()" value="%s" />]], <input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribeAll()" value="%s" />]],
translate("Manual subscription All")) translate("Manual subscription All"))
end end
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>") s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
@@ -19,13 +19,13 @@ m.description = (m.description or "") .. [[
<style> <style>
/* Scope to passwall2 CBI fields so it won't leak elsewhere */ /* Scope to passwall2 CBI fields so it won't leak elsewhere */
div[id^="cbid.passwall2."] .cbi-value-field { div[id^="cbid.passwall2."] .cbi-value-field {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1em; gap: 1em;
} }
div[id^="cbid.passwall2."] .cbi-checkbox { div[id^="cbid.passwall2."] .cbi-checkbox {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
} }
</style> </style>
]] ]]
@@ -982,7 +982,7 @@ function gen_config(var)
local routing = nil local routing = nil
local observatory = nil local observatory = nil
local burstObservatory = nil local burstObservatory = nil
local strategy = nil local strategy = nil
local COMMON = {} local COMMON = {}
local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt" local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt"
@@ -175,7 +175,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -169,7 +169,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -125,7 +125,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -137,7 +137,7 @@ local dropdown_data = {
</select> </select>
</div> </div>
<!-- 隐藏 input(保存实际配置值) --> <!-- 隐藏 input(保存实际配置值) -->
<input type="text" id="<%=cbid%>" name="<%=cbid%>" value="<%=current_key%>" data-update="change" style="display:none !important;" /> <input type="text" id="<%=cbid%>" name="<%=cbid%>" value="<%=current_key%>" data-update="change" style="display:none !important;" />
<!-- 模拟 ListValue 控件外观 --> <!-- 模拟 ListValue 控件外观 -->
<div class="cbi-input-value cbi-input-select v-dropdown-display" id="<%=cbid%>.display" tabindex="0"> <div class="cbi-input-value cbi-input-select v-dropdown-display" id="<%=cbid%>.display" tabindex="0">
<span id="<%=cbid%>.label" class="v-dropdown-label" title="<%=pcdata(current_label)%>"> <span id="<%=cbid%>.label" class="v-dropdown-label" title="<%=pcdata(current_label)%>">
@@ -164,7 +164,7 @@
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) { return function(colorStr) {
if (!colorStr) if (!colorStr)
return null; return null;
ctx.clearRect(0, 0, 1, 1); ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color // 2. Apply the color
ctx.fillStyle = colorStr; ctx.fillStyle = colorStr;
@@ -4,25 +4,25 @@ local api = map.api
-%> -%>
<style> <style>
.dns-con { .dns-con {
padding: 1rem; padding: 1rem;
} }
.faq-title { .faq-title {
color: var(--primary); color: var(--primary);
font-weight: bolder; font-weight: bolder;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
display: inline-block; display: inline-block;
} }
.reset-title { .reset-title {
color: var(--primary); color: var(--primary);
font-weight: bolder; font-weight: bolder;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
display: inline-block; display: inline-block;
margin-top: 1.2rem; margin-top: 1.2rem;
text-decoration: underline; text-decoration: underline;
} }
.dns-item { .dns-item {
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
line-height:1.2rem; line-height:1.2rem;
} }
.dns-list { .dns-list {
text-indent:1rem; text-indent:1rem;
@@ -5,11 +5,11 @@ local appname = map.config
local sid = "@global[0]" local sid = "@global[0]"
-%> -%>
<style> <style>
#cbi-<%=appname%>-socks .td.cbi-value-field > div { #cbi-<%=appname%>-socks .td.cbi-value-field > div {
white-space: nowrap; white-space: nowrap;
} }
@media (max-width: 1152px) { @media (max-width: 1152px) {
#cbi-<%=appname%>-socks .td.cbi-value-field > div { #cbi-<%=appname%>-socks .td.cbi-value-field > div {
white-space: normal; white-space: normal;
} }
} }
@@ -132,7 +132,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
//<![CDATA[ //<![CDATA[
var imgs = document.getElementsByTagName('img'); var imgs = document.getElementsByTagName('img');
@@ -87,9 +87,9 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
var checkBoxList = []; var checkBoxList = [];
var len = domList.length; var len = domList.length;
while(len--) { while(len--) {
var dom = domList[len];   var dom = domList[len];
if(dom.type == 'checkbox' && dom.checked) {   if(dom.type == 'checkbox' && dom.checked) {
checkBoxList.push(dom.name);   checkBoxList.push(dom.name);
} }
} }
XHR.get('<%=api.url("update_rules")%>', { XHR.get('<%=api.url("update_rules")%>', {
@@ -10,7 +10,7 @@ EEOF
if [ -e "/etc/config/ucitrack" ]; then if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF uci -q batch <<-EOF
delete ucitrack.@passwall2[-1] delete ucitrack.@passwall2[-1]
add ucitrack passwall2 add ucitrack passwall2
set ucitrack.@passwall2[-1].init=passwall2 set ucitrack.@passwall2[-1].init=passwall2
commit ucitrack commit ucitrack
@@ -157,20 +157,20 @@ insert_nftset() {
RS = "[ \t\n\r]+" RS = "[ \t\n\r]+"
ORS = "" ORS = ""
} }
$0 != "" { $0 != "" {
if (!first) { if (!first) {
printf "add element %s %s { \n", t, n printf "add element %s %s { \n", t, n
first = 1 first = 1
} else { } else {
print ",\n" print ",\n"
} }
printf " %s", $0 printf " %s", $0
} }
END { END {
if (first) { if (first) {
print "\n }\n" print "\n }\n"
} }
}' | nft -f - }' | nft -f -
} }
gen_nftset() { gen_nftset() {
+6 -6
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage PKG_NAME:=luci-app-vantage
PKG_VERSION:=1.0.10 PKG_VERSION:=1.0.11
PKG_RELEASE:=17 PKG_RELEASE:=18
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard LUCI_TITLE:=Vantage dashboard
@@ -20,13 +20,13 @@ LUCI_DESCRIPTION:=Live network dashboard (landing page under admin/dashboard): d
Read-only except for station names, which are kept in /etc/config/vantage. Read-only except for station names, which are kept in /etc/config/vantage.
# rpcd-mod-iwinfo provides the station lists (iwinfo info/assoclist). # rpcd-mod-iwinfo provides the station lists (iwinfo info/assoclist).
# rpcd-mod-ucode runs the app's plugin (/usr/share/rpcd/ucode/luci.vantage: # rpcd-mod-ucode runs the app's plugin (/usr/share/rpcd/ucode/luci.vantage:
# key-free wireless status, validated alias writes), which imports the uci # key-free wireless status, thermal zones, validated alias writes), which
# and ubus ucode modules; luci-base pulls all three in already, they are # imports the uci, ubus and fs ucode modules; luci-base pulls all four in
# listed because the plugin needs them. # already, they are listed because the plugin needs them.
# Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS # Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS
# names), umdns (mDNS names), hostapd ubus objects (WPS names, client # names), umdns (mDNS names), hostapd ubus objects (WPS names, client
# capabilities). # capabilities).
LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com> LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com>
LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage
@@ -356,6 +356,8 @@ html[data-darkmode="true"] .vt-app {
background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%; background-size: 5px 5px, 5px 5px; background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%; background-size: 5px 5px, 5px 5px;
} }
.vt-app .vt-select:hover { border-color: var(--vt-text-3); } .vt-app .vt-select:hover { border-color: var(--vt-text-3); }
/* focused or open: the ring of the app's other fields (filter, rename) */
.vt-app .vt-select:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
/* the open list: browsers that can style it (customizable select) draw /* the open list: browsers that can style it (customizable select) draw
it like the theme's dropdowns; others keep their own */ it like the theme's dropdowns; others keep their own */
@supports (appearance: base-select) { @supports (appearance: base-select) {
@@ -373,8 +375,17 @@ html[data-darkmode="true"] .vt-app {
min-height: 32px; padding: 0 10px; gap: 8px; min-height: 32px; padding: 0 10px; gap: 8px;
border-radius: var(--vt-r-sm); font-size: var(--vt-fs-base); cursor: pointer; border-radius: var(--vt-r-sm); font-size: var(--vt-fs-base); cursor: pointer;
} }
/* the option under the pointer or keyboard, as the theme's dropdowns mark it */ .vt-app .vt-select:open { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
.vt-app .vt-select option:is(:hover, :focus, :focus-visible) { background: var(--vt-surface-3); box-shadow: inset 2px 0 0 var(--vt-accent-text); outline: none; } /* below the field when it fits, as the theme's dropdowns open; the
browser's default opens toward the larger space, often upward */
.vt-app .vt-select::picker(select) { position-try-order: normal; }
/* the option under the pointer gets the fill, the keyboard's the fill
and the accent bar, as the theme's dropdowns mark them. Opened with
the pointer, the chosen option has :focus without :focus-visible:
unmarked, so it no longer looks like a second row under the pointer */
.vt-app .vt-select option:is(:hover, :focus-visible) { background: var(--vt-surface-3); }
.vt-app .vt-select option:focus-visible { box-shadow: inset 2px 0 0 var(--vt-accent-text); }
.vt-app .vt-select option:focus { outline: none; }
.vt-app .vt-select option:checked { font-weight: 600; } .vt-app .vt-select option:checked { font-weight: 600; }
.vt-app .vt-select option::checkmark { color: var(--vt-accent-text); } .vt-app .vt-select option::checkmark { color: var(--vt-accent-text); }
} }
@@ -1158,7 +1169,7 @@ h2, h3{
.vt-app .vt-rename .vt-rename-input { .vt-app .vt-rename .vt-rename-input {
height: var(--vt-ctl); width: 100%; padding: 0 10px; margin: 0; height: var(--vt-ctl); width: 100%; padding: 0 10px; margin: 0;
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface); border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface);
color: var(--vt-text); font: inherit; font-size: var(--vt-fs-md); box-shadow: none; color: var(--vt-text); font: inherit; font-size: var(--vt-fs-md); font-weight: 400; box-shadow: none; /* the label around it is semibold */
} }
.vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); } .vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
.vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; } .vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; }
@@ -103,6 +103,12 @@ return baseclass.extend({
return v == null ? DASH : (v / 65536).toFixed(2); return v == null ? DASH : (v / 65536).toFixed(2);
}, },
/* degrees Celsius, whole: 77.8 -> '78 °C' (no-break space) */
celsius: function(c) {
c = num(c);
return c == null ? DASH : Math.round(c) + '\u00a0°C';
},
/* upper-case, colon separated; anything else is returned unchanged */ /* upper-case, colon separated; anything else is returned unchanged */
mac: function(mac) { mac: function(mac) {
var s = String(mac == null ? '' : mac).trim(); var s = String(mac == null ? '' : mac).trim();
@@ -31,7 +31,8 @@ return baseclass.extend({
/* ------------------------------------------------------------ health */ /* ------------------------------------------------------------ health */
/* model + { cpu: { pct, cores } | null, uplinkRate: { rx, tx } | null } /* model + { cpu: { pct, cores } | null, uplinkRate: { rx, tx } | null,
temps: model.temps() | null }
(model.wired: no Wi-Fi checks) (model.wired: no Wi-Fi checks)
-> [ { id, level, title, value, reason, detail, page } ] -> [ { id, level, title, value, reason, detail, page } ]
value and reason are short enough for a health chip; detail is the value and reason are short enough for a health chip; detail is the
@@ -65,10 +66,13 @@ return baseclass.extend({
/* CPU */ /* CPU */
var cpu = live.cpu && finite(live.cpu.pct) ? live.cpu.pct : null, cores = (live.cpu && live.cpu.cores && live.cpu.cores.length) || 1; var cpu = live.cpu && finite(live.cpu.pct) ? live.cpu.pct : null, cores = (live.cpu && live.cpu.cores && live.cpu.cores.length) || 1;
var load = (dev.load || [])[0]; var load = (dev.load || [])[0];
/* the hottest sensor, when the device has any */
var hot = live.temps && live.temps[0] ? fmt.celsius(live.temps[0].celsius) : null;
if (cpu == null) out.push({ id: 'cpu', level: 'info', title: _('CPU', 'vantage'), value: fmt.DASH, reason: _('Measuring…'), detail: _('Measuring over the next poll'), page: PAGES.processes }); if (cpu == null) out.push({ id: 'cpu', level: 'info', title: _('CPU', 'vantage'), value: fmt.DASH, reason: _('Measuring…'), detail: _('Measuring over the next poll'), page: PAGES.processes });
else out.push({ id: 'cpu', level: cpu >= 90 ? 'err' : cpu >= 70 ? 'warn' : 'ok', title: _('CPU', 'vantage'), value: fmt.pct(cpu), else out.push({ id: 'cpu', level: cpu >= 90 ? 'err' : cpu >= 70 ? 'warn' : 'ok', title: _('CPU', 'vantage'), value: fmt.pct(cpu),
reason: cpu >= 70 ? _('Busy · load %s').format(fmt.load(load)) : _('Load %s · %d cores').format(fmt.load(load), cores), reason: (cpu >= 70 ? _('Busy · load %s').format(fmt.load(load)) : _('Load %s · %d cores').format(fmt.load(load), cores)) + (hot ? ' · ' + hot : ''),
detail: cpu >= 70 ? _('Busy; load %s on %d cores; see the busiest processes').format(fmt.load(load), cores) : _('Load %s on %d cores').format(fmt.load(load), cores), detail: (cpu >= 70 ? _('Busy; load %s on %d cores; see the busiest processes').format(fmt.load(load), cores) : _('Load %s on %d cores').format(fmt.load(load), cores)) +
(hot ? ' · ' + _('Temperature %s').format(hot) : ''),
page: PAGES.processes }); page: PAGES.processes });
/* memory, framed as available (as in the System tile) */ /* memory, framed as available (as in the System tile) */
@@ -14,6 +14,8 @@ var WIRELESS_NAME = /^(wlan\d|phy[0-9a-z]*-|ath\d|wl\d|ra\d|rai\d|rax\d|mld\d)/;
/* above any real link: a rate this high means a bad sample, not traffic */ /* above any real link: a rate this high means a bad sample, not traffic */
var MAX_BPS = 400e9; var MAX_BPS = 400e9;
/* netifd protocols that tunnel IPv4 over an IPv6 uplink */
var SOFTWIRE = /^(dslite|map|464xlat)$/;
function num(v) { v = +v; return isFinite(v) ? v : 0; } function num(v) { v = +v; return isFinite(v) ? v : 0; }
function finite(v) { return typeof v === 'number' && isFinite(v); } function finite(v) { return typeof v === 'number' && isFinite(v); }
@@ -59,6 +61,18 @@ return baseclass.extend({
return { pct: pct, cores: cur.cores.map(function(c, i) { return self.cpuPercent(prev.cores[i], c); }) }; return { pct: pct, cores: cur.cores.map(function(c, i) { return self.cpuPercent(prev.cores[i], c); }) };
}, },
/* luci.vantage thermal zones [ { zone, type, temp } ] (temp in
millidegrees) -> [ { name, celsius } ], the hottest first; an
implausible reading is left out */
temps: function(zones) {
return arr(zones).map(function(z) {
var c = z && typeof z.temp === 'number' && isFinite(z.temp) ? z.temp / 1000 : NaN;
if (!(c >= -40 && c <= 150)) return null;
var name = typeof z.type === 'string' && z.type ? z.type : typeof z.zone === 'string' ? z.zone : '';
return { name: fmt.safeText(name, 32), celsius: c };
}).filter(Boolean).sort(function(a, b) { return b.celsius - a.celsius; });
},
/* ---------------------------------------------------------- memory */ /* ---------------------------------------------------------- memory */
/* One bar, three segments that add up to total: /* One bar, three segments that add up to total:
@@ -174,9 +188,13 @@ return baseclass.extend({
/* Pick the interface carrying traffic in/out of this device, and the /* Pick the interface carrying traffic in/out of this device, and the
physical device its counters live on. Prefer interfaces with a default physical device its counters live on. Prefer interfaces with a default
route, keeping wan/wan6/*wan* priority within that set. Fall back to route in the main table, as the kernel picks among them: IPv4 before IPv6, then the
WAN names, then the first up interface. A bridge is followed to its lowest metric (multi-WAN: the backup uplink has the higher one), with
wired member (wireless members skipped; netifd 'ethernet' preferred); DSA with wan/wan6/*wan* priority only on a tie. A softwire (DS-Lite, MAP-E,
464XLAT) carries IPv4 over the IPv6 uplink, so it ranks after IPv6:
the uplink shown is the line, not the tunnel. Fall back to WAN names, then
the first up interface. A bridge is followed to its wired member
(wireless members skipped; netifd 'ethernet' preferred); DSA with
hardware offload uses the conduit's counters. */ hardware offload uses the conduit's counters. */
resolveUplink: function(ifaces, devs, wirelessNames) { resolveUplink: function(ifaces, devs, wirelessNames) {
var self = this; var self = this;
@@ -190,15 +208,39 @@ return baseclass.extend({
function preferredWan(list) { function preferredWan(list) {
return named(list, /^wan$/) || named(list, /^wan6$/) || named(list, /wan/i); return named(list, /^wan$/) || named(list, /^wan6$/) || named(list, /wan/i);
} }
function wanRank(i) {
return /^wan$/.test(i.interface) ? 0 : /^wan6$/.test(i.interface) ? 1 : /wan/i.test(i.interface) ? 2 : 3;
}
/* netifd lists a route's metric only when the route sets one; the
interface's metric applies otherwise. The kernel stores an IPv6
metric of 0 as 1024. */
function metric(i, r) {
var m = r.metric != null ? +r.metric : +i.metric;
m = isFinite(m) ? m : 0;
return r.target === '::' && m === 0 ? 1024 : m;
}
/* a default in another table (policy routing: a VPN's own table) is
not the one ordinary traffic takes */
function mainTable(i, r) {
var t = r.table != null ? r.table : i[r.target === '::' ? 'ip6table' : 'ip4table'];
return t == null || t === '' || t === 'main' || +t === 254;
}
function defRoute(i) { function defRoute(i) {
return arr(i.route).filter(function(r) { return arr(i.route).filter(function(r) {
return r && +r.mask === 0 && (r.target === '0.0.0.0' || r.target === '::'); return r && +r.mask === 0 && (r.target === '0.0.0.0' || r.target === '::') && mainTable(i, r);
}).sort(function(a, b) {
return (a.target === '::') - (b.target === '::') || metric(i, a) - metric(i, b);
})[0] || null; })[0] || null;
} }
/* An AP may retain an up, unmanaged WAN port without any routes. */ /* An AP may retain an up, unmanaged WAN port without any routes. */
var routed = up.filter(defRoute); var routed = up.map(function(i, n) {
var pick = preferredWan(routed) || routed[0] || preferredWan(up) || up[0]; var r = defRoute(i);
return r ? { i: i, family: SOFTWIRE.test(i.proto) ? 2 : r.target === '::' ? 1 : 0, metric: metric(i, r), wan: wanRank(i), n: n } : null;
}).filter(Boolean).sort(function(a, b) {
return a.family - b.family || a.metric - b.metric || a.wan - b.wan || a.n - b.n;
});
var pick = routed.length ? routed[0].i : preferredWan(up) || up[0];
if (!pick) return null; if (!pick) return null;
var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev]; var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev];
@@ -35,6 +35,7 @@ var callDevs = rpc.declare({ object: 'network.device', method: 'status', expect:
/* luci.vantage is the app's own rpcd ucode plugin: network.wireless status /* luci.vantage is the app's own rpcd ucode plugin: network.wireless status
without keys or RADIUS secrets, and the validated alias write */ without keys or RADIUS secrets, and the validated alias write */
var callWifi = rpc.declare({ object: 'luci.vantage', method: 'wireless', expect: { '': {} } }); var callWifi = rpc.declare({ object: 'luci.vantage', method: 'wireless', expect: { '': {} } });
var callThermal = rpc.declare({ object: 'luci.vantage', method: 'thermal', expect: { zones: [] } });
var callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect: { '': {} } }); var callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect: { '': {} } });
var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } }); var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } });
var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } }); var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } });
@@ -482,7 +483,8 @@ return view.extend({
L.resolveDefault(callDevs(), {}), L.resolveDefault(callDevs(), {}),
/* null when the call fails: no answer is not "no radios" */ /* null when the call fails: no answer is not "no radios" */
L.resolveDefault(callWifi(), null), L.resolveDefault(callWifi(), null),
L.resolveDefault(callHints(), {}) L.resolveDefault(callHints(), {}),
L.resolveDefault(callThermal(), [])
]).then(function(r) { ]).then(function(r) {
var wifiDevs = (r[4] && typeof r[4] === 'object') ? r[4] : null; var wifiDevs = (r[4] && typeof r[4] === 'object') ? r[4] : null;
var ifnames = []; var ifnames = [];
@@ -510,7 +512,7 @@ return view.extend({
else if (p[0] === 'hapdStatus') hapdStatus[p[1]] = p[2]; else if (p[0] === 'hapdStatus') hapdStatus[p[1]] = p[2];
else iwinfo[p[1]] = p[2]; else iwinfo[p[1]] = p[2];
}); });
return { info: r[0] || {}, stat: r[1] || '', ifaces: r[2] || [], devs: r[3] || {}, wifi: wifiDevs, hints: r[5] || {}, return { info: r[0] || {}, stat: r[1] || '', ifaces: r[2] || [], devs: r[3] || {}, wifi: wifiDevs, hints: r[5] || {}, thermal: r[6] || [],
assoc: assoc, hapd: hapd, hapdStatus: hapdStatus, iwinfo: iwinfo, at: Date.now(), started: started }; assoc: assoc, hapd: hapd, hapdStatus: hapdStatus, iwinfo: iwinfo, at: Date.now(), started: started };
}); });
}); });
@@ -579,6 +581,7 @@ return view.extend({
if (cpu) { st.cpu = cpu; series.push(st.hist.cpu, [ at, cpu.pct ], WINDOW_MS, HIST_MAX); } if (cpu) { st.cpu = cpu; series.push(st.hist.cpu, [ at, cpu.pct ], WINDOW_MS, HIST_MAX); }
if (!st.stat || stat.all.total !== st.stat.all.total) st.stat = stat; if (!st.stat || stat.all.total !== st.stat.all.total) st.stat = stat;
} }
st.temps = model.temps(raw.thermal);
var m = model.build({ var m = model.build({
board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo, board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo,
@@ -649,7 +652,7 @@ return view.extend({
this.stickyGen(m); this.stickyGen(m);
m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = p.state.firstSeen[c.mac]; c.isNew = !!p.isNew[c.mac]; }); m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = p.state.firstSeen[c.mac]; c.isNew = !!p.isNew[c.mac]; });
st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate }); st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate, temps: st.temps });
st.summary = insight.summary(st.health); st.summary = insight.summary(st.health);
st.radioInsights = insight.radioInsights(m.radios, st.radioRates); st.radioInsights = insight.radioInsights(m.radios, st.radioRates);
st.talkers = insight.topTalkers(m.clients, st.staRates, TALKERS); st.talkers = insight.topTalkers(m.clients, st.staRates, TALKERS);
@@ -744,7 +747,7 @@ return view.extend({
this.stickyGen(m); this.stickyGen(m);
m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = st.presence && st.presence.firstSeen[c.mac]; c.isNew = !!st.isNew[c.mac]; }); m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = st.presence && st.presence.firstSeen[c.mac]; c.isNew = !!st.isNew[c.mac]; });
st.talkers = insight.topTalkers(m.clients, st.staRates, TALKERS); st.talkers = insight.topTalkers(m.clients, st.staRates, TALKERS);
st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate }); st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate, temps: st.temps });
st.summary = insight.summary(st.health); st.summary = insight.summary(st.health);
this.m = m; this.m = m;
this.paint(); this.paint();
@@ -1724,6 +1727,7 @@ return view.extend({
el.cpuCores = E('div', { 'class': 'vt-cores' }); el.cpuCores = E('div', { 'class': 'vt-cores' });
el.cpuSpark = E('div', { 'class': 'vt-spark-slot' }); el.cpuSpark = E('div', { 'class': 'vt-spark-slot' });
el.cpuLoad = E('span', { 'class': 'vt-hint vt-tn', 'title': _('Load average over 1, 5 and 15 minutes') }); el.cpuLoad = E('span', { 'class': 'vt-hint vt-tn', 'title': _('Load average over 1, 5 and 15 minutes') });
el.cpuTemp = E('span', { 'class': 'vt-hint vt-tn' });
el.memHead = E('div', { 'class': 'vt-mem-head' }); el.memHead = E('div', { 'class': 'vt-mem-head' });
el.memBar = E('div', { 'class': 'vt-seg', 'role': 'img' }); el.memBar = E('div', { 'class': 'vt-seg', 'role': 'img' });
el.memLegend = E('div', { 'class': 'vt-legend' }); el.memLegend = E('div', { 'class': 'vt-legend' });
@@ -1736,7 +1740,8 @@ return view.extend({
E('div', { 'class': 'vt-sys-cpu-l' }, [ E('div', { 'class': 'vt-sys-cpu-l' }, [
E('span', { 'class': 'vt-label' }, [ _('CPU', 'vantage') ]), E('span', { 'class': 'vt-label' }, [ _('CPU', 'vantage') ]),
el.cpuVal, el.cpuVal,
el.cpuLoad el.cpuLoad,
el.cpuTemp
]), ]),
E('div', { 'class': 'vt-sys-cpu-r' }, [ E('div', { 'class': 'vt-sys-cpu-r' }, [
E('div', { 'class': 'vt-spark-box' }, [ el.cpuSpark, E('span', { 'class': 'vt-cap' }, [ _('last 5 min') ]) ]), E('div', { 'class': 'vt-spark-box' }, [ el.cpuSpark, E('span', { 'class': 'vt-cap' }, [ _('last 5 min') ]) ]),
@@ -1758,6 +1763,11 @@ return view.extend({
txt(el.cpuVal, cpu ? Math.round(cpu.pct) + '%' : fmt.DASH); txt(el.cpuVal, cpu ? Math.round(cpu.pct) + '%' : fmt.DASH);
el.cpuVal.className = 'vt-big vt-tn' + (cpu && cpu.pct >= 90 ? ' vt-t-err' : cpu && cpu.pct >= 70 ? ' vt-t-warn' : ''); el.cpuVal.className = 'vt-big vt-tn' + (cpu && cpu.pct >= 90 ? ' vt-t-err' : cpu && cpu.pct >= 70 ? ' vt-t-warn' : '');
txt(el.cpuLoad, _('Load avg %s').format(d.load.map(fmt.load).join(' · '))); txt(el.cpuLoad, _('Load avg %s').format(d.load.map(fmt.load).join(' · ')));
/* the hottest sensor; each one in the tooltip */
var temps = this.st.temps || [];
el.cpuTemp.hidden = !temps.length;
txt(el.cpuTemp, temps.length ? _('Temperature %s').format(fmt.celsius(temps[0].celsius)) : '');
setTitle(el.cpuTemp, temps.map(function(t) { return t.name + ' ' + fmt.celsius(t.celsius); }).join('\n'));
patch(el.cpuSpark, [ sparkline(this.st.hist.cpu, [ 1 ], 100, 'vt-spark-cpu', { scale: function() { return '100%'; } }) ]); patch(el.cpuSpark, [ sparkline(this.st.hist.cpu, [ 1 ], 100, 'vt-spark-cpu', { scale: function() { return '100%'; } }) ]);
patch(el.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) { patch(el.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) {
var fill = E('span', { 'class': 'vt-core-fill' }); var fill = E('span', { 'class': 'vt-core-fill' });
@@ -2790,6 +2800,9 @@ return view.extend({
handleKey: function(ev) { handleKey: function(ev) {
var t = ev.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)); var t = ev.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
/* Escape in an open chart style list (a select whose list the page
draws, focus on an option) closes the list, not Customize */
if (ev.key === 'Escape' && t && t.tagName === 'OPTION') return;
if (ev.key === 'Escape' && this.ui.drawer && !document.body.classList.contains('modal-overlay-active')) { if (ev.key === 'Escape' && this.ui.drawer && !document.body.classList.contains('modal-overlay-active')) {
this.closeDrawer(); this.closeDrawer();
return; return;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{ {
"luci-app-vantage": { "luci-app-vantage": {
"description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless)", "description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless) and its temperature sensors (luci.vantage thermal)",
"read": { "read": {
"file": { "file": {
"/proc/stat": [ "read" ] "/proc/stat": [ "read" ]
@@ -11,7 +11,7 @@
"network.interface": [ "dump" ], "network.interface": [ "dump" ],
"network.device": [ "status" ], "network.device": [ "status" ],
"luci-rpc": [ "getHostHints" ], "luci-rpc": [ "getHostHints" ],
"luci.vantage": [ "wireless" ], "luci.vantage": [ "thermal", "wireless" ],
"iwinfo": [ "assoclist", "info" ], "iwinfo": [ "assoclist", "info" ],
"hostapd.*": [ "get_clients", "get_status" ], "hostapd.*": [ "get_clients", "get_status" ],
"umdns": [ "hosts" ], "umdns": [ "hosts" ],
@@ -6,6 +6,9 @@
// wireless network.wireless status reduced to the fields the dashboard // wireless network.wireless status reduced to the fields the dashboard
// reads: no key, sae_password, RADIUS or 802.11r secrets, no // reads: no key, sae_password, RADIUS or 802.11r secrets, no
// EAP credentials. Read group luci-app-vantage. // EAP credentials. Read group luci-app-vantage.
// thermal the temperature of each thermal zone in /sys/class/thermal
// (millidegrees Celsius) with its type; nothing else is read.
// Read group luci-app-vantage.
// set_alias { mac, name, icon }: name one station, or remove its name // set_alias { mac, name, icon }: name one station, or remove its name
// with name ''. Validated here, whatever the browser did, and // with name ''. Validated here, whatever the browser did, and
// written to /etc/config/vantage only. Write group // written to /etc/config/vantage only. Write group
@@ -19,12 +22,15 @@
import { cursor } from 'uci'; import { cursor } from 'uci';
import { connect } from 'ubus'; import { connect } from 'ubus';
import { lsdir, readfile } from 'fs';
const CONFIG = 'vantage'; const CONFIG = 'vantage';
const NAME_MAX = 48; // code points, after collapsing spaces const NAME_MAX = 48; // code points, after collapsing spaces
const NAME_MAX_UNITS = NAME_MAX * 8; // raw input cap, in UTF-16 code units (as names.js) const NAME_MAX_UNITS = NAME_MAX * 8; // raw input cap, in UTF-16 code units (as names.js)
const MAX_ALIASES = 512; // client sections; bounds /etc/config/vantage const MAX_ALIASES = 512; // client sections; bounds /etc/config/vantage
const ICONS = [ 'phone', 'laptop', 'tablet', 'desktop', 'tv', 'speaker', 'printer', 'camera', 'console', 'watch', 'iot', 'router', 'device' ]; const ICONS = [ 'phone', 'laptop', 'tablet', 'desktop', 'tv', 'speaker', 'printer', 'camera', 'console', 'watch', 'iot', 'router', 'device' ];
const THERMAL = '/sys/class/thermal';
const MAX_ZONES = 16;
/* ------------------------------------------------------------ helpers */ /* ------------------------------------------------------------ helpers */
@@ -306,6 +312,32 @@ function set_alias(args) {
return { ok: true, op, mac }; return { ok: true, op, mac };
} }
/* ------------------------------------------------------------ thermal */
// [ { zone, type, temp } ] in zone order, at most MAX_ZONES; a zone whose
// temperature cannot be read (switched off, no sensor yet) is left out,
// a type that is not a plain word is sent as ''
function thermal() {
let zones = filter(lsdir(THERMAL) ?? [], n => match(n, /^thermal_zone[0-9]{1,3}$/));
let out = [];
for (let n in sort(zones, (a, b) => int(substr(a, 12)) - int(substr(b, 12)))) {
if (length(out) >= MAX_ZONES)
break;
let temp = trim(readfile(THERMAL + '/' + n + '/temp', 16) ?? '');
if (!match(temp, /^-?[0-9]{1,7}$/))
continue;
let kind = trim(readfile(THERMAL + '/' + n + '/type', 64) ?? '');
push(out, { zone: n, type: match(kind, /^[A-Za-z0-9_.-]{1,40}$/) ? kind : '', temp: int(temp) });
}
return out;
}
/* ------------------------------------------------------------ methods */ /* ------------------------------------------------------------ methods */
const methods = { const methods = {
@@ -325,6 +357,12 @@ const methods = {
} }
}, },
thermal: {
call: function() {
return { zones: thermal() };
}
},
set_alias: { set_alias: {
args: { mac: 'mac', name: 'name', icon: 'icon' }, args: { mac: 'mac', name: 'name', icon: 'icon' },
call: function(request) { call: function(request) {
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more