⛄ Sync 2026-11-01 05:43:29
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
}
|
}
|
||||||
@@ -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)?
|
||||||
|
|||||||
@@ -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)?
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 模式下自动选择加速引擎。"
|
||||||
@@ -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 模式下自動選擇加速引擎。"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
|  |  |
|
||||||
| <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" />
|
|  |  |
|
||||||
| <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" />
|
|  |  |
|
||||||
| <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" />
|
|  |  |
|
||||||
| <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" />
|
|  |  |
|
||||||
| <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" />
|
|  |  |
|
||||||
|
|  | 
|
||||||
|
|
||||||
## 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||