🌴 Sync 2026-10-16 01:08:07

This commit is contained in:
github-actions[bot] committed 2026-10-16 01:08:07 +08:00
1 parent f95bba4fb6
commit 16f740db5d
186 files changed
+9575 -5557

No files matched your search

+4 -4
View File
@@ -1,17 +1,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
PKG_SHORT_SHA:=bc76c13
PKG_COMMIT_DATE:=2026.10.11
PKG_SHORT_SHA:=94b13ae
PKG_COMMIT_DATE:=2026.10.13
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=12
PKG_RELEASE:=13
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=38f826f2f89637be271fb441f2f10b0d66422b17
PKG_SOURCE_VERSION:=4b487e294f46e98f51548f8c5253df804d7d4b38
PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
+2 -2
View File
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_DOCKERMANAGER:=$(ARCH)
PKG_NAME:=dockermanager
PKG_VERSION:=0.1.1
PKG_RELEASE:=8
PKG_VERSION:=0.1.2
PKG_RELEASE:=9
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
PKG_HASH:=skip
@@ -21,6 +21,7 @@
}
}
},
"auth": { "mode": "passthrough" },
"backend": {
"type": "http",
"transport": "unix",
@@ -65,6 +66,6 @@
"height": 760,
"singleton": true
},
"capabilities": ["docker"],
"capabilities": ["docker", "compose", "multi-target"],
"categories": ["system"]
}
+1
View File
@@ -7,3 +7,4 @@ config dockermanager
option 'port' '8192'
option 'bind_addr' '0.0.0.0'
option 'base_path' '/apps/dockermanager/'
option 'integration_token_file' '/etc/lighthost/docker-manager.token'
+65 -38
View File
@@ -6,63 +6,90 @@ USE_PROCD=1
get_config() {
config_get_bool enabled "$1" enabled 0
config_get data_dir "$1" data_dir ""
config_get listen_mode "$1" listen_mode "auto"
config_get_bool external_port_enabled "$1" external_port_enabled 0
config_get socket_path "$1" socket_path "/var/run/dockermanager.sock"
config_get port "$1" port "8192"
config_get bind_addr "$1" bind_addr "0.0.0.0"
config_get base_path "$1" base_path "/apps/dockermanager/"
config_get listen_mode "$1" listen_mode "auto"
config_get_bool external_port_enabled "$1" external_port_enabled 0
config_get socket_path "$1" socket_path "/var/run/dockermanager.sock"
config_get integration_token_file "$1" integration_token_file "/etc/lighthost/docker-manager.token"
}
mkdir_p() {
[ -d "$1" ] || mkdir -p "$1"
valid_data_dir() {
case "$1" in
/mnt/*|/root/*) return 0 ;;
*) return 1 ;;
esac
}
prepare_token() {
case "$integration_token_file" in
/etc/lighthost/*) ;;
*)
logger -t dockermanager "refusing unsafe integration_token_file: $integration_token_file"
return 1
;;
esac
mkdir -p "$(dirname "$integration_token_file")" || return 1
if [ ! -s "$integration_token_file" ]; then
umask 077
hexdump -v -n 32 -e '1/1 "%02x"' /dev/urandom > "$integration_token_file" || return 1
fi
if grep -q '^lighthost:' /etc/group; then
chown root:lighthost "$integration_token_file" || return 1
chmod 640 "$integration_token_file" || return 1
else
chown root:root "$integration_token_file" || return 1
chmod 600 "$integration_token_file" || return 1
fi
}
start_service() {
config_load dockermanager
config_foreach get_config dockermanager
if [ "$enabled" != 1 ]; then
[ "$enabled" = 1 ] || return 0
valid_data_dir "$data_dir" || {
logger -t dockermanager "refusing unsafe data_dir: $data_dir"
return 1
fi
if [ -z "$data_dir" ]; then
logger -t dockermanager "data_dir is empty; set option data_dir in /etc/config/dockermanager"
return 1
fi
}
mkdir -p "$data_dir/state" "$data_dir/cache" || return 1
prepare_token || return 1
mkdir_p "$data_dir" || return 1
mkdir_p "$data_dir/state" || return 1
mkdir_p "$data_dir/cache" || return 1
local effective_listen_mode
effective_listen_mode="$listen_mode"
if [ "$effective_listen_mode" = auto ]; then
if [ "$external_port_enabled" = 1 ]; then
effective_listen_mode=tcp
else
effective_listen_mode=unix
fi
fi
case "$effective_listen_mode" in
tcp|unix) ;;
*)
logger -t dockermanager "invalid listen_mode: $effective_listen_mode"
return 1
;;
esac
logger -t dockermanager "Starting Docker Manager service"
procd_open_instance docker-manager
if [ "$listen_mode" = "tcp" ] || [ "$external_port_enabled" = 1 ]; then
procd_set_param env \
DOCKER_MANAGER_LISTEN_MODE="tcp" \
DOCKER_MANAGER_ADDR="$bind_addr:$port" \
DOCKER_MANAGER_BASE_PATH="$base_path" \
DOCKER_MANAGER_DATA_DIR="$data_dir" \
DOCKER_MANAGER_STATIC_ROOT="/usr/share/dockermanager/www" \
DOCKER_MANAGER_PLATFORM="openwrt" \
DOCKER_MANAGER_AUTH_MODE="openwrt_luci"
else
procd_set_param env \
DOCKER_MANAGER_LISTEN_MODE="unix" \
DOCKER_MANAGER_SOCKET_PATH="$socket_path" \
DOCKER_MANAGER_BASE_PATH="$base_path" \
DOCKER_MANAGER_DATA_DIR="$data_dir" \
DOCKER_MANAGER_STATIC_ROOT="/usr/share/dockermanager/www" \
DOCKER_MANAGER_PLATFORM="openwrt" \
DOCKER_MANAGER_AUTH_MODE="openwrt_luci"
fi
procd_set_param env \
"DOCKER_MANAGER_ADDR=$bind_addr:$port" \
"DOCKER_MANAGER_BASE_PATH=$base_path" \
"DOCKER_MANAGER_DATA_DIR=$data_dir" \
"DOCKER_MANAGER_STATIC_ROOT=/usr/share/dockermanager/www" \
"DOCKER_MANAGER_PLATFORM=openwrt" \
"DOCKER_MANAGER_AUTH_MODE=openwrt_luci" \
"DOCKER_MANAGER_LISTEN_MODE=$effective_listen_mode" \
"DOCKER_MANAGER_SOCKET_PATH=$socket_path" \
"DOCKER_MANAGER_INTEGRATION_TOKEN_FILE=$integration_token_file"
procd_set_param command /usr/sbin/docker-manager
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_set_param respawn 3600 5 5
procd_close_instance
logger -t dockermanager "Docker Manager service started"
}
service_triggers() {
procd_add_reload_trigger "dockermanager"
procd_add_reload_trigger dockermanager
}
+2 -2
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_NAME:=easytier-noweb
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.4)
PKG_RELEASE:=3
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.7.0)
PKG_RELEASE:=4
ifeq ($(ARCH),mipsel)
APP_ARCH:=mipsel
+2 -2
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_NAME:=easytier
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.4)
PKG_RELEASE:=6
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.7.0)
PKG_RELEASE:=7
ifeq ($(ARCH),mipsel)
APP_ARCH:=mipsel
+2 -6
View File
@@ -10,7 +10,7 @@ PKG_ARCH_FASTNET:=$(ARCH)
PKG_NAME:=fastnet
PKG_VERSION:=0.7.7
PKG_RELEASE:=5
PKG_RELEASE:=6
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/fastnet-runtime-v$(PKG_VERSION)/
@@ -37,9 +37,6 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
if [ "$$(readlink /usr/share/linkease/apps.d/17-fastnet.json 2>/dev/null)" = "/usr/share/fastnet/fastnet-plugin.json" ]; then
rm -f /usr/share/linkease/apps.d/17-fastnet.json
fi
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/17-fastnet.json 2>/dev/null)" = "/usr/share/fastnet/fastnet-plugin.json" ]; then
rm -f /usr/share/linkeasefull/desktop-apps.d/17-fastnet.json
fi
fi
exit 0
endef
@@ -67,14 +64,13 @@ define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/fastnet $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/fastnet $(1)/usr/share/linkease/apps.d
$(INSTALL_BIN) $(PKG_BUILD_DIR)/FastNet.$(PKG_ARCH_FASTNET) $(1)/usr/sbin/FastNet
$(INSTALL_CONF) ./files/fastnet.config $(1)/etc/config/fastnet
$(INSTALL_BIN) ./files/fastnet.init $(1)/etc/init.d/fastnet
$(INSTALL_BIN) ./files/fastnet.uci-default $(1)/etc/uci-defaults/fastnet
$(INSTALL_DATA) ./files/fastnet-plugin.json $(1)/usr/share/fastnet/fastnet-plugin.json
ln -sf /usr/share/fastnet/fastnet-plugin.json $(1)/usr/share/linkease/apps.d/17-fastnet.json
ln -sf /usr/share/fastnet/fastnet-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/17-fastnet.json
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=frpc-next
PKG_VERSION:=0.71.0
PKG_RELEASE:=1
PKG_VERSION:=0.71.1
PKG_RELEASE:=2
# Architecture mapping for pre-built binaries
ifeq ($(ARCH),mipsel)
+5 -5
View File
@@ -3,17 +3,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=honk
PKG_VERSION:=2026.10.12_pre1
PKG_RELEASE:=18
PKG_VERSION:=2026.10.15_pre1
PKG_RELEASE:=19
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
# because that is what the GitHub release asset name uses.
HONK_RELEASE_TAG:=debug.2026.10.12.native-api.1
HONK_RELEASE_TAG:=debug.2026.10.15.native-api.1
HONK_ASSET_TAG:=debug
# SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh.
HONK_HASH_X86_64:=09c4e52b17de28c8b96a61b7494da4196a87889aa8151bad3a027ac4a7ad9d86
HONK_HASH_AARCH64:=dffa343b6c4b1d914bd5ac5f20727fa949761b827f73c3f95cde3d7f42d71701
HONK_HASH_X86_64:=b492c22fbf9347b741fe8904fbe1c6fb128e1f7b391b60f3925cec4cf14b5085
HONK_HASH_AARCH64:=ce25b0df9bfeb371a5340392df91606a8da6d209f1779d59dd1bd5abafe2c8a4
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILE:=LICENSE
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=linkease-app-entry
# This lightweight runtime is released independently from the full desktop.
PKG_VERSION:=3.0.22
PKG_RELEASE:=3
PKG_VERSION:=3.0.23
PKG_RELEASE:=4
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
+2 -2
View File
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.22
PKG_RELEASE:=46
PKG_SOURCE_DATE:=3.0.23
PKG_RELEASE:=47
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -1 +1 @@
26.284.22350~8125d12
26.287.39313~74c485a
@@ -1 +1 @@
26.285.40306~d12d553
26.287.65283~327d358
+1 -1
View File
@@ -1,2 +1,2 @@
9c9ad92d8e1621d0708c83afc215bb141e7bafb8
327d358bd28096a6702da8dd71b422792331f659
e192c5983e50169ed984d48b094798869a0a780b
+38 -22
View File
@@ -3,19 +3,23 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: de\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"Last-Translator: Pavel Borecki <pavelbo@users.noreply.hosted.weblate.org>\n"
"Language-Team: German <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsadguardhome/de/>\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"PO-Revision-Date: 2026-09-23 19:35+0000\n"
"X-Generator: Weblate 2026.10.dev0\n"
"PO-Revision-Date: 2026-10-13 13:20+0000\n"
"X-Generator: Weblate 2026.11.dev0\n"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
#, fuzzy
msgid ""
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
"kills."
msgstr ""
"Eine weiche Speicherkappe für die Go-Laufzeit, die es dem garbage collector "
"ermöglicht, häufiger zu laufen, da die Nutzung sich der Grenze nähert, um "
"Out-of-Memory (OOM) kills zu verhindern."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
@@ -29,27 +33,32 @@ msgstr "Erweiterte Einstellungen"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
msgid "Configuration file"
msgstr ""
msgstr "Konfiguration fehlgeschlagen"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
msgid ""
"Configuration file must be stored in its own directory, and not in '/etc'."
msgstr ""
"Die Konfigurationsdatei muss in ihrem eigenen Verzeichnis gespeichert werden "
"und nicht in '/etc'."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
msgid "Directory where filters, logs, and statistics are stored."
msgstr ""
"Verzeichnis, in dem Filter, Protokolle und Statistiken gespeichert werden."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
msgid "File System Access"
msgstr ""
msgstr "Zugriff auf das Dateisystem"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
msgid ""
"Files and directories that AdGuard Home should have read-only or read-write "
"access to."
msgstr ""
"Dateien und Verzeichnisse, auf die AdGuard Home nur lesen oder schreiben "
"soll."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
msgid "General Settings"
@@ -59,14 +68,16 @@ msgstr "Allgemeine Einstellungen"
msgid ""
"Go environment variables that tune garbage collector and memory management."
msgstr ""
"Go Umgebungsvariablen, die Garbage Kollektor und Speicher Management "
"verbessern."
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
msgid "Grant permissions for the AdGuard Home LuCI app"
msgstr ""
msgstr "Erlaube Zugriff auf die AdGuard Hime LuCI Applikation"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
msgid "Group the service runs under."
msgstr ""
msgstr "Gruppen Sie die Service Ausführung unter."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
@@ -75,12 +86,13 @@ msgstr ""
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
#, fuzzy
msgid "If empty, defaults to"
msgstr ""
msgstr "Wenn leer, standardmäßig"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
msgid "Modify at your own risk."
msgstr ""
msgstr "Ändern auf eigenes Risiko."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
msgid "Not running"
@@ -88,24 +100,24 @@ msgstr "Angehalten"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
msgid "Parent directory will be owned by the service user."
msgstr ""
msgstr "Das Parent-Verzeichnis gehört dem Service-Benutzer."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
msgid "Path must be absolute."
msgstr ""
msgstr "Pfad muss absolut sein."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
msgid "Path must not end with a slash."
msgstr ""
msgstr "Der Pfad darf nicht mit einem Slash enden."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
msgid "Read-only access"
msgstr ""
msgstr "Nur Lesezugriff"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
msgid "Read-write access"
msgstr ""
msgstr "Lese- und Schreibzugriff"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
msgid "Running"
@@ -117,17 +129,20 @@ msgstr "Dienststatus"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
msgid "Service group"
msgstr ""
msgstr "Service- Gruppe"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
msgid "Service user"
msgstr ""
msgstr "Service-Benutzer"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
#, fuzzy
msgid ""
"The maximum number of operating system threads that can execute user-level "
"Go code simultaneously."
msgstr ""
"Die maximale Anzahl von Betriebssystemausführungssträgen, die Benutzer-Level "
"Go-Code gleichzeitig ausführen können."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
msgid ""
@@ -136,8 +151,9 @@ msgid ""
msgstr ""
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
#, fuzzy
msgid "User the service runs under."
msgstr ""
msgstr "Benutzer unter dem der Dienst läuft."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
msgid "Verbose logging"
@@ -149,20 +165,20 @@ msgstr "Version"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
msgid "Will be owned by the service user."
msgstr ""
msgstr "Wird dem Service-Nutzer gehören."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
msgid "Working directory"
msgstr ""
msgstr "Arbeitsverzeichnis"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
msgid "unset"
msgstr ""
msgstr "nicht gesetzt"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
msgid "unset and 100"
msgstr ""
msgstr "nicht gesetzt und 100"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
msgid "unset and matching the number of CPUs"
msgstr ""
msgstr "ungesetzte und passende Zahl der CPUs"
+40 -25
View File
@@ -3,12 +3,13 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: pt_BR\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"Last-Translator: Janderson Vieira Santos "
"<ritew@users.noreply.hosted.weblate.org>\n"
"Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
"openwrt/luciapplicationsadguardhome/pt_BR/>\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"PO-Revision-Date: 2026-09-29 13:09+0000\n"
"X-Generator: Weblate 2026.10.dev0\n"
"PO-Revision-Date: 2026-10-13 13:20+0000\n"
"X-Generator: Weblate 2026.11.dev0\n"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
msgid ""
@@ -16,6 +17,9 @@ msgid ""
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
"kills."
msgstr ""
"Um limite flexível de memória para o runtime do Go, permitindo que o coletor "
"de lixo seja executado com mais frequência à medida que o uso se aproxima do "
"limite para evitar finalizações por falta de memória (OOM)."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
@@ -29,27 +33,31 @@ msgstr "Configurações avançadas"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
msgid "Configuration file"
msgstr ""
msgstr "Arquivo de configuração"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
msgid ""
"Configuration file must be stored in its own directory, and not in '/etc'."
msgstr ""
"O arquivo de configuração deve ser armazenado em seu próprio diretório, e "
"não em '/etc'."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
msgid "Directory where filters, logs, and statistics are stored."
msgstr ""
msgstr "Diretório onde filtros, registros e estatísticas são armazenados."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
msgid "File System Access"
msgstr ""
msgstr "Acesso ao sistema de arquivos"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
msgid ""
"Files and directories that AdGuard Home should have read-only or read-write "
"access to."
msgstr ""
"Arquivos e diretórios aos quais o AdGuard Home deve ter acesso somente "
"leitura ou de leitura e escrita."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
msgid "General Settings"
@@ -59,14 +67,16 @@ msgstr "Configurações gerais"
msgid ""
"Go environment variables that tune garbage collector and memory management."
msgstr ""
"Variáveis de ambiente do Go que ajustam o coletor de lixo e o gerenciamento "
"de memória."
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
msgid "Grant permissions for the AdGuard Home LuCI app"
msgstr ""
msgstr "Conceder permissões para o aplicativo LuCI do AdGuard Home"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
msgid "Group the service runs under."
msgstr ""
msgstr "Grupo sob o qual o serviço é executado."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
@@ -76,36 +86,36 @@ msgstr ""
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
msgid "If empty, defaults to"
msgstr ""
msgstr "Se vazio, o padrão é"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
msgid "Modify at your own risk."
msgstr ""
msgstr "Modifique por sua conta e risco."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
msgid "Not running"
msgstr ""
msgstr "Não está em execução"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
msgid "Parent directory will be owned by the service user."
msgstr ""
msgstr "O diretório pai pertencerá ao usuário do serviço."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
msgid "Path must be absolute."
msgstr ""
msgstr "O caminho deve ser absoluto."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
msgid "Path must not end with a slash."
msgstr ""
msgstr "O caminho não deve terminar com uma barra."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
msgid "Read-only access"
msgstr ""
msgstr "Acesso somente leitura"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
msgid "Read-write access"
msgstr ""
msgstr "Acesso de leitura e escrita"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
msgid "Running"
@@ -117,31 +127,36 @@ msgstr "Condição do Serviço"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
msgid "Service group"
msgstr ""
msgstr "Grupo do serviço"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
msgid "Service user"
msgstr ""
msgstr "Usuário do serviço"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
msgid ""
"The maximum number of operating system threads that can execute user-level "
"Go code simultaneously."
msgstr ""
"O número máximo de threads do sistema operacional que podem executar código "
"Go no nível de usuário simultaneamente."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
msgid ""
"Tunes the garbage collector's aggressiveness by setting the percentage of "
"heap growth allowed before the next collection cycle triggers."
msgstr ""
"Ajusta a agressividade do coletor de lixo definindo a porcentagem de "
"crescimento da heap permitida antes do acionamento do próximo ciclo de "
"coleta."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
msgid "User the service runs under."
msgstr ""
msgstr "Usuário sob o qual o serviço é executado."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
msgid "Verbose logging"
msgstr ""
msgstr "Registro detalhado"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
msgid "Version"
@@ -149,20 +164,20 @@ msgstr "Versão"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
msgid "Will be owned by the service user."
msgstr ""
msgstr "Pertencerá ao usuário do serviço."
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
msgid "Working directory"
msgstr ""
msgstr "Diretório de trabalho"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
msgid "unset"
msgstr ""
msgstr "não definido"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
msgid "unset and 100"
msgstr ""
msgstr "não definido e 100"
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
msgid "unset and matching the number of CPUs"
msgstr ""
msgstr "não definido e correspondente ao número de CPUs"
+1 -1
View File
@@ -3,7 +3,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.1
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI Airoha SoC Status (NPU, CPU, Frame Engine)
@@ -6,29 +6,34 @@
get_status() {
# === NPU Status ===
# Read the actually-loaded NPU firmware path from DTS (firmware-name property).
local npu_fw=""
local npu_dt="/proc/device-tree/soc/npu@1e900000/firmware-name"
if [ -f "$npu_dt" ]; then
# firmware-name is a string list (null-separated). First entry is the rv32 code.
local fw_rel=$(tr '\0' '\n' < "$npu_dt" 2>/dev/null | grep -m1 "rv32")
[ -n "$fw_rel" ] && npu_fw="/lib/firmware/$fw_rel"
fi
local npu_ver="Unknown"
if [ -n "$npu_fw" ] && [ -f "$npu_fw" ]; then
npu_ver=$(strings "$npu_fw" 2>/dev/null | grep -oE '([0-9]+\.[0-9]+\.[0-9]+-)?TLB[0-9.]+[-_v0-9]*' | head -1)
[ -z "$npu_ver" ] && npu_ver="Unknown"
fi
local npu_loaded="false"
local npu_device=""
local npu_driver_path="/sys/bus/platform/drivers/airoha-npu"
if [ -d "$npu_driver_path" ]; then
npu_device=$(ls -1 "$npu_driver_path" 2>/dev/null | grep '\.npu$' | head -1)
if [ -n "$npu_device" ]; then
npu_loaded="true"
[ -n "$npu_device" ] && npu_loaded="true"
fi
local npu_ver="" npu_fw="" fw_rel="" compatible=""
local npu_dt="$npu_driver_path/$npu_device/of_node"
if [ "$npu_loaded" = "true" ]; then
# Prefer the version reported by the running firmware.
npu_ver=$(dmesg 2>/dev/null | sed -n 's/.*airoha-npu .*NPU fw version: \([0-9][0-9]*\.[0-9][0-9]*\) *$/\1/p' | tail -1)
if [ -f "$npu_dt/firmware-name" ]; then
fw_rel=$(tr '\0' '\n' < "$npu_dt/firmware-name" | grep -m1 "rv32")
else
# Match the driver's defaults when DTS does not override them.
compatible=$(tr '\0' '\n' < "$npu_dt/compatible" 2>/dev/null)
case "$compatible" in
*airoha,en7581-npu*) fw_rel="airoha/en7581_npu_rv32.bin" ;;
*airoha,an7583-npu*) fw_rel="airoha/an7583_npu_rv32.bin" ;;
esac
fi
[ -n "$fw_rel" ] && npu_fw="/lib/firmware/$fw_rel"
# Boot messages may have rolled out of the kernel log buffer.
if [ -z "$npu_ver" ] && [ -n "$npu_fw" ] && [ -f "$npu_fw" ]; then
npu_ver=$(strings "$npu_fw" 2>/dev/null | grep -oE '([0-9]+\.[0-9]+\.[0-9]+-)?TLB[0-9.]+[-_v0-9]*' | head -1)
fi
fi
+2 -2
View File
@@ -22,11 +22,11 @@ PKG_NAME:=luci-app-amlogic
# DD = day (01 = 1st)
# NN = daily release sequence (01 = first release of the day, 02 = second, ...)
# Example below: 26.10.0101 = October 1, 2026, 1st release of the day
PKG_VERSION:=26.10.0301
PKG_VERSION:=26.10.1501
# PKG_RELEASE marks the LuCI variant; onekey-install.sh relies on it (do NOT bump)
# 1 = Lua version (lua branch) → tag: 26.10.0101
# 2 = JS version (main branch) → tag: 26.10.0101-js
PKG_RELEASE:=20
PKG_RELEASE:=21
PKG_LICENSE:=GPL-2.0 License
PKG_MAINTAINER:=ophub <https://github.com/ophub/luci-app-amlogic>
@@ -181,25 +181,40 @@ check_updated() {
https://github.com/${server_firmware_url}/releases/expanded_assets/${firmware_releases_tag}
)"
# Set the regular expression for the OpenWrt filename
op_file_pattern=".*_${BOARD}_.*k${main_line_version}\.[0-9]+.*${firmware_suffix}"
# Find the <li> list item where the OpenWrt file is located
li_block=$(awk -v pattern="${op_file_pattern}" -v RS='</li>' '$0 ~ pattern { print $0 "</li>"; exit }' <<<"${html_code}")
[[ -z "${li_block}" ]] && tolog "02.03.01 No matching download links found." "1"
# Find the <li> list items where the OpenWrt file is located.
asset_records="$(awk -v board="${BOARD}" -v kver="${main_line_version}" -v suffix="${firmware_suffix}" -v RS='</li>' '
BEGIN {
href_pattern = "/releases/download/[^\"]*_" board "_[^\"]*k" kver "\\.[0-9]+[^\"]*" suffix
block_pattern = ".*_" board "_.*k" kver "\\.[0-9]+.*" suffix
}
$0 ~ block_pattern {
file_name = ""; file_date = ""; file_sha256 = ""
if (match($0, href_pattern)) {
file_path = substr($0, RSTART, RLENGTH)
file_name = file_path; sub(/.*\//, "", file_name)
}
if (match($0, /datetime="[^"]*"/)) { file_date = substr($0, RSTART + 10, RLENGTH - 11) }
if (match($0, /value="sha256:[^"]*/)) { file_sha256 = substr($0, RSTART + 14, RLENGTH - 14) }
if (file_name != "") { printf "%s\t%s\t%s\n", file_date, file_name, file_sha256 }
}' <<<"${html_code}")"
[[ -z "${asset_records}" ]] && tolog "02.03.01 No matching download links found." "1"
# Select the OpenWrt file with the newest upload date.
latest_record="$(echo "${asset_records}" | sort -t$'\t' -k1,1 | tail -n 1)"
# Find the OpenWrt filename
latest_url=$(echo "${li_block}" | grep -oE "/[^\"]*_${BOARD}_.*k${main_line_version}\.[0-9]+[^\"]*${firmware_suffix}" | sort -urV | head -n 1 | xargs basename 2>/dev/null)
latest_url="$(echo "${latest_record}" | cut -f2)"
tolog "02.03.02 OpenWrt file: ${latest_url}"
# Find the date of the latest update
latest_updated_at=$(echo "${li_block}" | grep -o 'datetime="[^"]*"' | sed 's/datetime="//; s/"//')
latest_updated_at="$(echo "${latest_record}" | cut -f1)"
tolog "02.03.03 Latest updated at: ${latest_updated_at}"
# Format the date for display
date_display_format="$(echo ${latest_updated_at} | tr 'T' '(' | tr 'Z' ')')"
[[ -z "${latest_url}" || -z "${latest_updated_at}" ]] && tolog "02.03.04 The download URL or date is invalid." "1"
# Find the firmware sha256 value
latest_firmware_sha256="$(echo "${li_block}" | grep -o 'value="sha256:[^"]*' | sed 's/value="sha256://')"
latest_firmware_sha256="$(echo "${latest_record}" | cut -f3)"
tolog "02.03.05 OpenWrt sha256: ${latest_firmware_sha256}"
# Check the firmware update code
+5 -2
View File
@@ -8,10 +8,13 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for Docker Manager
LUCI_DEPENDS:=+dockermanager +luci-lib-linkeaseauth +linkease-app-entry
# apps_compat/apps_openwrt are absent from the legacy 1.0.3 auth package.
LUCI_EXTRA_DEPENDS:=luci-lib-linkeaseauth (>=1.2.1)
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.1
PKG_RELEASE:=3
PKG_VERSION:=0.1.2-r2
# Keep the revision in PKG_VERSION for LuCI builders that clear PKG_RELEASE.
PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk
+4 -2
View File
@@ -7,8 +7,10 @@
include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.4)
PKG_RELEASE:=14
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.7.0)
PKG_RELEASE:=15
PKG_PO_VERSION:=$(PKG_VERSION)
LUCI_TITLE:=LuCI support for EasyTier
LUCI_DEPENDS:=+kmod-tun +easytier +luci-compat
+129 -53
View File
@@ -163,7 +163,7 @@ function get_upload_config()
easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core",
webbin = uci:get_first("easytier", "easytier", "webbin") or "/usr/bin/easytier-web",
github_proxys = {},
fallback_version = uci:get_first("easytier", "easytier", "fallback_version") or "v2.6.4"
fallback_version = uci:get_first("easytier", "easytier", "fallback_version") or "v2.7.0"
}
-- 读取代理列表(list 类型)
@@ -200,7 +200,7 @@ function save_upload_config()
-- 保存配置
uci:set("easytier", "@easytier[0]", "easytierbin", data.easytierbin or "/usr/bin/easytier-core")
uci:set("easytier", "@easytier[0]", "webbin", data.webbin or "/usr/bin/easytier-web")
uci:set("easytier", "@easytier[0]", "fallback_version", data.fallback_version or "v2.6.4")
uci:set("easytier", "@easytier[0]", "fallback_version", data.fallback_version or "v2.7.0")
-- 删除旧的代理列表
uci:delete("easytier", "@easytier[0]", "github_proxys")
@@ -595,69 +595,130 @@ function clear_version_cache()
luci.http.write("OK")
end
-- 连接信息各分区与 easytier-cli 子命令的对应关系。
-- 实测每调用一次 easytier-cli 都要新起一个进程,单次进程启动开销约 200ms(路由器上更慢),
-- RPC 往返本身只要几毫秒,因此耗时几乎完全取决于进程个数,改为按分区按需加载。
local CONNINFO_CLI_PARTS = {
node = "node",
peer = "peer",
route = "route",
connector = "connector",
stun = "stun",
peer_center = "peer-center",
vpn_portal = "vpn-portal",
proxy = "proxy",
acl = "acl stats",
mapped_listener = "mapped-listener",
stats = "stats",
}
-- cmdline 只读本地 /proc,不需要 easytier-cli
local CONNINFO_LOCAL_PARTS = { cmdline = true }
-- 分区结果的短时缓存,避免轮询与手动刷新、来回切换分区时重复起进程
local CONNINFO_CACHE_DIR = "/tmp/easytier_conninfo"
local CONNINFO_CACHE_TTL = 3
local function read_conninfo_cache(name)
local handle = io.open(CONNINFO_CACHE_DIR .. "/" .. name, "r")
if not handle then
return nil
end
local content = handle:read("*all") or ""
handle:close()
local ts, data = content:match("^(%d+)\n([%s%S]*)$")
if not ts or (os.time() - tonumber(ts)) > CONNINFO_CACHE_TTL then
return nil
end
return data
end
local function write_conninfo_cache(name, data)
luci.sys.call("mkdir -p " .. CONNINFO_CACHE_DIR)
local handle = io.open(CONNINFO_CACHE_DIR .. "/" .. name, "w")
if handle then
handle:write(tostring(os.time()) .. "\n" .. data)
handle:close()
end
end
function act_conninfo()
local e = {}
local uci = require "luci.model.uci".cursor()
local easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
local clibin = easytierbin:gsub("easytier%-core$", "easytier-cli")
local process_status = luci.sys.exec("pgrep easytier-core")
if process_status ~= "" then
-- 获取各类CLI信息
local function get_cli_output(cmd)
local handle = io.popen(clibin .. " " .. cmd .. " 2>&1")
if handle then
local result = handle:read("*all")
handle:close()
return result or ""
-- 解析要加载的分区,前端只请求当前显示的分区;
-- 不传 parts 时加载除 stun 以外的所有分区(stun 会实时访问 STUN 服务器,单次实测约 3 秒)
local parts = luci.http.formvalue("parts")
local want_cli, want_local = {}, {}
if parts and parts ~= "" then
for name in parts:gmatch("[%w_]+") do
if CONNINFO_CLI_PARTS[name] then
want_cli[name] = true
elseif CONNINFO_LOCAL_PARTS[name] then
want_local[name] = true
end
return ""
end
e.node = get_cli_output("node")
e.peer = get_cli_output("peer")
e.connector = get_cli_output("connector")
e.stun = get_cli_output("stun")
e.route = get_cli_output("route")
e.peer_center = get_cli_output("peer-center")
e.vpn_portal = get_cli_output("vpn-portal")
e.proxy = get_cli_output("proxy")
e.acl = get_cli_output("acl stats")
e.mapped_listener = get_cli_output("mapped-listener")
e.stats = get_cli_output("stats")
-- 获取启动参数
local cmdhandle = io.popen("cat /proc/$(pidof easytier-core)/cmdline 2>/dev/null | tr '\\0' ' '")
if cmdhandle then
e.cmdline = cmdhandle:read("*all") or ""
cmdhandle:close()
else
e.cmdline = ""
else
for name in pairs(CONNINFO_CLI_PARTS) do
if name ~= "stun" then
want_cli[name] = true
end
end
-- 检查是否使用配置文件启动
if e.cmdline:match("%-%-config%-file") or e.cmdline:match("%-c%s+/") then
e.config_file = safe_read_file("/etc/easytier/config.toml") or ""
else
e.config_file = ""
for name in pairs(CONNINFO_LOCAL_PARTS) do
want_local[name] = true
end
end
local process_status = luci.sys.exec("pgrep easytier-core")
if process_status ~= "" then
-- 只运行被请求的分区,命中缓存的分区直接复用结果
for name in pairs(want_cli) do
local cached = read_conninfo_cache(name)
if cached then
e[name] = cached
else
local handle = io.popen(clibin .. " " .. CONNINFO_CLI_PARTS[name] .. " 2>&1")
local result = ""
if handle then
result = handle:read("*all") or ""
handle:close()
end
e[name] = result
write_conninfo_cache(name, result)
end
end
if want_local.cmdline then
-- 获取启动参数
local cmdhandle = io.popen("cat /proc/$(pidof easytier-core)/cmdline 2>/dev/null | tr '\\0' ' '")
if cmdhandle then
e.cmdline = cmdhandle:read("*all") or ""
cmdhandle:close()
else
e.cmdline = ""
end
-- 检查是否使用配置文件启动,配置内容随本次响应一起返回,前端无需再请求一次
if e.cmdline:match("%-%-config%-file") or e.cmdline:match("%-c%s+/") then
e.config_file = safe_read_file("/etc/easytier/config.toml") or ""
else
e.config_file = ""
end
end
else
local errMsg = i18n.translate("Error: Program not running! Please start the program and refresh")
e.node = errMsg
e.peer = errMsg
e.connector = errMsg
e.stun = errMsg
e.route = errMsg
e.peer_center = errMsg
e.vpn_portal = errMsg
e.proxy = errMsg
e.acl = errMsg
e.mapped_listener = errMsg
e.stats = errMsg
e.cmdline = errMsg
for name in pairs(want_cli) do
e[name] = errMsg
end
if want_local.cmdline then
e.cmdline = errMsg
end
-- 未运行时清掉缓存,避免下次启动后读到旧的错误输出
luci.sys.call("rm -rf " .. CONNINFO_CACHE_DIR)
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
@@ -682,6 +743,11 @@ function get_web_config()
web_geoip_db = uci:get_first("easytier", "easytier", "web_geoip_db") or "",
web_disable_registration = uci:get_first("easytier", "easytier", "web_disable_registration") or "0",
web_allow_auto_create_user = uci:get_first("easytier", "easytier", "web_allow_auto_create_user") or "0",
-- EasyTier 2.7.0 新增的 web 参数
web_heartbeat_min_response_ms = uci:get_first("easytier", "easytier", "web_heartbeat_min_response_ms") or "",
web_heartbeat_timeout_ms = uci:get_first("easytier", "easytier", "web_heartbeat_timeout_ms") or "",
web_gateway_peer_url = uci:get_first("easytier", "easytier", "web_gateway_peer_url") or "",
web_gateway_relay_data = uci:get_first("easytier", "easytier", "web_gateway_relay_data") or "0",
web_weblog = uci:get_first("easytier", "easytier", "web_weblog") or "off",
-- OIDC配置
web_oidc_enabled = uci:get_first("easytier", "easytier", "web_oidc_enabled") or "0",
@@ -723,6 +789,11 @@ function save_web_config()
local web_geoip_db = http.formvalue("web_geoip_db") or ""
local web_disable_registration = http.formvalue("web_disable_registration") or "0"
local web_allow_auto_create_user = http.formvalue("web_allow_auto_create_user") or "0"
-- EasyTier 2.7.0 新增的 web 参数
local web_heartbeat_min_response_ms = http.formvalue("web_heartbeat_min_response_ms") or ""
local web_heartbeat_timeout_ms = http.formvalue("web_heartbeat_timeout_ms") or ""
local web_gateway_peer_url = http.formvalue("web_gateway_peer_url") or ""
local web_gateway_relay_data = http.formvalue("web_gateway_relay_data") or "0"
local web_weblog = http.formvalue("web_weblog") or "off"
-- OIDC
local web_oidc_enabled = http.formvalue("web_oidc_enabled") or "0"
@@ -756,6 +827,11 @@ function save_web_config()
uci:set("easytier", "@easytier[0]", "web_geoip_db", web_geoip_db)
uci:set("easytier", "@easytier[0]", "web_disable_registration", web_disable_registration)
uci:set("easytier", "@easytier[0]", "web_allow_auto_create_user", web_allow_auto_create_user)
-- EasyTier 2.7.0 新增的 web 参数
uci:set("easytier", "@easytier[0]", "web_heartbeat_min_response_ms", web_heartbeat_min_response_ms)
uci:set("easytier", "@easytier[0]", "web_heartbeat_timeout_ms", web_heartbeat_timeout_ms)
uci:set("easytier", "@easytier[0]", "web_gateway_peer_url", web_gateway_peer_url)
uci:set("easytier", "@easytier[0]", "web_gateway_relay_data", web_gateway_relay_data)
uci:set("easytier", "@easytier[0]", "web_weblog", web_weblog)
-- OIDC
uci:set("easytier", "@easytier[0]", "web_oidc_enabled", web_oidc_enabled)
+100 -8
View File
@@ -45,10 +45,11 @@ end
web_config = s:taboption("general", Value, "web_config", translate("Web Server Address"),
translate("Web configuration server address. (-w parameter)<br>"
.. "For a self-hosted Web server, use the format: udp://server_address:22020/username<br>"
.. "For the official Web server, use the format: username<br>"
.. "Since EasyTier 2.7.0 a full URL is required, the token-only shorthand is no longer supported<br>"
.. "Self-hosted Web server, use the format: udp://server_address:22020/username<br>"
.. "Official Web server, use the format: udp://config-server.easytier.cn:22020/username<br>"
.. "Official Web Console: <a href='https://easytier.cn/web'>easytier.cn/web</a>"))
web_config.placeholder = "admin"
web_config.placeholder = "udp://config-server.easytier.cn:22020/my-token"
web_config:depends("etcmd", "web")
network_name = s:taboption("general", Value, "network_name", translate("Network Name"),
@@ -199,13 +200,53 @@ instance_name = s:taboption("privacy", Value, "instance_name", translate("Instan
instance_name.placeholder = "default"
instance_name:depends("etcmd", "etcmd")
vpn_portal = s:taboption("privacy", Value, "vpn_portal", translate("VPN Portal URL"),
translate("Defines the URL of the VPN portal, allowing other VPN clients to connect.<br>"
.. "Example: wg://0.0.0.0:11011/10.14.14.0/24 means the VPN portal is a WireGuard server listening on vpn."
.. "example.com:11010, and the VPN clients are in the 10.14.14.0/24 network (--vpn-portal parameter)"))
vpn_portal.placeholder = "wg://0.0.0.0:11011/10.14.14.0/24"
vpn_portal = s:taboption("privacy", Value, "vpn_portal", translate("VPN Portal Listen Address"),
translate("WireGuard VPN portal listen address, allowing other VPN clients to connect.<br>"
.. "Since EasyTier 2.7.0 only wg://HOST:PORT is accepted; the client network can no longer be "
.. "appended to the URL, please add each client in \"VPN Portal Clients\" below "
.. "(--vpn-portal parameter)"))
vpn_portal.placeholder = "wg://0.0.0.0:11011"
vpn_portal.validate = function(self, value, section)
value = value and tostring(value):match("^%s*(.-)%s*$") or ""
if value == "" then return value end
-- EasyTier 2.7.0 起不再允许把客户端网段写在 URL 里,这里提前拦截,避免核心启动失败
if not (value:match("^wg://%d+%.%d+%.%d+%.%d+:%d+$") or value:match("^wg://%[[%x:]+%]:%d+$")) then
return nil, translate("Only wg://HOST:PORT is accepted since EasyTier 2.7.0, please fill the client network into \"VPN Portal Clients\"")
end
return value
end
vpn_portal:depends("etcmd", "etcmd")
vpn_portal_clients = s:taboption("privacy", DynamicList, "vpn_portal_clients", translate("VPN Portal Clients"),
translate("One WireGuard client per entry in the form NAME=CIDR, for example client1=10.14.14.1/24.<br>"
.. "NAME may contain letters, digits and dashes only; the address must be a usable host address "
.. "inside the client network. (--vpn-portal-client parameter)"))
vpn_portal_clients.placeholder = "client1=10.14.14.1/24"
vpn_portal_clients.validate = function(self, value, section)
if type(value) == "table" then
local i
for i = 1, #value do
local item = value[i] and tostring(value[i]):match("^%s*(.-)%s*$") or ""
if item ~= "" then
-- 与 easytier-core 的校验保持一致:名称只能是字母数字与短横线,地址必须是 主机/前缀
if not item:match("^[%w][%w%-]*=[%d%.]+/%d+$") then
return nil, translate("Invalid client, expected NAME=CIDR, for example client1=10.14.14.1/24")
end
end
end
end
return value
end
vpn_portal_clients:depends("etcmd", "etcmd")
vpn_portal_private_key = s:taboption("privacy", Value, "vpn_portal_private_key", translate("VPN Portal Private Key"),
translate("Base64 encoded 32-byte WireGuard private key of the portal.<br>"
.. "Required since EasyTier 2.7.0, the portal will not start without it. "
.. "It is handed to easytier-core through an environment variable so that it never shows up in the process list. "
.. "(--vpn-portal-private-key parameter)"))
vpn_portal_private_key.password = true
vpn_portal_private_key:depends("etcmd", "etcmd")
mtu = s:taboption("privacy", Value, "mtu", translate("MTU"),
translate("MTU for the TUN device, default is 1380 when unencrypted, and 1360 when encrypted"))
mtu.datatype = "range(1,1500)"
@@ -314,6 +355,23 @@ socks_port.datatype = "range(1,65535)"
socks_port.placeholder = "1080"
socks_port:depends("etcmd", "etcmd")
disable_upnp = s:taboption("privacy", Flag, "disable_upnp", translate("Disable UPnP"),
translate("EasyTier 2.7.0 tries to map listener ports through UPnP/NAT-PMP by default. "
.. "Enable this to turn the automatic port mapping off. (--disable-upnp parameter)"))
disable_upnp:depends("etcmd", "etcmd")
stun_servers = s:taboption("privacy", DynamicList, "stun_servers", translate("STUN Servers"),
translate("Custom UDP STUN servers used for NAT type detection and hole punching. "
.. "Leave empty to use the built-in defaults. (--stun-servers parameter)"))
stun_servers.placeholder = "stun.miwifi.com:3478"
stun_servers:depends("etcmd", "etcmd")
tcp_stun_servers = s:taboption("privacy", DynamicList, "tcp_stun_servers", translate("STUN Servers (TCP)"),
translate("Custom TCP STUN servers used for NAT type detection and hole punching. "
.. "Leave empty to use the built-in defaults. (--tcp-stun-servers parameter)"))
tcp_stun_servers.placeholder = "stun.miwifi.com:3478"
tcp_stun_servers:depends("etcmd", "etcmd")
disable_p2p = s:taboption("privacy", Flag, "disable_p2p", translate("Disable P2P"),
translate("Disable P2P communication; only use nodes specified by -p to forward packets (--disable-p2p parameter)"))
disable_p2p:depends("etcmd", "etcmd")
@@ -322,6 +380,15 @@ p2p_only = s:taboption("privacy", Flag, "p2p_only", translate("P2P only"),
translate("only communicate with peers that already establish p2p connection. (--p2p-only parameter)"))
p2p_only:depends("etcmd", "etcmd")
lazy_p2p = s:taboption("privacy", Flag, "lazy_p2p", translate("Lazy P2P"),
translate("Only try to establish P2P connections when traffic actually needs to be forwarded. "
.. "(--lazy-p2p parameter)"))
lazy_p2p:depends("etcmd", "etcmd")
need_p2p = s:taboption("privacy", Flag, "need_p2p", translate("Need P2P"),
translate("Require a P2P connection before forwarding traffic through relays. (--need-p2p parameter)"))
need_p2p:depends("etcmd", "etcmd")
disable_udp = s:taboption("privacy", Flag, "disable_udp", translate("Disable UDP"),
translate("Disable UDP hole punching (--disable-udp-hole-punching parameter)"))
disable_udp:depends("etcmd", "etcmd")
@@ -373,6 +440,14 @@ relay_kcp = s:taboption("privacy", Flag, "relay_kcp", translate("Relay foreign n
translate("If true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets). (--enable-relay-foreign-network-kcp parameter)"))
relay_kcp:depends("etcmd", "etcmd")
disable_relay_quic = s:taboption("privacy", Flag, "disable_relay_quic", translate("Disable relay quic"),
translate("If true, disable relaying quic packets, avoiding consuming too much bandwidth. (--disable-relay-quic parameter)"))
disable_relay_quic:depends("etcmd", "etcmd")
relay_quic = s:taboption("privacy", Flag, "relay_quic", translate("Relay foreign network quic"),
translate("If true, allow relaying quic packets from foreign networks. (--enable-relay-foreign-network-quic parameter)"))
relay_quic:depends("etcmd", "etcmd")
quic_proxy = s:taboption("privacy", Flag, "quic_proxy", translate("Enable QUIC Proxy"),
translate("Proxy tcp streams with QUIC, improving the latency and throughput on the network with udp packet loss.<br>"
.. "All nodes in the virtual network must be using EasyTier version v2.3.2 or higher for this feature. "
@@ -396,6 +471,12 @@ accept_dns = s:taboption("privacy", Flag, "accept_dns", translate("Enable Magic
.. "Magic DNS will modify your system DNS settings, please enable with caution. (--accept-dns parameter)"))
accept_dns:depends("etcmd", "etcmd")
tld_dns_zone = s:taboption("privacy", Value, "tld_dns_zone", translate("Magic DNS Zone"),
translate("Top level DNS zone used by Magic DNS, default is et.net. Only takes effect when Magic DNS is enabled. "
.. "(--tld-dns-zone parameter)"))
tld_dns_zone.placeholder = "et.net."
tld_dns_zone:depends("accept_dns", "1")
private_mode = s:taboption("privacy", Flag, "private_mode", translate("Enable Private Mode"),
translate("When enabled, nodes with a different network name and password are not allowed to handshake or "
.. "relay via this node. (--private-mode parameter)"))
@@ -406,6 +487,17 @@ foreign_relay_bps_limit = s:taboption("privacy", Value, "foreign_relay_bps_limit
.. "(--foreign-relay-bps-limit parameter)"))
foreign_relay_bps_limit:depends("etcmd", "etcmd")
instance_recv_bps_limit = s:taboption("privacy", Value, "instance_recv_bps_limit", translate("Instance Receive Rate Limit"),
translate("the maximum inbound bps limit for this network instance, default is no limit. "
.. "unit: BPS (bytes per second). (--instance-recv-bps-limit parameter)"))
instance_recv_bps_limit:depends("etcmd", "etcmd")
socket_mark = s:taboption("privacy", Value, "socket_mark", translate("Socket Mark"),
translate("Set SO_MARK on all underlay sockets, Linux only and requires CAP_NET_ADMIN. "
.. "Usually used together with policy routing. (--socket-mark parameter)"))
socket_mark.datatype = "range(0,4294967295)"
socket_mark:depends("etcmd", "etcmd")
extra_args = s:taboption("privacy", DynamicList, "extra_args", translate("Extra Parameters"),
translate("Additional command-line arguments passed to the backend process"))
extra_args.placeholder = "--tcp-whitelist=80"
@@ -900,18 +900,133 @@ var translations = {
// Stats指标名称翻译
"traffic_control_bytes_rx": "<%:Traffic Control Bytes RX%>",
"traffic_control_bytes_tx": "<%:Traffic Control Bytes TX%>",
"traffic_control_bytes_forwarded": "<%:Traffic Control Bytes Forwarded%>",
"traffic_control_bytes_rx_by_instance": "<%:Traffic Control Bytes RX By Instance%>",
"traffic_control_bytes_tx_by_instance": "<%:Traffic Control Bytes TX By Instance%>",
"traffic_control_packets_rx": "<%:Traffic Control Packets RX%>",
"traffic_control_packets_tx": "<%:Traffic Control Packets TX%>",
"traffic_control_packets_forwarded": "<%:Traffic Control Packets Forwarded%>",
"traffic_control_packets_rx_by_instance": "<%:Traffic Control Packets RX By Instance%>",
"traffic_control_packets_tx_by_instance": "<%:Traffic Control Packets TX By Instance%>",
"traffic_bytes_rx_by_instance": "<%:Traffic Bytes RX By Instance%>",
"traffic_bytes_tx_by_instance": "<%:Traffic Bytes TX By Instance%>",
"traffic_packets_rx_by_instance": "<%:Traffic Packets RX By Instance%>",
"traffic_packets_tx_by_instance": "<%:Traffic Packets TX By Instance%>",
"traffic_bytes_foreign_forward_rx": "<%:Traffic Bytes Foreign Forward RX%>",
"traffic_bytes_foreign_forward_tx": "<%:Traffic Bytes Foreign Forward TX%>",
"traffic_bytes_foreign_forward_forwarded": "<%:Traffic Bytes Foreign Forward Forwarded%>",
"traffic_packets_foreign_forward_rx": "<%:Traffic Packets Foreign Forward RX%>",
"traffic_packets_foreign_forward_tx": "<%:Traffic Packets Foreign Forward TX%>",
"traffic_packets_foreign_forward_forwarded": "<%:Traffic Packets Foreign Forward Forwarded%>",
"peer_rpc_errors": "<%:Peer RPC Errors%>",
"tcp_proxy_connect": "<%:TCP Proxy Connect%>",
"udp_broadcast_relay_packets_captured": "<%:UDP Broadcast Relay Packets Captured%>",
"udp_broadcast_relay_packets_forwarded": "<%:UDP Broadcast Relay Packets Forwarded%>",
"udp_broadcast_relay_packets_forward_failed": "<%:UDP Broadcast Relay Packets Forward Failed%>",
"udp_broadcast_relay_packets_ignored": "<%:UDP Broadcast Relay Packets Ignored%>",
"vpn_portal_client": "<%:VPN Portal Client%>",
"vpn_portal_client_bytes_rx": "<%:VPN Portal Client Bytes RX%>",
"vpn_portal_client_bytes_tx": "<%:VPN Portal Client Bytes TX%>",
"vpn_portal_client_packets_rx": "<%:VPN Portal Client Packets RX%>",
"vpn_portal_client_packets_tx": "<%:VPN Portal Client Packets TX%>",
// Stats 标签(Labels 列)字段翻译
"compression_algo": "<%:Compression Algorithm%>",
"direction": "<%:Direction%>",
"dst_ip": "<%:Destination IP%>",
"mapped_dst_ip": "<%:Mapped Destination IP%>",
"error_type": "<%:Error Type%>",
"protocol": "<%:Protocol%>",
"from_instance_id": "<%:From Instance ID%>",
"to_instance_id": "<%:To Instance ID%>"
};
var currentTab = 'node';
var isRefreshing = false;
// 连接信息分区表:id 为 TAB 元素后缀,key 为后端 api_conninfo?parts= 使用的分区名
var connInfoSections = [
{id: 'node', key: 'node'},
{id: 'peer', key: 'peer'},
{id: 'route', key: 'route'},
{id: 'connector', key: 'connector'},
{id: 'stun', key: 'stun'},
{id: 'peercenter', key: 'peer_center'},
{id: 'vpnportal', key: 'vpn_portal'},
{id: 'proxy', key: 'proxy'},
{id: 'acl', key: 'acl'},
{id: 'mappedlistener', key: 'mapped_listener'},
{id: 'stats', key: 'stats'},
{id: 'cmdline', key: 'cmdline'}
];
// 已经成功加载过的分区,切换 TAB 时命中就不再请求
var connInfoLoaded = {};
// 待请求的分区队列:同一时刻只允许一个请求在跑,避免一次性起一堆 easytier-cli 进程
var connInfoQueue = [];
var connInfoBusy = false;
// 最近一次响应中的配置文件内容,供"命令行"分区直接使用
var lastConfigFile = null;
// ACL 统计是 core 直接输出的英文结构体文本(core 侧没有多语言),
// 字段名单独放在这里精确匹配,避免 name / bytes 这类通用词影响其它卡片
var aclFieldTranslations = {
"name": "<%:Name%>",
"prio": "<%:Priority%>",
"action": "<%:Action%>",
"enabled": "<%:Enabled%>",
"proto": "<%:Protocol%>",
"ports": "<%:Ports%>",
"src_ports": "<%:Source Ports%>",
"src_ips": "<%:Source IPs%>",
"dst_ips": "<%:Destination IPs%>",
"stateful": "<%:Stateful%>",
"rate": "<%:Rate Limit%>",
"burst": "<%:Burst Limit%>",
"pkts": "<%:Packets%>",
"bytes": "<%:Bytes%>",
"src": "<%:Source Address%>",
"dst": "<%:Destination Address%>",
"state": "<%:State%>",
"created": "<%:Created%>",
"last_seen": "<%:Last Seen%>"
};
// ACL 规则里的枚举取值
var aclValueTranslations = {
"Allow": "<%:Allow%>",
"Drop": "<%:Drop (no response)%>",
"Noop": "<%:Noop%>",
"Any": "<%:Any%>",
"Unspecified": "<%:Unspecified%>",
"New": "<%:New%>",
"Established": "<%:Established%>",
"Related": "<%:Related%>",
"Invalid": "<%:Invalid%>",
"<default/none>": "<%:Default / None%>"
};
// 翻译 ACL 统计文本:先翻译枚举取值,再翻译字段名
function translateAclStats(content) {
var text = content;
text = text.replace(/\baction: (Noop|Allow|Drop)\b/g, function(m, v) {
return 'action: ' + (aclValueTranslations[v] || v);
});
text = text.replace(/\bproto: (Unspecified|Icmpv6|Tcp|Udp|Icmp|Any)\b/g, function(m, v) {
return 'proto: ' + (aclValueTranslations[v] || v);
});
text = text.replace(/\bstate: (New|Established|Related|Invalid)\b/g, function(m, v) {
return 'state: ' + (aclValueTranslations[v] || v);
});
text = text.replace(/\b(name|prio|action|enabled|proto|ports|src_ports|src_ips|dst_ips|stateful|rate|burst|pkts|bytes|src|dst|state|created|last_seen): /g,
function(m, field) {
return (aclFieldTranslations[field] || field) + ': ';
});
text = text.replace(/<default\/none>/g, aclValueTranslations['<default/none>'] || '<default/none>');
return text;
}
function tr(text) {
return translations[text] || text;
@@ -996,37 +1111,16 @@ function parseTable(content, tabName) {
var html = renderCopyablePre(content);
// 检查是否使用配置文件启动
if (content.indexOf('--config-file') !== -1 || content.match(/-c\s+\//)) {
XHR.get('<%=url("admin/vpn/easytier/api_conninfo")%>', null, function(x, data) {
if (data && data.config_file) {
var configDiv = document.createElement('div');
configDiv.className = 'et-pre';
configDiv.style.marginTop = '16px';
configDiv.style.borderTop = '2px solid var(--border-color)';
configDiv.style.paddingTop = '16px';
var title = document.createElement('div');
title.style.fontWeight = '600';
title.style.marginBottom = '8px';
title.style.color = 'var(--primary-color)';
title.textContent = tr('Configuration File Content') + ':';
var contentDiv = document.createElement('pre');
contentDiv.style.margin = '0';
contentDiv.style.whiteSpace = 'pre-wrap';
contentDiv.style.overflowWrap = 'anywhere';
contentDiv.style.wordBreak = 'break-word';
contentDiv.textContent = data.config_file;
configDiv.appendChild(title);
configDiv.appendChild(contentDiv);
var container = document.getElementById('tab_cmdline');
if (container) {
container.appendChild(configDiv);
}
}
});
// 配置文件内容随 cmdline 分区一起返回,这里不再单独请求一次 api_conninfo
if ((content.indexOf('--config-file') !== -1 || content.match(/-c\s+\//)) && lastConfigFile) {
html += '<div class="et-pre" style="margin-top:16px;border-top:2px solid var(--border-color);padding-top:16px;">' +
'<div style="font-weight:600;margin-bottom:8px;color:var(--primary-color);">' +
escapeHtml(tr('Configuration File Content')) + ':' +
'</div>' +
'<pre style="margin:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;">' +
escapeHtml(lastConfigFile) +
'</pre>' +
'</div>';
}
return html;
@@ -1091,7 +1185,12 @@ function parseTable(content, tabName) {
translatedContent = translatedContent.replace(/###############\s*([^#]+)\s*###############/g, function(match, p1) {
return '############### ' + tr(p1.trim()) + ' ###############';
});
// ACL 统计的结构体字段名与枚举取值按精确规则翻译
if (tabName === 'acl') {
translatedContent = translateAclStats(translatedContent);
}
return '<div class="et-pre">' + (translatedContent.indexOf('<svg') !== -1 || translatedContent.indexOf('<span') !== -1 ? translatedContent : escapeHtml(translatedContent)) + '</div>';
}
@@ -1179,96 +1278,157 @@ function parseTable(content, tabName) {
}
function switchTab(tabName) {
if (isRefreshing) return;
var tabs = document.querySelectorAll('.et-tab');
var contents = document.querySelectorAll('.et-tab-content');
tabs.forEach(function(tab) {
tab.classList.remove('active');
});
contents.forEach(function(content) {
content.classList.remove('active');
});
var activeTab = document.querySelector('.et-tab[data-tab="' + tabName + '"]');
var activeContent = document.getElementById('tab_' + tabName);
if (activeTab && activeContent) {
activeTab.classList.add('active');
activeContent.classList.add('active');
currentTab = tabName;
// 按需加载:只请求当前显示的分区,没有加载过才发起请求
queueConnInfo([tabName], false);
}
}
// 把 TAB 名换算成后端分区名
function connInfoKeyOfTab(tabName) {
for (var i = 0; i < connInfoSections.length; i++) {
if (connInfoSections[i].id === tabName) {
return connInfoSections[i].key;
}
}
return null;
}
// 分区加载失败(超时/网络错误)时给出提示,避免加载动画一直转下去
function reportConnInfoError(keys) {
for (var i = 0; i < keys.length; i++) {
var sec = null;
for (var j = 0; j < connInfoSections.length; j++) {
if (connInfoSections[j].key === keys[i]) sec = connInfoSections[j];
}
if (!sec) continue;
var el = document.getElementById('tab_' + sec.id);
if (el) {
el.innerHTML = '<div class="et-error"><%:Loading timed out. Please click Refresh to try again.%></div>';
el.setAttribute('data-loaded', '1');
}
}
}
// 把分区加入请求队列
function queueConnInfo(tabNames, force) {
var changed = false;
for (var i = 0; i < tabNames.length; i++) {
var key = connInfoKeyOfTab(tabNames[i]);
if (!key) continue;
if (!force && connInfoLoaded[key]) continue;
if (connInfoQueue.indexOf(key) !== -1) continue;
connInfoQueue.push(key);
changed = true;
}
if (changed) {
pumpConnInfoQueue();
}
}
// 依次请求队列里的分区,同一时刻只允许一个请求在跑,
// 避免一次起多个 easytier-cli 进程把路由器拖慢
function pumpConnInfoQueue() {
if (connInfoBusy || connInfoQueue.length === 0) return;
var keys = connInfoQueue;
connInfoQueue = [];
connInfoBusy = true;
setRefreshBtnBusy(true);
var xhr = new XMLHttpRequest();
var finished = false;
var onDone = function(data) {
if (finished) return;
finished = true;
connInfoBusy = false;
setRefreshBtnBusy(false);
if (data) {
if (typeof data.config_file === 'string') {
lastConfigFile = data.config_file;
}
for (var i = 0; i < keys.length; i++) {
connInfoLoaded[keys[i]] = true;
}
updateConnInfo(data);
} else {
reportConnInfoError(keys);
}
// 继续处理下一个分区
pumpConnInfoQueue();
};
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "api_conninfo")%>?parts=' + encodeURIComponent(keys.join(',')), true);
xhr.timeout = 20000;
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
var data = null;
if (xhr.status === 200) {
try {
data = JSON.parse(xhr.responseText);
} catch (e) {
data = null;
}
}
onDone(data);
};
xhr.ontimeout = function() { onDone(null); };
xhr.onerror = function() { onDone(null); };
xhr.onabort = function() { onDone(null); };
xhr.send();
}
function setRefreshBtnBusy(busy) {
var btn = document.getElementById('et_conninfo_refresh');
if (!btn) return;
var btnText = btn.querySelector('span');
btn.disabled = busy;
if (busy) {
btn.classList.add('loading');
if (btnText) btnText.textContent = '<%:Refreshing...%>';
} else {
btn.classList.remove('loading');
if (btnText) btnText.textContent = '<%:Refresh%>';
}
}
function refreshConnInfo() {
if (isRefreshing) return;
isRefreshing = true;
var btn = document.getElementById('et_conninfo_refresh');
var btnText = btn.querySelector('span');
var originalText = btnText.textContent;
btn.disabled = true;
btn.classList.add('loading');
btnText.textContent = '<%:Refreshing...%>';
var xhr = new XMLHttpRequest();
xhr.open('GET', '<%=url("admin", "vpn", "easytier", "api_conninfo")%>', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
isRefreshing = false;
btn.disabled = false;
btn.classList.remove('loading');
btnText.textContent = originalText;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
updateConnInfo(data);
} catch (e) {
console.error('Failed to parse response:', e);
}
}
// 手动刷新:立即刷新当前显示的分区,其余分区标记失效,切过去时再重新加载,
// 这样点一次刷新不会同时起十几个 easytier-cli 进程
var activeKey = connInfoKeyOfTab(currentTab);
for (var i = 0; i < connInfoSections.length; i++) {
if (connInfoSections[i].key !== activeKey) {
delete connInfoLoaded[connInfoSections[i].key];
}
};
xhr.onerror = function() {
isRefreshing = false;
btn.disabled = false;
btn.classList.remove('loading');
btnText.textContent = originalText;
};
xhr.onabort = function() {
isRefreshing = false;
btn.disabled = false;
btn.classList.remove('loading');
btnText.textContent = originalText;
};
xhr.send();
}
queueConnInfo([currentTab], true);
}
function updateConnInfo(data) {
if (!data) return;
var sections = [
{id: 'node', key: 'node'},
{id: 'peer', key: 'peer'},
{id: 'route', key: 'route'},
{id: 'connector', key: 'connector'},
{id: 'stun', key: 'stun'},
{id: 'peercenter', key: 'peer_center'},
{id: 'vpnportal', key: 'vpn_portal'},
{id: 'proxy', key: 'proxy'},
{id: 'acl', key: 'acl'},
{id: 'mappedlistener', key: 'mapped_listener'},
{id: 'stats', key: 'stats'},
{id: 'cmdline', key: 'cmdline'}
];
sections.forEach(function(sec) {
connInfoSections.forEach(function(sec) {
var el = document.getElementById('tab_' + sec.id);
if (el && data[sec.key] !== undefined) {
el.innerHTML = parseTable(data[sec.key], sec.id);
el.setAttribute('data-loaded', '1');
}
});
}
@@ -1290,9 +1450,16 @@ document.addEventListener('DOMContentLoaded', function() {
// 加载贡献者
loadContributors();
// 初始加载数据
refreshConnInfo();
// 初始只加载当前显示的分区,其余分区等切过去时再按需请求
queueConnInfo([currentTab], false);
// 定时刷新:只刷新当前显示的分区,不再每次把十几个 easytier-cli 全部跑一遍。
// 间隔取 10 秒,低性能设备上也能跑得过来(单次只起一个进程)
window.setInterval(function() {
if (document.hidden) return;
queueConnInfo([currentTab], true);
}, 10000);
});
// 贡献者轮播管理 (ES5)
@@ -1506,11 +1673,6 @@ function loadContributors() {
contributorsCarousel.init();
}
// 自动刷新
XHR.poll(5, '<%=url("admin", "vpn", "easytier", "api_conninfo")%>', null, function(x, data) {
if (!isRefreshing && data) {
updateConnInfo(data);
}
});
// 自动刷新已改为只刷新当前显示的分区,见上方 DOMContentLoaded 中的定时器
//]]>
</script>
@@ -383,7 +383,7 @@ local translate = i18n.translate
<select id="fallback_version_select" class="config-input" style="display:none;width:100%;">
<option value=""><%=translate("Loading versions...")%></option>
</select>
<input type="text" id="fallback_version" class="config-input" placeholder="v2.6.4" style="display:none;width:100%;">
<input type="text" id="fallback_version" class="config-input" placeholder="v2.7.0" style="display:none;width:100%;">
</div>
<button type="button" class="upload-btn" onclick="showDownloadModal()" style="padding:8px 16px;min-width:auto;flex-shrink:0;">
<%=translate("Online Download")%>
@@ -758,14 +758,14 @@ function loadConfig() {
proxyList = Array.isArray(proxys) ? proxys : [];
renderProxyList();
versionManager.setVersion(config.fallback_version || 'v2.6.4');
versionManager.setVersion(config.fallback_version || 'v2.7.0');
// 保存原始配置
originalConfig = {
easytierbin: config.easytierbin || '/usr/bin/easytier-core',
webbin: config.webbin || '/usr/bin/easytier-web',
github_proxys: JSON.stringify(proxyList),
fallback_version: config.fallback_version || 'v2.6.4'
fallback_version: config.fallback_version || 'v2.7.0'
};
checkDiskSpace('easytierbin');
@@ -843,7 +843,7 @@ function getCurrentConfig() {
easytierbin: document.getElementById('easytierbin').value || '/usr/bin/easytier-core',
webbin: document.getElementById('webbin').value || '/usr/bin/easytier-web',
github_proxys: JSON.stringify(proxyList),
fallback_version: versionManager.getCurrentVersion() || 'v2.6.4'
fallback_version: versionManager.getCurrentVersion() || 'v2.7.0'
};
}
@@ -868,7 +868,7 @@ function saveConfig() {
easytierbin: document.getElementById('easytierbin').value || '/usr/bin/easytier-core',
webbin: document.getElementById('webbin').value || '/usr/bin/easytier-web',
github_proxys: proxyList,
fallback_version: versionManager.getCurrentVersion() || 'v2.6.4'
fallback_version: versionManager.getCurrentVersion() || 'v2.7.0'
};
var xhr = new XMLHttpRequest();
@@ -677,6 +677,35 @@ input:disabled + .slider {
<span class="description"><%:Automatically create local user when easytier-core connects with an unknown username%>. (--allow-auto-create-user <%:parameter%>)</span>
</div>
<div class="form-group">
<label for="web_heartbeat_min_response_ms"><%:Heartbeat Minimum Response Time (ms)%></label>
<input type="number" class="form-control" id="web_heartbeat_min_response_ms" name="web_heartbeat_min_response_ms" placeholder="3500">
<span class="description"><%:Minimum heartbeat response delay used by the config server when measuring client latency%>. (--heartbeat-min-response-ms <%:parameter%>)</span>
</div>
<div class="form-group">
<label for="web_heartbeat_timeout_ms"><%:Heartbeat Timeout (ms)%></label>
<input type="number" class="form-control" id="web_heartbeat_timeout_ms" name="web_heartbeat_timeout_ms" placeholder="15000">
<span class="description"><%:How long a client may stay silent before the config server marks it offline%>. (--heartbeat-timeout-ms <%:parameter%>)</span>
</div>
<div class="form-group">
<label for="web_gateway_peer_url"><%:Gateway Peer URL%></label>
<input type="text" class="form-control" id="web_gateway_peer_url" name="web_gateway_peer_url" placeholder="tcp://public.easytier.top:11010">
<span class="description"><%:Peer URL used by this web instance when it works as a gateway%>. (--gateway-peer-url <%:parameter%>)</span>
</div>
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_gateway_relay_data"><%:Gateway Relay Data%></label>
<label class="switch">
<input type="checkbox" id="web_gateway_relay_data" name="web_gateway_relay_data">
<span class="slider"></span>
</label>
</div>
<span class="description"><%:Allow this web gateway instance to relay data traffic%>. (--gateway-relay-data <%:parameter%>)</span>
</div>
<div class="form-group">
<label for="web_weblog"><%:Log Level%></label>
<select class="form-control" id="web_weblog" name="web_weblog">
@@ -905,6 +934,11 @@ function loadConfig() {
document.getElementById('web_geoip_db').value = data.web_geoip_db || '';
document.getElementById('web_disable_registration').checked = data.web_disable_registration === '1';
document.getElementById('web_allow_auto_create_user').checked = data.web_allow_auto_create_user === '1';
// EasyTier 2.7.0 新增的 web 参数
document.getElementById('web_heartbeat_min_response_ms').value = data.web_heartbeat_min_response_ms || '';
document.getElementById('web_heartbeat_timeout_ms').value = data.web_heartbeat_timeout_ms || '';
document.getElementById('web_gateway_peer_url').value = data.web_gateway_peer_url || '';
document.getElementById('web_gateway_relay_data').checked = data.web_gateway_relay_data === '1';
document.getElementById('web_weblog').value = data.web_weblog || 'off';
// OIDC配置
@@ -953,6 +987,11 @@ function getCurrentConfig() {
web_geoip_db: document.getElementById('web_geoip_db').value,
web_disable_registration: document.getElementById('web_disable_registration').checked ? '1' : '0',
web_allow_auto_create_user: document.getElementById('web_allow_auto_create_user').checked ? '1' : '0',
// EasyTier 2.7.0 新增的 web 参数
web_heartbeat_min_response_ms: document.getElementById('web_heartbeat_min_response_ms').value,
web_heartbeat_timeout_ms: document.getElementById('web_heartbeat_timeout_ms').value,
web_gateway_peer_url: document.getElementById('web_gateway_peer_url').value,
web_gateway_relay_data: document.getElementById('web_gateway_relay_data').checked ? '1' : '0',
web_weblog: document.getElementById('web_weblog').value,
// OIDC
web_oidc_enabled: document.getElementById('web_oidc_enabled').checked ? '1' : '0',
+114 -4
View File
@@ -906,10 +906,12 @@ msgstr ""
#: applications/luci-app-easytier/luasrc/model/cbi/easytier.lua:65
msgid ""
"Web configuration server address. (-w parameter)<br>For a self-hosted Web "
"server, use the format: udp://server_address:22020/username<br>For the "
"official Web server, use the format: username<br>Official Web Console: <a "
"href='https://easytier.cn/web'>easytier.cn/web</a>"
"Web configuration server address. (-w parameter)<br>Since EasyTier 2.7.0 a "
"full URL is required, the token-only shorthand is no longer supported<br>Self-"
"hosted Web server, use the format: udp://server_address:22020/username<br>"
"Official Web server, use the format: udp://config-server.easytier.cn:22020/"
"username<br>Official Web Console: <a href='https://easytier.cn/web'>easytier."
"cn/web</a>"
msgstr ""
#: applications/luci-app-easytier/luasrc/model/cbi/easytier.lua:365
@@ -1412,3 +1414,111 @@ msgstr ""
msgid "Duplicate extra parameter"
msgstr ""
msgid "VPN Portal Listen Address"
msgstr ""
msgid "WireGuard VPN portal listen address, allowing other VPN clients to connect.<br>Since EasyTier 2.7.0 only wg://HOST:PORT is accepted; the client network can no longer be appended to the URL, please add each client in \"VPN Portal Clients\" below (--vpn-portal parameter)"
msgstr ""
msgid "Only wg://HOST:PORT is accepted since EasyTier 2.7.0, please fill the client network into \"VPN Portal Clients\""
msgstr ""
msgid "VPN Portal Clients"
msgstr ""
msgid "One WireGuard client per entry in the form NAME=CIDR, for example client1=10.14.14.1/24.<br>NAME may contain letters, digits and dashes only; the address must be a usable host address inside the client network. (--vpn-portal-client parameter)"
msgstr ""
msgid "Invalid client, expected NAME=CIDR, for example client1=10.14.14.1/24"
msgstr ""
msgid "VPN Portal Private Key"
msgstr ""
msgid "Base64 encoded 32-byte WireGuard private key of the portal.<br>Required since EasyTier 2.7.0, the portal will not start without it. It is handed to easytier-core through an environment variable so that it never shows up in the process list. (--vpn-portal-private-key parameter)"
msgstr ""
msgid "Disable relay quic"
msgstr ""
msgid "If true, disable relaying quic packets, avoiding consuming too much bandwidth. (--disable-relay-quic parameter)"
msgstr ""
msgid "Relay foreign network quic"
msgstr ""
msgid "If true, allow relaying quic packets from foreign networks. (--enable-relay-foreign-network-quic parameter)"
msgstr ""
msgid "Magic DNS Zone"
msgstr ""
msgid "Top level DNS zone used by Magic DNS, default is et.net. Only takes effect when Magic DNS is enabled. (--tld-dns-zone parameter)"
msgstr ""
msgid "Instance Receive Rate Limit"
msgstr ""
msgid "the maximum inbound bps limit for this network instance, default is no limit. unit: BPS (bytes per second). (--instance-recv-bps-limit parameter)"
msgstr ""
msgid "Socket Mark"
msgstr ""
msgid "Set SO_MARK on all underlay sockets, Linux only and requires CAP_NET_ADMIN. Usually used together with policy routing. (--socket-mark parameter)"
msgstr ""
msgid "Lazy P2P"
msgstr ""
msgid "Only try to establish P2P connections when traffic actually needs to be forwarded. (--lazy-p2p parameter)"
msgstr ""
msgid "Need P2P"
msgstr ""
msgid "Require a P2P connection before forwarding traffic through relays. (--need-p2p parameter)"
msgstr ""
msgid "Disable UPnP"
msgstr ""
msgid "EasyTier 2.7.0 tries to map listener ports through UPnP/NAT-PMP by default. Enable this to turn the automatic port mapping off. (--disable-upnp parameter)"
msgstr ""
msgid "STUN Servers"
msgstr ""
msgid "Custom UDP STUN servers used for NAT type detection and hole punching. Leave empty to use the built-in defaults. (--stun-servers parameter)"
msgstr ""
msgid "STUN Servers (TCP)"
msgstr ""
msgid "Custom TCP STUN servers used for NAT type detection and hole punching. Leave empty to use the built-in defaults. (--tcp-stun-servers parameter)"
msgstr ""
msgid "Heartbeat Minimum Response Time (ms)"
msgstr ""
msgid "Minimum heartbeat response delay used by the config server when measuring client latency"
msgstr ""
msgid "Heartbeat Timeout (ms)"
msgstr ""
msgid "How long a client may stay silent before the config server marks it offline"
msgstr ""
msgid "Gateway Peer URL"
msgstr ""
msgid "Peer URL used by this web instance when it works as a gateway"
msgstr ""
msgid "Gateway Relay Data"
msgstr ""
msgid "Allow this web gateway instance to relay data traffic"
msgstr ""
+284 -5
View File
@@ -818,12 +818,15 @@ msgstr "Web服务器地址"
#: applications/luci-app-easytier/luasrc/model/cbi/easytier.lua:65
msgid ""
"Web configuration server address. (-w parameter)<br>For a self-hosted Web "
"server, use the format: udp://server_address:22020/username<br>For the "
"official Web server, use the format: username<br>Official Web Console: <a "
"href='https://easytier.cn/web'>easytier.cn/web</a>"
"Web configuration server address. (-w parameter)<br>Since EasyTier 2.7.0 a "
"full URL is required, the token-only shorthand is no longer supported<br>Self-"
"hosted Web server, use the format: udp://server_address:22020/username<br>"
"Official Web server, use the format: udp://config-server.easytier.cn:22020/"
"username<br>Official Web Console: <a href='https://easytier.cn/web'>easytier."
"cn/web</a>"
msgstr ""
"Web配置服务器地址。(-w 参数)<br>自建Web服务器 输入格式:udp://服务器地址:22020/账户名<br>官方Web服务器 输入格式:账户名 <br>"
"Web配置服务器地址。(-w 参数)<br>EasyTier 2.7.0 起必须填写完整 URL,不再支持仅填 token 的简写方式<br>"
"自建Web服务器 输入格式:udp://服务器地址:22020/账户名<br>官方Web服务器 输入格式:udp://config-server.easytier.cn:22020/账户名<br>"
"官方Web控制台:<a href='https://easytier.cn/web' target='_blank'>https://easytier.cn/web</a>"
#: applications/luci-app-easytier/luasrc/model/cbi/easytier.lua:365
@@ -1663,6 +1666,9 @@ msgstr "节点中心服务不可用,此功能要求网络中包含多个节点
msgid "Refreshing..."
msgstr "正在刷新..."
msgid "Loading timed out. Please click Refresh to try again."
msgstr "加载超时,请点击刷新重试。"
msgid "Failed to parse response:"
msgstr "解析响应失败:"
@@ -1802,9 +1808,174 @@ msgstr "按实例统计接收数据包"
msgid "Traffic Control Packets TX By Instance"
msgstr "按实例统计发送数据包"
msgid "Traffic Control Bytes Forwarded"
msgstr "流量控制转发字节数"
msgid "Traffic Control Packets Forwarded"
msgstr "流量控制转发数据包"
msgid "Traffic Bytes RX By Instance"
msgstr "按实例统计接收字节数(总计)"
msgid "Traffic Bytes TX By Instance"
msgstr "按实例统计发送字节数(总计)"
msgid "Traffic Packets RX By Instance"
msgstr "按实例统计接收数据包(总计)"
msgid "Traffic Packets TX By Instance"
msgstr "按实例统计发送数据包(总计)"
msgid "Traffic Bytes Foreign Forward RX"
msgstr "跨网中转接收字节数"
msgid "Traffic Bytes Foreign Forward TX"
msgstr "跨网中转发送字节数"
msgid "Traffic Bytes Foreign Forward Forwarded"
msgstr "跨网中转转发字节数"
msgid "Traffic Packets Foreign Forward RX"
msgstr "跨网中转接收数据包"
msgid "Traffic Packets Foreign Forward TX"
msgstr "跨网中转发送数据包"
msgid "Traffic Packets Foreign Forward Forwarded"
msgstr "跨网中转转发数据包"
msgid "Peer RPC Errors"
msgstr "节点 RPC 错误数"
msgid "TCP Proxy Connect"
msgstr "TCP 代理连接数"
msgid "UDP Broadcast Relay Packets Captured"
msgstr "UDP 广播中继捕获数据包"
msgid "UDP Broadcast Relay Packets Forwarded"
msgstr "UDP 广播中继转发数据包"
msgid "UDP Broadcast Relay Packets Forward Failed"
msgstr "UDP 广播中继转发失败数据包"
msgid "UDP Broadcast Relay Packets Ignored"
msgstr "UDP 广播中继忽略数据包"
msgid "VPN Portal Client"
msgstr "VPN 门户客户端"
msgid "VPN Portal Client Bytes RX"
msgstr "VPN 门户客户端接收字节数"
msgid "VPN Portal Client Bytes TX"
msgstr "VPN 门户客户端发送字节数"
msgid "VPN Portal Client Packets RX"
msgstr "VPN 门户客户端接收数据包"
msgid "VPN Portal Client Packets TX"
msgstr "VPN 门户客户端发送数据包"
msgid "From Instance ID"
msgstr "来源实例ID"
msgid "Direction"
msgstr "方向"
msgid "Destination IP"
msgstr "目标 IP"
msgid "Mapped Destination IP"
msgstr "映射后目标 IP"
msgid "Error Type"
msgstr "错误类型"
msgid "Protocol"
msgstr "协议"
msgid "Name"
msgstr "名称"
msgid "Priority"
msgstr "优先级"
msgid "Action"
msgstr "动作"
msgid "Enabled"
msgstr "已启用"
msgid "Ports"
msgstr "端口"
msgid "Source Ports"
msgstr "源端口"
msgid "Source IPs"
msgstr "源 IP"
msgid "Destination IPs"
msgstr "目标 IP"
msgid "Stateful"
msgstr "有状态"
msgid "Rate Limit"
msgstr "速率限制"
msgid "Burst Limit"
msgstr "突发限制"
msgid "Packets"
msgstr "数据包数"
msgid "Bytes"
msgstr "字节数"
msgid "Source Address"
msgstr "源地址"
msgid "Destination Address"
msgstr "目标地址"
msgid "Created"
msgstr "创建时间"
msgid "Last Seen"
msgstr "最近活跃"
msgid "Allow"
msgstr "允许"
msgid "Drop (no response)"
msgstr "丢弃(不回应)"
msgid "Noop"
msgstr "不处理"
msgid "Any"
msgstr "任意"
msgid "Unspecified"
msgstr "未指定"
msgid "New"
msgstr "新建"
msgid "Established"
msgstr "已建立"
msgid "Related"
msgstr "关联"
msgid "Invalid"
msgstr "无效"
msgid "Default / None"
msgstr "默认 / 无"
msgid "To Instance ID"
msgstr "目标实例ID"
@@ -2107,3 +2278,111 @@ msgstr "数据库重置成功"
msgid "Restart service to apply changes?"
msgstr "重启服务以生效?"
msgid "VPN Portal Listen Address"
msgstr "VPN 门户监听地址"
msgid "WireGuard VPN portal listen address, allowing other VPN clients to connect.<br>Since EasyTier 2.7.0 only wg://HOST:PORT is accepted; the client network can no longer be appended to the URL, please add each client in \"VPN Portal Clients\" below (--vpn-portal parameter)"
msgstr "WireGuard VPN 门户监听地址,允许其他 VPN 客户端接入。<br>EasyTier 2.7.0 起只接受 wg://主机:端口,客户端网段不能再写在地址里,请在下方“VPN 门户客户端”中逐个添加(--vpn-portal 参数)"
msgid "Only wg://HOST:PORT is accepted since EasyTier 2.7.0, please fill the client network into \"VPN Portal Clients\""
msgstr "EasyTier 2.7.0 起只接受 wg://主机:端口,请把客户端网段填写到“VPN 门户客户端”中"
msgid "VPN Portal Clients"
msgstr "VPN 门户客户端"
msgid "One WireGuard client per entry in the form NAME=CIDR, for example client1=10.14.14.1/24.<br>NAME may contain letters, digits and dashes only; the address must be a usable host address inside the client network. (--vpn-portal-client parameter)"
msgstr "每条一个 WireGuard 客户端,格式为 名称=CIDR,例如 client1=10.14.14.1/24。<br>名称只能包含字母、数字和短横线;地址必须是该网段内可用的主机地址(--vpn-portal-client 参数)"
msgid "Invalid client, expected NAME=CIDR, for example client1=10.14.14.1/24"
msgstr "客户端格式错误,应为 名称=CIDR,例如 client1=10.14.14.1/24"
msgid "VPN Portal Private Key"
msgstr "VPN 门户私钥"
msgid "Base64 encoded 32-byte WireGuard private key of the portal.<br>Required since EasyTier 2.7.0, the portal will not start without it. It is handed to easytier-core through an environment variable so that it never shows up in the process list. (--vpn-portal-private-key parameter)"
msgstr "门户的 WireGuard 私钥(base64 编码的 32 字节)。<br>EasyTier 2.7.0 起必填,未填写时门户不会启动。私钥通过环境变量传给 easytier-core,不会出现在进程列表中(--vpn-portal-private-key 参数)"
msgid "Disable relay quic"
msgstr "禁用转发 QUIC"
msgid "If true, disable relaying quic packets, avoiding consuming too much bandwidth. (--disable-relay-quic parameter)"
msgstr "开启后禁止转发 QUIC 报文,避免占用过多带宽(--disable-relay-quic 参数)"
msgid "Relay foreign network quic"
msgstr "转发外部网络 QUIC"
msgid "If true, allow relaying quic packets from foreign networks. (--enable-relay-foreign-network-quic parameter)"
msgstr "开启后允许转发来自其他网络的 QUIC 报文(--enable-relay-foreign-network-quic 参数)"
msgid "Magic DNS Zone"
msgstr "魔法 DNS 区域"
msgid "Top level DNS zone used by Magic DNS, default is et.net. Only takes effect when Magic DNS is enabled. (--tld-dns-zone parameter)"
msgstr "魔法 DNS 使用的顶级域名区域,默认 et.net.,仅在启用魔法 DNS 时生效(--tld-dns-zone 参数)"
msgid "Instance Receive Rate Limit"
msgstr "实例接收限速"
msgid "the maximum inbound bps limit for this network instance, default is no limit. unit: BPS (bytes per second). (--instance-recv-bps-limit parameter)"
msgstr "本网络实例的整体入站限速,默认不限速,单位字节每秒(--instance-recv-bps-limit 参数)"
msgid "Socket Mark"
msgstr "Socket Mark"
msgid "Set SO_MARK on all underlay sockets, Linux only and requires CAP_NET_ADMIN. Usually used together with policy routing. (--socket-mark parameter)"
msgstr "为所有底层套接字设置 SO_MARK,仅 Linux 支持且需要 CAP_NET_ADMIN 权限,通常配合策略路由使用(--socket-mark 参数)"
msgid "Lazy P2P"
msgstr "延迟 P2P"
msgid "Only try to establish P2P connections when traffic actually needs to be forwarded. (--lazy-p2p parameter)"
msgstr "仅在有实际数据需要转发时才尝试建立 P2P 连接(--lazy-p2p 参数)"
msgid "Need P2P"
msgstr "强制 P2P"
msgid "Require a P2P connection before forwarding traffic through relays. (--need-p2p parameter)"
msgstr "必须建立 P2P 连接后才允许通过中继转发数据(--need-p2p 参数)"
msgid "Disable UPnP"
msgstr "禁用 UPnP"
msgid "EasyTier 2.7.0 tries to map listener ports through UPnP/NAT-PMP by default. Enable this to turn the automatic port mapping off. (--disable-upnp parameter)"
msgstr "EasyTier 2.7.0 默认会尝试通过 UPnP/NAT-PMP 自动映射监听端口,开启本项可关闭自动端口映射(--disable-upnp 参数)"
msgid "STUN Servers"
msgstr "STUN 服务器"
msgid "Custom UDP STUN servers used for NAT type detection and hole punching. Leave empty to use the built-in defaults. (--stun-servers parameter)"
msgstr "用于 NAT 类型探测与打洞的自定义 UDP STUN 服务器,留空则使用内置默认值(--stun-servers 参数)"
msgid "STUN Servers (TCP)"
msgstr "STUN 服务器(TCP)"
msgid "Custom TCP STUN servers used for NAT type detection and hole punching. Leave empty to use the built-in defaults. (--tcp-stun-servers parameter)"
msgstr "用于 NAT 类型探测与打洞的自定义 TCP STUN 服务器,留空则使用内置默认值(--tcp-stun-servers 参数)"
msgid "Heartbeat Minimum Response Time (ms)"
msgstr "心跳最小响应时间(毫秒)"
msgid "Minimum heartbeat response delay used by the config server when measuring client latency"
msgstr "配置服务器测量客户端延迟时使用的心跳最小响应延迟"
msgid "Heartbeat Timeout (ms)"
msgstr "心跳超时时间(毫秒)"
msgid "How long a client may stay silent before the config server marks it offline"
msgstr "客户端多久无心跳后会被配置服务器标记为离线"
msgid "Gateway Peer URL"
msgstr "网关对等节点地址"
msgid "Peer URL used by this web instance when it works as a gateway"
msgstr "本 Web 实例作为网关时使用的对等节点地址"
msgid "Gateway Relay Data"
msgstr "网关转发数据"
msgid "Allow this web gateway instance to relay data traffic"
msgstr "允许本 Web 网关实例转发数据流量"
@@ -3,6 +3,8 @@ instance_id = "f9f05421-7c6b-47d6-a67d-4d58d49f51d8"
dhcp = true
listeners = []
exit_nodes = []
# 注意:顶层配置结构里没有 rpc_portal 字段,该行会被 easytier-core 静默忽略。
# RPC 端口只能通过命令行 -r 指定(本插件的“RPC 端口”选项),保持默认即可。
rpc_portal = "0.0.0.0:15888"
[network_identity]
@@ -12,4 +14,18 @@ network_secret = "mysecret"
[[peer]]
uri = "tcp://public.easytier.top:11010"
# EasyTier 2.7.0 起 WireGuard 门户写法(默认关闭,需要时取消注释)
# 旧版把客户端网段写在 wireguard_listen 之外的 client_cidr 键已不再支持,会导致配置文件解析失败。
# 门户私钥为 base64 编码的 32 字节,2.7.0 起必须填写,否则门户监听不会启动。
# [vpn_portal_config]
# enabled = true
# wireguard_listen = "0.0.0.0:51820"
# wireguard_private_key = ""
#
# [[vpn_portal_config.clients]]
# name = "client1"
# virtual_ip = "10.14.14.1/24"
# groups = []
+106 -9
View File
@@ -107,7 +107,7 @@ check_bin() {
# 如果获取失败,从 UCI 配置或使用默认版本
if [ -z "$tag" ]; then
tag=$(uci -q get easytier.@easytier[0].fallback_version)
[ -z "$tag" ] && tag="v2.6.4"
[ -z "$tag" ] && tag="v2.7.0"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 开始在线下载${tag}版本,${proxy}https://github.com/EasyTier/EasyTier/releases/download/${tag}/easytier-linux-${cpucore}-${tag}.zip下载较慢耐心等候" >>/tmp/easytier.log
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 开始在线下载${tag}版本,${proxy}https://github.com/EasyTier/EasyTier/releases/download/${tag}/easytier-linux-${cpucore}-${tag}.zip下载较慢耐心等候" >>/tmp/easytierweb.log
@@ -167,7 +167,10 @@ check_config() {
wireguard_port=$(cat $config_file | grep "wireguard_listen = " | awk -F ":" '{print $2}'| tr -d '/", ')
log="$(uci -q get easytier.@easytier[0].log || echo off)"
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 检测配置文件可用性 ${easytierbin} --check-config --config-file ${config_file}" >>/tmp/easytier.log
${easytierbin} --check-config --config-file ${config_file} >> /tmp/easytier.log 2>&1 &
${easytierbin} --check-config --config-file ${config_file} >> /tmp/easytier.log 2>&1
if [ "$?" != "0" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 配置文件 ${config_file} 校验失败,请查看上方 easytier-core 的报错。EasyTier 2.7.0 起配置校验更严格:旧的 [vpn_portal_config] client_cidr 写法与 [flags] 中的未知键都会导致解析失败" >>/tmp/easytier.log
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 开始运行 ${easytierbin} -c ${config_file}" >>/tmp/easytier.log
procd_open_instance
@@ -218,6 +221,8 @@ get_etconfig() {
desvice_name="$(uci -q get easytier.@easytier[0].desvice_name)"
instance_name="$(uci -q get easytier.@easytier[0].instance_name)"
vpn_portal="$(uci -q get easytier.@easytier[0].vpn_portal)"
vpn_portal_clients="$(uci -q get easytier.@easytier[0].vpn_portal_clients)"
vpn_portal_private_key="$(uci -q get easytier.@easytier[0].vpn_portal_private_key)"
mtu="$(uci -q get easytier.@easytier[0].mtu)"
default_protocol="$(uci -q get easytier.@easytier[0].default_protocol)"
disable_encryption="$(uci -q get easytier.@easytier[0].disable_encryption || echo 0)"
@@ -263,7 +268,18 @@ get_etconfig() {
auto_config_interface="$(uci -q get easytier.@easytier[0].auto_config_interface || echo 1)"
interface_netmask="$(uci -q get easytier.@easytier[0].interface_netmask)"
auto_config_firewall="$(uci -q get easytier.@easytier[0].auto_config_firewall || echo 1)"
# EasyTier 2.7.0 新增参数
tld_dns_zone="$(uci -q get easytier.@easytier[0].tld_dns_zone)"
instance_recv_bps_limit="$(uci -q get easytier.@easytier[0].instance_recv_bps_limit)"
socket_mark="$(uci -q get easytier.@easytier[0].socket_mark)"
disable_upnp="$(uci -q get easytier.@easytier[0].disable_upnp || echo 0)"
lazy_p2p="$(uci -q get easytier.@easytier[0].lazy_p2p || echo 0)"
need_p2p="$(uci -q get easytier.@easytier[0].need_p2p || echo 0)"
disable_relay_quic="$(uci -q get easytier.@easytier[0].disable_relay_quic || echo 0)"
relay_quic="$(uci -q get easytier.@easytier[0].relay_quic || echo 0)"
stun_servers="$(uci -q get easytier.@easytier[0].stun_servers)"
tcp_stun_servers="$(uci -q get easytier.@easytier[0].tcp_stun_servers)"
}
set_firewall() {
@@ -449,7 +465,8 @@ start_et() {
procd_open_instance "easytier_core"
get_tz
[ -z "$SET_TZ" ] || procd_set_param env TZ="$SET_TZ"
core_cmd=""
# 门户私钥通过环境变量下发给 easytier-core,避免出现在进程命令行(状态页会展示命令行)
[ -z "$vpn_portal_private_key" ] || procd_set_param env ET_VPN_PORTAL_PRIVATE_KEY="$vpn_portal_private_key"
if [ "$etcmd" = "etcmd" ] ; then
#默认方式启动
[ -z "$network_name" ] || core_cmd="${core_cmd} --network-name $(shell_quote "$network_name")"
@@ -536,12 +553,46 @@ start_et() {
[ "$listenermode" = "ON" ] || core_cmd="${core_cmd} --no-listener"
[ -z "$desvice_name" ] || core_cmd="${core_cmd} --hostname $(shell_quote "$desvice_name")"
[ -z "$instance_name" ] || core_cmd="${core_cmd} -m $instance_name"
[ -z "$vpn_portal" ] || core_cmd="${core_cmd} --vpn-portal $vpn_portal"
# EasyTier 2.7.0 起 --vpn-portal 只接受 wg://主机:端口,客户端必须逐个具名枚举
if [ -n "$vpn_portal" ]; then
case "$vpn_portal" in
wg://*/*)
# 旧版把客户端网段写在 URL path 里,2.7.0 会直接拒绝并让进程退出,这里跳过门户并提示用户修改
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : VPN 门户地址 ${vpn_portal} 是 2.6.4 旧写法,EasyTier 2.7.0 起只接受 wg://主机:端口,已跳过门户启动;请在界面把客户端网段填入 VPN 门户客户端列表" >>/tmp/easytier.log
;;
wg://*)
if [ -z "$vpn_portal_private_key" ]; then
# 2.7.0 起门户必须有私钥,否则门户监听会启动失败并连带整个实例退出
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 已配置 VPN 门户但未填写门户私钥,EasyTier 2.7.0 起门户必须有私钥,已跳过门户启动;请在界面填写 VPN 门户私钥" >>/tmp/easytier.log
else
core_cmd="${core_cmd} --vpn-portal $vpn_portal"
fi
;;
*)
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : VPN 门户地址 ${vpn_portal} 格式不正确,应为 wg://主机:端口,已跳过门户启动" >>/tmp/easytier.log
;;
esac
fi
# 门户具名客户端,格式 名称=地址/前缀,可配置多条
if [ -n "$vpn_portal_clients" ]; then
if [[ "$(grep "list vpn_portal_clients" /etc/config/easytier | awk '{print $3}' | wc -l)" -eq 1 ]]; then
core_cmd="${core_cmd} --vpn-portal-client $vpn_portal_clients"
else
for vpn_portal_client in $(cat /etc/config/easytier | grep 'list vpn_portal_clients' | awk -F 'list vpn_portal_clients' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
core_cmd="${core_cmd} --vpn-portal-client $vpn_portal_client"
done
fi
fi
[ -z "$mtu" ] || core_cmd="${core_cmd} --mtu $mtu"
[ "$default_protocol" = "-" ] || core_cmd="${core_cmd} --default-protocol $default_protocol"
[ -z "$tunname" ] || core_cmd="${core_cmd} --dev-name $tunname"
[ "$disable_encryption" = "0" ] || core_cmd="${core_cmd} -u"
[ "$multi_thread" = "0" ] || core_cmd="${core_cmd} --multi-thread"
# 2.7.0 起 multi_thread 的默认值由 false 改为 true,这里始终显式传值,保证升级后行为与界面勾选一致
if [ "$multi_thread" = "0" ]; then
core_cmd="${core_cmd} --multi-thread false"
else
core_cmd="${core_cmd} --multi-thread true"
fi
[ -z "$multi_thread_count" ] || core_cmd="${core_cmd} --multi-thread-count $multi_thread_count"
[ "$no_tun" = "0" ] || core_cmd="${core_cmd} --no-tun"
[ "$smoltcp" = "0" ] || core_cmd="${core_cmd} --use-smoltcp"
@@ -571,8 +622,28 @@ start_et() {
[ "$disable_sym" = "0" ] || core_cmd="${core_cmd} --disable-sym-hole-punching"
[ "$disable_relay_kcp" = "0" ] || core_cmd="${core_cmd} --disable-relay-kcp true"
[ "$relay_kcp" = "0" ] || core_cmd="${core_cmd} --enable-relay-foreign-network-kcp true"
[ "$disable_relay_quic" = "0" ] || core_cmd="${core_cmd} --disable-relay-quic true"
[ "$relay_quic" = "0" ] || core_cmd="${core_cmd} --enable-relay-foreign-network-quic true"
[ -z "$udp_white_port" ] || core_cmd="${core_cmd} --udp-whitelist $udp_white_port"
[ -z "$tcp_white_port" ] || core_cmd="${core_cmd} --tcp-whitelist $tcp_white_port"
# EasyTier 2.7.0 新增的常用参数
[ -z "$tld_dns_zone" ] || core_cmd="${core_cmd} --tld-dns-zone $tld_dns_zone"
[ -z "$instance_recv_bps_limit" ] || core_cmd="${core_cmd} --instance-recv-bps-limit $instance_recv_bps_limit"
[ -z "$socket_mark" ] || core_cmd="${core_cmd} --socket-mark $socket_mark"
[ "$disable_upnp" = "0" ] || core_cmd="${core_cmd} --disable-upnp"
[ "$lazy_p2p" = "0" ] || core_cmd="${core_cmd} --lazy-p2p"
[ "$need_p2p" = "0" ] || core_cmd="${core_cmd} --need-p2p"
# 自定义 STUN 服务器,可配置多条
if [ -n "$stun_servers" ] ; then
for stun_server in $(cat /etc/config/easytier | grep 'list stun_servers' | awk -F 'list stun_servers' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
core_cmd="${core_cmd} --stun-servers $stun_server"
done
fi
if [ -n "$tcp_stun_servers" ] ; then
for tcp_stun_server in $(cat /etc/config/easytier | grep 'list tcp_stun_servers' | awk -F 'list tcp_stun_servers' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
core_cmd="${core_cmd} --tcp-stun-servers $tcp_stun_server"
done
fi
if [ -n "$extra_args" ] ; then
while IFS= read -r extra_arg; do
[ -n "$extra_arg" ] && core_cmd="${core_cmd} $extra_arg"
@@ -595,7 +666,18 @@ EOF
fi
et_uuid="$(cat /etc/easytier/et_machine_id | tr -d ' \n')"
[ -z "$et_uuid" ] || core_cmd="${core_cmd} --machine-id $et_uuid"
[ -z "$web_config" ] || core_cmd="${core_cmd} -w $web_config"
# EasyTier 2.7.0 起 -w 必须填写完整 URL(例如 udp://config-server.easytier.cn:22020/账户名),
# 旧版只填账户名的写法会解析失败并让整个进程退出,这里跳过 Web 客户端并提示用户修改
case "$web_config" in
"")
;;
*://*)
core_cmd="${core_cmd} -w $web_config"
;;
*)
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : Web 服务器地址 ${web_config} 不是完整 URL,EasyTier 2.7.0 起必须填写完整地址(例如 udp://config-server.easytier.cn:22020/账户名),已跳过 Web 客户端启动;请在界面把 Web 服务器地址改为完整地址" >>/tmp/easytier.log
;;
esac
[ -z "$desvice_name" ] || core_cmd="${core_cmd} --hostname $(shell_quote "$desvice_name")"
fi
procd_set_param command /bin/sh -c "
@@ -623,7 +705,10 @@ EOF
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
# procd 进程守护参数:threshold=3600 秒、timeout=5 秒、retry=20 次
# 存活满 1 小时后崩溃计数归零;1 小时内崩溃超过 20 次(即第 21 次)才判定崩溃循环并放弃拉起,
# 避免参数配置错误这类必然失败的场景无限重启刷日志、空烧 CPU
procd_set_param respawn 3600 5 20
procd_close_instance
# 不再自动放行 SOCKS5 端口到 WAN —— 无认证 SOCKS5 代理暴露给公网有严重安全风险
@@ -691,6 +776,11 @@ start_web() {
html_addr="$(uci -q get easytier.@easytier[0].web_html_addr)"
disable_registration="$(uci -q get easytier.@easytier[0].web_disable_registration || echo 0)"
allow_auto_create_user="$(uci -q get easytier.@easytier[0].web_allow_auto_create_user || echo 0)"
# EasyTier 2.7.0 新增的 web 参数
heartbeat_min_response_ms="$(uci -q get easytier.@easytier[0].web_heartbeat_min_response_ms)"
heartbeat_timeout_ms="$(uci -q get easytier.@easytier[0].web_heartbeat_timeout_ms)"
gateway_peer_url="$(uci -q get easytier.@easytier[0].web_gateway_peer_url)"
gateway_relay_data="$(uci -q get easytier.@easytier[0].web_gateway_relay_data || echo 0)"
# OIDC配置
oidc_enabled="$(uci -q get easytier.@easytier[0].web_oidc_enabled || echo 0)"
oidc_issuer_url="$(uci -q get easytier.@easytier[0].web_oidc_issuer_url)"
@@ -729,6 +819,11 @@ start_web() {
[ -n "$html_addr" ] && [ "$html_addr" != "0.0.0.0" ] && web_cmd="${web_cmd} --web-server-addr $html_addr"
[ "$disable_registration" = "1" ] && web_cmd="${web_cmd} --disable-registration"
[ "$allow_auto_create_user" = "1" ] && web_cmd="${web_cmd} --allow-auto-create-user"
# EasyTier 2.7.0 新增的 web 参数
[ -z "$heartbeat_min_response_ms" ] || web_cmd="${web_cmd} --heartbeat-min-response-ms $heartbeat_min_response_ms"
[ -z "$heartbeat_timeout_ms" ] || web_cmd="${web_cmd} --heartbeat-timeout-ms $heartbeat_timeout_ms"
[ -z "$gateway_peer_url" ] || web_cmd="${web_cmd} --gateway-peer-url $gateway_peer_url"
[ "$gateway_relay_data" = "1" ] && web_cmd="${web_cmd} --gateway-relay-data true"
# OIDC配置参数
if [ "$oidc_enabled" = "1" ] ; then
[ -z "$oidc_issuer_url" ] || web_cmd="${web_cmd} --oidc-issuer-url $oidc_issuer_url"
@@ -773,7 +868,9 @@ start_web() {
"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
# procd 进程守护参数:threshold=3600 秒、timeout=5 秒、retry=20 次
# 存活满 1 小时后崩溃计数归零;1 小时内崩溃超过 20 次(即第 21 次)才判定崩溃循环并放弃拉起
procd_set_param respawn 3600 5 20
procd_close_instance
if [ ! -z "$web_port" ] && [ "$fw_web" = "1" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') easytier : 添加防火墙规则 easytier_web 放行服务端口 ${web_port} " >>/tmp/easytierweb.log
@@ -42,7 +42,7 @@ get_latest_version() {
# 如果获取失败,从 UCI 配置或使用默认版本
if [ -z "$tag" ]; then
tag=$(uci -q get easytier.@easytier[0].fallback_version)
[ -z "$tag" ] && tag="v2.6.2"
[ -z "$tag" ] && tag="v2.7.0"
fi
echo "$tag"
+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.53
PKG_RELEASE:=67
PKG_RELEASE:=68
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -11,5 +11,5 @@
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
*/
return baseclass.extend({
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
});
@@ -290,6 +290,7 @@
.fwlive-map[data-view="simple"] col.fwlive-col-iface { width: 5.5rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-proto { width: 4rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-rule { width: 12rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-flow { width: 320px; }
.fwlive-map[data-view="detailed"] .fwlive-scroll #fwlive-table {
width: max-content;
min-width: 100%;
@@ -176,18 +176,16 @@ const DENY_ACTION_UNDERSCORE = new RegExp(
'(?:^|[^A-Za-z0-9])(?:' + DENY_CLASS_WORDS.join('|') + ')(?:[^A-Za-z0-9]|$)',
'i'
);
const NETFILTER_KV_GLUE = new RegExp(
'([^\\s])(?=(' + CLASSIFY_SPEC.glueKeys.join('|') + ')=)',
'g'
);
const NETFILTER_KV_GLUE = new RegExp('(' + CLASSIFY_SPEC.glueKeys.join('|') + ')=');
return baseclass.extend({
CLASSIFY_SPEC: CLASSIFY_SPEC,
validateClassifySpec: validateClassifySpec,
TCP_FLAG_TAIL:
/\b((?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
/(?:^|\s)((?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR))*)(?:\s+OPT\s+\([^)]*\))?\s*$/i,
NETFILTER_KV_GLUE: NETFILTER_KV_GLUE,
KV_SPAN: /\b[A-Z][A-Z0-9_]*=[^\s]+/g,
kvHas: function (msg, key) {
if (typeof key === 'string' && Object.prototype.hasOwnProperty.call(KV_HAS_PATTERNS, key)) {
@@ -207,7 +205,13 @@ return baseclass.extend({
normalizeNetfilterMessage: function (message) {
if (typeof message !== 'string') message = '';
return message.replace(this.NETFILTER_KV_GLUE, '$1 ');
const match = this.NETFILTER_KV_GLUE.exec(message);
if (!match) return message;
const idx = match.index;
if (idx === 0) return message;
const prev = message.charAt(idx - 1);
if (/\s/.test(prev)) return message;
return message.slice(0, idx) + ' ' + message.slice(idx);
},
parseKeyValueLog: function (message) {
@@ -216,13 +220,19 @@ return baseclass.extend({
const normalized = this.normalizeNetfilterMessage(message);
let match;
while ((match = re.exec(normalized)) !== null) out[match[1]] = match[2];
while ((match = re.exec(normalized)) !== null) {
if (out[match[1]] === undefined) out[match[1]] = match[2];
}
return out;
},
stripKvSpans: function (message) {
return String(message).replace(this.KV_SPAN, '');
},
detectAction: function (message) {
const m = message.match(this.ACTION_RE);
const m = this.stripKvSpans(message).match(this.ACTION_RE);
return m ? m[2].toUpperCase() : 'UNKNOWN';
},
@@ -320,19 +330,9 @@ return baseclass.extend({
return hint.replace(/-/g, ' ');
},
inferActionRaw: function (message, kv, actionRaw) {
inferActionRaw: function (_message, _kv, actionRaw) {
if (actionRaw && actionRaw !== 'UNKNOWN') return actionRaw;
const msg = this.normalizeNetfilterMessage(message || '');
const withoutKv = msg.replace(/\b[A-Z]+=[^\s]*/g, ' ');
if (this.DENY_ACTION.test(withoutKv) || this.DENY_ACTION_UNDERSCORE.test(withoutKv))
return 'UNKNOWN';
if (/^kernel:/i.test(msg.trim())) return 'UNKNOWN';
const hasTuple = !!(kv.IN || kv.OUT) && !!(kv.SRC || kv.DST || kv.PROTO);
if (hasTuple) return 'PASS';
return 'UNKNOWN';
},
@@ -340,7 +340,7 @@ return baseclass.extend({
if (kv.TCPFLAGS) return kv.TCPFLAGS;
if (kv.FLAGS) return kv.FLAGS;
const m = message.match(this.TCP_FLAG_TAIL);
const m = this.stripKvSpans(message).match(this.TCP_FLAG_TAIL);
if (!m) return '';
return m[1].trim().toUpperCase().replace(/\s+/g, ',');
@@ -8,7 +8,7 @@
* Logging toolbar and empty-state DOM renderers for luci-app-fwlive.
*
* renderToolbar(host, state, callbacks) → void
* host - #fwlive-logging-bar strip slot (cleared and rebuilt; element kept)
* host - #fwlive-logging-bar strip slot (controls rebuilt; live status kept)
* state - { loggingStatus, loggingBusy, loggingNotice, loggingNoticeFail }
* callbacks - { onEnable(), onDisable() }
*
@@ -28,7 +28,16 @@
* toggleFailureNotice(code, fallback) → string
* rpcd enable/disable error code → notice; unknown codes return fallback.
*
* Modules must not mutate state. host is cleared then rebuilt (idempotent replace).
* toggleRpcCatchNotice(err, accessNotice, fallback) → string
* ubus code 6 / JSON-RPC -32002 → accessNotice; any other rejection → fallback.
*
* captureFocus(host, active) / restoreFocus(host, focused)
* chips.js pattern: if focus was inside host, move it to the replacement control.
* hasLoggingAction(host, action) → bool
* true when host still contains that data-fwlive-logging-action.
*
* Modules must not mutate state. Toolbar controls are replaced; the role=status
* node is reused so live announcements survive rebuilds.
*/
const CONSENT_STORAGE_KEY = 'fwlive-logging-consent-v1';
@@ -81,14 +90,141 @@ function toggleFailureNotice(code, fallback) {
}
}
function luciRpcFailureCode(err) {
const msg = err && typeof err.message === 'string' ? err.message : '';
let match = /failed with ubus code (-?\d+):/.exec(msg);
if (!match) match = /failed with error (-?\d+):/.exec(msg);
if (!match) return null;
const code = Number(match[1]);
return code === code ? code : null;
}
function toggleRpcCatchNotice(err, accessNotice, fallback) {
const code = luciRpcFailureCode(err);
if (code === 6 || code === -32002) return accessNotice;
return fallback;
}
function nodeIsInside(node, host) {
while (node) {
if (node === host) return true;
node = node.parentNode;
}
return false;
}
function captureFocus(host, active) {
if (!host || !active || !nodeIsInside(active, host)) return null;
const action =
typeof active.getAttribute === 'function'
? active.getAttribute('data-fwlive-logging-action')
: null;
return { action: action || 'host' };
}
function findLoggingControl(root, action) {
if (!root || !action) return null;
const pending = [root];
while (pending.length) {
const node = pending.pop();
if (!node || node.nodeType !== 1) continue;
if (node.getAttribute && node.getAttribute('data-fwlive-logging-action') === action)
return node;
const children = node.childNodes || [];
for (let i = children.length - 1; i >= 0; i--) pending.push(children[i]);
}
return null;
}
function primaryLoggingControl(root) {
return (
findLoggingControl(root, 'enable') ||
findLoggingControl(root, 'disable') ||
findLoggingControl(root, 'dismiss')
);
}
function restoreFocus(host, focused) {
if (!focused || !host) return;
let target = focused.action ? findLoggingControl(host, focused.action) : null;
if (!target) target = primaryLoggingControl(host);
if (target && typeof target.focus === 'function') target.focus();
}
function hasLoggingAction(host, action) {
return !!findLoggingControl(host, action);
}
function toolbarLiveStatus(host) {
let live = host._fwliveLoggingLive;
if (!live || live.nodeType !== 1) {
live = E(
'span',
{
'class': 'fwlive-logging-live',
'role': 'status'
},
['']
);
host._fwliveLoggingLive = live;
}
if (live.parentNode !== host) host.appendChild(live);
return live;
}
function replaceToolbarControls(host, live) {
const kids = [];
for (let i = 0; i < host.childNodes.length; i++) kids.push(host.childNodes[i]);
for (let i = 0; i < kids.length; i++) {
if (kids[i] !== live) host.removeChild(kids[i]);
}
}
function placeToolbarNode(host, live, node) {
if (live && live.parentNode === host) host.insertBefore(node, live);
else host.appendChild(node);
}
function setLiveText(live, text) {
const msg = text == null ? '' : String(text);
let tn = null;
const kids = live.childNodes || [];
for (let i = 0; i < kids.length; i++) {
if (kids[i].nodeType === 3) {
tn = kids[i];
break;
}
}
if (!tn) {
const tmp = E('span', {}, [msg]);
tn = tmp.childNodes && tmp.childNodes[0];
if (tn) live.appendChild(tn);
return;
}
tn.textContent = msg;
if (tn.nodeValue !== undefined) tn.nodeValue = msg;
}
function updateLiveStatus(live, state) {
setLiveText(live, state.loggingNotice || '');
live.setAttribute(
'class',
state.loggingNotice
? 'fwlive-logging-live ' + loggingNoticeClass(state)
: 'fwlive-logging-live'
);
live.setAttribute('role', 'status');
}
function enableLoggingButton(state, callbacks) {
return E(
'button',
{
'class': 'cbi-button cbi-button-action',
'type': 'button',
'data-fwlive-logging-action': 'enable',
'title': _('Enable WAN zone drop/reject logging (same as Network → Firewall).'),
'disabled': state.loggingBusy ? '' : null,
'aria-disabled': state.loggingBusy ? 'true' : null,
'click': function () {
callbacks.onEnable();
}
@@ -130,26 +266,25 @@ function loggingNoticeClass(state) {
: 'fwlive-logging-notice';
}
function appendLoggingNotice(host, state) {
if (!state.loggingNotice) return;
host.appendChild(E('span', { 'class': loggingNoticeClass(state) }, [state.loggingNotice]));
}
function appendBlockerStatus(host, blocker, st) {
function appendBlockerStatus(host, live, blocker, st) {
if (blocker === 'no_wan_zone') {
host.appendChild(
placeToolbarNode(
host,
live,
E(
'span',
{ 'class': 'fwlive-logging-status' },
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
)
);
host.appendChild(links.firewallZonesLink());
placeToolbarNode(host, live, links.firewallZonesLink());
return;
}
if (blocker === 'nf_log_missing') {
host.appendChild(
placeToolbarNode(
host,
live,
E('span', { 'class': 'fwlive-logging-status' }, [
_('WAN logging unavailable: missing kernel log modules')
])
@@ -158,7 +293,9 @@ function appendBlockerStatus(host, blocker, st) {
}
if (blocker === 'unknown') {
host.appendChild(
placeToolbarNode(
host,
live,
E('span', { 'class': 'fwlive-logging-status' }, [_('WAN logging unavailable')])
);
}
@@ -179,14 +316,17 @@ function appendWanLogDisableControl(host, state, callbacks) {
E('span', { 'class': 'fwlive-log-label' }, [_('WAN logging on')]),
E('span', { 'class': 'fwlive-log-rate' }, [_('· %s').format(limit)])
];
host.appendChild(
placeToolbarNode(
host,
host._fwliveLoggingLive,
E(
'button',
{
'class': 'cbi-button fwlive-log-merged',
'type': 'button',
'data-fwlive-logging-action': 'disable',
'title': _('WAN logging on (%s). Click to disable.').format(limit),
'disabled': busy ? '' : null,
'aria-disabled': busy ? 'true' : null,
'click': function () {
callbacks.onDisable();
}
@@ -197,7 +337,10 @@ function appendWanLogDisableControl(host, state, callbacks) {
}
function renderToolbar(host, state, callbacks) {
host.innerHTML = '';
const live = toolbarLiveStatus(host);
replaceToolbarControls(host, live);
updateLiveStatus(live, state);
const st = state.loggingStatus;
if (!st) {
host.style.display = 'none';
@@ -207,21 +350,16 @@ function renderToolbar(host, state, callbacks) {
host.style.display = 'contents';
const blocker = blockerCode(state);
if (blocker) appendBlockerStatus(host, blocker, st);
if (blocker) appendBlockerStatus(host, live, blocker, st);
if (st.wan_log) {
appendWanLogDisableControl(host, state, callbacks);
appendLoggingNotice(host, state);
return;
}
if (blocker) {
appendLoggingNotice(host, state);
return;
}
if (blocker) return;
host.appendChild(enableLoggingButton(state, callbacks));
appendLoggingNotice(host, state);
placeToolbarNode(host, live, enableLoggingButton(state, callbacks));
}
function wanLogLimitDescription(limit) {
@@ -265,7 +403,8 @@ function buildConsentPanel(state, callbacks) {
E('label', {}, [
E('input', {
'type': 'checkbox',
'id': dontShowId
'id': dontShowId,
'data-fwlive-logging-action': 'consent-dont-show'
}),
' ',
_('Don’t show this again')
@@ -279,6 +418,7 @@ function buildConsentPanel(state, callbacks) {
{
'class': 'cbi-button',
'type': 'button',
'data-fwlive-logging-action': 'dismiss',
'click': function () {
const box = document.getElementById(dontShowId);
const persist = !!(box && box.checked);
@@ -446,6 +586,10 @@ return baseclass.extend({
consentDismissedPermanent: consentDismissedPermanent,
persistConsentDismissed: persistConsentDismissed,
toggleFailureNotice: toggleFailureNotice,
toggleRpcCatchNotice: toggleRpcCatchNotice,
captureFocus: captureFocus,
restoreFocus: restoreFocus,
hasLoggingAction: hasLoggingAction,
renderToolbar: renderToolbar,
buildEmptyStateNodes: buildEmptyStateNodes,
renderEmptyState: renderEmptyState,
@@ -12,6 +12,7 @@ function createScheduler(options) {
if (!Number.isFinite(capacity) || capacity <= 0)
throw new TypeError('capacity must be a positive number');
const now = options.now || Date.now;
const isPaused = options.isPaused;
let pending = null;
let nextForce = null;
@@ -30,6 +31,7 @@ function createScheduler(options) {
const epoch = getEpoch();
if (typeof requestFrame !== 'function') {
nextForce = null;
if (typeof isPaused === 'function' && isPaused()) return;
render(!!force);
return;
}
@@ -49,6 +51,7 @@ function createScheduler(options) {
pending = null;
frameId = null;
if (disposed || request.epoch !== getEpoch()) return;
if (typeof isPaused === 'function' && isPaused()) return;
/* Drop only the reservation this frame used. A newer forceNextRender
* after an unforced schedule must survive this callback. */
@@ -67,13 +67,15 @@ const callFwliveLoggingStatus = rpc.declare({
const callFwliveEnableLogging = rpc.declare({
object: 'fwlive',
method: 'enable_wan_logging',
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } },
reject: true
});
const callFwliveDisableLogging = rpc.declare({
object: 'fwlive',
method: 'disable_wan_logging',
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } },
reject: true
});
function storedValue(key, fallback) {
@@ -597,6 +599,14 @@ return view.extend({
const td = tr.querySelector('td');
if (!td || typeof getComputedStyle !== 'function') return;
/* Measure without the solid fallback so a palette change cannot treat
* fallback paint as evidence that theme tint works. */
if (map.getAttribute('data-tint-fallback') === '1') {
map.style.removeProperty('--fwlive-pass-color');
map.style.removeProperty('--fwlive-deny-color');
map.removeAttribute('data-tint-fallback');
}
const hadPass = tr.classList.contains('fwlive-row-pass');
const hadDeny = tr.classList.contains('fwlive-row-deny');
const probeClass = hadDeny ? 'fwlive-row-deny' : 'fwlive-row-pass';
@@ -927,13 +937,12 @@ return view.extend({
this.loggingNoticeFail = false;
this._loggingNoticeFromToggle = !!this.loggingNotice;
if (opts.onSuccess) opts.onSuccess(res);
if (this.loggingStatus && typeof opts.wanLog === 'boolean')
this.loggingStatus = Object.assign({}, this.loggingStatus, {
wan_log: opts.wanLog
});
await this.loadLoggingStatus();
} catch (_e) {
this.loggingNotice = opts.catchNotice();
} catch (e) {
this.loggingNotice =
typeof opts.catchNotice === 'function'
? opts.catchNotice(e)
: _('Could not reach the router; check the current logging state.');
this.loggingNoticeFail = true;
this._loggingNoticeFromToggle = true;
await this.loadLoggingStatus();
@@ -948,7 +957,6 @@ return view.extend({
async handleEnableLogging() {
return this.runLoggingToggle({
wanLog: true,
call: () => callFwliveEnableLogging(),
initialUi: () => {
this.updateEmptyStateUi();
@@ -963,19 +971,28 @@ return view.extend({
)
: _('WAN logging is already enabled.'),
onSuccess: () => logging.persistConsentDismissed(),
catchNotice: () => _('Administrator access is required to enable logging.')
catchNotice: (err) =>
logging.toggleRpcCatchNotice(
err,
_('Administrator access is required to enable logging.'),
_('Could not reach the router; check the current logging state.')
)
});
},
async handleDisableLogging() {
return this.runLoggingToggle({
wanLog: false,
call: () => callFwliveDisableLogging(),
initialUi: () => this.updateLoggingToolbarUi(),
failureNotice: (res) =>
logging.toggleFailureNotice(res && res.error, _('Could not disable logging.')),
successNotice: (res) => (res.changed ? _('WAN drop/reject logging is off.') : ''),
catchNotice: () => _('Administrator access is required to disable logging.')
catchNotice: (err) =>
logging.toggleRpcCatchNotice(
err,
_('Administrator access is required to disable logging.'),
_('Could not reach the router; check the current logging state.')
)
});
},
@@ -1033,10 +1050,15 @@ return view.extend({
const sig = this.loggingUiSignature();
if (sig === this._loggingToolbarSig) return;
const focused = logging.captureFocus(
bar,
typeof document !== 'undefined' ? document.activeElement : null
);
logging.renderToolbar(bar, this.loggingState(), {
onEnable: () => this.handleEnableLogging(),
onDisable: () => this.handleDisableLogging()
});
logging.restoreFocus(bar, focused);
this._loggingToolbarSig = sig;
},
@@ -1048,10 +1070,24 @@ return view.extend({
if (sig === this._loggingEmptySig) return;
const visible = empty.style.display !== 'none';
const focused = logging.captureFocus(
empty,
typeof document !== 'undefined' ? document.activeElement : null
);
logging.renderEmptyState(empty, this.loggingState(), {
onEnable: () => this.handleEnableLogging(),
onDismissConsent: (persist) => this.handleDismissConsent(persist)
});
logging.restoreFocus(empty, focused);
/* Consent Enable disappears once logging is on. The replacement is the toolbar control. */
if (
focused &&
(focused.action === 'enable' || focused.action === 'disable') &&
!logging.hasLoggingAction(empty, focused.action)
) {
const bar = document.getElementById('fwlive-logging-bar');
logging.restoreFocus(bar, focused);
}
if (visible) empty.style.display = 'block';
this._loggingEmptySig = sig;
},
@@ -1361,6 +1397,7 @@ return view.extend({
if (this.renderScheduler) return this.renderScheduler;
this.renderScheduler = renderScheduler.create({
getEpoch: () => this.currentPollEpoch(),
isPaused: () => !!this.tablePaused,
render: (force) => this.renderRows(force),
renderCost: renderPolicy.renderCost,
now: () => this.nowMs(),
@@ -1884,10 +1921,14 @@ return view.extend({
this.requestPoll()
.then(() => {
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
if (epoch === this.currentPollEpoch()) {
this.resolvePaintPending = false;
this.renderRows(true);
if (epoch !== this.currentPollEpoch()) return;
/* Pause can be pressed again before this RPC returns. */
if (this.tablePaused) {
this.updateStatus();
return;
}
this.resolvePaintPending = false;
this.renderRows(true);
})
.catch(function () {});
}
@@ -2426,7 +2467,11 @@ return view.extend({
}
try {
await this.resolveHostnamesForEntries(this.filteredRows());
Promise.resolve(this.resolveHostnamesForEntries(this.filteredRows())).catch(
function () {
/* resolve unavailable — show IPs */
}
);
} catch (_e) {
/* resolve unavailable — show IPs */
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -40,9 +40,6 @@ FWLIVE_ADAPTIVE_HOT_EXIT_MS=500
FWLIVE_ADAPTIVE_COOL_CAP=250
FWLIVE_ADAPTIVE_HOT_FLOOR=250
FWLIVE_ADAPTIVE_WARM_MIN=50
FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS=300
FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS=600
FWLIVE_ADAPTIVE_COOLDOWN_PROBE_CS=300
fwlive_adaptive_lock_path() {
if [ -n "${FWLIVE_ADAPTIVE_LOCK_FILE:-}" ]; then
@@ -303,7 +300,8 @@ fwlive_adaptive_double() {
# Args: requested_limit duration_ms prev_limit prev_bucket prev_warm
# prev_completed_cs planning
# Prints next limit. A full-sized cold sample may retain one upward probe
# Prints next limit. Sample-sequence only: completed_cs > 0 means a prior
# decision exists. A full-sized cold sample may retain one upward probe
# above the number of lines served; planning then holds that retained probe.
fwlive_adaptive_compute_limit() {
_req=$1
@@ -314,48 +312,35 @@ fwlive_adaptive_compute_limit() {
_prev_c=$6
_planning=${7:-0}
_max="${POLL_LINES_MAX:-2000}"
_now=$(fwlive_adaptive_clock_cs)
_bucket=$(fwlive_adaptive_bucket_for_ms "$_ms")
_limit=$_max
_cd=0
_probe=0
# Hot/warm: hold the previous limit while samples stay hot, and probe
# upward on the first healthy sample. Record may retain that step above
# the lines just served (same as a full-sized cold probe).
case "$_prev_b" in
hot) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS ;;
warm) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS ;;
hot|warm)
if [ "$_prev_c" -gt 0 ]; then
if [ "$_ms" -gt "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] || \
[ "$_bucket" = hot ]; then
_limit=$_prev_l
fwlive_adaptive_clamp "$_req" "$_max"
printf '%s\n' "$_limit"
return 0
fi
_limit=$_prev_l
fwlive_adaptive_double "$_max"
if [ "$_planning" = 1 ]; then
fwlive_adaptive_clamp "$_req" "$_max"
else
fwlive_adaptive_clamp "$_max" "$_max"
fi
printf '%s\n' "$_limit"
return 0
fi
;;
esac
_elapsed=$((_now - _prev_c))
if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \
[ "$_elapsed" -lt "$_cd" ]; then
if [ "$_ms" -gt "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] || \
[ "$_bucket" = hot ]; then
_limit=$_prev_l
fwlive_adaptive_clamp "$_req" "$_max"
printf '%s\n' "$_limit"
return 0
fi
fi
# Once a hot/warm cooldown expires, deliberately test a larger request.
# Planning raises the next request. Recording raises the retained limit
# only when that probe completed below the hot-exit threshold. A still-hot
# probe falls through to the hot floor below.
if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \
[ "$_elapsed" -ge "$_cd" ]; then
_healthy=0
if [ "$_ms" -le "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] && \
[ "$_bucket" != hot ]; then
_healthy=1
fi
if [ "$_planning" = 1 ] || [ "$_healthy" = 1 ]; then
_limit=$_prev_l
fwlive_adaptive_double "$_max"
fwlive_adaptive_clamp "$_req" "$_max"
printf '%s\n' "$_limit"
return 0
fi
fi
case "$_bucket" in
cold)
@@ -376,12 +361,11 @@ fwlive_adaptive_compute_limit() {
fi
;;
cool)
# A healthy cooldown probe may retain its raised limit for the
# short probe window. Ordinary cool samples still use the fixed
# 250-line cap.
# Retain a raised limit while samples stay cold or cool once a
# prior decision exists. A fresh state still applies the 250-line
# cool cap.
if [ "$_prev_l" -gt "$FWLIVE_ADAPTIVE_COOL_CAP" ] && \
[ "$_prev_c" -gt 0 ] && \
[ "$_elapsed" -lt "$FWLIVE_ADAPTIVE_COOLDOWN_PROBE_CS" ]; then
[ "$_prev_c" -gt 0 ]; then
_limit=$_prev_l
else
_limit=$FWLIVE_ADAPTIVE_COOL_CAP
@@ -446,7 +430,6 @@ fwlive_adaptive__record_body() {
_prev_w=$4
_prev_c=$6
_bucket=$(fwlive_adaptive_bucket_for_ms "$_ms")
_now=$(fwlive_adaptive_clock_cs)
_limit=$(fwlive_adaptive_compute_limit "$_served" "$_ms" "$_prev_l" "$_prev_b" "$_prev_w" "$_prev_c")
_warm=0
_shed=0
@@ -454,7 +437,9 @@ fwlive_adaptive__record_body() {
warm) _warm=1 ;;
hot) _shed=1 ;;
esac
fwlive_adaptive_write_state "$_ms" "$_limit" "$_bucket" "$_warm" "$_shed" "$_now"
# 1 means a decision was recorded. 0 is fresh, so a missing uptime
# must not be stored here or the next sample drops hot/warm protection.
fwlive_adaptive_write_state "$_ms" "$_limit" "$_bucket" "$_warm" "$_shed" 1
}
fwlive_adaptive_record() {
@@ -5,13 +5,20 @@
# source: core/fwlive-log.js CLASSIFY_SPEC
# Standalone awk program loaded by fwlive-is-firewall-event.sh.
function normalize(s, keys, n, i, k) {
function normalize(s, keys, n, i, k, best, pos) {
# Glue only at the first KEY=. Exact-case glueKeys.
n = split("IN OUT SRC DST PROTO SPT DPT LEN MAC TYPE CODE TTL TOS PREC DF", keys, " ")
best = 0
for (i = 1; i <= n; i++) {
k = keys[i]
while (match(s, "[^[:space:]]" k "="))
s = substr(s, 1, RSTART) " " substr(s, RSTART + 1)
if (match(s, k "=")) {
pos = RSTART
if (best == 0 || pos < best)
best = pos
}
}
if (best > 1 && substr(s, best - 1, 1) !~ /[[:space:]]/)
s = substr(s, 1, best - 1) " " substr(s, best)
return s
}
function trim(s) {
@@ -30,7 +37,25 @@ function non_fw_prefix(s, lc) {
lc = tolower(s)
return lc ~ "^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^a-z0-9_-]|$)"
}
function strip_kv(s, out, i, n, prev, rest) {
# Same boundary as JS /\b[A-Z][A-Z0-9_]*=/. tagX=DROP keeps DROP.
out = ""
i = 1
n = length(s)
while (i <= n) {
prev = (i == 1) ? "" : substr(s, i - 1, 1)
rest = substr(s, i)
if ((prev == "" || prev !~ /[A-Za-z0-9_]/) && match(rest, /^[A-Z][A-Z0-9_]*=[^[:space:]]+/)) {
i += RLENGTH
continue
}
out = out substr(s, i, 1)
i++
}
return out
}
function detect_action(s, words, n, i, w, wl, lc, start, pos, before, afterc, best, bestpos) {
s = strip_kv(s)
n = split("ACCEPT ALLOW PASS DROP REJECT DENY BLOCK", words, " ")
lc = tolower(s)
best = ""
@@ -100,9 +125,11 @@ function json_get_msg(obj, s, i, c, esc, out, hex, n) {
}
function is_fw(s, action) {
s = trim(normalize(s))
fw_norm = s
if (s == "") return 0
if (non_fw_prefix(s)) return 0
action = detect_action(s)
fw_action = action
if (((has_kv(s, "SRC") && has_kv(s, "DST")) || ((has_kv(s, "IN") || has_kv(s, "OUT")) && (has_kv(s, "SRC") || has_kv(s, "DST") || has_kv(s, "PROTO") || has_kv(s, "SPT") || has_kv(s, "DPT"))) || (action != "UNKNOWN" && (has_kv(s, "IN") || has_kv(s, "OUT") || has_kv(s, "PROTO") || has_kv(s, "SRC") || has_kv(s, "DST"))))) return 1
if ((has_hint(s) && action != "UNKNOWN")) return 1
if ((has_hint(s) && (has_kv(s, "IN") || has_kv(s, "OUT") || has_kv(s, "SRC") || has_kv(s, "DST") || has_kv(s, "PROTO")))) return 1
@@ -124,14 +151,14 @@ function utf8_prefix(s, max_bytes, out, i, c, width) {
return out
}
function summary_kv(s, key, part) {
s = normalize(s)
s = fw_norm
if (!match(s, "(^|[[:space:]])" key "=[^[:space:]]+")) return ""
part = substr(s, RSTART, RLENGTH)
sub(/^[^=]*=/, "", part)
return part
}
function summary_rule(s, first) {
s = trim(normalize(s))
s = fw_norm
sub(/^\[[[:space:]]*[0-9.]+\][[:space:]]*/, "", s)
if (tolower(s) ~ /^fw4:[[:space:]]*/) return "fw4"
# Shared with parseRuleHint: token then IN=/OUT=/SRC=/DST=/PROTO=, else colon tag.
@@ -182,7 +209,7 @@ function summary_top(counts, limit, out, i, k, best, best_n) {
}
function summary_add(s, action, src, dst, rule) {
summary_count++
action = tolower(detect_action(normalize(s)))
action = tolower(fw_action)
if (action == "drop" || action == "reject" || action == "deny" || action == "block") summary_add_count(summary_drop_counts, action)
src = summary_kv(s, "SRC")
if (src == "") src = summary_kv(s, "DST")
@@ -594,7 +594,14 @@ maybe_snapshot_wan_log_baseline() {
else
current=$(wan_zone_log_value "$zone")
fi
printf '%s' "$current" >"$path" 2>/dev/null || return 1
# Redirection truncates before the write. If the write fails, drop the
# partial marker so the next call re-snapshots instead of treating an
# empty file as a valid "unset" baseline. A kill between open and write
# remains an accepted residual.
if ! printf '%s' "$current" >"$path" 2>/dev/null; then
rm -f "$path"
return 1
fi
return 0
}
@@ -610,8 +617,13 @@ maybe_snapshot_wan_log_baseline() {
restore_wan_log_baseline() {
path="$(wan_log_baseline_path)"
[ -f "$path" ] || return 0
# Empty file is a valid "option was unset" baseline.
baseline=$(cat "$path" 2>/dev/null || true)
# Empty file is a valid "option was unset" baseline. A failed read
# must not become that empty value: restore would then delete the
# WAN log option and retire the only recovery marker.
if ! baseline=$(cat "$path" 2>/dev/null); then
logger -t fwlive "WAN log baseline restore skipped: baseline read failed" 2>/dev/null || true
return 1
fi
find_wan_zone_section_state
zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
+32 -19
View File
@@ -38,10 +38,6 @@ POLL_LINES_MAX=2000
# shellcheck disable=SC1090,SC1091
. "$ADAPTIVE_SH"
slug_key() {
printf '%s\n' "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--'
}
# Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply.
RULES_MAP_MAX_KEYS=512
RULES_MAP_MAX_BYTES=65536
@@ -105,6 +101,14 @@ uci_rule_names() {
_firewall_show=$(uci -q show firewall 2>/dev/null || true)
printf '%s\n' "$_firewall_show" |
awk -v max="$RULES_MAP_MAX_LINES" '
function slug_uci(s, out, i, c) {
out = ""
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
out = out ((c == "_") ? "-" : tolower(c))
}
return out
}
function emit(value, quote) {
quote = sprintf("%c", 39)
if (substr(value, 1, 1) == quote && substr(value, length(value), 1) == quote)
@@ -113,7 +117,7 @@ uci_rule_names() {
truncated = 1
exit 42
}
print value
print value "\t" slug_uci(value)
count++
}
{
@@ -179,15 +183,18 @@ map_uci_rule_names() {
_uci_names_truncated=''
fi
fi
while IFS= read -r name || [ -n "$name" ]; do
_tab=$(printf '\t')
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_map_overflow" ] && break
[ -n "$line" ] || continue
name="${line%%${_tab}*}"
slug="${line#*"$_tab"}"
[ -n "$name" ] || continue
is_uci_style_name "$name" || continue
map_add "$name" "$name"
map_add_escaped "$name" "$name"
[ -n "$_map_overflow" ] && break
slug=$(slug_key "$name")
if [ "$slug" != "$name" ]; then
map_add "$slug" "$name"
if [ -n "$slug" ] && [ "$slug" != "$name" ]; then
map_add_escaped "$slug" "$name"
fi
done <<EOF
$_uci_names
@@ -733,7 +740,8 @@ parse_nslookup_name() {
} else if (match(low, /[^a-z0-9_]name[ \t]*=[ \t]*/)) {
pre = substr(low, 1, RSTART - 1)
sub(/[ \t]+$/, "", pre)
if (pre != "domain" && pre !~ /[^a-z0-9_]domain$/)
if (pre != "domain" && pre !~ /[^a-z0-9_]domain$/ && \
pre != "canonical" && pre !~ /[^a-z0-9_]canonical$/)
host = substr($0, RSTART + RLENGTH)
}
if (host != "") {
@@ -1016,14 +1024,6 @@ run_selftest() {
echo 'is_uci_style_name: expected reject empty' >&2
return 1
fi
# echo would swallow -n/-E and dash echo would expand backslashes.
_slug=$(slug_key '-n')
check_eq 'slug_key -n' "$_slug" '-n' "expected -n got $_slug" || return 1
_slug=$(slug_key '-E')
check_eq 'slug_key -E' "$_slug" '-e' "expected -e got $_slug" || return 1
_slug=$(slug_key "$(printf 'a\\tb')")
check_eq 'slug_key backslash' "$_slug" "$(printf 'a\\tb')" "expected literal backslash got $_slug" || return 1
if is_resolvable_address 'not an ip'; then
echo 'is_resolvable_address: expected reject for spaces' >&2
return 1
@@ -1250,6 +1250,19 @@ Address: 127.0.0.1:53")
check_eq parse_nslookup_name "$got" '' "domain tab name = must not parse as name got '$got'" || return 1
got=$(parse_nslookup_name "name = dns.google. hostname = evil.example.")
check_eq parse_nslookup_name "$got" "dns.google" "standalone name = must beat hostname = got '$got'" || return 1
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53
Non-authoritative answer:
37.191.152.204.in-addr.arpa canonical name = 37.32-27.191.152.204.in-addr.arpa
37.32-27.191.152.204.in-addr.arpa name = zeus-pub2.kernel.org")
check_eq parse_nslookup_name "$got" "zeus-pub2.kernel.org" "RFC 2317 PTR must beat canonical name got '$got'" || return 1
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53
Non-authoritative answer:
37.191.152.204.in-addr.arpa canonical name = 37.32-27.191.152.204.in-addr.arpa")
check_eq parse_nslookup_name "$got" '' "CNAME-only must not return canonical target got '$got'" || return 1
run_logging_selftest || return 1
+1 -1
View File
@@ -41,7 +41,7 @@ LUCI_DEPENDS:= \
PKG_NAME:=luci-app-homeproxy-pro
PKG_VERSION:=28.10.1.14
PKG_RELEASE:=27
PKG_RELEASE:=28
LUCI_BASENAME:=homeproxy-pro
+4
View File
@@ -1,3 +1,7 @@
<p align="center">
<img src="assets/daed-note.png" alt="转战 daed,值得研究 https://github.com/szwjp/daed" width="193" height="17">
</p>
<div align="center">
# luci-app-homeproxy-pro
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -7,7 +7,7 @@ PKG_BASE_VERSION?=0.22.0
# Rewritten automatically by .github/workflows/sync-upstream.yml; the LuCI
# package always carries the same version as the core it drives.
PKG_VERSION?=$(PKG_BASE_VERSION).202610030155
PKG_VERSION?=$(PKG_BASE_VERSION).202610130311
PKG_RELEASE?=1
include $(INCLUDE_DIR)/package.mk
@@ -3,6 +3,7 @@
'require rpc';
'require fs';
'require uci';
'require ui';
// Shared helpers for the meow LuCI views. Runtime data comes straight from
// the meow REST API through authenticated LuCI RPC. The standalone panel
@@ -16,6 +17,21 @@ var callServiceList = rpc.declare({
});
return baseclass.extend({
// Prefer LuCI's native timed notification API; OpenWrt 23.05 and 24.10.0
// lack it and need a local timer. 24.10.0's addNotification(title,
// children, timeout, ...classes) takes `type` as a (non-numeric, ignored)
// timeout, so set the class explicitly; classList.add is idempotent.
addTimeLimitedNotification: function(title, children, timeout, type) {
if (typeof ui.addTimeLimitedNotification === 'function')
return ui.addTimeLimitedNotification(title, children, timeout, type);
var notification = ui.addNotification(title, children, type);
if (type)
notification.classList.add(type);
setTimeout(function() { notification.remove(); }, timeout);
return notification;
},
apiBase: function() {
var port = uci.get('meow', 'main', 'panel_port') || '9090';
return 'http://' + window.location.hostname +
@@ -5,6 +5,7 @@
'require rpc';
'require uci';
'require ui';
'require tools.meow as meow';
// LAN clients known to this router. The main control is a per-client proxy
// bypass: ticked MACs are stored as `tproxy.bypass_mac` in /etc/config/meow and
@@ -323,7 +324,7 @@ return view.extend({
return fs.exec('/etc/init.d/meow-arp', [ 'restart' ]).catch(function() {});
})
.then(function() {
ui.addTimeLimitedNotification(null, E('p', arpEnabled
meow.addTimeLimitedNotification(null, E('p', arpEnabled
? _('Bypassing %d client(s); steering %d.').format(bypass.length, steer.length)
: _('Bypassing %d client(s); steering disabled.').format(bypass.length)), 4000, 'info');
})
@@ -71,7 +71,7 @@ return view.extend({
if (err)
ui.addNotification(_('Invalid configuration'), E('pre', {}, err), 'error');
else
ui.addTimeLimitedNotification(null, E('p', _('Configuration test passed')), 3000, 'info');
meow.addTimeLimitedNotification(null, E('p', _('Configuration test passed')), 3000, 'info');
}).catch(function(e) {
ui.addNotification(null, E('p', _('Unable to validate: %s').format(e.message)), 'error');
});
@@ -109,7 +109,7 @@ return view.extend({
});
});
}).then(function(restarted) {
ui.addTimeLimitedNotification(null, E('p', restarted
meow.addTimeLimitedNotification(null, E('p', restarted
? _('Configuration saved and service restarted.')
: _('Configuration saved; service remains stopped.')), 5000, 'info');
}).catch(function(error) {
@@ -151,7 +151,7 @@ return view.extend({
}).then(function(running) {
if (running !== want)
throw new Error(want ? _('meow did not start; see the Log tab.') : _('meow is still running.'));
ui.addTimeLimitedNotification(null, E('p', {
meow.addTimeLimitedNotification(null, E('p', {
start: _('meow started.'), stop: _('meow stopped.'), restart: _('meow restarted.')
}[action]), 3000, 'info');
}).catch(function(e) {
+2 -2
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.1
PKG_RELEASE:=9
PKG_VERSION:=1.2.2
PKG_RELEASE:=10
LUCI_TITLE:=LuCI Support for momo
LUCI_DEPENDS:=+luci-base +momo
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.326
PKG_RELEASE:=29
PKG_VERSION:=0.1.333
PKG_RELEASE:=30
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.4
PKG_RELEASE:=333
PKG_VERSION:=26.10.16
PKG_RELEASE:=334
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -75,6 +75,7 @@ function index()
entry({"admin", "services", appname, "get_redir_log"}, call("get_redir_log")).leaf = true
entry({"admin", "services", appname, "get_socks_log"}, call("get_socks_log")).leaf = true
entry({"admin", "services", appname, "get_chinadns_log"}, call("get_chinadns_log")).leaf = true
entry({"admin", "services", appname, "get_smartdns_log"}, call("get_smartdns_log")).leaf = true
entry({"admin", "services", appname, "get_log"}, call("get_log")).leaf = true
entry({"admin", "services", appname, "clear_log"}, call("clear_log")).leaf = true
entry({"admin", "services", appname, "index_status"}, call("index_status")).leaf = true
@@ -315,6 +316,16 @@ function get_chinadns_log()
end
end
function get_smartdns_log()
local path = api.TMP_PATH .. "/acl/default/smartdns.log"
http.prepare_content("text/plain; charset=utf-8")
if fs.access(path) then
http.write(luci.sys.exec("tail -n 5000 " .. util.shellquote(path)))
else
http.write(i18n.translate("Not enabled log"))
end
end
function get_log()
-- luci.sys.exec("[ -f /tmp/log/passwall.log ] && sed '1!G;h;$!d' /tmp/log/passwall.log > /tmp/log/passwall_show.log")
http.write(luci.sys.exec("[ -f '/tmp/log/passwall.log' ] && cat /tmp/log/passwall.log"))
@@ -403,8 +414,8 @@ function index_status()
local e = {}
local dns_shunt = uci_get("@global[0]", "dns_shunt") or "dnsmasq"
if dns_shunt == "smartdns" then
local port = api.get_cache_var("SMARTDNS_LOCAL_PORT") or 0
e.dns_mode_status = (port ~= 0) and luci.sys.call("netstat -apn | grep ':%s ' >/dev/null" % port) == 0 or false
local config_file = api.TMP_PATH .. "/acl/default/smartdns.conf"
e.dns_mode_status = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep -F %s | grep 'smartdns' >/dev/null" % util.shellquote(config_file)) == 0
elseif dns_shunt == "chinadns-ng" then
e.dns_mode_status = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep 'default' | grep 'chinadns_ng' >/dev/null" % api.TMP_PATH) == 0
else
@@ -176,26 +176,10 @@ o.write = function(self, section, value)
if old and old ~= value then
m:set(section, "flush_set", "1")
end
if value ~= "smartdns" then
m:del(section, "group_domestic")
end
return ListValue.write(self, section, value)
end
if has_smartdns then
o:value("smartdns", "SmartDNS")
o = s:taboption("DNS", Value, "group_domestic", translate("Domestic group name"))
o.placeholder = "local"
o.rmempty = false
o:depends("dns_shunt", "smartdns")
o.description = translate("You only need to configure domestic DNS packets in SmartDNS, and fill in the domestic DNS group name here.")
o.validate = function(self, value, section)
value = api.trim(value)
if value == "" then
return nil, translatef("%s cannot be empty.", "SmartDNS " .. translate("Domestic group name"))
end
return value
end
end
o = s:taboption("DNS", ListValue, "direct_dns_mode", translate("Direct DNS") .. " " .. translate("Request protocol"))
@@ -215,8 +199,25 @@ o:value("180.184.2.2")
o:value("114.114.114.114")
o:value("114.114.115.115")
o:value("119.28.28.28")
o:depends("direct_dns_mode", "udp")
o:depends("direct_dns_mode", "tcp")
o:depends({dns_shunt = "dnsmasq", direct_dns_mode = "udp"})
o:depends({dns_shunt = "dnsmasq", direct_dns_mode = "tcp"})
o:depends({dns_shunt = "chinadns-ng", direct_dns_mode = "udp"})
o:depends({dns_shunt = "chinadns-ng", direct_dns_mode = "tcp"})
if has_smartdns then
o = s:taboption("DNS", DynamicList, "smartdns_direct_dns", translate("Direct DNS"))
o.placeholder = translate("Auto")
o.rmempty = true
o:value("223.5.5.5")
o:value("119.29.29.29")
o:value("tcp://223.5.5.5")
o:value("tcp://114.114.114.114")
o:value("tls://dot.pub,1.12.12.12")
o:value("tls://dns.alidns.com,223.5.5.5")
o:value("https://223.5.5.5/dns-query")
o:value("https://1.12.12.12/dns-query")
o:depends({dns_shunt = "smartdns"})
end
o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"))
o.default = "0"
@@ -695,6 +696,90 @@ o.cfgvalue = function(t, n)
end
o:depends("log_chinadns_ng", "1")
if has_smartdns then
o = s:taboption("log", Flag, "log_smartdns", translate("Enable") .. " SmartDNS " .. translate("Log"))
o.default = "0"
o.rmempty = false
o:depends("dns_shunt", "smartdns")
o = s:taboption("log", ListValue, "smartdns_log_level", "SmartDNS " .. translate("Log Level"))
o.default = "error"
o:value("fatal", "Fatal")
o:value("error", "Error")
o:value("warn", "Warning")
o:value("notice", "Notice")
o:value("info", "Info")
o:value("debug", "Debug")
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
o = s:taboption("log", Value, "smartdns_log_size", translate("Log Size") .. " (MB)")
o.default = "1"
o.datatype = "and(uinteger,min(1))"
o.rmempty = false
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
o = s:taboption("log", DummyValue, "_smartdns_log", translate("Log File"))
o.rawhtml = true
o.cfgvalue = function(t, n)
local log_file = api.TMP_PATH .. "/acl/default/smartdns.log"
local log_url = api.url("get_smartdns_log")
local s = "<code>%s</code>&nbsp;&nbsp;" % log_file
if api.fs.access(log_file) then
local btn = string.format(
'<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
translate("View Log"),
log_url
)
s = s .. btn
end
return s
end
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
if api.fs.access("/usr/lib/smartdns_ui.so") or api.fs.access("/usr/lib/smartdns/smartdns_ui.so") then
o = s:taboption("log", Flag, "smartdns_ui", "SmartDNS " .. translate("Enable WebUI"))
o.default = "0"
o.rmempty = false
o:depends("dns_shunt", "smartdns")
o = s:taboption("log", Value, "smartdns_ui_port", translate("WebUI Port"))
o.default = "9880"
o.datatype = "range(1,65535)"
o.rmempty = false
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o = s:taboption("log", Value, "smartdns_ui_data_dir", translate("WebUI Database Directory"))
o.default = api.CACHE_PATH .. "/smartdns"
o.rmempty = false
o.validate = function(self, value, section)
value = api.trim(value)
if value:sub(1, 1) ~= "/" or value:find("%s") then
return nil, translate("An absolute path without spaces is required.")
end
return value
end
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o = s:taboption("log", Value, "smartdns_ui_log_max_age", translate("WebUI Log Retention Days"))
o.default = "7"
o.datatype = "and(uinteger,min(1))"
o.rmempty = false
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o = s:taboption("log", DummyValue, "_smartdns_ui_link", "WebUI")
o.rawhtml = true
o.cfgvalue = function(self, section)
local port = tonumber(m:get(section, "smartdns_ui_port")) or 9880
if port < 1 or port > 65535 or port % 1 ~= 0 then port = 9880 end
return string.format(
[[<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="var host = window.location.hostname; if (host.indexOf(':') !== -1 &amp;&amp; host.charAt(0) !== '[') host = '[' + host + ']'; window.open('http://' + host + ':%d', '_blank', 'noopener,noreferrer');" />]],
translate("Open WebUI"), port
)
end
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
end
end
o = s:taboption("log", DummyValue, "_log_tips", " ")
o.rawhtml = true
o.cfgvalue = function(t, n)
@@ -41,15 +41,9 @@ o:value("vless", translate("VLESS"))
o:value("shadowsocks", translate("Shadowsocks"))
o:value("trojan", translate("Trojan"))
o:value("wireguard", translate("WireGuard"))
if api.compare_versions(xray_version, ">=", "26.1.13") then
o:value("hysteria2", translate("Hysteria2"))
end
if api.compare_versions(xray_version, ">=", "26.9.30") then
o:value("masque", "MASQUE")
end
if api.compare_versions(xray_version, ">=", "1.8.12") then
o:value("_balancing", translate("Balancing"))
end
o:value("hysteria2", translate("Hysteria2"))
o:value("masque", "MASQUE")
o:value("_balancing", translate("Balancing"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
local protocol_val = m:get(s.section, "protocol")
@@ -274,6 +268,29 @@ o:depends({ protocol = "trojan" })
o:depends({ protocol = "masque" })
-- [[ MASQUE ]] --
o = s:option(Flag, "masque_warp", "WARP")
o.default = "0"
o:depends({ protocol = "masque" })
for _, field in ipairs({
{ "masque_warp_private_key", translate("WARP Device Private Key") },
{ "masque_warp_public_key", translate("WARP Endpoint Public Key") }
}) do
o = s:option(TextValue, field[1], field[2])
o.rows = 3
o.wrap = "off"
o:depends({ protocol = "masque", masque_warp = "1" })
o.cfgvalue = function(self, section)
return (m:get(section, self.config_option) or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
return api.trim(value):gsub("\r\n?", "\n"):gsub("\n", "\\n")
end
end
o = s:option(DynamicList, "masque_warp_address", "WARP " .. translate("Local Address"))
o:depends({ protocol = "masque", masque_warp = "1" })
o = s:option(Value, "masque_path", translate("MASQUE Path"))
o:depends({ protocol = "masque" })
@@ -338,11 +355,7 @@ o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
if api.compare_versions(xray_version, ">", "26.5.9") then
o:depends({ protocol = "hysteria2"})
else
o:depends({ protocol = "__hide"})
end
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
@@ -442,6 +455,7 @@ o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true, reality = false })
o:depends({ protocol = "masque" })
-- o = s:option(Value, "minversion", translate("minversion"))
-- o.default = "1.3"
@@ -575,10 +589,8 @@ o = s:option(Value, "wireguard_mtu", translate("MTU"))
o.default = "1420"
o:depends({ protocol = "wireguard" })
if api.compare_versions(xray_version, ">=", "1.8.0") then
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
end
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive"))
o.default = "0"
@@ -771,6 +783,7 @@ o:depends({ protocol = "trojan" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "wireguard" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "finalmask", " ")
o:depends({ use_finalmask = true })
@@ -387,7 +387,7 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -400,7 +400,7 @@ o.validate = function(self, value)
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -325,7 +325,7 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -338,7 +338,7 @@ o.validate = function(self, value)
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -138,9 +138,7 @@ function gen_outbound(flag, node, tag, proxy_table)
if node.protocol == "masque" then
node.transport = "masque"
node.stream_security = "tls"
node.alpn = nil
node.utls = nil
node.finalmask = nil
end
if node.protocol == "http" and node.stream_security == "tls" then
@@ -277,9 +275,14 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil,
masqueSettings = (node.protocol == "masque") and {
path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil,
warp = (node.masque_warp == "1") and {
privateKey = (node.masque_warp_private_key or ""):gsub("\\n", "\n"),
publicKey = (node.masque_warp_public_key or ""):gsub("\\n", "\n"),
address = node.masque_warp_address
} or nil,
headers = (function()
local headers = {}
if (node.username and node.username ~= "") or (node.password and node.password ~= "") then
if node.masque_warp ~= "1" and ((node.username and node.username ~= "") or (node.password and node.password ~= "")) then
headers["authorization"] = "Basic " .. api.base64Encode((node.username or "") .. ":" .. (node.password or ""))
end
if node.user_agent and node.user_agent ~= "" then
@@ -229,5 +229,37 @@ local sid = "@global[0]"
select.insertAdjacentElement("afterend", logLink);
};
})();
(function () {
function initSmartdnsDirectAuto() {
const field = document.getElementById("cbi-<%=appname%>-" + global_id + "-smartdns_direct_dns");
if (!field) return;
const update = () => {
const list = field.querySelector(".cbi-dynlist");
if (!list) return;
const hasDns = Array.from(list.querySelectorAll('.item > input[type="hidden"]'))
.some(input => input.value.trim() !== "");
const text = hasDns ? <%=api.jsonc.stringify(translate("-- Please choose --"))%> : "<%:Auto%>";
const placeholder = list.querySelector(".add-item .cbi-dropdown > ul > li[placeholder]");
// 只修改空选项的提示文字,不添加 DNS 条目,也不改变正在输入的内容。
if (placeholder && placeholder.textContent !== text) {
placeholder.textContent = text;
}
const input = list.querySelector('.add-item > input[type="text"]');
if (input) input.placeholder = hasDns ? "" : "<%:Auto%>";
};
// 同时处理 LuCI 延迟渲染,以及添加、删除 DNS 后的空列表状态。
new MutationObserver(update).observe(field, { childList: true, subtree: true });
update();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initSmartdnsDirectAuto, { once: true });
} else {
initSmartdnsDirectAuto();
}
})();
//]]>
</script>
+27 -213
View File
@@ -70,15 +70,9 @@ msgstr "HTTP 请求延迟,仅供参考"
msgid "Initial handshake time"
msgstr "首次握手耗时"
msgid "Kernel Unsupported"
msgstr "内核不支持"
msgid "Settings"
msgstr "设置"
msgid "Main Settings"
msgstr "节点选择"
msgid "Basic Settings"
msgstr "基本设置"
@@ -109,9 +103,6 @@ msgstr "运行日志"
msgid "Node Config"
msgstr "节点配置"
msgid "Shunt Policy Config"
msgstr "分流方案配置"
msgid "Running Status"
msgstr "运行状态"
@@ -127,24 +118,15 @@ msgstr "GitHub 连接"
msgid "YouTube Conn."
msgstr "YouTube 连接"
msgid "Node Check"
msgstr "节点检测"
msgid "Check..."
msgstr "检测中..."
msgid "Clear"
msgstr "清除"
msgid "Main switch"
msgstr "主开关"
msgid "Proxy Node"
msgstr "代理节点"
msgid "Edit Current Node"
msgstr "编辑当前节点"
msgid "Socks Config"
msgstr "Socks 配置"
@@ -166,18 +148,6 @@ msgstr "0 为不使用"
msgid "Current node: %s"
msgstr "当前节点:%s"
msgid "DNS Shunt"
msgstr "DNS分流"
msgid "Domestic group name"
msgstr "国内分组名"
msgid "You only need to configure domestic DNS packets in SmartDNS, and fill in the domestic DNS group name here."
msgstr "您只需要在SmartDNS配置好国内DNS分组,并在此处填入国内DNS分组名。"
msgid "%s cannot be empty."
msgstr "%s 不能为空。"
msgid "Filter Mode"
msgstr "过滤模式"
@@ -190,9 +160,6 @@ msgstr "直连 DNS"
msgid "Remote DNS"
msgstr "远程 DNS"
msgid "Resolver For The List Proxied"
msgstr "解析被代理的域名列表"
msgid "Requery DNS By %s"
msgstr "通过 %s 请求 DNS"
@@ -277,9 +244,6 @@ msgstr "适合让节点服务器获取目标域名的场景。"
msgid "Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."
msgstr "例如:流媒体的 DNS 解锁、减少 DNS 查询延迟等。"
msgid "Redirect"
msgstr "重定向"
msgid "DNS Redirect"
msgstr "DNS 重定向"
@@ -343,27 +307,9 @@ msgstr "在主菜单中隐藏:"
msgid "Show in main menu:"
msgstr "在主菜单中显示:"
msgid "DNS Export Of Multi WAN"
msgstr "国内 DNS 指定解析出口"
msgid "Node Export Of Multi WAN"
msgstr "节点指定出口"
msgid "Only support Multi Wan."
msgstr "只有多线接入才有效。"
msgid "Not Specify"
msgstr "不指定"
msgid "custom"
msgstr "自定义"
msgid "Process"
msgstr "进程"
msgid "1 Process"
msgstr "单进程"
msgid "Use %s"
msgstr "使用 %s"
@@ -394,9 +340,6 @@ msgstr "中国列表以外"
msgid "China List"
msgstr "中国列表"
msgid "Localhost"
msgstr "本机"
msgid "Switch Mode"
msgstr "切换模式"
@@ -418,12 +361,6 @@ msgstr "缺少组件,透明代理不可用。"
msgid "Want different devices to use different proxy modes/ports/nodes? Please use access control."
msgstr "希望不同设备使用不同的代理模式/端口/节点?请使用访问控制。"
msgid "Operation"
msgstr "操作"
msgid "Add Node"
msgstr "添加节点"
msgid "Add the node via the link"
msgstr "通过链接添加节点"
@@ -451,15 +388,9 @@ msgstr "删除选择的节点"
msgid "To Top"
msgstr "置顶"
msgid "Select"
msgstr "选择"
msgid "Selected:"
msgstr "已选:"
msgid "DeSelect"
msgstr "反选"
msgid "Select all"
msgstr "全选"
@@ -481,9 +412,6 @@ msgstr "应用为分流规则节点:"
msgid "Are you sure set this node?"
msgstr "你确定要设置这个节点吗?"
msgid "the server?"
msgstr "服务器吗?"
msgid "You choose node is:"
msgstr "你选择的节点是:"
@@ -493,9 +421,6 @@ msgstr "超时"
msgid "Node Remarks"
msgstr "节点备注"
msgid "Add Mode"
msgstr "添加方式"
msgid "Drag to reorder"
msgstr "拖动以重排"
@@ -617,12 +542,6 @@ msgstr "黑洞(屏蔽)"
msgid "There are no available nodes, please add or subscribe nodes first."
msgstr "没有可用节点,请先添加或订阅节点。"
msgid "No shunt rules? Click me to go to add."
msgstr "没有分流规则?点我前往去添加。"
msgid "When using, localhost will connect this node first and then use this node to connect the default node."
msgstr "当使用时,本机将首先连接到此节点,然后再使用此节点连接到默认节点落地。"
msgid "Domain Strategy"
msgstr "域名解析策略"
@@ -692,33 +611,12 @@ msgstr "代码错误,请协助捉虫:"
msgid "Faltal on set option, please help in debug:"
msgstr "代码错误,请协助捉虫:"
msgid "Address"
msgstr "地址"
msgid "Address (Support Domain Name)"
msgstr "地址(支持域名)"
msgid "Trojan Verify Cert"
msgstr "验证证书"
msgid "Trojan Cert Path"
msgstr "证书路径"
msgid "Finger Print"
msgstr "指纹伪造"
msgid "Avoid using randomized, unless you have to."
msgstr "避免使用 randomized , 除非你必须要。"
msgid "Original"
msgstr "原版"
msgid "Transport Plugin"
msgstr "传输层插件"
msgid "Shadowsocks secondary encryption"
msgstr "Shadowsocks 二次加密"
msgid "Obfs Type"
msgstr "混淆类型"
@@ -731,18 +629,9 @@ msgstr "认证类型"
msgid "Auth Password"
msgstr "认证密码"
msgid "Client BBR Control"
msgstr "客户端 BBR 流控"
msgid "Commands the client to use the BBR flow control algorithm"
msgstr "命令客户端使用 BBR 流量控制算法"
msgid "PinSHA256"
msgstr "PinSHA256"
msgid "Certificate fingerprint"
msgstr "证书指纹"
msgid "Max upload Mbps"
msgstr "最大上行(Mbps)"
@@ -773,12 +662,6 @@ msgstr "延迟启动"
msgid "Encrypt Method"
msgstr "加密方式"
msgid "Encrypt Key"
msgstr "加密密钥"
msgid "Latency"
msgstr "延迟"
msgid "Automatic detection delay"
msgstr "自动检测延迟"
@@ -797,12 +680,6 @@ msgstr "测试"
msgid "Node num"
msgstr "节点数量"
msgid "Self add"
msgstr "自添"
msgid "Apply"
msgstr "应用"
msgid "Use"
msgstr "使用"
@@ -821,9 +698,6 @@ msgstr "开机时延时启动"
msgid "Units:seconds"
msgstr "单位:秒"
msgid "Units:minutes"
msgstr "单位:分钟"
msgid "stop automatically mode"
msgstr "定时关闭模式"
@@ -905,15 +779,9 @@ msgstr "劫持ICMP (PING)"
msgid "Hijacking ICMPv6 (IPv6 PING)"
msgstr "劫持ICMPv6 (IPv6 PING)"
msgid "Sniffing"
msgstr "流量嗅探"
msgid "TCP Proxy Way"
msgstr "TCP 代理方式"
msgid "Proxy Settings"
msgstr "代理配置"
msgid "Auto Switch"
msgstr "自动切换"
@@ -926,9 +794,6 @@ msgstr "超时秒数"
msgid "Timeout retry num"
msgstr "超时重试次数"
msgid "Main node"
msgstr "主节点"
msgid "List of backup nodes"
msgstr "备用节点的列表"
@@ -947,9 +812,6 @@ msgstr "通过关键字删除备用节点列表的节点"
msgid "Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase."
msgstr "请输入节点关键字,注意区分空格、大写和小写。"
msgid "Configure this node with 127.0.0.1: this port"
msgstr "使用 127.0.0.1 和此端口配置节点"
msgid "Enable Load Balancing"
msgstr "开启负载均衡"
@@ -1001,9 +863,6 @@ msgstr "节点地址"
msgid "Node address cannot be empty."
msgstr "节点地址不能为空。"
msgid "Node Port"
msgstr "节点端口"
msgid "Node Weight"
msgstr "负载比重"
@@ -1040,9 +899,6 @@ msgstr "回滚中..."
msgid "Update Options"
msgstr "更新选项"
msgid "Enable custom URL"
msgstr "启用自定义规则地址"
msgid "GFW domains(gfwlist) Update URL"
msgstr "防火墙域名列表(gfwlist)更新URL"
@@ -1142,9 +998,6 @@ msgstr "请注意优先级问题,排序越上面优先级越高。"
msgid "This ID already exists."
msgstr "该 ID 已存在。"
msgid "Update..."
msgstr "更新中"
msgid "It is the latest version"
msgstr "已是最新版本"
@@ -1181,21 +1034,9 @@ msgstr "组件更新"
msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."
msgstr "如果你希望从内存中运行,请更改路径,/tmp 开头,然后保存应用后,再手动更新。"
msgid "Make sure there is enough space to install %s"
msgstr "确保有足够的空间安装 %s"
msgid "App Path"
msgstr "程序路径"
msgid "%s App Path"
msgstr "%s 程序路径"
msgid "%s Client App Path"
msgstr "%s 客户端程序路径"
msgid "alternate API URL for version checking"
msgstr "用于版本检查的 API URL"
msgid "GitHub Proxy"
msgstr "GitHub 反代"
@@ -1226,18 +1067,12 @@ msgstr "请先保存并应用后再手动订阅。"
msgid "Subscribe URL cannot be empty."
msgstr "订阅网址不能为空。"
msgid "Subscribe via proxy"
msgstr "通过代理订阅"
msgid "Update Once on Boot"
msgstr "开机更新一次"
msgid "Updates the subscription the first time PassWall runs automatically after each system boot."
msgstr "路由每次启动后,首次自动运行 PassWall 时更新订阅。"
msgid "Enable auto update subscribe"
msgstr "开启自动更新订阅"
msgid "Manual subscription"
msgstr "手动订阅"
@@ -1322,9 +1157,6 @@ msgstr "隧道接口"
msgid "Invalid interface name"
msgstr "无效的接口名称"
msgid "Use Interface With ACLs"
msgstr "使用接口控制"
msgid "Remarks"
msgstr "备注"
@@ -1445,9 +1277,6 @@ msgstr "TCP 封装 UDP"
msgid "Connection Timeout"
msgstr "连接超时时间"
msgid "Local Port"
msgstr "本地端口"
msgid "Fast Open"
msgstr "快速打开"
@@ -1475,15 +1304,6 @@ msgstr "混淆"
msgid "Obfs_param"
msgstr "混淆参数"
msgid "Plugin Name"
msgstr "插件名称"
msgid "Plugin Arguments"
msgstr "插件参数"
msgid "User Level"
msgstr "用户等级(level)"
msgid "Transport"
msgstr "传输方式"
@@ -1517,12 +1337,6 @@ msgstr "伪装域名"
msgid "Camouflage Type"
msgstr "伪装类型"
msgid "Transport Layer Encryption"
msgstr "传输层加密"
msgid "Whether or not transport layer encryption is enabled, \"none\" for unencrypted, \"tls\" for using TLS, \"xtls\" for using XTLS."
msgstr "是否启入传输层加密,支持的选项有 \"none\" 表示不加密,\"tls\" 表示使用 TLS,\"xtls\" 表示使用 XTLS。"
msgid "Transfer mode"
msgstr "传输模式"
@@ -1568,12 +1382,6 @@ msgstr "身份认证"
msgid "This username already exists."
msgstr "该用户名已存在。"
msgid "Node Number"
msgstr "节点数量"
msgid "You can only set up a maximum of %s nodes for the time being, Used for access control."
msgstr "目前最多只能设置 %s 个节点,用于给访问控制使用。"
msgid "Prefer firewall tools"
msgstr "首选防火墙工具"
@@ -1628,18 +1436,6 @@ msgstr "接受局域网访问"
msgid "When selected, it can accessed lan , this will not be safe!"
msgstr "当勾选时,可以直接访问局域网,这将不安全!(非特殊情况不建议开启)"
msgid "Enable Remote"
msgstr "启用转发"
msgid "You can forward to Nginx/Caddy/V2ray/Xray WebSocket and more."
msgstr "您可以转发到 Nginx/Caddy/V2ray/Xray WebSocket 等。"
msgid "Remote Address"
msgstr "远程地址"
msgid "Remote Port"
msgstr "远程端口"
msgid "as:"
msgstr "如:"
@@ -1694,6 +1490,27 @@ msgstr "启用节点日志"
msgid "Log Level"
msgstr "日志等级"
msgid "Log Size"
msgstr "日志大小"
msgid "Enable WebUI"
msgstr "启用 WebUI"
msgid "Open WebUI"
msgstr "打开 WebUI"
msgid "WebUI Port"
msgstr "WebUI 端口"
msgid "WebUI Database Directory"
msgstr "WebUI 数据库目录"
msgid "WebUI Log Retention Days"
msgstr "WebUI 日志保存天数"
msgid "An absolute path without spaces is required."
msgstr "请输入不含空格的绝对路径。"
msgid "Not enabled log"
msgstr "未启用日志"
@@ -1706,9 +1523,6 @@ msgstr "正常使用时建议关闭日志,以减少系统开销。"
msgid "UDP Forward"
msgstr "UDP 转发"
msgid "DNS Settings"
msgstr "DNS 设置"
msgid "Null"
msgstr "无"
@@ -1739,9 +1553,6 @@ msgstr "请指定文件路径。"
msgid "%s not enough space."
msgstr "%s 空间不足。"
msgid "Can't find client in file: %s"
msgstr "无法在文件中找到客户端:%s"
msgid "Client file is required."
msgstr "请指定客户端文件。"
@@ -1820,9 +1631,6 @@ msgstr "握手服务器端口"
msgid "Override the connection destination address with the sniffed domain.<br />Otherwise use sniffed domain for routing only.<br />If using shunt nodes, configure the domain shunt rules correctly."
msgstr "用探测出的域名覆盖连接目标地址。<br />否则仅将探测得到的域名用于路由。<br />如使用分流节点,请正确设置域名分流规则。"
msgid "Override the connection destination address with the sniffed domain.<br />When enabled, traffic will match only by domain, ignoring IP rules.<br />If using shunt nodes, configure the domain shunt rules correctly."
msgstr "用探测出的域名覆盖连接目标地址。<br />启用后仅使用域名进行流量匹配,将忽略IP规则。<br />如使用分流节点,请正确设置域名分流规则。"
msgid "Protocol parameter. Will waste traffic randomly if enabled."
msgstr "协议参数。如果启用会随机浪费流量。"
@@ -2231,3 +2039,9 @@ msgstr "自定义 HTTP 请求头"
msgid "MASQUE Address"
msgstr "MASQUE 隧道地址"
msgid "WARP Device Private Key"
msgstr "WARP 设备私钥"
msgid "WARP Endpoint Public Key"
msgstr "WARP 服务端公钥"
@@ -658,10 +658,11 @@ start_global() {
local _remote_dns_client_ip=$(config_n_get @global[0] remote_dns_client_ip)
[ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}"
[ "${DNS_CACHE}" = "0" ] && _args="${_args} dns_cache=0"
resolve_dns_port=${NEXT_DNS_LISTEN_PORT}
resolve_dns_port=$(get_new_port "$NEXT_DNS_LISTEN_PORT" tcp,udp)
[ -n "$resolve_dns_port" ] || return 1
_args="${_args} dns_listen_port=${resolve_dns_port}"
case "$(config_n_get @global[0] direct_dns_mode "auto")" in
case "$(get_direct_dns_mode)" in
udp)
_args="${_args} direct_dns_udp_server=$(normalize_dns "$(config_n_get @global[0] direct_dns 223.5.5.5:53)")"
;;
@@ -694,7 +695,7 @@ start_global() {
USE_FAKEDNS=1
}
_args="${_args} remote_rewrite_ttl=$(config_n_get @global[0] remote_rewrite_ttl)"
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
NEXT_DNS_LISTEN_PORT=$(expr $resolve_dns_port + 1)
}
_args="${_args} use_proxy_list=$USE_PROXY_LIST use_gfw_list=$USE_GFW_LIST chn_list=$CHN_LIST"
run_singbox flag=$_flag node=$NODE redir_port=$REDIR_PORT tcp_proxy_way=$TCP_PROXY_WAY config_file=$config_file log_file=$log_file ${_args}
@@ -741,10 +742,11 @@ start_global() {
local _remote_dns_client_ip=$(config_n_get @global[0] remote_dns_client_ip)
[ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}"
[ "${DNS_CACHE}" = "0" ] && _args="${_args} dns_cache=0"
resolve_dns_port=${NEXT_DNS_LISTEN_PORT}
resolve_dns_port=$(get_new_port "$NEXT_DNS_LISTEN_PORT" tcp,udp)
[ -n "$resolve_dns_port" ] || return 1
_args="${_args} dns_listen_port=${resolve_dns_port}"
case "$(config_n_get @global[0] direct_dns_mode "auto")" in
case "$(get_direct_dns_mode)" in
udp)
_args="${_args} direct_dns_udp_server=$(normalize_dns "$(config_n_get @global[0] direct_dns 223.5.5.5:53)")"
;;
@@ -773,7 +775,7 @@ start_global() {
resolve_dns_log="${resolve_dns_log} + FakeDNS"
USE_FAKEDNS=1
}
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
NEXT_DNS_LISTEN_PORT=$(expr $resolve_dns_port + 1)
}
_args="${_args} use_proxy_list=$USE_PROXY_LIST use_gfw_list=$USE_GFW_LIST chn_list=$CHN_LIST"
run_xray flag=$_flag node=$NODE redir_port=$REDIR_PORT tcp_proxy_way=$TCP_PROXY_WAY config_file=$config_file log_file=$log_file ${_args}
@@ -1066,19 +1068,12 @@ stop_crontab() {
#echolog "清除定时执行命令。"
}
restart_smartdns() {
rm -rf /tmp/smartdns.cache
/etc/init.d/smartdns reload >/dev/null 2>&1
}
del_smartdns_conf() {
command -v smartdns >/dev/null 2>&1 || return
rm -rf "/tmp/etc/smartdns/${CONFIG}.conf"
local custom_conf="/etc/smartdns/custom.conf"
if [ -f "$custom_conf" ] && grep -q "${CONFIG}" "$custom_conf"; then
sed -i "/${CONFIG}/d" "$custom_conf" >/dev/null 2>&1
rm -rf /tmp/smartdns.cache
/etc/init.d/smartdns reload >/dev/null 2>&1
get_direct_dns_mode() {
# SmartDNS 使用独立直连列表,其余 DNS 分流模式继续读取原来的设置。
if [ "$ENABLED_DEFAULT_ACL" = "1" ] && [ "$DNS_SHUNT" = "smartdns" ]; then
echo auto
else
config_n_get @global[0] direct_dns_mode "auto"
fi
}
@@ -1095,7 +1090,7 @@ start_dns() {
local china_ng_local_dns="${LOCAL_DNS}"
local v2ray_local_dns
local direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto")
local direct_dns_mode=$(get_direct_dns_mode)
#获取访问控制节点所使用的DNS分流模式
local ACL_RULE_DNSMASQ=0
@@ -1119,15 +1114,15 @@ start_dns() {
china_ng_local_dns="tcp://${DIRECT_DNS}"
v2ray_local_dns="direct_dns_tcp_server=${DIRECT_DNS}"
#当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。
[ "$DNS_SHUNT" != "chinadns-ng" ] || [ "$ACL_RULE_DNSMASQ" = "1" ] && {
# ChinaDNS-NG and SmartDNS can query TCP upstreams directly.
if { [ "$DNS_SHUNT" != "chinadns-ng" ] && [ "$DNS_SHUNT" != "smartdns" ]; } || [ "$ACL_RULE_DNSMASQ" = "1" ]; then
LOCAL_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}"
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${NEXT_DNS_LISTEN_PORT} -c ${china_ng_local_dns} -d chn
echolog " - ChinaDNS-NG(${LOCAL_DNS}) -> ${china_ng_local_dns}"
echolog " * 请确保上游直连 DNS 支持 TCP 查询。"
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
}
fi
;;
auto)
#Automatic logic is already done by default
@@ -1274,9 +1269,9 @@ start_dns() {
local china_ng_listen=0
[ "${DNS_SHUNT}" = "smartdns" ] && {
if command -v smartdns > /dev/null 2>&1; then
local smartdns_bin=$(first_type smartdns)
if [ -n "$smartdns_bin" ]; then
rm -rf $TMP_PATH2/dnsmasq_default*
local group_domestic=$(config_n_get @global[0] group_domestic)
local smartdns_remote_dns=$(config_n_get @global[0] smartdns_remote_dns)
if [ -n "${smartdns_remote_dns}" ] && [ "${smartdns_remote_dns}" != "nil" ]; then
smartdns_remote_dns=$(echo ${smartdns_remote_dns} | tr -s ' ' '|')
@@ -1284,27 +1279,35 @@ start_dns() {
smartdns_remote_dns="tcp://1.1.1.1"
fi
local smartdns_default_dns=$(config_n_get @global[0] smartdns_default_dns direct)
echolog " - 域名解析:使用SmartDNS,请确保配置正常。"
china_ng_listen="127.0.0.1#${SMARTDNS_LISTEN_PORT}"
if [ "${DNS_REDIRECT}" = "1" ]; then
echolog " - DNS 重定向 -> SmartDNS(${china_ng_listen}),直连 DNS -> 国内分组(${group_domestic:-null})"
else
echolog " - SmartDNS(127.0.0.1#${SMARTDNS_LOCAL_PORT}) -> 国内分组(${group_domestic:-null}),SmartDNS(${china_ng_listen}) -> Dnsmasq"
fi
china_ng_listen="${china_ng_listen},::1#${SMARTDNS_LISTEN_PORT}"
local smartdns_listen_port=$(get_new_port "$(expr $NEXT_DNS_LISTEN_PORT + 1)")
[ -n "$smartdns_listen_port" ] || {
echolog " * SmartDNS 无法获取可用的 DNS 监听端口!"
return 1
}
local smartdns_config="${GLOBAL_ACL_PATH}/smartdns.conf"
local subnet_ip=$(config_n_get @global[0] remote_dns_client_ip)
lua $APP_PATH/helper_smartdns_add.lua -FLAG "default" -SMARTDNS_CONF "/tmp/etc/smartdns/$CONFIG.conf" \
-LISTEN_PORT ${SMARTDNS_LISTEN_PORT} -LOCAL_PORT ${SMARTDNS_LOCAL_PORT} -DNS_REDIRECT ${DNS_REDIRECT} \
-LOCAL_GROUP ${group_domestic:-null} -REMOTE_GROUP "passwall_proxy" -REMOTE_PROXY_SERVER ${GLOBAL_SOCKS_server} -USE_DEFAULT_DNS "${smartdns_default_dns}" \
-REMOTE_DNS ${smartdns_remote_dns} -DNS_MODE ${DNS_MODE:-socks} -TUN_DNS ${TUN_DNS} \
echolog " - SmartDNS(127.0.0.1#${smartdns_listen_port}),直连 DNS:${SMARTDNS_DIRECT_DNS} -> 分组(direct)"
[ "${SMARTDNS_LOCAL_PORT:-0}" != "0" ] && echolog " - SmartDNS 直连入口:${LOCAL_DNS} -> 分组(direct)"
lua "$APP_PATH/helper_smartdns_add.lua" -FLAG "default" \
-LISTEN_PORT "$smartdns_listen_port" -DIRECT_DNS "$SMARTDNS_DIRECT_DNS" -LOCAL_PORT "${SMARTDNS_LOCAL_PORT:-0}" -DNS_REDIRECT "${DNS_REDIRECT}" \
-REMOTE_PROXY_SERVER "${GLOBAL_SOCKS_server}" -USE_DEFAULT_DNS "${smartdns_default_dns}" \
-REMOTE_DNS "${smartdns_remote_dns}" -DNS_MODE "${DNS_MODE:-socks}" -TUN_DNS "${TUN_DNS}" \
-USE_DIRECT_LIST "${USE_DIRECT_LIST}" -USE_PROXY_LIST "${USE_PROXY_LIST}" -USE_BLOCK_LIST "${USE_BLOCK_LIST}" -USE_GFW_LIST "${USE_GFW_LIST}" -CHN_LIST "${CHN_LIST}" \
-NODE ${NODE} -DEFAULT_PROXY_MODE "${TCP_PROXY_MODE}" -NO_PROXY_IPV6 ${FILTER_PROXY_IPV6:-0} -NFTFLAG ${nftflag:-0} \
-SUBNET ${subnet_ip:-0} -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0}
restart_smartdns
-NODE "${NODE}" -DEFAULT_PROXY_MODE "${TCP_PROXY_MODE}" -NO_PROXY_IPV6 "${FILTER_PROXY_IPV6:-0}" -NFTFLAG "${nftflag:-0}" \
-SUBNET "${subnet_ip:-0}" -NO_LOGIC_LOG "${NO_LOGIC_LOG:-0}" \
-LOG "$(config_n_get @global[0] log_smartdns 0)" \
-LOG_LEVEL "$(config_n_get @global[0] smartdns_log_level error)" \
-LOG_SIZE "$(config_n_get @global[0] smartdns_log_size 1)" \
-UI "$(config_n_get @global[0] smartdns_ui 0)" \
-UI_PORT "$(config_n_get @global[0] smartdns_ui_port 9880)" \
-UI_DATA_DIR "$(config_n_get @global[0] smartdns_ui_data_dir "$TMP_PATH2/smartdns")" \
-UI_LOG_MAX_AGE "$(config_n_get @global[0] smartdns_ui_log_max_age 7)" || {
echolog " * SmartDNS 配置生成失败!"
return 1
}
ln_run "$smartdns_bin" smartdns "/dev/null" -f -c "$smartdns_config" -p "${GLOBAL_ACL_PATH}/smartdns.pid"
china_ng_listen="127.0.0.1#${smartdns_listen_port},::1#${smartdns_listen_port}"
USE_DEFAULT_DNS="chinadns_ng"
else
DNS_SHUNT="dnsmasq"
@@ -1385,7 +1388,7 @@ start_dns() {
lua $APP_PATH/helper_dnsmasq.lua logic_restart -LOG 1
elif [ "${DNS_SHUNT}" = "smartdns" ]; then
# SmartDNS reads DHCP leases itself; no forwarding dnsmasq instance is needed.
DNS_REDIRECT_PORT=${SMARTDNS_LISTEN_PORT}
DNS_REDIRECT_PORT=${smartdns_listen_port}
set_cache_var "ACL_default_dns_port" "${DNS_REDIRECT_PORT}"
else
#Run a copy dnsmasq instance, DNS hijack for that need proxy devices.
@@ -1584,7 +1587,7 @@ acl_app() {
_china_ng_listen="127.0.0.1#${chinadns_port},::1#${chinadns_port}"
_chinadns_local_dns="${LOCAL_DNS}"
_direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto")
_direct_dns_mode=$(get_direct_dns_mode)
case "${_direct_dns_mode}" in
udp)
_chinadns_local_dns=$(normalize_dns "$(config_n_get @global[0] direct_dns 223.5.5.5:53)")
@@ -1738,8 +1741,16 @@ start() {
fi
[ "$ENABLED_DEFAULT_ACL" = 1 ] && {
mkdir -p ${GLOBAL_ACL_PATH}
start_global
start_dns
start_global || {
echolog "全局节点启动失败,停止服务。"
stop
return 1
}
start_dns || {
echolog "DNS 启动失败,停止服务。"
stop
return 1
}
}
[ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh start
set_cache_var "USE_TABLES" "$USE_TABLES"
@@ -1792,7 +1803,6 @@ stop() {
unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab $1
del_smartdns_conf
rm -rf $GLOBAL_DNSMASQ_CONF
rm -rf $GLOBAL_DNSMASQ_CONF_PATH
[ "1" = "1" ] && {
@@ -1871,7 +1881,7 @@ get_config() {
REDIRECT_LIST="socks ss-rust ssr sing-box xray naiveproxy hysteria2"
TPROXY_LIST="socks ss-rust ssr sing-box xray hysteria2"
NEXT_DNS_LISTEN_PORT=15353
NEXT_DNS_LISTEN_PORT=$(get_new_port 15353 tcp,udp)
TUN_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}"
DNS_CACHE=0
DIRECT_DNS_QUERY_STRATEGY="UseIP"
@@ -1895,17 +1905,35 @@ get_config() {
set_cache_var GLOBAL_DNSMASQ_CONF_PATH ${GLOBAL_ACL_PATH}/dnsmasq.d
SMARTDNS_LOCAL_PORT=0
SMARTDNS_LISTEN_PORT=0
[ "$ENABLED_DEFAULT_ACL" = 1 ] && [ "$DNS_SHUNT" = "smartdns" ] && {
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
SMARTDNS_LOCAL_PORT=${NEXT_DNS_LISTEN_PORT}
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
SMARTDNS_LISTEN_PORT=${NEXT_DNS_LISTEN_PORT}
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
LOCAL_DNS="127.0.0.1#${SMARTDNS_LOCAL_PORT}"
SMARTDNS_DIRECT_DNS=
if [ "$ENABLED_DEFAULT_ACL" = "1" ] && [ "$DNS_SHUNT" = "smartdns" ]; then
# 显式列出空白字符,兼容未启用字符类支持的 BusyBox tr。
SMARTDNS_DIRECT_DNS=$(config_n_get @global[0] smartdns_direct_dns | tr -s ' \t\r\n\v\f' '|' | sed 's/^|//;s/|$//')
[ "$SMARTDNS_DIRECT_DNS" = "nil" ] && SMARTDNS_DIRECT_DNS=
if [ -z "$SMARTDNS_DIRECT_DNS" ]; then
SMARTDNS_DIRECT_DNS="$LOCAL_DNS"
else
local smartdns_single_dns="${SMARTDNS_DIRECT_DNS%%,*}"
local smartdns_protocol=udp
case "$smartdns_single_dns" in
*://*)
smartdns_protocol=$(printf '%s' "${smartdns_single_dns%%://*}" | tr 'A-Z' 'a-z')
smartdns_single_dns="${smartdns_single_dns#*://}"
;;
esac
if [ "${SMARTDNS_DIRECT_DNS#*|}" = "$SMARTDNS_DIRECT_DNS" ] && [ "$smartdns_protocol" = "udp" ]; then
LOCAL_DNS=$(normalize_dns "$smartdns_single_dns" 53)
local smartdns_host_ip="${SMARTDNS_DIRECT_DNS#*,}"
[ "$smartdns_host_ip" != "$SMARTDNS_DIRECT_DNS" ] && [ -n "$smartdns_host_ip" ] && \
LOCAL_DNS=$(normalize_dns "$smartdns_host_ip" "${LOCAL_DNS##*#}")
else
SMARTDNS_LOCAL_PORT=${NEXT_DNS_LISTEN_PORT}
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
LOCAL_DNS="127.0.0.1#${SMARTDNS_LOCAL_PORT}"
fi
fi
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
set_cache_var "SMARTDNS_LOCAL_PORT" "${SMARTDNS_LOCAL_PORT}"
}
fi
}
get_local_dns() {
@@ -2,9 +2,8 @@ local api = require "luci.passwall.api"
local var = api.get_args(arg)
local FLAG = var["-FLAG"]
local SMARTDNS_CONF = var["-SMARTDNS_CONF"]
local LOCAL_GROUP = var["-LOCAL_GROUP"]
local REMOTE_GROUP = var["-REMOTE_GROUP"]
local DIRECT_DNS = var["-DIRECT_DNS"]
local LOCAL_PORT = tonumber(var["-LOCAL_PORT"]) or 0
local REMOTE_PROXY_SERVER = var["-REMOTE_PROXY_SERVER"]
local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"]
local REMOTE_DNS = var["-REMOTE_DNS"]
@@ -22,8 +21,14 @@ local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
local NFTFLAG = var["-NFTFLAG"]
local SUBNET = var["-SUBNET"]
local LISTEN_PORT = var["-LISTEN_PORT"]
local LOCAL_PORT = var["-LOCAL_PORT"]
local DNS_REDIRECT = var["-DNS_REDIRECT"] == "1"
local LOG = var["-LOG"] == "1"
local LOG_LEVEL = var["-LOG_LEVEL"] or "error"
local LOG_SIZE = tonumber(var["-LOG_SIZE"]) or 1
local UI = var["-UI"] == "1"
local UI_PORT = tonumber(var["-UI_PORT"]) or 9880
local UI_DATA_DIR = var["-UI_DATA_DIR"]
local UI_LOG_MAX_AGE = tonumber(var["-UI_LOG_MAX_AGE"]) or 7
local sys = api.sys
local fs = api.fs
@@ -35,6 +40,7 @@ local USER_RULES_PATH = "/etc/passwall/rules"
local CACHE_RULES_PATH = api.CACHE_PATH .. "/user_rules"
local FLAG_PATH = TMP_PATH .. "/acl/" .. FLAG
local TMP_CONF_FILE = FLAG_PATH .. "/smartdns.conf"
local SMARTDNS_DATA_PATH = api.CACHE_PATH .. "/smartdns"
local config_lines = {}
local tmp_lines = {}
local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview")
@@ -60,11 +66,6 @@ local function is_file_nonzero(path)
return nil
end
local function get_new_port(start_port, protocol)
local cmd_format = ". /usr/share/passwall/utils.sh ; echo -n $(get_new_port %s %s)"
return tonumber(sys.exec(string.format(cmd_format, api.util.shellquote(tostring(start_port or "auto")), api.util.shellquote(protocol or "tcp,udp"))))
end
local function insert_unique(dest_table, value, lookup_table)
if not lookup_table[value] then
table.insert(dest_table, value)
@@ -116,128 +117,101 @@ end
sys.call("mkdir -p %s" % FLAG_PATH)
sys.call("mkdir -p %s" % CACHE_RULES_PATH)
sys.call("mkdir -p " .. api.util.shellquote(SMARTDNS_DATA_PATH))
local LOCAL_EXTEND_ARG = ""
local LOCAL_SERVERS = {}
local SERVER_TYPES = {
udp = "server", tcp = "server-tcp", tls = "server-tls",
https = "server-https", quic = "server-quic", h3 = "server-h3"
}
if not LOCAL_GROUP or LOCAL_GROUP == "null" or LOCAL_GROUP == "" then
log(" * 注意:SmartDNS 国内分组名未设置,DNS 将无法正常工作!!!")
local LOG_LEVELS = {off = true, fatal = true, error = true, warn = true, notice = true, info = true, debug = true}
if not LOG_LEVELS[LOG_LEVEL] then LOG_LEVEL = "error" end
if LOG_SIZE < 1 or LOG_SIZE % 1 ~= 0 then LOG_SIZE = 1 end
if UI_PORT < 1 or UI_PORT > 65535 or UI_PORT % 1 ~= 0 then UI_PORT = 9880 end
if UI_LOG_MAX_AGE < 1 or UI_LOG_MAX_AGE % 1 ~= 0 then UI_LOG_MAX_AGE = 7 end
if not UI_DATA_DIR or UI_DATA_DIR == "" then UI_DATA_DIR = SMARTDNS_DATA_PATH end
local UI_PLUGIN = "/usr/lib/smartdns_ui.so"
if not fs.access(UI_PLUGIN) then UI_PLUGIN = "/usr/lib/smartdns/smartdns_ui.so" end
local ENABLE_UI = UI and fs.access(UI_PLUGIN)
local data_dir = ENABLE_UI and UI_DATA_DIR or SMARTDNS_DATA_PATH
if data_dir ~= SMARTDNS_DATA_PATH then
sys.call("mkdir -p " .. api.util.shellquote(data_dir))
end
-- 统一生成直连、远程上游,协议和默认端口由 SmartDNS 根据 URL 识别。
local function make_server_param(endpoint)
local dns = api.split(api.trim(endpoint), ",")
local address = api.trim(dns[1] or "")
if address == "" then return nil end
local protocol, host_address = address:match("^(%w+)://(.+)$")
protocol = protocol and protocol:lower()
address = host_address or address
local host, port = address:match("^(.-)#(%d+)$")
if host then
if host:find(":", 1, true) and not host:find("[", 1, true) then host = "[" .. host .. "]" end
address = host .. ":" .. port
elseif datatypes.ip6addr(address) and not address:find("[", 1, true) then
address = "[" .. address .. "]"
end
local server_param = "server " .. (protocol and (protocol .. "://") or "") .. address
if dns[2] and dns[2] ~= "" then
-- 保留 URL 中的域名,固定连接 IP 交给 SmartDNS 处理,避免手动替换影响 SNI 和 HTTP Host。
server_param = server_param .. " -host-ip " .. api.trim(dns[2]):gsub("^%[(.*)%]$", "%1")
end
return server_param
end
local DIRECT_SERVERS = {}
for endpoint in string.gmatch(DIRECT_DNS or "", "[^|]+") do
local server_param = make_server_param(endpoint)
if server_param then
-- 复用 PassWall 的直连标记,使所有协议及动态域名上游绕过本机透明代理。
table.insert(DIRECT_SERVERS, server_param .. " -set-mark 255")
end
end
if #DIRECT_SERVERS == 0 then
log(" * 注意:SmartDNS 没有可用的直连 DNS 服务器,DNS 将无法正常工作!!!")
os.exit(1)
else
api.uci:foreach("smartdns", "server", function(s)
if s.enabled ~= "0" and s.server_group == LOCAL_GROUP and s.ip and s.ip ~= "" and SERVER_TYPES[s.type or "udp"] then
table.insert(LOCAL_SERVERS, s)
end
end)
if #LOCAL_SERVERS == 0 then
log(" * 注意:SmartDNS 国内分组 " .. LOCAL_GROUP .. " 没有可用的 DNS 服务器,DNS 将无法正常工作!!!")
os.exit(1)
end
--从smartdns配置中读取参数
local custom_conf_path = "/etc/smartdns/custom.conf"
local options = {
{key = "dualstack_ip_selection", config_key = "dualstack-ip-selection", yes_no = true, arg_yes = "-d yes", arg_no = "-d no", default = "yes"},
{key = "speed_check_mode", config_key = "speed-check-mode", prefix = "-c ", default = "ping,tcp:80,tcp:443"},
{key = "serve_expired", config_key = "serve-expired", yes_no = true, arg_yes = "", arg_no = "-no-serve-expired", default = "yes"},
{key = "response_mode", config_key = "response-mode", prefix = "-r ", default = "first-ping"},
{key = "rr_ttl", config_key = "rr-ttl", prefix = "-rr-ttl "},
{key = "rr_ttl_min", config_key = "rr-ttl-min", prefix = "-rr-ttl-min "},
{key = "rr_ttl_max", config_key = "rr-ttl-max", prefix = "-rr-ttl-max "},
{key = "rr_ttl_reply_max", config_key = "rr-ttl-reply-max", prefix = "-rr-ttl-reply-max "},
{
key = "force_aaaa_soa",
config_key = "force-qtype-SOA",
prefix = "-address ",
get_value = function(custom_config)
local soa = custom_config["force-qtype-SOA"]
return ((soa and soa:match("(^|%s)28(%s|$)"))
or custom_config["force-AAAA-SOA"] == "yes"
or api.uci_get("smartdns", "@smartdns[0]", "force_aaaa_soa") == "1")
and "#6" or "-6"
end
}
}
-- 从 custom.conf 中读取值,以最后出现的值为准
local custom_config = {}
local f_in = io.open(custom_conf_path, "r")
if f_in then
for line in f_in:lines() do
line = api.trim(line)
if line ~= "" and not line:match("^#") then
local param, value = line:match("^(%S+)%s+(.+)$")
if param and value then custom_config[param] = value end
end
end
f_in:close()
end
-- 从 smartdns 配置中读取值,优先级以 custom.conf 为准
for _, opt in ipairs(options) do
local val
if opt.get_value then
val = opt.get_value(custom_config)
else
val = custom_config[opt.config_key] or api.uci_get("smartdns", "@smartdns[0]", opt.key) or opt.default
end
if val == "yes" then val = "1" elseif val == "no" then val = "0" end
local arg
if opt.yes_no then
arg = (val == "1" and opt.arg_yes or opt.arg_no)
elseif val and (not opt.value or (opt.invert and val ~= opt.value) or (not opt.invert and val == opt.value)) then
arg = (opt.prefix or "") .. (opt.arg or val)
end
if arg and arg ~= "" then
LOCAL_EXTEND_ARG = LOCAL_EXTEND_ARG .. (LOCAL_EXTEND_ARG ~= "" and " " or "") .. arg
end
end
end
if not REMOTE_GROUP or REMOTE_GROUP == "nil" then
REMOTE_GROUP = "passwall_proxy"
end
if REMOTE_DNS then
REMOTE_DNS = REMOTE_DNS:gsub("#", ":")
end
local SANDBOX_GROUP = "passwall_" .. FLAG
local AUTO_GROUP = "passwall_auto"
local AUTO_ENTRY_GROUP = AUTO_GROUP .. "_entry"
local AUTO_GROUP = "auto"
local auto_config_lines = {}
-- 直连规则显式覆盖远程兜底的缓存、IPv6、测速和双栈优选设置。
local DIRECT_RULE_OPTIONS = " -enable-cache -address -6 -speed-check-mode ping,tcp:80,tcp:443 -response-mode first-ping -d yes"
--设置默认 DNS 分组(兜底组),全局代理模式仍使用远程组。
local DEFAULT_DNS_GROUP = (USE_DEFAULT_DNS == "direct" and LOCAL_GROUP) or
(USE_DEFAULT_DNS == "remote" and REMOTE_GROUP) 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)
local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0") and 1
if only_global == 1 then
DEFAULT_DNS_GROUP = REMOTE_GROUP
DEFAULT_DNS_GROUP = "proxy"
end
local AUTO_PORT
if DEFAULT_DNS_GROUP == AUTO_GROUP then
AUTO_PORT = get_new_port(15453, "udp")
if not AUTO_PORT then
log(" * 注意:SmartDNS 自动模式无法获取可用的 UDP 回环端口!")
os.exit(1)
end
end
sys.call('sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1')
local force_https_soa = api.uci_get_c("@global[0]", "force_https_soa") or 0
local force_qtype_soa = tonumber(force_https_soa) == 1 and "force-qtype-SOA 65" or "force-qtype-SOA -,65"
local proxy_server_name = "psw-proxy-server"
local listen_address = "[::]:" .. LISTEN_PORT .. (DNS_REDIRECT and "" or "@lo")
local local_domain = DNS_REDIRECT and api.uci_get("dhcp", "@dnsmasq[0]", "domain")
config_lines = {
tonumber(LISTEN_PORT) ~= 0 and ("bind " .. listen_address .. " -group " .. SANDBOX_GROUP) or "",
tonumber(LISTEN_PORT) ~= 0 and ("bind-tcp " .. listen_address .. " -group " .. SANDBOX_GROUP) or "",
(tonumber(LOCAL_PORT) ~= 0 and LOCAL_GROUP) and "bind [::]:" .. LOCAL_PORT .. "@lo -group " .. LOCAL_GROUP or "",
(tonumber(LOCAL_PORT) ~= 0 and LOCAL_GROUP) and "bind-tcp [::]:" .. LOCAL_PORT .. "@lo -group " .. LOCAL_GROUP or "",
"server 223.5.5.5 -bootstrap-dns",
tonumber(LISTEN_PORT) ~= 0 and ("bind " .. listen_address) or "",
tonumber(LISTEN_PORT) ~= 0 and ("bind-tcp " .. listen_address) or "",
LOCAL_PORT > 0 and ("bind [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
LOCAL_PORT > 0 and ("bind-tcp [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
"log-level " .. LOG_LEVEL,
"log-num " .. (LOG and "1" or "0"),
LOG and string.format("log-file %q", FLAG_PATH .. "/smartdns.log") or "",
LOG and ("log-size " .. LOG_SIZE .. "M") or "",
string.format("cache-file %q", SMARTDNS_DATA_PATH .. "/smartdns.cache"),
string.format("data-dir %q", data_dir),
"server 223.5.5.5 -bootstrap-dns -set-mark 255",
-- Set the suffix before loading hosts/leases so initial records are expanded too.
(local_domain and local_domain ~= "") and "local-domain " .. local_domain or "",
DNS_REDIRECT and "hosts-file /tmp/hosts/*" or "",
is_file_nonzero("/etc/hosts") and "hosts-file /etc/hosts" or "",
DNS_MODE == "socks" and string.format("proxy-server socks5://%s -name %s", REMOTE_PROXY_SERVER, proxy_server_name) or ""
}
if ENABLE_UI then
merge_array(config_lines, {
"plugin " .. UI_PLUGIN,
"smartdns-ui.www-root /usr/share/smartdns/wwwroot",
"smartdns-ui.ip http://[::]:" .. UI_PORT,
"smartdns-ui.max-query-log-age " .. (UI_LOG_MAX_AGE * 86400)
})
end
if DNS_REDIRECT then
local hosts = {}
api.uci:foreach("dhcp", "host", function(s)
@@ -260,48 +234,20 @@ if DNS_REDIRECT then
table.insert(config_lines, string.format("odhcpd-lease-file %q", odhcpd_lease))
end
end
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 复制国内组中启用的 UCI DNS 服务器,原组不添加白名单过滤。
for _, s in ipairs(LOCAL_SERVERS) do
local server_type = SERVER_TYPES[s.type or "udp"]
local address = s.ip
if not address:find("://", 1, true) then
if address:find(":", 1, true) and not address:find("[", 1, true) then
address = "[" .. address .. "]"
end
if s.port and s.port ~= "" then address = address .. ":" .. s.port end
end
local args = {server_type, address}
for _, key in ipairs({"tls_host_verify", "host_name", "http_host", "spki_pin", "set_mark"}) do
if s[key] and s[key] ~= "" then
table.insert(args, "-" .. key:gsub("_", "-"))
table.insert(args, s[key])
end
end
for _, key in ipairs({"no_check_certificate", "blacklist_ip", "check_edns"}) do
if s[key] == "1" and not (key == "blacklist_ip" and NO_PROXY_IPV6 == "1") then
table.insert(args, "-" .. key:gsub("_", "-"))
end
end
if s.use_proxy == "1" then table.insert(args, "-proxy default-proxy") end
-- 国内副本作为主用服务器,不继承后备标记。
local addition_arg = api.trim((" " .. (s.addition_arg or "")):gsub("(%s)%-%-?fallback%f[%s%z]", "%1"))
if NO_PROXY_IPV6 == "1" then
-- 国内副本只使用白名单,避免远程 IPv6 黑名单同时过滤国内结果。
addition_arg = api.trim((" " .. addition_arg):gsub("(%s)%-%-?blacklist%-ip%f[%s%z]", "%1"))
end
if addition_arg ~= "" then table.insert(args, addition_arg) end
table.insert(args, "-exclude-default-group -whitelist-ip")
table.insert(auto_config_lines, table.concat(args, " "))
for _, server_param in ipairs(DIRECT_SERVERS) do
table.insert(config_lines, server_param .. " -group direct -exclude-default-group")
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 自动组的直连副本仅接受大陆 IP,普通直连组保持默认行为。
table.insert(auto_config_lines, server_param .. " -group " .. AUTO_GROUP .. " -exclude-default-group -whitelist-ip")
end
end
-- 由下方 DNS_MODE 分支调用,服务器参数来自 PassWall 的 REMOTE_DNS 或 TUN_DNS。
local function add_remote_server(server_param)
table.insert(config_lines, server_param .. " -group " .. REMOTE_GROUP .. " -exclude-default-group")
table.insert(config_lines, server_param .. " -group proxy -exclude-default-group")
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 远程副本作为自动组的后备服务器,保留代理、ECS 和传输协议参数。
local auto_param = server_param .. " -exclude-default-group -fallback"
local auto_param = server_param .. " -group " .. AUTO_GROUP .. " -exclude-default-group -fallback"
if NO_PROXY_IPV6 == "1" then auto_param = auto_param .. " -blacklist-ip" end
table.insert(auto_config_lines, auto_param)
end
@@ -309,49 +255,7 @@ end
if DNS_MODE == "socks" then
for w in string.gmatch(REMOTE_DNS, '[^|]+') do
local server_dns = api.trim(w)
local server_param
local dnsType = string.match(server_dns, "^(.-)://")
dnsType = dnsType and string.lower(dnsType) or nil
local dnsServer = string.match(server_dns, "://(.+)") or server_dns
if dnsType and dnsType ~= "" and dnsType ~= "udp" then
if dnsType == "tcp" then
server_param = "server-tcp " .. dnsServer
elseif dnsType == "tls" then
server_param = "server-tls " .. dnsServer
elseif dnsType == "quic" then
server_param = "server-quic " .. dnsServer
elseif dnsType == "https" or dnsType == "h3" then
local http_host = nil
local url = w
local port = 443
local s = api.split(w, ",")
if s and #s > 1 then
url = s[1]
local dns_ip = s[2]
local host_port = api.get_domain_from_url(s[1])
if host_port and #host_port > 0 then
http_host = host_port
local s2 = api.split(host_port, ":")
if s2 and #s2 > 1 then
http_host = s2[1]
port = s2[2]
end
url = url:gsub(http_host, dns_ip)
end
end
server_dns = url
if http_host then
server_dns = server_dns .. " -http-host " .. http_host
end
server_param = (dnsType == "https" and "server-https " or "server-h3 ") .. server_dns
end
else
server_param = "server " .. dnsServer
end
local server_param = make_server_param(w)
if not api.is_local_ip(w) then
server_param = server_param .. " -proxy " .. proxy_server_name
@@ -364,11 +268,11 @@ if DNS_MODE == "socks" then
end
else
add_remote_server("server " .. TUN_DNS:gsub("#", ":"))
log(" - " .. DNS_MODE:gsub("^%l",string.upper) .. " " .. TUN_DNS .. " -> " .. REMOTE_GROUP)
log(" - " .. DNS_MODE:gsub("^%l",string.upper) .. " " .. TUN_DNS .. " -> 分组(proxy)")
end
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- IP 集合和过滤规则集中在自动规则组内。
-- 仅自动组的服务器启用 IP 过滤,普通直连和代理组保持各自行为。
if NO_PROXY_IPV6 == "1" then
table.insert(auto_config_lines, "ip-rules ::/0 -blacklist-ip")
end
@@ -384,33 +288,20 @@ if DEFAULT_DNS_GROUP == AUTO_GROUP then
end
table.insert(auto_config_lines, ip_rules)
else
log(" * 注意:SmartDNS 智能模式大陆 IP 列表不存在或为空:" .. path)
log(" * 注意:SmartDNS 自动模式大陆 IP 列表不存在或为空:" .. path)
end
end
-- 兜底通过 UDP 回环入口进入自动规则组,避免 IP 过滤影响其他查询。
table.insert(config_lines, "bind [::]:" .. AUTO_PORT .. "@lo -group " .. AUTO_GROUP)
table.insert(config_lines, "server [::1]:" .. AUTO_PORT .. " -group " .. AUTO_ENTRY_GROUP .. " -exclude-default-group")
table.insert(config_lines, "group-begin " .. AUTO_GROUP .. " -inherit none")
table.insert(config_lines, force_qtype_soa)
merge_array(config_lines, auto_config_lines)
table.insert(config_lines, "domain-rules /./ -nameserver " .. AUTO_GROUP .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or ""))
table.insert(config_lines, "group-end")
end
-- PassWall 规则组不继承外部规则。
table.insert(config_lines, "group-begin " .. SANDBOX_GROUP .. " -inherit none")
table.insert(config_lines, force_qtype_soa)
if DEFAULT_DNS_GROUP then
local domain_rules_str = "domain-rules /./ -nameserver " .. (DEFAULT_DNS_GROUP == AUTO_GROUP and AUTO_ENTRY_GROUP or DEFAULT_DNS_GROUP)
if DEFAULT_DNS_GROUP == REMOTE_GROUP then
domain_rules_str = domain_rules_str .. " -speed-check-mode none -d no -no-serve-expired"
local domain_rules_str = "domain-rules /./ -nameserver " .. DEFAULT_DNS_GROUP
if DEFAULT_DNS_GROUP == "proxy" then
-- 代理仍禁止写入缓存,去掉会被直连继承且无法反向覆盖的过期缓存限制。
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
domain_rules_str = domain_rules_str .. " -address " .. ((only_global and IS_SHUNT_NODE) and "-6" or (NO_PROXY_IPV6 == "1" and "#6" or "-6"))
elseif DEFAULT_DNS_GROUP == LOCAL_GROUP then
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
elseif DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 转接层只转发请求,测速、双栈优选及缓存交给自动规则组处理。
domain_rules_str = domain_rules_str .. " -speed-check-mode none -d no -no-cache"
end
table.insert(config_lines, domain_rules_str)
end
@@ -513,12 +404,11 @@ if is_file_nonzero(file_vpslist) then
"#4:" .. setflag .. "psw_vps",
"#6:" .. setflag .. "psw_vps6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--9")
log(string.format(" - 节点列表中的域名(vpslist)使用分组:%s", LOCAL_GROUP or "默认"))
log(" - 节点列表中的域名(vpslist)使用分组:direct")
end
--直连(白名单)列表
@@ -579,12 +469,11 @@ if USE_DIRECT_LIST == "1" and is_file_nonzero(file_direct_host) then
"#4:" .. setflag .. "psw_white",
"#6:" .. setflag .. "psw_white6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--7")
log(string.format(" - 域名白名单(whitelist)使用分组:%s", LOCAL_GROUP or "默认"))
log(" - 域名白名单(whitelist)使用分组:direct")
end
--代理(黑名单)列表
@@ -641,9 +530,8 @@ if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_proxy_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_black"
}
@@ -651,11 +539,11 @@ if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_black6")
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--6")
log(string.format(" - 代理域名表(blacklist)使用分组:%s", REMOTE_GROUP or "默认"))
log(" - 代理域名表(blacklist)使用分组:proxy")
end
--GFW列表
@@ -664,9 +552,8 @@ if USE_GFW_LIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, RULES_PATH .. "/gfwlist")
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_gfw"
}
@@ -674,11 +561,11 @@ if USE_GFW_LIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_gfw6")
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--5")
log(string.format(" - 防火墙域名表(gfwlist)使用分组:%s", REMOTE_GROUP or "默认"))
log(" - 防火墙域名表(gfwlist)使用分组:proxy")
end
--中国列表
@@ -693,19 +580,17 @@ if CHN_LIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
"#4:" .. setflag .. "psw_chn",
"#6:" .. setflag .. "psw_chn6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--4")
log(string.format(" - 中国域名表(chnlist)使用分组:%s", LOCAL_GROUP or "默认"))
log(" - 中国域名表(chnlist)使用分组:direct")
end
--回中国模式
if CHN_LIST == "proxy" then
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_chn"
}
@@ -713,11 +598,11 @@ if CHN_LIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_chn6")
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--4")
log(string.format(" - 中国域名表(chnlist)使用分组:%s", REMOTE_GROUP or "默认"))
log(" - 中国域名表(chnlist)使用分组:proxy")
end
end
@@ -775,7 +660,7 @@ if IS_SHUNT_NODE and not only_global then
SHUNT_LIST = SHUNT_LIST .. domain_list .. (_node_id:sub(1, 1) == "_" and "not-node" or "node")
if _node_id ~= "_direct" and _node_id ~= "_blackhole" then
log(string.format(" - Sing-Box/Xray分流规则(%s)使用分组:%s", s.remarks, REMOTE_GROUP or "默认"))
log(string.format(" - Sing-Box/Xray分流规则(%s)使用分组:proxy", s.remarks))
end
end
end)
@@ -853,13 +738,13 @@ if IS_SHUNT_NODE and not only_global then
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_direct_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--1")
end
@@ -869,13 +754,13 @@ if IS_SHUNT_NODE and not only_global then
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_proxy_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--2")
end
@@ -885,19 +770,18 @@ if IS_SHUNT_NODE and not only_global then
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_black_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none -no-serve-expired"
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--3")
end
end
table.insert(config_lines, "group-end")
if #config_lines > 0 then
local f_out = io.open(TMP_CONF_FILE, "w")
@@ -914,8 +798,6 @@ if DEFAULT_DNS_GROUP then
log(string.format(" - 默认 DNS 分组:%s", DEFAULT_DNS_GROUP == AUTO_GROUP and "自动" or DEFAULT_DNS_GROUP))
end
fs.symlink(TMP_CONF_FILE, SMARTDNS_CONF)
sys.call(string.format('echo "conf-file %s" >> /etc/smartdns/custom.conf', string.gsub(SMARTDNS_CONF, "passwall", "passwall*")))
if DNS_REDIRECT then
log(" - DNS 重定向直接使用 SmartDNS,DHCP 租约由 SmartDNS 自动加载。")
else
@@ -187,38 +187,30 @@ nft_rule_dual() {
nft "add rule $NFTABLE_NAME $chain_name $match_condition @${static_set} ${final_action}"
}
# Avoid terminal stdin or open-ended pipes to prevent hanging.
insert_nftset() {
local nftset_name="${1}"; shift
local suffix=""
[ -n "$nftset_name" ] || return 0
if [ -n "$nftset_name" ] && { [ $# -gt 0 ] || [ ! -t 0 ]; }; then
{
if [ $# -gt 0 ] && [ $# -le 1000 ]; then
printf "%s\n" "$@"
elif [ $# -gt 1000 ]; then
printf "%s\n" "$*"
else
cat
fi | awk -v s="$suffix" -v n="$nftset_name" -v t="$NFTABLE_NAME" '
BEGIN {
RS = "[ \t\n\r]+"
ORS = ""
}
$0 != "" {
if (!first) {
printf "add element %s %s { \n", t, n
first = 1;
} else {
print ",\n"
}
print $0 s
}
END {
if (first) print "\n }\n"
}
'
} | nft -f -
fi
awk -v n="$nftset_name" -v t="$NFTABLE_NAME" '
BEGIN {
RS = "[ \t\n\r]+"
ORS = ""
}
$0 != "" {
if (!first) {
printf "add element %s %s { \n", t, n
first = 1
} else {
print ",\n"
}
printf " %s", $0
}
END {
if (first) {
print "\n }\n"
}
}' | nft -f -
}
gen_nftset() {
@@ -235,9 +227,6 @@ gen_nftset() {
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $gc_interval_time; auto-merge; }"
fi
fi
if [ $# -gt 0 ]; then
insert_nftset "$nftset_name" "$@"
fi
}
get_jump_nft() {
@@ -969,7 +958,8 @@ add_firewall_rule() {
gen_nftset $NFTSET_VPS ipv4_addr 0
gen_nftset $NFTSET_GFW ipv4_addr "2d"
gen_nftset $NFTSET_LOCAL ipv4_addr 0
gen_nftset $NFTSET_LAN ipv4_addr 0 $(gen_lanlist)
gen_nftset $NFTSET_LAN ipv4_addr 0
gen_lanlist | insert_nftset $NFTSET_LAN
gen_nftset $NFTSET_CHN ipv4_addr "2d"
if [ -f $RULES_PATH/chnroute.nft ] && [ -s $RULES_PATH/chnroute.nft ] && [ $(awk 'END{print NR}' $RULES_PATH/chnroute.nft) -ge 8 ]; then
nft -f $RULES_PATH/chnroute.nft
@@ -990,7 +980,8 @@ add_firewall_rule() {
gen_nftset $NFTSET_VPS6 ipv6_addr 0
gen_nftset $NFTSET_GFW6 ipv6_addr "2d"
gen_nftset $NFTSET_LOCAL6 ipv6_addr 0
gen_nftset $NFTSET_LAN6 ipv6_addr 0 $(gen_lanlist_6)
gen_nftset $NFTSET_LAN6 ipv6_addr 0
gen_lanlist_6 | insert_nftset $NFTSET_LAN6
gen_nftset $NFTSET_CHN6 ipv6_addr "2d"
if [ -f $RULES_PATH/chnroute6.nft ] && [ -s $RULES_PATH/chnroute6.nft ] && [ $(awk 'END{print NR}' $RULES_PATH/chnroute6.nft) -ge 8 ]; then
#echolog "使用缓存加载chnroute6..."
@@ -1627,9 +1618,6 @@ stop() {
arg1=$1
shift
case $arg1 in
insert_nftset)
insert_nftset "$@"
;;
filter_direct_node_list)
filter_direct_node_list
;;
+2 -2
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
PKG_RELEASE:=88
PKG_RELEASE:=89
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -31,7 +31,7 @@ LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/AnyTLS/Socks5
LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
@@ -62,6 +62,7 @@ ipv6_policy_rules() {
;;
router)
echo "add rule inet ss_spec $chain ip6 daddr @china6 return"
echo "add rule inet ss_spec $chain ip6 saddr @gmlan6 ip6 daddr != @china6 goto $target"
echo "add rule inet ss_spec $chain goto $target"
;;
all) echo "add rule inet ss_spec $chain goto $target" ;;
+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage
PKG_VERSION:=1.0.5
PKG_RELEASE:=11
PKG_VERSION:=1.0.8
PKG_RELEASE:=12
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard
@@ -334,6 +334,24 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-edit-hidden:empty { display: none; }
.vt-app .vt-edit-hidden .vt-fchip .vt-ico { width: 13px; height: 13px; }
.vt-app .vt-editbar-actions { display: flex; gap: 8px; margin-left: auto; }
.vt-app .vt-edit-field { display: flex; align-items: center; gap: 8px; min-width: 0; }
.vt-app .vt-edit-field label { font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; }
/* narrow: the label above, the select across the bar, so a long choice
("Mirrored, scale per direction") is not cut */
@container vt (max-width: 699px) {
.vt-app .vt-edit-field { flex: 1 1 100%; flex-direction: column; align-items: stretch; gap: 4px; }
}
.vt-app .vt-select {
appearance: none; -webkit-appearance: none; min-width: 0; max-width: 100%;
height: var(--vt-ctl); padding: 0 30px 0 10px; margin: 0;
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl);
color: var(--vt-text); font: inherit; font-size: var(--vt-fs-base); box-shadow: none; cursor: pointer;
/* the chevron, as the theme's selects draw it */
background: var(--vt-surface) no-repeat;
background-image: linear-gradient(45deg, transparent 50%, var(--vt-text-3) 50%), linear-gradient(135deg, var(--vt-text-3) 50%, transparent 50%);
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); }
@container vt (max-width: 699px) {
.vt-app .vt-editbar { padding: 8px 8px 8px 12px; gap: 8px; }
.vt-app .vt-editbar-text { flex-basis: 0; }
@@ -372,6 +390,11 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-ov-hide { margin-left: 4px; }
.vt-app .vt-ov-size { min-width: 26px; text-align: center; font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-2); }
.vt-app.vt-edit-1col .vt-ov-col { display: none; }
/* a narrow tile's bar: two rows at most (the name, then the controls),
not three; no ‹ ›, drag and the arrow keys on the grip still move it */
.vt-app .vt-cell { container: vtcell / inline-size; }
/* tiles with a container of their own keep the vtcell name too (below) */
@container vtcell (max-width: 340px) { .vt-app .vt-ov-lr { display: none; } }
.vt-app.vt-editing .vt-cell { transition: top .2s ease, left .2s ease; }
.vt-app .vt-cell.vt-dragging { z-index: 8; transition: none; opacity: .92; box-shadow: 0 18px 40px rgba(0, 0, 0, .28); }
@@ -546,7 +569,7 @@ h2, h3{
/* -------------------------------------------------- 5. hero: network path */
.vt-app .vt-hero { container: vthero / inline-size; overflow: hidden; display: flex; flex-direction: column; }
.vt-app .vt-hero { container: vthero vtcell / inline-size; overflow: hidden; display: flex; flex-direction: column; }
.vt-app .vt-kpis {
position: relative; z-index: 1;
@@ -619,6 +642,8 @@ h2, h3{
.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; }
.vt-app .vt-node-title { font-size: var(--vt-fs-base); font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* text alone (the gateway's DNS): a block, so the ellipsis can show */
.vt-app .vt-node-sub:not(:has(> *)) { display: block; }
.vt-app .vt-node-ico {
flex: none; width: 32px; height: 32px; border-radius: 9px;
display: inline-flex; align-items: center; justify-content: center;
@@ -687,12 +712,13 @@ h2, h3{
/* ------------------------------------------------------------ 6. system */
.vt-app .vt-sys { container: vtsys / inline-size; display: flex; flex-direction: column; gap: 16px; }
.vt-app .vt-sys { container: vtsys vtcell / inline-size; display: flex; flex-direction: column; gap: 16px; }
.vt-app .vt-sys .vt-tile-head { margin-bottom: 0; }
.vt-app .vt-sys-cpu { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 16px; align-items: end; }
.vt-app .vt-sys-cpu-l { display: flex; flex-direction: column; gap: 3px; }
/* a narrow System column puts the sparkline under the load figures */
@container vtsys (max-width: 330px) { .vt-app .vt-sys-cpu { grid-template-columns: minmax(0, 1fr); gap: 4px; } }
/* narrow: the sparkline under the numbers, so the load figures never meet its label */
@container vtsys (max-width: 420px) { .vt-app .vt-sys-cpu { grid-template-columns: minmax(0, 1fr); gap: 4px; } }
.vt-app .vt-sys-cpu-r { display: flex; align-items: flex-end; gap: 10px; min-width: 0; }
.vt-app .vt-spark-box { flex: 1; min-width: 0; padding-top: 12px; }
.vt-app .vt-spark-box .vt-spark { height: 38px; }
@@ -775,11 +801,24 @@ h2, h3{
.vt-app .vt-series-line { fill: none; stroke: var(--vt-sc); stroke-width: 1.75; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.vt-app .vt-series-area { stroke: none; }
.vt-app .vt-chart-mid { fill: none; stroke: color-mix(in oklab, var(--vt-chart-axis) 55%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* scale labels sit just above their line (top set per style), or just
below it */
.vt-app .vt-mult-max {
position: absolute; right: 0; top: -3px; transform: translateY(-100%);
position: absolute; right: 0; top: 0; transform: translateY(calc(-100% - 3px));
font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
}
.vt-app .vt-mult-max-up { top: auto; bottom: -3px; transform: translateY(100%); }
.vt-app .vt-mult-max-below { transform: translateY(3px); }
/* a stretch without samples: hatched, named when there's room */
.vt-app .vt-mult-gap {
position: absolute; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; overflow: hidden;
border-radius: 3px; pointer-events: none;
background: repeating-linear-gradient(135deg, var(--vt-chart-grid) 0 2px, transparent 2px 6px);
font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap;
}
.vt-app .vt-mult-gap span { padding: 2px 4px; border-radius: 3px; background: var(--vt-surface); }
/* a band too narrow for its label shows the hatching alone */
.vt-app .vt-mult-gap { container-type: inline-size; }
@container (max-width: 60px) { .vt-app .vt-mult-gap span { display: none; } }
.vt-app .vt-mult-dot {
position: absolute; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; border-radius: 50%; z-index: 2;
background: var(--vt-sc); box-shadow: 0 0 0 2px var(--vt-surface); pointer-events: none;
@@ -1105,6 +1144,69 @@ h2, h3{
.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: var(--vt-fs-micro); }
.vt-app .vt-form-err { color: var(--vt-err); font-size: var(--vt-fs-sm); }
/* ------------------------------------------- 11b. firewall overview */
/* view/vantage/firewall.js: warnings, then tiles for what is open from
the internet, the zones and what is switched off */
/* no internet-facing zone: the page cannot tell, neither green nor a warning */
.vt-app .vt-orb-unknown { --vt-oc: var(--vt-text-3); }
.vt-app .vt-fg-warns { display: grid; gap: 10px; margin-bottom: 14px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
/* a warning's title is a sentence, not the dashboard's small caps label */
.vt-app .vt-fg-warns .vt-chip-title { text-transform: none; letter-spacing: 0; font-size: var(--vt-fs-base); font-weight: 650; color: var(--vt-text); }
.vt-app .vt-fg-warns .vt-chip-reason { margin-top: 2px; }
.vt-app .vt-fg-grid { display: grid; gap: 14px; align-items: start; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); }
.vt-app .vt-fg-group {
margin: 2px 0 4px; color: var(--vt-text-3);
font-size: var(--vt-fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.vt-app .vt-fg-rows + .vt-fg-group { margin-top: 14px; }
.vt-app .vt-fg-rows { display: flex; flex-direction: column; }
.vt-app .vt-fg-row {
display: flex; align-items: center; gap: 12px;
min-height: var(--vt-ctl); padding: 8px 10px; margin: 0 -10px;
border-radius: var(--vt-r-sm); color: var(--vt-text); text-decoration: none;
}
.vt-app .vt-fg-row + .vt-fg-row { box-shadow: inset 0 1px 0 var(--vt-line); }
.vt-app .vt-fg-row:hover { background: var(--vt-surface-2); text-decoration: none; }
.vt-app .vt-fg-row-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.vt-app .vt-fg-row-name { font-size: var(--vt-fs-base); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vt-app .vt-fg-row-sub { color: var(--vt-text-3); font-size: var(--vt-fs-sm); }
.vt-app .vt-fg-anywhere { color: var(--vt-text-2); }
.vt-app .vt-fg-to { flex: none; display: inline-flex; align-items: center; gap: 6px; max-width: 45%; color: var(--vt-text-2); font-size: var(--vt-fs-sm); }
.vt-app .vt-fg-to > .vt-mono { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vt-app .vt-fg-to .vt-ico, .vt-app .vt-fg-zone-to > .vt-ico { flex: none; width: 15px; height: 15px; color: var(--vt-text-3); }
.vt-app .vt-fg-std { margin-top: 12px; }
.vt-app .vt-fg-std > summary { cursor: pointer; padding: 4px 0; color: var(--vt-text-2); font-size: var(--vt-fs-sm); }
.vt-app .vt-fg-std > .vt-hint { margin: 2px 0 6px; }
.vt-app .vt-fg-zones { display: flex; flex-direction: column; }
.vt-app .vt-fg-zone { padding: 10px 0; }
.vt-app .vt-fg-zone:first-child { padding-top: 0; }
.vt-app .vt-fg-zone + .vt-fg-zone { border-top: 1px solid var(--vt-line); }
.vt-app .vt-fg-zone-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.vt-app .vt-fg-zone-name { font-size: var(--vt-fs-md); font-weight: 650; }
.vt-app .vt-fg-tag {
padding: 1px 7px; border-radius: 999px;
background: var(--vt-info-soft); color: color-mix(in oklab, var(--vt-info) 80%, var(--vt-text));
font-size: var(--vt-fs-micro); font-weight: 600;
}
.vt-app .vt-fg-zone-in { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; color: var(--vt-text-3); font-size: var(--vt-fs-sm); }
.vt-app .vt-fg-pol { padding: 1px 8px; border-radius: var(--vt-r-sm); font-size: var(--vt-fs-sm); font-weight: 600; }
.vt-app .vt-fg-pol.is-accept { background: var(--vt-ok-soft); color: color-mix(in oklab, var(--vt-ok) 72%, var(--vt-text)); }
.vt-app .vt-fg-pol.is-stop { background: var(--vt-err-soft); color: color-mix(in oklab, var(--vt-err) 80%, var(--vt-text)); }
.vt-app .vt-fg-pol.is-risk { background: var(--vt-warn-soft); color: color-mix(in oklab, var(--vt-warn) 65%, var(--vt-text)); box-shadow: inset 0 0 0 1px var(--vt-warn); }
.vt-app .vt-fg-zone-to { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.vt-app .vt-fg-dests { display: flex; flex-wrap: wrap; gap: 6px; }
.vt-app .vt-fg-dest { padding: 1px 8px; border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); font-size: var(--vt-fs-sm); font-weight: 600; }
.vt-app .vt-fg-dest.is-risk { border-color: var(--vt-warn); background: var(--vt-warn-soft); }
.vt-app .vt-fg-nets { margin-top: 4px; }
.vt-app .vt-fg-note {
display: flex; align-items: flex-start; gap: 8px; margin: 12px 0 0; padding: 8px 10px;
border-radius: var(--vt-r-sm); background: var(--vt-info-soft); color: var(--vt-text-2);
font-size: var(--vt-fs-sm); text-decoration: none;
}
.vt-app a.vt-fg-note:hover { color: var(--vt-text); text-decoration: none; }
.vt-app .vt-fg-note > .vt-ico { flex: none; width: 15px; height: 15px; margin-top: 1px; color: var(--vt-info); }
.vt-app .vt-fg-foot { margin: 12px 0 0; }
/* ------------------------------------------------ 12. small screens, motion */
@container vt (max-width: 699px) {
@@ -1131,7 +1233,6 @@ h2, h3{
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
.vt-app .vt-chip-ico { width: 22px; height: 22px; }
.vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: var(--vt-fs-micro); }
.vt-app .vt-sys-cpu { grid-template-columns: auto minmax(0, 1fr); }
.vt-app .vt-chart { --vt-mh: 108px; --vt-mg: 12px; }
.vt-app .vt-mult-now { font-size: var(--vt-fs-md); }
.vt-app .vt-mx-odd { display: none; }
@@ -1140,7 +1241,7 @@ h2, h3{
/* clients as cards when the tile is too narrow for the table: one
column on phones, a two-up grid on tablets */
.vt-app .vt-clients { container: vtcl / inline-size; }
.vt-app .vt-clients { container: vtcl vtcell / inline-size; }
@container vtcl (max-width: 919px) {
.vt-app .vt-table-wrap { margin: 0; padding: 0; overflow: visible; }
.vt-app .vt-ctable { min-width: 0; }
@@ -0,0 +1,929 @@
'use strict';
'require baseclass';
/* Firewall at a glance: what a firewall configuration (uci `firewall`, as
`uci get` returns it) lets in from the internet, read the way fw4
(firewall4 as OpenWrt 25.12 ships it, /usr/share/ucode/fw4.uc) reads
it. Pure functions over the config and the interface dump; the view
renders the result. What fw4 does that this follows:
- options parse like fw4's parse_options: booleans are 1/on/true/yes
and 0/off/false/no, policies and targets match by prefix in any case
('acc' is accept, an empty value the first choice), and a value fw4
cannot parse makes it skip the whole section;
- a switched-off or skipped zone does not exist: rules naming it are
skipped, and its networks, like an uplink no zone holds or the
family a zone leaves out, meet the `defaults` policies;
- zones match devices: networks on one device (a modem's wan and
wan_4, wan and wan6) are one, whichever of them a zone names;
- zone policies fall back to `defaults`, whose own fallback is DROP; a
zone's forward policy only covers traffic back out of the same zone,
so traffic into another zone without a forwarding or rule meets
defaults.forward;
- a forwarding or rule from '*' applies to every zone, the internet's
included;
- protocols: unset is tcp and udp; 'tcpudp', 6, 17, 1 and 58 are
spelled out; 'all' matches everything; fw4 drops a leading '!'; ports
only apply to tcp and udp;
- an ipset limits who connects only when it matches the source
address;
- a rule without a target does nothing; a redirect whose target is not
snat is a DNAT, to the router itself without dest_ip, else for each
family to the first dest_ip address of that family (masked to the
prefix given), and is skipped from '*' or without a source zone;
- a script include needs fw4_compatible (default on, except for
/etc/firewall.user).
A zone faces the internet when it masquerades (masq or masq6), holds an
uplink (an interface with the default route and a public IPv4 address:
a router with public addresses routes without NAT), or by OpenWrt's
naming is called wan or wan6 or holds the wan or wan6 network.
Some setups depend on what fw4 does at run time in ways this does not
follow: a zone with covered subnets (it takes some source addresses,
on any device when it names none), several zones that can take
traffic on an internet device (the first in config order wins; device
names compare as patterns, and a device need not be in the dump), and
internet rules or port forwards that use a set this config does not
define and enable (its definition may come from a service this cannot
see). For those it gives no verdict (complex lists why), and the
warnings it would give are only settings to check (unconfirmed). */
/* ports whose exposure from the internet deserves a warning: the router's
own SSH, telnet and web admin */
var ADMIN_PORTS = [ 22, 23, 80, 443 ];
/* the device fw4 puts in place of an excluded '+': none has that name */
var NEVER = '/never/';
/* control messages and multicast membership: open nothing a scanner
could talk to */
var QUIET_PROTO = /^(icmp|ipv6-icmp|igmp)$/;
/* UDP ports a router answers as a client: DHCP (68), DHCPv6 (546) */
var CLIENT_UDP = [ 68, 546 ];
/* IPsec passed on to a host behind the router: OpenWrt's stock
Allow-IPSec-ESP and Allow-ISAKMP are forward rules into lan. To the
router itself they reach its own IKE responder: an opening */
var IPSEC_PROTO = /^(esp|ah)$/;
var IPSEC_UDP = [ 500, 4500 ];
var WAN = /^wan6?$/;
/* IPv4 blocks of the IANA special-purpose registry that are not globally
reachable, plus multicast, as [ octets..., prefix length, reachable ]:
numbers, not dotted text. The most specific block decides, so the two
reachable anycast addresses inside 192.0.0.0/24 count as public. */
var SPECIAL4 = [
[ 0, 0, 0, 0, 8, false ], /* "this network" */
[ 10, 0, 0, 0, 8, false ], /* private */
[ 100, 64, 0, 0, 10, false ], /* shared (CGNAT) */
[ 127, 0, 0, 0, 8, false ], /* loopback */
[ 169, 254, 0, 0, 16, false ], /* link local */
[ 172, 16, 0, 0, 12, false ], /* private */
[ 192, 0, 0, 0, 24, false ], /* IETF protocol assignments */
[ 192, 0, 0, 9, 32, true ], /* PCP anycast */
[ 192, 0, 0, 10, 32, true ], /* TURN anycast */
[ 192, 0, 2, 0, 24, false ], /* documentation */
[ 192, 88, 99, 2, 32, false ], /* 6a44 relay anycast */
[ 192, 168, 0, 0, 16, false ], /* private */
[ 198, 18, 0, 0, 15, false ], /* benchmarking */
[ 198, 51, 100, 0, 24, false ], /* documentation */
[ 203, 0, 113, 0, 24, false ], /* documentation */
[ 224, 0, 0, 0, 4, false ], /* multicast */
[ 240, 0, 0, 0, 4, false ] /* reserved, limited broadcast */
];
function num4(o) { return ((o[0] << 24) >>> 0) + (o[1] << 16) + (o[2] << 8) + o[3]; }
function public4(addr) {
var o = String(addr || '').split('.');
if (o.length !== 4 || o.some(function(x) { return !/^\d{1,3}$/.test(x) || +x > 255; })) return false;
var n = num4(o.map(Number)), best = null;
SPECIAL4.forEach(function(b) {
var mask = b[4] ? (0xffffffff << (32 - b[4])) >>> 0 : 0;
if (((n & mask) >>> 0) === ((num4(b) & mask) >>> 0) && (!best || b[4] > best[4])) best = b;
});
return !best || best[5];
}
/* ---- fw4's option parsers; null is a value fw4 refuses */
function lc(v) { return String(v).toLowerCase(); }
/* to_array: a uci list as it is, an option split at blanks */
function toArray(v) {
if (Array.isArray(v)) return v.map(String);
if (v == null) return [];
v = String(v).trim();
return v === '' ? [] : v.split(/[ \t]+/);
}
function invert(v) {
var neg = false;
v = String(v).replace(/^[ \t]*!/, function() { neg = true; return ''; }).trim();
return v.length ? { invert: neg, val: v } : null;
}
function bool(v) {
if (v === '1' || v === 'on' || v === 'true' || v === 'yes') return true;
if (v === '0' || v === 'off' || v === 'false' || v === 'no') return false;
return null;
}
function oneOf(choices) {
return function(v) {
v = lc(v);
for (var i = 0; i < choices.length; i++)
if (choices[i].substr(0, v.length) === v) return choices[i];
return null;
};
}
var policy = oneOf([ 'accept', 'reject', 'drop' ]);
var target = oneOf([ 'accept', 'reject', 'drop', 'notrack', 'helper', 'mark', 'dscp', 'dnat', 'snat', 'masquerade' ]);
var includeType = oneOf([ 'script', 'nftables' ]);
function family(v) {
if (v === 'any' || v === 'all' || v === '*') return 0;
if (v === 'inet' || v.indexOf('4') > -1) return 4;
return v.indexOf('6') > -1 ? 6 : null;
}
function str(v) { return v; }
function identifier(v) { return /^[a-zA-Z_.][a-zA-Z0-9\/_.-]*$/.test(v) ? v : null; }
function device(v) { return invert(v); }
/* -> [ name ]: 'tcpudp' is two */
function protocol(v) {
var p = invert(v);
if (!p) return null;
switch (lc(p.val)) {
case 'all': case 'any': case '*': return [ 'all' ];
case '1': case 'icmp': return [ 'icmp' ];
case '58': case 'icmpv6': case 'ipv6-icmp': return [ 'ipv6-icmp' ];
case 'tcpudp': return [ 'tcp', 'udp' ];
case '6': return [ 'tcp' ];
case '17': return [ 'udp' ];
}
return [ lc(p.val) ];
}
function port(v) {
var p = invert(v), m = p && /^([0-9]{1,5})([-:]([0-9]{1,5}))?$/.exec(p.val);
if (!m) return null;
var lo = +m[1], hi = m[3] ? +m[3] : lo;
return (lo > hi || hi > 65535) ? null : { invert: p.invert, min: lo, max: hi };
}
function mac(v) {
var m = invert(v);
return m && /^[0-9a-f]{1,2}([:-][0-9a-f]{1,2}){5}$/i.test(m.val) ? m : null;
}
/* a set reference: its name, then the direction of each of its fields
('src', 'dst' or null: the set's own) */
function setmatch(v) {
var m = invert(v);
if (!m) return null;
var name = /^[^ \t]+/.exec(m.val)[0];
var dir = m.val.slice(name.length).trim().split(/[ \t,]/).slice(0, 3).map(function(d) {
return (d === 'dst' || d === 'dest') ? 'dst' : d === 'src' ? 'src' : null;
});
return { invert: m.invert, val: name, dir: dir };
}
/* an ipset's match entry -> { dir, type } */
function ipsettype(v) {
var m = /^(src|dst|dest)_(.+)$/.exec(v);
var t = oneOf([ 'ip', 'port', 'mac', 'net', 'set' ])(m ? m[2] : v);
return t ? { dir: (!m || m[1] === 'src') ? 'src' : 'dst', type: t } : null;
}
/* ucode's +val: a number, NaN refused */
function int(v) {
if (/^\s*[+-]?(inf|nan)/i.test(v)) return null;
var n = Number(v);
return n === n ? n : null;
}
function limit(v) {
var l = invert(v), m = l && /^([0-9]+)(\/([a-z]+))?$/.exec(l.val);
return m && (!m[3] || oneOf([ 'second', 'minute', 'hour', 'day' ])(m[3])) ? l : null;
}
var rejectCode = oneOf([ 'tcp-reset', 'port-unreachable', 'admin-prohibited', 'host-unreachable', 'no-route' ]);
/* conntrack helpers firewall4 defines (/usr/share/firewall4/helpers); fw4
refuses any other name */
var HELPERS = [ 'amanda', 'ftp', 'RAS', 'Q.931', 'irc', 'netbios-ns', 'pptp', 'sane', 'sip', 'snmp', 'tftp', 'rtsp' ];
function cthelper(v) {
var h = invert(v);
return h && HELPERS.indexOf(h.val) >= 0 ? h : null;
}
/* inet_pton: a dotted quad without leading zeros, or IPv6 with an
optional :: and trailing quad -> [ byte ], null when it is neither */
function ipBytes(a) {
a = String(a);
var q = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(a);
if (q) {
q = q.slice(1);
return q.every(function(x) { return (x === '0' || x[0] !== '0') && +x <= 255; }) ? q.map(Number) : null;
}
if (a.indexOf(':') < 0) return null;
var tail = [], m = /^(.*:)(\d{1,3}(?:\.\d{1,3}){3})$/.exec(a);
if (m) {
tail = ipBytes(m[2]);
if (!tail) return null;
a = /::$/.test(m[1]) ? m[1] : m[1].slice(0, -1);
}
var halves = a.split('::');
if (halves.length > 2) return null;
var groups = function(s) { return s === '' ? [] : s.split(':'); };
var head = groups(halves[0]), rest = halves.length === 2 ? groups(halves[1]) : [];
var need = 8 - tail.length / 2, n = head.length + rest.length;
if (head.concat(rest).some(function(g) { return !/^[0-9a-f]{1,4}$/i.test(g); })) return null;
if (halves.length === 2 ? n > need - 1 : n !== need) return null;
while (halves.length === 2 && head.length + rest.length < need) head.push('0');
var out = [];
head.concat(rest).forEach(function(g) { var x = parseInt(g, 16); out.push(x >> 8, x & 255); });
return out.concat(tail);
}
function ipFamily(a) {
var b = ipBytes(a);
return b ? (b.length === 4 ? 4 : 6) : 0;
}
/* the leading one bits of a mask, -1 when they are not contiguous */
function maskBits(m) {
var bits = 0, zero = false;
for (var i = 0; i < m.length; i++) {
for (var b = 7; b >= 0; b--) {
if (m[i] & (1 << b)) { if (zero) return -1; bits++; }
else zero = true;
}
}
return bits;
}
/* the address with all but its first `bits` bits cleared */
function masked(bytes, bits) {
return bytes.map(function(x, i) {
var b = Math.max(0, Math.min(8, bits - 8 * i));
return x & ((0xff << (8 - b)) & 0xff);
});
}
function cmpBytes(a, b) {
for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] - b[i];
return 0;
}
/* an address, address/bits, address/mask or range -> { family, all,
bytes, bits, nc } or { family, all, range: [ lo, hi ] } (all: the
whole address space, as 0.0.0.0/0 or ::/0; nc: a mask that is not
contiguous, which fw4 applies to the address); null when it is
malformed, undefined when it is no address (a network name) */
function subnet(s) {
var p = s.split('/'), r, a, b, bits;
if (p.length === 2) {
a = ipBytes(p[0]);
if (!a) return null;
b = ipBytes(p[1]);
if (b) {
if (b.length !== a.length) return null;
bits = maskBits(b);
return { family: a.length === 4 ? 4 : 6, all: bits === 0, nc: bits < 0,
bytes: bits < 0 ? a.map(function(x, i) { return x & b[i]; }) : a, bits: bits };
}
if (!/^\d+$/.test(p[1])) return null;
return { family: a.length === 4 ? 4 : 6, all: +p[1] === 0, bytes: a, bits: +p[1] };
}
if (p.length > 2) return null;
r = s.split('-');
if (r.length === 2 && ipBytes(r[0])) {
a = ipBytes(r[0]);
b = ipBytes(r[1]);
if (!b || b.length !== a.length) return null;
return { family: a.length === 4 ? 4 : 6, range: [ a, b ],
all: a.every(function(x) { return x === 0; }) && b.every(function(x) { return x === 255; }) };
}
a = ipBytes(s);
return a ? { family: a.length === 4 ? 4 : 6, all: false, bytes: a, bits: a.length * 8 } : undefined;
}
/* ---- the interface dump */
function isUplink(i) {
return !!(i.up &&
(i.route || []).some(function(r) { return r && r.target === '0.0.0.0' && +r.mask === 0; }) &&
(i['ipv4-address'] || []).some(function(a) { return a && public4(a.address); }));
}
/* network.interface dump -> { name: { up, uplink, device, zone, addrs, ips } },
null when there is none. As fw4 reads it: the device is l3_device, else
device; addrs are the interface's subnets (what its name stands for in
a rule), ips the router's own addresses on it as bytes */
function interfaces(dump) {
if (!Array.isArray(dump)) return null;
var out = Object.create(null);
var list = function(v) { return (Array.isArray(v) ? v : []).filter(function(a) { return a && a.address && ipBytes(a.address); }); };
dump.forEach(function(i) {
if (!i || !i.interface) return;
var dev = i.l3_device != null ? i.l3_device : i.device;
var own = list(i['ipv4-address']).concat(list(i['ipv6-address']),
list((Array.isArray(i['ipv6-prefix-assignment']) ? i['ipv6-prefix-assignment'] : []).map(function(p) { return p && p['local-address']; })));
out[String(i.interface)] = {
up: !!i.up, uplink: isUplink(i),
device: dev ? String(dev) : null,
zone: (i.data && i.data.zone) ? String(i.data.zone) : null,
addrs: own.map(function(a) {
var b = ipBytes(a.address);
return { family: b.length === 4 ? 4 : 6, all: +a.mask === 0, bytes: b, bits: +a.mask };
}),
ips: own.map(function(a) { return ipBytes(a.address); })
};
});
return out;
}
/* ---- ports and protocols */
function inRange(p) { return function(r) { return p >= r[0] && p <= r[1]; }; }
/* ports: null (every port) or { pos: [ [ min, max ] ], neg: [...] } */
function hasPort(ports, p) {
return !ports || ((!ports.pos.length || ports.pos.some(inRange(p))) && !ports.neg.some(inRange(p)));
}
function portRanges(list) {
if (!list || !list.length) return null;
var pr = function(neg) { return list.filter(function(x) { return x.invert === neg; }).map(function(x) { return [ x.min, x.max ]; }); };
return { pos: pr(false), neg: pr(true) };
}
function rangeText(rs) {
return rs.map(function(r) { return r[0] === r[1] ? String(r[0]) : r[0] + '-' + r[1]; }).join(', ');
}
/* every port in the ranges is one of the list */
function within(rs, list) {
return rs.length > 0 && rs.every(function(r) {
if (r[1] - r[0] >= list.length) return false;
for (var x = r[0]; x <= r[1]; x++) if (list.indexOf(x) < 0) return false;
return true;
});
}
function lenient(fn, v) {
return toArray(v).map(fn).filter(Boolean).reduce(function(a, x) { return a.concat(x); }, []);
}
/* fw4's infer_family over the families a section refers to -> [ 4 ], [ 6 ],
[ 4, 6 ], or false when two of them conflict (fw4 skips it) */
function families(fam, refs, has4, has6, redirect) {
var res = fam;
for (var i = 0; i < refs.length; i++) {
if (!refs[i] || refs[i] === res) continue;
if (!res) { res = refs[i]; continue; }
return false;
}
/* a redirect with neither family nor address is IPv4 (fw4 keeps fw3's
behaviour) */
if (redirect && !res && !has4 && !has6) return res === 0 ? [ 4, 6 ] : [ 4 ];
if (res == null) res = (has4 && !has6) ? 4 : (has6 && !has4) ? 6 : 0;
return res ? [ res ] : [ 4, 6 ];
}
function famOf(addrs) {
var f = 0;
(addrs || []).forEach(function(a) { f |= a.family === 4 ? 1 : 2; });
return f === 1 ? 4 : f === 2 ? 6 : f ? 0 : null;
}
return baseclass.extend({
ADMIN_PORTS: ADMIN_PORTS,
public4: public4,
/* uplinks of a network.interface dump: interfaces that are up, hold the
IPv4 default route and a public IPv4 address. An access point's
default route runs to the main router over a private address and
does not count. -> [ interface name ] */
uplinks: function(ifaces) {
var ifs = interfaces(ifaces) || {};
return Object.keys(ifs).filter(function(n) { return ifs[n].uplink; });
},
/* values: { section: { '.type', '.name', '.index', option: value } },
dump: network.interface dump, or null when it could not be read ->
{ routed, internet: [ zone ], outside: [...], anyTo, defaults, zones, open,
forwards, standard, off, ignored, warnings, complex, includes, level }.
level is err, warn, info (open, nothing risky), ok, or unknown (no
internet-facing zone or uplink, or a setup this does not follow:
complex says why, and unconfirmed holds the warnings that apply if
traffic reaches them as configured; either way this cannot tell
what the internet reaches, and there are no warnings). Only what this config says: ports UPnP opens at run time
and rules in included files are not in it (includes lists those). */
analyse: function(values, dump) {
var secs = Object.keys(values || {}).map(function(k) { return values[k]; })
.filter(function(s) { return s && typeof s === 'object'; })
.sort(function(a, b) { return (a['.index'] || 0) - (b['.index'] || 0); });
var of = function(type) { return secs.filter(function(s) { return s['.type'] === type; }); };
var ifs = interfaces(dump);
var ups = ifs ? Object.keys(ifs).filter(function(n) { return ifs[n].uplink; }) : [];
var warnings = [], off = [], ignored = [];
/* fw4's parse_options: { option: [ parser, default, list ] } ->
values, or { bad: option } for a value fw4 refuses */
var parse = function(s, spec) {
var out = {};
for (var k in spec) {
var fn = spec[k][0], v = s[k] != null ? s[k] : spec[k][1], r;
if (v == null) { out[k] = null; continue; }
if (spec[k][2]) {
r = [];
var items = toArray(v);
for (var i = 0; i < items.length; i++) {
var x = fn(items[i]);
if (x === null) return { bad: k };
r = r.concat(x);
}
out[k] = r.length ? r : null;
}
else {
if (Array.isArray(v)) return { bad: k };
r = fn(String(v));
if (r === null) return { bad: k };
out[k] = r;
}
}
return out;
};
/* an invalid defaults section stops fw4 before it writes any
ruleset: what is loaded is then unknown */
var dsec = of('defaults')[0] || {};
var DEFS = { input: [ policy, 'drop' ], output: [ policy, 'drop' ], forward: [ policy, 'drop' ],
tcp_reject_code: [ rejectCode, 'tcp-reset' ], any_reject_code: [ rejectCode, 'port-unreachable' ],
synflood_rate: [ limit, '25/second' ], synflood_burst: [ int, '50' ], tcp_ecn: [ int ] };
[ 'drop_invalid', 'syn_flood', 'synflood_protect', 'tcp_syncookies', 'tcp_window_scaling', 'accept_redirects',
'accept_source_route', 'auto_helper', 'custom_chains', 'disable_ipv6', 'flow_offloading', 'flow_offloading_hw',
'auto_includes' ].forEach(function(k) { DEFS[k] = [ bool ]; });
var defs = parse(dsec, DEFS);
if (defs.bad) {
warnings.push({ level: 'warn', code: 'defaults-invalid', option: defs.bad });
defs = { input: 'drop', output: 'drop', forward: 'drop' };
}
/* network: an address, or a network name with the dump's addresses
-> { invert, text, addrs } (addrs null: unknown, no dump) */
var network = function(v) {
var n = invert(v);
if (!n) return null;
var s = subnet(n.val);
if (s === null) return null;
if (s) return { invert: n.invert, text: n.val, addrs: [ s ] };
if (!ifs) return { invert: n.invert, text: n.val, addrs: null };
return ifs[n.val] ? { invert: n.invert, text: n.val, addrs: ifs[n.val].addrs } : null;
};
/* zones; a network or device entry is { net | dev, neg }. With the
dump a network stands for its device, as fw4 matches it, and a
network the dump puts in a zone (data.zone) joins that zone; a
network the dump does not have, or without a device, matches
nothing. Without it, networks compare by name. */
var zones = [], lostNets = [];
var internetish = function(s, nets) {
return bool(s.masq) || bool(s.masq6) || WAN.test(String(s.name)) || nets.some(function(n) { return WAN.test(n) || ups.indexOf(n) >= 0; });
};
var resolve = function(e) {
if (e.net == null || !ifs) return e;
var i = ifs[e.net];
return i && i.device ? { net: e.net, dev: i.device, neg: e.neg } : null;
};
of('zone').forEach(function(s) {
var nets = toArray(s.network).map(invert).filter(function(e) { return e && !e.invert; }).map(function(e) { return e.val; });
var item = { kind: 'zone', name: s.name != null ? String(s.name) : null };
/* every option fw4 parses: a value it refuses in any of them
skips the zone */
var z = bool(s.enabled) === false ? null : parse(s, {
enabled: [ bool, '1' ], name: [ identifier ], family: [ family ],
network: [ device, null, true ], device: [ device, null, true ], subnet: [ network, null, true ],
input: [ policy, defs.input ], output: [ policy, defs.output ], forward: [ policy, defs.forward ],
masq: [ bool ], masq_allow_invalid: [ bool ], masq_src: [ network, null, true ], masq_dest: [ network, null, true ],
masq6: [ bool ], extra: [ str ], extra_src: [ str ], extra_dest: [ str ], mtu_fix: [ bool ], custom_chains: [ bool ],
log: [ int ], log_limit: [ limit ], auto_helper: [ bool, '1' ], helper: [ cthelper, null, true ], counter: [ bool, '1' ]
});
if (!z || z.bad || !z.name) {
if (!z) off.push(item);
else { item.bad = z.bad || 'name'; ignored.push(item); }
/* its networks fall to the default policies */
if (internetish(s, nets)) lostNets = lostNets.concat(nets);
return;
}
var ents = (z.network || []).map(function(e) { return { net: e.val, neg: e.invert }; })
.concat(ifs ? Object.keys(ifs).filter(function(n) { return ifs[n].zone === z.name; }).map(function(n) { return { net: n, neg: false }; }) : [])
.map(resolve)
.concat((z.device || []).map(function(e) { return { dev: e.val, neg: e.invert }; }))
/* fw4 skips a wildcard that is not at the end */
.filter(function(e) { return e && !(e.dev != null && /\+./.test(e.dev)); })
/* and makes an excluded '+' a device that never matches */
.map(function(e) { return (e.neg && e.dev === '+') ? { dev: NEVER, neg: false } : e; });
zones.push({
name: z.name, family: z.family || null,
input: z.input.toUpperCase(), output: z.output.toUpperCase(), forward: z.forward.toUpperCase(),
masq: !!(z.masq || z.masq6),
networks: nets,
pos: ents.filter(function(e) { return !e.neg && e.dev !== '+'; }),
neg: ents.filter(function(e) { return e.neg; }),
anyDev: ents.some(function(e) { return !e.neg && e.dev === '+'; }),
/* covered subnets: fw4 takes only those addresses into the
zone (of a family without one, none) */
subnet: !!z.subnet,
to: [], internet: false
});
});
var byName = Object.create(null);
zones.forEach(function(z) { if (!byName[z.name]) byName[z.name] = z; });
/* traffic from network n: its name and, with the dump, its device */
var via = function(n) { return { net: n, dev: ifs && ifs[n] ? ifs[n].device : null }; };
var hit = function(e, t) {
if (e.dev == null) return e.net === t.net;
if (!t.dev) return false;
return /\+$/.test(e.dev) ? t.dev.indexOf(e.dev.slice(0, -1)) === 0 : t.dev === e.dev;
};
/* does zone z take traffic t (fw4's zone match on devices: one
excluded is never taken; with none named, an exclusion or '+'
takes every other device)? */
var takes = function(z, t) {
if (z.neg.some(function(e) { return hit(e, t); })) return false;
return z.pos.length ? z.pos.some(function(e) { return hit(e, t); }) : (z.neg.length > 0 || z.anyDev);
};
var covers = function(z, n) { return takes(z, via(n)); };
var zoned = function(n) { return zones.some(function(z) { return covers(z, n); }); };
zones.forEach(function(z) {
z.internet = z.masq || WAN.test(z.name) ||
z.networks.some(function(n) { return WAN.test(n); }) ||
ups.some(function(u) { return covers(z, u); });
});
var internet = zones.filter(function(z) { return z.internet; });
/* internet traffic no zone takes: it meets the default policies */
var outside = [];
var addOut = function(name, fam, zone) {
if (!outside.some(function(o) { return o.name === name && o.family === fam; }))
outside.push({ name: name, family: fam, zone: !!zone });
};
ups.forEach(function(u) { if (!zoned(u)) addOut(u, null); });
lostNets.forEach(function(n) {
if (ifs && !(ifs[n] && ifs[n].up)) return;
if (!zoned(n)) addOut(n, null);
});
/* what a zone takes: with the dump the interfaces that are up on its
devices, else the networks and devices it names */
var reach = function(z) {
return ifs ? Object.keys(ifs).filter(function(n) { return ifs[n].up && covers(z, n); }).map(via)
: z.pos.map(function(e) { return { net: e.net != null ? e.net : null, dev: e.dev != null ? e.dev : null }; });
};
/* a zone limited to one family leaves the other family of what it
takes to another zone, or to the default policies */
internet.forEach(function(z) {
if (!z.family) return;
var other = z.family === 4 ? 6 : 4;
var what = reach(z);
var taken = what.length > 0 && what.every(function(t) {
return zones.some(function(y) { return y !== z && y.family !== z.family && takes(y, t); });
});
if (!taken) addOut(z.name, other, true);
});
var routed = internet.length > 0 || outside.length > 0;
var fromInternet = function(src) { return routed && (src === '*' || !!(byName[src] && byName[src].internet)); };
var intoInternet = function(dest) { return dest !== '*' && !!(byName[dest] && byName[dest].internet); };
var zoneRef = function(v) { return (v === '*' || byName[v]) ? v : null; };
var zoneFam = function(ref) { return (ref && ref !== '*') ? byName[ref].family : null; };
/* setups this does not follow (see the top), as { why, name, other }:
it gives no verdict for them */
var complex = [];
var addComplex = function(why, name, other) {
if (!complex.some(function(c) { return c.why === why && ((c.name === name && c.other === (other || null)) || (c.name === other && c.other === name)); }))
complex.push({ why: why, name: name, other: other || null });
};
/* covered subnets on an internet zone, or on a zone that names no
device and so takes those addresses on every one */
zones.forEach(function(z) { if (z.subnet && (z.internet || !z.pos.length)) addComplex('subnet', z.name); });
/* another zone that does or may take traffic on an internet zone's
devices (in a family both have); without the dump a network and a
device do not compare, so that may. Device names compare as
patterns (a '+' at the end matches any rest): do a and b share a
device, and does a take every device b does? */
var stem = function(d) { return /\+$/.test(d) ? d.slice(0, -1) : null; };
var overlaps = function(a, b) {
var p = stem(a), q = stem(b);
return p != null ? (q != null ? p.indexOf(q) === 0 || q.indexOf(p) === 0 : b.indexOf(p) === 0)
: q != null ? a.indexOf(q) === 0 : a === b;
};
var covering = function(a, b) {
var p = stem(a), q = stem(b);
return p != null ? (q != null ? q : b).indexOf(p) === 0 : q == null && a === b;
};
/* the devices both a and b take, as a pattern (null for none) */
var both = function(a, b) {
if (!overlaps(a, b)) return null;
var p = stem(a), q = stem(b);
return p == null ? a : q == null ? b : p.length >= q.length ? a : b;
};
/* t is a network or device an internet zone takes, less those it
excludes (t.not), or { any, not }: every device but those. Does e,
an entry of another zone that excludes not, take some of it (as an
exclusion, all of it)? */
var hit3 = function(e, t, neg, not) {
var gone = function(d, n) {
return (t.not || []).concat(not || []).some(function(x) { return x.dev != null ? d != null && covering(x.dev, d) : n != null && x.net === n; });
};
if (e.dev === NEVER || t.dev === NEVER) return false;
if (t.any) return neg ? false : !gone(e.dev, e.net);
if (e.dev == null ? t.net != null : t.dev != null) {
if (e.dev == null) return e.net === t.net && (neg || !gone(null, e.net));
if (neg) return covering(e.dev, t.dev);
var d = both(e.dev, t.dev);
return d != null && !gone(d, null);
}
return ifs ? false : null;
};
var takes3 = function(z, t) {
var out = z.neg.map(function(e) { return hit3(e, t, true); });
if (out.indexOf(true) >= 0) return false;
var into = z.pos.length ? z.pos.map(function(e) { return hit3(e, t, false, z.neg); }) : [ z.neg.length > 0 || z.anyDev ];
var r = into.indexOf(true) >= 0 ? true : into.indexOf(null) >= 0 ? null : false;
return (r && out.indexOf(null) >= 0) ? null : r;
};
/* what an internet zone takes: its interfaces, the devices it names
(one may exist outside the dump, a VPN's say), and with no device
named, every one it does not exclude */
var points = function(z) {
var pts = reach(z);
if (ifs) pts = pts.concat(z.pos.filter(function(e) { return e.net == null; }).map(function(e) { return { net: null, dev: e.dev }; }));
if (!z.pos.length && (z.anyDev || z.neg.length)) pts.push({ any: true });
return pts.map(function(t) { t.not = z.neg; return t; });
};
internet.forEach(function(z) {
points(z).forEach(function(t) {
zones.forEach(function(y) {
if (y === z || (y.family && z.family && y.family !== z.family)) return;
if (takes3(y, t) !== false) addComplex('shared', z.name, y.name);
});
});
});
/* the router's own addresses: none known without the dump */
var own = ifs ? Object.keys(ifs).reduce(function(a, n) { return a.concat(ifs[n].ips); }, []) : null;
/* is an address (as fw4 DNATs to it: masked to its prefix, or a
range) one of them? */
var isOwn = function(x) {
return own.some(function(b) {
if (x.range) return b.length === x.range[0].length && cmpBytes(b, x.range[0]) >= 0 && cmpBytes(b, x.range[1]) <= 0;
return b.length === x.bytes.length && cmpBytes(b, masked(x.bytes, x.bits)) === 0;
});
};
/* ipsets fw4 loads from this config: { name: { family, match } }. A
set it does not define may come from a service at run time, or
not exist: either way it says nothing this page can rely on */
var ipsets = Object.create(null);
of('ipset').forEach(function(s) {
var p = parse(s, {
enabled: [ bool, '1' ], reload_set: [ bool ], counters: [ bool ], comment: [ str ],
name: [ identifier ], family: [ family, '4' ], storage: [ str ], match: [ ipsettype, null, true ],
iprange: [ str ], portrange: [ str ], netmask: [ int ], maxelem: [ int ], hashsize: [ int ],
timeout: [ int, '-1' ], external: [ str ], entry: [ str, null, true ], loadfile: [ str ]
});
if (p.bad || !p.enabled || !p.name || !p.match) return;
var types = p.match.map(function(m) { return m.type; });
if (types.indexOf('set') >= 0 || (p.family === 0 && (types.indexOf('ip') >= 0 || types.indexOf('net') >= 0))) return;
if (!ipsets[p.name]) ipsets[p.name] = { family: p.family, match: p.match };
});
/* a set limits the source when it matches the source address in
one of its fields: the reference's direction, else the set's.
A negated, unknown, destination or MAC/port-only set does not */
var sourceSet = function(x) {
var set = !x.invert && ipsets[x.val];
return !!set && set.match.some(function(m, i) {
return (m.type === 'ip' || m.type === 'net') && (x.dir[i] || m.dir) === 'src';
});
};
/* a rule or port forward from the internet that uses a set this
config does not define and enable, read from the config as it
is: the set may come from a service this cannot see, and whether
fw4 keeps the section depends on it */
var setless = Object.create(null), hidden = [];
of('rule').concat(of('redirect')).forEach(function(s) {
if (bool(s.enabled) === false || s.ipset == null || s.src == null || !fromInternet(String(s.src))) return;
var ref = Array.isArray(s.ipset) ? null : setmatch(String(s.ipset));
if (!ref || !ipsets[ref.val]) {
addComplex('set', s.name != null ? String(s.name) : null);
setless[s['.name']] = true;
}
});
/* a warning about such a section is not even a setting to check */
var warnFor = function(s, w) {
warnings.push(w);
if (setless[s['.name']]) hidden.push(w);
};
/* what a rule or redirect matches, read leniently so that a section
fw4 skips can still be described */
var describe = function(kind, s) {
var redirect = kind === 'redirect';
var proto = lenient(protocol, s.proto != null ? s.proto : 'tcpudp');
var any = proto.indexOf('all') >= 0;
proto = any ? [ 'all' ] : proto.filter(function(p, i) { return proto.indexOf(p) === i; });
var l4 = !any && proto.some(function(p) { return p === 'tcp' || p === 'udp'; });
var ports = l4 ? portRanges(lenient(port, redirect ? s.src_dport : s.dest_port)) : null;
/* ipset is one option, not a list: 'name dst' is one set */
var ips = lenient(network, s.src_ip);
var sets = (Array.isArray(s.ipset) ? s.ipset : s.ipset != null ? [ s.ipset ] : [])
.map(function(v) { return setmatch(String(v)); }).filter(Boolean);
var srcSets = sets.filter(sourceSet);
var pos = ips.filter(function(a) { return !a.invert; }), neg = ips.filter(function(a) { return a.invert; });
var addrs = ips.reduce(function(a, x) { return a.concat(x.addrs || []); }, []);
var dips = lenient(network, s.dest_ip).reduce(function(a, x) { return a.concat(x.addrs || []); }, []);
var sdips = redirect ? lenient(network, s.src_dip).reduce(function(a, x) { return a.concat(x.addrs || []); }, []) : [];
var fam = s.family != null ? family(String(s.family)) : null;
var src = s.src != null ? String(s.src) : null, dest = s.dest != null ? String(s.dest) : null;
var refs = [ sets.length && ipsets[sets[0].val] ? ipsets[sets[0].val].family : null,
famOf(addrs), famOf(dips), famOf(sdips), byName[src] ? byName[src].family : null, byName[dest] ? byName[dest].family : null ];
var has = function(f) { return addrs.concat(dips, sdips).some(function(a) { return a.family === f; }); };
var fams = [];
var add = function(f) {
(families(f, refs, has(4), has(6), redirect) || []).forEach(function(x) { if (fams.indexOf(x) < 0) fams.push(x); });
};
/* ipv6-icmp is IPv6 whatever the family says */
if (proto.some(function(p) { return p !== 'ipv6-icmp'; })) add(fam);
if (proto.indexOf('ipv6-icmp') >= 0) add(6);
/* restricted when every family the section applies to has a
positive source list that is not the whole address space, or
a set matches the source address; a negated address admits
everyone else, and a MAC address on internet traffic is the
upstream router's */
var restricted = fams.length > 0 && (srcSets.length > 0 || fams.every(function(f) {
var mine = pos.filter(function(a) { return !a.addrs || a.addrs.some(function(x) { return x.family === f; }); });
return mine.length > 0 && !mine.some(function(a) { return a.addrs && a.addrs.some(function(x) { return x.family === f && x.all; }); });
}));
var only = pos.filter(function(a) { return !(a.addrs && a.addrs.length && a.addrs.every(function(x) { return x.all; })); }).map(function(a) { return a.text; })
.concat(srcSets.map(function(x) { return '@' + x.val; }));
return {
kind: kind, name: s.name != null ? String(s.name) : null, src: src, dest: dest,
proto: proto, ports: ports, portText: ports ? rangeText(ports.pos) : '',
portsExcept: ports ? rangeText(ports.neg) : '',
icmp: proto.some(function(p) { return /icmp/.test(p); }) ? toArray(s.icmp_type) : [],
from: { anywhere: !restricted, only: restricted ? only : [], except: restricted ? [] : neg.map(function(a) { return a.text; }) },
family: fams.length === 1 ? fams[0] : null,
families: fams
};
};
var admin = function(item, ports) {
return item.from.anywhere && (item.proto[0] === 'all' || item.proto.indexOf('tcp') >= 0) &&
ADMIN_PORTS.some(function(p) { return hasPort(ports, p); });
};
/* standard housekeeping: ping and other control messages, multicast
membership, DHCP client replies, IPsec passed on to a host */
var quiet = function(item) {
if (item.proto[0] === 'all') return false;
return item.proto.every(function(p) {
if (QUIET_PROTO.test(p)) return true;
if (IPSEC_PROTO.test(p)) return !!item.dest;
if (p !== 'udp' || !item.ports) return false;
return within(item.ports.pos, item.dest ? CLIENT_UDP.concat(IPSEC_UDP) : CLIENT_UDP);
});
};
/* zones every zone forwards to (forwardings from '*') */
var anyTo = [];
of('forwarding').forEach(function(s) {
var item = { kind: 'forwarding', name: null, src: s.src != null ? String(s.src) : null, dest: s.dest != null ? String(s.dest) : null };
if (bool(s.enabled) === false) { off.push(item); return; }
var f = parse(s, { enabled: [ bool, '1' ], family: [ family ], src: [ zoneRef ], dest: [ zoneRef ] });
if (!f.bad && (!f.src || !f.dest)) f.bad = !f.src ? 'src' : 'dest';
if (f.bad) { item.bad = f.bad; ignored.push(item); return; }
if (!families(f.family, [ zoneFam(f.src), zoneFam(f.dest) ])) { item.bad = 'family'; ignored.push(item); return; }
if (f.src === '*' && anyTo.indexOf(f.dest) < 0) anyTo.push(f.dest);
(f.src === '*' ? zones : [ byName[f.src] ]).forEach(function(z) {
if (z.name !== f.dest && z.to.indexOf(f.dest) < 0) z.to.push(f.dest);
});
if (fromInternet(f.src) && !intoInternet(f.dest))
warnings.push({ level: 'err', code: 'forward-into', zone: f.src, dest: f.dest });
});
if (routed && defs.forward === 'accept') warnings.push({ level: 'err', code: 'default-forward' });
internet.forEach(function(z) {
if (z.input === 'ACCEPT') warnings.push({ level: 'err', code: 'input-accept', zone: z.name });
});
outside.forEach(function(o) {
if (defs.input === 'accept') warnings.push({ level: 'err', code: 'outside-input', iface: o.name, family: o.family });
});
/* without the dump an uplink outside every zone cannot be seen */
if (!ifs && routed && defs.input === 'accept' && !outside.length) warnings.push({ level: 'warn', code: 'uplinks-unknown' });
var open = [], fwd = [], standard = [];
of('rule').forEach(function(s) {
var item = describe('rule', s);
if (bool(s.enabled) === false) { off.push(item); return; }
var r = parse(s, {
enabled: [ bool, '1' ], family: [ family ], src: [ zoneRef ], dest: [ zoneRef ],
proto: [ protocol, 'tcpudp', true ], src_ip: [ network, null, true ], src_mac: [ mac, null, true ],
src_port: [ port, null, true ], dest_ip: [ network, null, true ], dest_port: [ port, null, true ],
ipset: [ setmatch ], target: [ target ]
});
if (r.bad || !item.families.length) { item.bad = r.bad || 'family'; ignored.push(item); return; }
if (r.target !== 'accept' || !fromInternet(r.src)) return;
if (quiet(item)) { standard.push(item); return; }
if (r.dest) {
fwd.push(item);
/* every port from any address into another zone: the same
as a forwarding */
if (!intoInternet(r.dest) && item.from.anywhere && !r.dest_ip &&
(item.proto[0] === 'all' || (item.proto.some(function(p) { return p === 'tcp' || p === 'udp'; }) && !(item.ports && item.ports.pos.length))))
warnFor(s, { level: 'err', code: 'rule-into', name: item.name, dest: r.dest });
return;
}
open.push(item);
if (admin(item, item.ports))
warnFor(s, { level: 'warn', code: 'admin-open', kind: 'rule', name: item.name, ports: item.portText });
});
of('redirect').forEach(function(s) {
var item = describe('redirect', s);
item.host = s.dest_ip != null ? String(s.dest_ip) : null;
if (bool(s.enabled) === false) { off.push(item); return; }
var r = parse(s, {
enabled: [ bool, '1' ], family: [ family ], src: [ zoneRef ], dest: [ zoneRef ],
proto: [ protocol, 'tcpudp', true ], src_ip: [ network ], src_mac: [ mac, null, true ], src_port: [ port ],
src_dip: [ network ], src_dport: [ port ], dest_ip: [ network ], dest_port: [ port ],
ipset: [ setmatch ], target: [ target, 'dnat' ]
});
/* fw4 makes every target but snat a DNAT; SNAT opens nothing */
if (!r.bad && r.target === 'snat') return;
if (!r.bad && !r.src) r.bad = 'src';
if (!r.bad && r.src === '*') r.bad = 'src-any';
if (!r.bad && r.dest_ip && (r.dest_ip.invert || (r.dest_ip.addrs || []).some(function(a) { return a.nc; }))) r.bad = 'dest_ip';
if (r.bad || !item.families.length) { item.bad = r.bad || 'family'; ignored.push(item); return; }
if (!fromInternet(r.src)) return;
/* the port on the target: dest_port, or the same as outside (fw4
copies src_dport); null when every port is sent on */
var l4 = item.proto.some(function(p) { return p === 'tcp' || p === 'udp'; });
var inner = !l4 ? null : r.dest_port ? portRanges([ r.dest_port ]) : item.ports;
item.hostPort = inner ? rangeText(inner.pos) : '';
/* local: to the router itself (no dest_ip, or in a family it
applies to the first dest_ip address of that family, which is
the one fw4 DNATs to, is one of the router's own however it is
spelled); null when that cannot be told without the dump */
var first = function(f) { return r.dest_ip.addrs.filter(function(a) { return a.family === f; })[0]; };
item.local = !r.dest_ip ? true : (!own || !r.dest_ip.addrs) ? null
: item.families.some(function(f) { var a = first(f); return !a || isOwn(a); });
if (item.local === null) {
fwd.push(item);
if (admin(item, inner))
warnFor(s, { level: 'warn', code: 'admin-unsure', kind: 'redirect', name: item.name, host: item.host, ports: item.portText });
return;
}
if (!item.local) { fwd.push(item); return; }
open.push(item);
if (admin(item, inner))
warnFor(s, { level: 'warn', code: 'admin-open', kind: 'redirect', name: item.name, ports: item.portText });
});
/* custom rules: files the firewall config names, whose rules this
config does not show (the Custom Rules tab, UPnP's include) */
var includes = [];
of('include').forEach(function(s) {
var path = s.path != null ? String(s.path) : null;
var item = { kind: 'include', name: null, path: path };
if (bool(s.enabled) === false) return;
var inc = parse(s, {
enabled: [ bool, '1' ], path: [ str ], type: [ includeType, 'script' ],
fw4_compatible: [ bool, path !== '/etc/firewall.user' ? '1' : '0' ]
});
if (!inc.bad && !path) inc.bad = 'path';
if (!inc.bad && inc.type === 'script' && !inc.fw4_compatible) inc.bad = 'fw4_compatible';
if (!inc.bad && inc.type !== 'nftables' && (s.position != null || s.chain != null)) inc.bad = s.position != null ? 'position' : 'chain';
if (inc.bad) { item.bad = inc.bad; ignored.push(item); return; }
includes.push(item);
});
/* errors first */
warnings = warnings.filter(function(w) { return w.level === 'err'; }).concat(warnings.filter(function(w) { return w.level !== 'err'; }));
var level = (complex.length || !routed) ? 'unknown'
: warnings.some(function(w) { return w.level === 'err'; }) ? 'err'
: warnings.length ? 'warn' : (open.length || fwd.length) ? 'info' : 'ok';
return {
routed: routed, internet: internet.map(function(z) { return z.name; }), outside: outside, anyTo: anyTo,
defaults: { input: defs.input.toUpperCase(), output: defs.output.toUpperCase(), forward: defs.forward.toUpperCase() },
zones: zones, open: open, forwards: fwd, standard: standard, off: off, ignored: ignored,
warnings: (routed && !complex.length) ? warnings : [], complex: complex,
/* what a rule with a set it cannot find opens is unknown too */
unconfirmed: (routed && complex.length) ? warnings.filter(function(w) { return hidden.indexOf(w) < 0; }) : [],
includes: includes, level: level, interfaces: !!ifs
};
}
});
@@ -22,6 +22,9 @@ function obj(v) { return (v && typeof v === 'object' && !Array.isArray(v)) ? v :
function arr(v) { return Array.isArray(v) ? v : []; }
return baseclass.extend({
/* the highest rate a sample may carry; above it a counter was reset */
MAX_BPS: MAX_BPS,
/* ------------------------------------------------------------- CPU */
/* /proc/stat -> { all: { idle, total }, cores: [ { idle, total } ] } */
@@ -295,17 +298,19 @@ return baseclass.extend({
/* host-hint IPv4 addresses inside one of the given subnets
([ 'a.b.c.d/nn', ... ]); hints are keyed by MAC only, so a hint may
name any address. IPv6 link-local and global addresses are kept. */
name any address. IPv6 link-local and global addresses are kept, at
most 8 addresses in all: a device cannot fill the page or the
reverse DNS queue with addresses. */
hintAddrs: function(hint, subnets) {
var self = this, out = [];
var self = this, out = [], MAX = 8;
hint = obj(hint);
arr(hint.ipaddrs).forEach(function(ip) {
if (typeof ip !== 'string' || out.indexOf(ip) >= 0) return;
if (typeof ip !== 'string' || out.indexOf(ip) >= 0 || out.length >= MAX) return;
if (!subnets.length || subnets.some(function(s) { var p = s.split('/'); return +p[1] >= 8 && self.ipv4InSubnet(ip, p[0], p[1]); }))
out.push(ip);
});
arr(hint.ip6addrs).forEach(function(ip) {
if (typeof ip === 'string' && /^[0-9a-f:]+$/i.test(ip) && ip.length <= 45 && out.indexOf(ip) < 0) out.push(ip);
if (typeof ip === 'string' && /^[0-9a-f:]+$/i.test(ip) && ip.length <= 45 && out.indexOf(ip) < 0 && out.length < MAX) out.push(ip);
});
return out;
},
@@ -23,12 +23,13 @@ return baseclass.extend({
/* Stored series (sessionStorage) are untrusted: keep arrays of exactly
`width` finite numbers with values >= 0 (except t), strictly
increasing t, nothing after notAfter, at most maxLen (newest). */
valid: function(arr, width, maxLen, notAfter) {
/* maxValue (optional): larger values are dropped as well */
valid: function(arr, width, maxLen, notAfter, maxValue) {
if (!Array.isArray(arr)) return [];
var out = [], lastT = -Infinity;
for (var i = 0; i < arr.length; i++) {
var p = arr[i], ok = Array.isArray(p) && p.length === width;
for (var j = 0; ok && j < width; j++) ok = finite(p[j]) && (j === 0 || p[j] >= 0);
for (var j = 0; ok && j < width; j++) ok = finite(p[j]) && (j === 0 || (p[j] >= 0 && !(maxValue != null && p[j] > maxValue)));
if (!ok || p[0] <= lastT || (notAfter != null && p[0] > notAfter)) continue;
out.push(p.slice());
lastT = p[0];
@@ -124,6 +125,115 @@ return baseclass.extend({
}).join('');
},
/* Throughput chart styles, the default first. chartGeom maps a value
to a height for each direction (frac: 0 at the top of the plot, 1 at
the bottom) and gives the baselines, grid lines and scale labels.
Linear scales never go below CHART_FLOOR, so an idle link stays a
flat line, and round up with niceCeil. */
CHART_STYLES: [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ],
CHART_FLOOR: 1e4,
chartGeom: function(style, peakDn, peakUp) {
var self = this, floor = this.CHART_FLOOR;
/* peaks are capped so the scale arithmetic stays finite */
var cap = function(v) { return finite(v) ? Math.min(Math.max(v, 0), 1e13) : 0; };
var pd = cap(peakDn), pu = cap(peakUp);
var nice = function(v) { return self.niceCeil(Math.max(v * 1.05, floor)); };
var clamp = function(x) { return Math.max(0, Math.min(1, x)); };
/* a direction drawn over [top, base] (rising) or [base, top] (hanging) */
var lin = function(scale, top, base) {
return { scale: scale, base: base, flip: base < top,
frac: function(v) { return base + (top - base) * clamp(v / scale); } };
};
var shared = nice(Math.max(pd, pu)), sd = nice(pd), su = nice(pu);
switch (style) {
case 'log':
/* a grid line per x10 from 1 kbit/s (less sits on the baseline)
to at least 1 Mbit/s, so idle chatter stays low */
var lo = 1e3, hi = Math.max(1e6, Math.pow(10, Math.ceil(Math.log10(Math.max(pd, pu, 1) * 1.05))));
var span = Math.log10(hi / lo), grid = [];
var lf = function(v) { return 1 - clamp(Math.log10(Math.max(v, lo) / lo) / span); };
for (var v = lo * 10; v < hi; v *= 10) grid.push(lf(v));
var dir = { scale: hi, base: 1, flip: false, frac: lf };
return { dn: dir, up: dir, grid: [ 0 ].concat(grid), axis: [ 1 ],
labels: [ { at: 0, v: hi }, { at: 1, v: lo, below: true } ] };
case 'mirror':
return { dn: lin(shared, 0, .5), up: lin(shared, 1, .5), grid: [ 0, 1 ], axis: [ .5 ],
labels: [ { at: 0, v: shared }, { at: 1, v: shared, below: true } ] };
case 'mirror-split':
return { dn: lin(sd, 0, .5), up: lin(su, 1, .5), grid: [ 0, 1 ], axis: [ .5 ],
labels: [ { at: 0, v: sd, dir: 'dn' }, { at: 1, v: su, dir: 'up', below: true } ] };
case 'lanes':
return { dn: lin(sd, 0, .36), up: lin(su, .64, 1), grid: [ 0, .64 ], axis: [ .36, 1 ],
/* each lane's scale on its own top line, the upload's in the gap
between the lanes, clear of both curves: at the 72 px minimum
the gap (20 px) holds the label and the download's end dot */
labels: [ { at: 0, v: sd, dir: 'dn' }, { at: .64, v: su, dir: 'up' } ] };
default:
return { dn: lin(shared, 0, 1), up: lin(shared, 0, 1), grid: [ 0, .5 ], axis: [ 1 ],
labels: [ { at: 0, v: shared } ] };
}
},
/* Like segments(), with y = frac(v) * h for any scale */
trace: function(series, idx, t0, t1, w, h, maxGap, frac) {
var segs = [], cur = null, lastT = null, span = (t1 - t0) || 1;
(series || []).forEach(function(p) {
var v = p[idx];
if (!finite(v) || p[0] < t0 - (maxGap || 0)) { cur = null; return; }
if (cur && maxGap && lastT != null && p[0] - lastT > maxGap) cur = null;
if (!cur) { cur = []; segs.push(cur); }
cur.push([ r1((p[0] - t0) / span * w), r1(frac(v) * h) ]);
lastT = p[0];
});
return segs;
},
/* Stretches of [t0, t1] without samples (the page was hidden, polls
failed), as [ x0, x1 ] fractions of the width */
gaps: function(series, t0, t1, maxGap) {
var out = [], span = (t1 - t0) || 1, prev = null;
(series || []).forEach(function(p) {
if (!Array.isArray(p) || !finite(p[0])) return;
if (prev != null && p[0] - prev > maxGap && p[0] > t0)
out.push([ Math.max(0, (prev - t0) / span), Math.min(1, (p[0] - t0) / span) ]);
prev = p[0];
});
return out;
},
/* Smooth path through each segment: monotone cubic (Fritsch-Carlson),
so the curve never overshoots a sample */
curvePath: function(segs) {
return (segs || []).filter(function(s) { return s.length > 0; }).map(function(p) {
var n = p.length, d = [], m = [], i;
if (n < 3) return n === 1 ? 'M' + p[0][0] + ' ' + p[0][1] + 'h0.1' : 'M' + p[0][0] + ' ' + p[0][1] + 'L' + p[1][0] + ' ' + p[1][1];
for (i = 0; i < n - 1; i++) d.push((p[i + 1][1] - p[i][1]) / ((p[i + 1][0] - p[i][0]) || 1));
m[0] = d[0]; m[n - 1] = d[n - 2];
for (i = 1; i < n - 1; i++) m[i] = d[i - 1] * d[i] <= 0 ? 0 : (d[i - 1] + d[i]) / 2;
for (i = 0; i < n - 1; i++) {
if (d[i] === 0) { m[i] = 0; m[i + 1] = 0; continue; }
var a = m[i] / d[i], b = m[i + 1] / d[i], s = a * a + b * b;
if (s > 9) { var k = 3 / Math.sqrt(s); m[i] = k * a * d[i]; m[i + 1] = k * b * d[i]; }
}
var out = 'M' + p[0][0] + ' ' + p[0][1];
for (i = 0; i < n - 1; i++) {
var h = (p[i + 1][0] - p[i][0]) / 3;
out += 'C' + r1(p[i][0] + h) + ' ' + r1(p[i][1] + m[i] * h) + ' ' + r1(p[i + 1][0] - h) + ' ' +
r1(p[i + 1][1] - m[i + 1] * h) + ' ' + p[i + 1][0] + ' ' + p[i + 1][1];
}
return out;
}).join('');
},
/* curvePath() closed down (or up) to the baseline y = base */
curveArea: function(segs, base) {
var self = this;
return (segs || []).filter(function(s) { return s.length > 1; }).map(function(s) {
return self.curvePath([ s ]) + 'L' + s[s.length - 1][0] + ' ' + base + 'L' + s[0][0] + ' ' + base + 'Z';
}).join('');
},
/* index of the point closest in time to t (for hover), or -1 */
nearest: function(series, t) {
var best = -1, bd = Infinity;
@@ -0,0 +1,387 @@
'use strict';
'require view';
'require rpc';
'require ui';
'require vantage.fwglance as fwglance';
/* Network -> Firewall -> Overview (luci-app-vantage): what the firewall
lets in from the internet, which zone reaches which, what is switched
off, and the risky cases, read from uci `firewall`. Read-only: every
item links to the LuCI page that changes it. The menu entry is gated by
luci-app-firewall's own ACL group, which already lets its holder read
and change this config; the app grants nothing for it. It shows what
the config says: ports UPnP opens at run time and rules in custom rule
files are not in it, and the page says so. All DOM is built with E()
and text nodes. */
/* Both reject on a ubus error (permission denied, not found, ...) rather
than resolve an `expect` default: an empty config would read as an
access point, an empty interface list as a router whose uplinks are all
zoned. load() checks the shape of what they return. */
var callUciGet = rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], reject: true });
/* which interface holds the default route, to find an uplink zone that
does not masquerade: LuCI's luci-base-network-status group grants it
(also the app's read group); without it the page falls back to
masquerading and naming */
var callIfDump = rpc.declare({ object: 'network.interface', method: 'dump', reject: true });
function isObject(v) { return v != null && typeof v === 'object' && !Array.isArray(v); }
/* the dashboard's sheet, revalidated on every load (see overview.js) */
var cssPromise = null;
function loadCss() {
if (cssPromise) return cssPromise;
cssPromise = fetch(L.resource('vantage/app.css'), { cache: 'no-cache' }).then(function(r) { return r.ok ? r.text() : ''; }).then(function(text) {
if (!text || !('adoptedStyleSheets' in document) || typeof CSSStyleSheet !== 'function') return;
var sheet = new CSSStyleSheet();
sheet.replaceSync(text);
document.adoptedStyleSheets = document.adoptedStyleSheets.concat([ sheet ]);
}).catch(function() {});
return cssPromise;
}
var SVGNS = 'http://www.w3.org/2000/svg';
var ICONS = {
globe: [ 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18Z', 'M3.4 9.2h17.2M3.4 14.8h17.2', 'M12 3c2.4 2.5 3.6 5.5 3.6 9s-1.2 6.5-3.6 9c-2.4-2.5-3.6-5.5-3.6-9s1.2-6.5 3.6-9Z' ],
zones: [ 'M4 5.5h6.5v6.5H4zM13.5 5.5H20v6.5h-6.5zM8.75 15.5h6.5V22h-6.5z', 'M7.25 12v1.75h9.5V12M12 13.75v1.75' ],
off: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M6 6l12 12' ],
check: [ 'M5 12.5l4.5 4.5L19 7.5' ],
warn: [ 'M10.3 4.6 3 17.5A2 2 0 0 0 4.7 20.5h14.6A2 2 0 0 0 21 17.5L13.7 4.6a2 2 0 0 0-3.4 0Z', 'M12 9.5v4.5', 'M12 17h.01' ],
err: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M9 9l6 6M15 9l-6 6' ],
chevron: [ 'M9.5 6l6 6-6 6' ],
info: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M12 11v5.5', 'M12 7.8h.01' ],
arrow: [ 'M5 12h13.5', 'M13 6.5l5.5 5.5-5.5 5.5' ]
};
function icon(name) {
var s = document.createElementNS(SVGNS, 'svg');
s.setAttribute('class', 'vt-ico');
s.setAttribute('viewBox', '0 0 24 24');
s.setAttribute('aria-hidden', 'true');
s.setAttribute('focusable', 'false');
ICONS[name].forEach(function(d) {
var p = document.createElementNS(SVGNS, 'path');
p.setAttribute('d', d);
s.appendChild(p);
});
return s;
}
function page(tab) { return L.url('admin', 'network', 'firewall', tab); }
/* is admin/services/upnp in the menu tree (luci-app-upnp installed)? */
function hasUpnp(tree) {
var n = tree;
[ 'admin', 'services', 'upnp' ].forEach(function(k) { n = n && n.children ? n.children[k] : null; });
return !!n;
}
/* "TCP, UDP 27015-27030", "IPv6 ICMPv6 echo-request", "IPv4 all protocols":
the address family when the item has one only */
function what(item) {
var fam = item.family ? 'IPv' + item.family + ' ' : '';
if (item.proto[0] === 'all') return fam + _('all protocols');
var l4 = item.proto.filter(function(p) { return p === 'tcp' || p === 'udp'; });
var parts = [];
if (l4.length) {
var ports = item.portText ? ' ' + item.portText : '';
if (item.portsExcept) ports += ' ' + (item.portText ? _('except %s') : _('all ports except %s')).format(item.portsExcept);
parts.push(l4.map(function(p) { return p.toUpperCase(); }).join(', ') + ports);
}
item.proto.forEach(function(p) {
if (l4.indexOf(p) < 0) parts.push(p === 'ipv6-icmp' ? 'ICMPv6' : p.toUpperCase());
});
var icmp = item.icmp && item.icmp.length ? ' ' + item.icmp.join(', ') : '';
return fam + parts.join(', ') + icmp;
}
function from(item) {
var f = item.from;
if (!f.anywhere) return _('from %s').format(f.only.join(', '));
return f.except.length ? _('from anywhere except %s').format(f.except.join(', ')) : _('from anywhere');
}
/* where a port forward sends the traffic: the router's own port, or a host
and port ("[2001:db8::10]:443") */
function target(i) {
if (i.local) return (i.hostPort && i.hostPort !== i.portText) ? _('router port %s').format(i.hostPort) : _('this router');
var host = (i.hostPort && /:/.test(i.host)) ? '[' + i.host + ']' : (i.host || '');
return host + (i.hostPort ? ':' + i.hostPort : '');
}
function zoneName(z) { return z === '*' ? _('any zone') : z; }
function named(item) {
if (item.name) return item.name;
return item.kind === 'redirect' ? _('Unnamed port forward') : _('Unnamed rule');
}
function polBadge(p, risky) {
var cls = risky ? 'is-risk' : p === 'ACCEPT' ? 'is-accept' : 'is-stop';
return E('span', { 'class': 'vt-fg-pol ' + cls }, [ p === 'ACCEPT' ? _('accept') : p === 'DROP' ? _('drop') : _('reject') ]);
}
/* a setup fwglance does not follow (its complex) */
function complexWhy(c) {
if (c.why === 'subnet') return _('the covered subnets of zone %s').format(c.name);
if (c.why === 'shared') return _('zones %s and %s if they share a device').format(c.name, c.other);
return _('the set in %s').format(c.name || _('an unnamed rule'));
}
/* why fw4 skips a section (fwglance's item.bad) */
function ignoredWhy(i) {
if (i.bad === 'fw4_compatible') return _('ignored: not marked as compatible with firewall4');
if (i.bad === 'src-any') return _('ignored: a port forward needs one source zone, not any zone');
if (i.bad === 'family') return _('ignored: its address families conflict');
if (i.bad === 'src' && !i.src) return _('ignored: no source zone');
if ((i.bad === 'src' || i.bad === 'dest') && i[i.bad] && i[i.bad] !== '*')
return _('ignored: zone %s does not exist or is switched off').format(i[i.bad]);
return _('ignored: option %s has a value firewall4 refuses').format(i.bad);
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
/* a failed read is an error, not an empty config (which would read
as an access point); a failed interface dump is no dump (null),
which leaves the uplinks unknown, never an empty list */
return Promise.all([ loadCss(), L.resolveDefault(callUciGet('firewall'), null), L.resolveDefault(ui.menu.load(), null),
L.resolveDefault(callIfDump(), null) ]).then(function(r) {
var values = isObject(r[1]) && isObject(r[1].values) ? r[1].values : null;
var dump = isObject(r[3]) && Array.isArray(r[3]['interface']) ? r[3]['interface'] : null;
if (!values) return { error: true };
var fw = fwglance.analyse(values, dump);
fw.upnp = hasUpnp(r[2]);
return fw;
});
},
render: function(fw) {
if (fw.error)
return E('div', { 'class': 'vt-app vt-fg' }, [ E('div', { 'class': 'vt-body' }, [
this.head('err', _('Could not read the firewall configuration'),
_('This page cannot tell what is open. Reload it; if that does not help, check the firewall pages.'))
]) ]);
return E('div', { 'class': 'vt-app vt-fg' }, [
E('div', { 'class': 'vt-body' }, [
this.renderHead(fw),
fw.warnings.length ? E('div', { 'class': 'vt-fg-warns', 'role': 'list', 'aria-label': _('Warnings') }, fw.warnings.map(this.renderWarning)) : '',
/* a setup it does not follow: what it would warn about, as
cautions to check */
fw.unconfirmed.length ? E('div', { 'class': 'vt-fg-warns', 'role': 'list', 'aria-label': _('Settings to check') }, fw.unconfirmed.map(function(w) {
var c = {};
for (var k in w) c[k] = w[k];
c.level = 'warn';
return this.renderWarning(c);
}, this)) : '',
E('div', { 'class': 'vt-fg-grid' }, [
(fw.routed && !fw.complex.length) ? this.renderOpen(fw) : '',
this.renderZones(fw),
(fw.off.length || fw.ignored.length) ? this.renderOff(fw) : ''
])
])
]);
},
head: function(level, head, sub) {
return E('header', { 'class': 'vt-head' }, [
E('span', { 'class': 'vt-orb vt-orb-' + (level === 'info' ? 'ok' : level) }, [ E('span', { 'class': 'vt-orb-core' }) ]),
E('div', { 'class': 'vt-head-text' }, [
E('h2', { 'class': 'vt-headline' }, [ head ]),
E('p', { 'class': 'vt-subline' }, [ sub ])
])
]);
},
renderHead: function(fw) {
var head, sub, n = fw.open.length + fw.forwards.length;
var facing = fw.internet.concat(fw.outside.map(function(o) { return o.family ? _('%s (IPv%d)').format(o.name, o.family) : o.name; })).join(', ');
if (fw.complex.length) {
head = _('This setup is beyond what this overview checks');
sub = _('What is open depends on how firewall4 handles %s, which this page does not follow. Check the firewall pages.').format(fw.complex.map(complexWhy).filter(function(t, i, a) { return a.indexOf(t) === i; }).join('; '));
if (fw.unconfirmed.length)
sub += ' ' + _('The settings below apply if traffic reaches them as configured.');
}
else if (!fw.routed) {
head = _('No internet-facing zone found');
sub = _('No zone masquerades, holds the uplink or is called wan, so this page cannot tell which side faces the internet. On an access point that is expected; on a router, check the zones below.');
}
else if (fw.warnings.length) {
head = fw.warnings.length === 1 ? _('1 thing needs attention') : _('%d things need attention').format(fw.warnings.length);
sub = _('Internet-facing: %s').format(facing);
}
else if (n) {
head = _('Open from the internet: %d').format(n);
sub = _('%d on this router, %d into your network').format(fw.open.length, fw.forwards.length);
}
else {
head = _('Nothing is open in your firewall rules');
sub = fw.standard.length ? N_(fw.standard.length, 'Only standard housekeeping (ping, DHCP renewals and the like): %d rule', 'Only standard housekeeping (ping, DHCP renewals and the like): %d rules').format(fw.standard.length) : _('Internet-facing: %s').format(facing);
}
return this.head(fw.level, head, sub);
},
renderWarning: function(w) {
var title, reason, tab = 'zones';
if (w.code === 'input-accept') {
title = _('%s accepts every connection').format(w.zone);
reason = _('Anything on the internet can reach the router itself. Set this zone\'s Input to reject.');
}
else if (w.code === 'forward-into') {
title = _('%s is forwarded into %s').format(w.zone === '*' ? _('Any zone') : w.zone, zoneName(w.dest));
reason = _('Anything on the internet can reach that network. Remove the forwarding unless you mean it.');
}
else if (w.code === 'default-forward') {
title = _('Forwarding is accepted by default');
reason = _('Traffic from the internet that no forwarding covers still reaches your other networks. Set Forward to reject in General Settings.');
}
else if (w.code === 'outside-input') {
title = _('%s is in no zone and accepts every connection').format(w.family ? _('%s (IPv%d)').format(w.iface, w.family) : w.iface);
reason = _('The default Input policy applies there and accepts, so anything on the internet can reach the router itself. Add it to the wan zone or set Input to reject in General Settings.');
}
else if (w.code === 'uplinks-unknown') {
title = _('The uplinks could not be checked');
reason = _('The interface list could not be read, and the default Input policy accepts: an uplink in no zone would be open. Set Input to reject in General Settings.');
}
else if (w.code === 'defaults-invalid') {
title = _('The firewall cannot load these settings');
reason = _('Option %s in General Settings has a value firewall4 refuses, so it loads no new rules: what is open is unknown.').format(w.option);
}
else if (w.code === 'rule-into') {
title = _('%s lets the internet into %s').format(w.name || _('Unnamed rule'), zoneName(w.dest));
reason = _('It accepts every port from any address, like a zone forwarding. Limit it to the ports and addresses you need.');
tab = 'rules';
}
else if (w.code === 'admin-unsure') {
title = _('%s may open the router\'s admin').format(w.name || w.ports);
reason = _('It forwards SSH or web ports from any address on the internet to %s. The interface list could not be read, so this page cannot tell whether that is the router itself.').format(w.host);
tab = 'forwards';
}
else {
title = _('Router admin open: %s').format(w.name || w.ports);
reason = _('SSH or the web interface answers any address on the internet. Limit the rule to the addresses you use.');
tab = w.kind === 'redirect' ? 'forwards' : 'rules';
}
return E('a', { 'class': 'vt-chip vt-chip-' + w.level, 'role': 'listitem', 'href': page(tab) }, [
E('span', { 'class': 'vt-chip-ico' }, [ icon(w.level === 'err' ? 'err' : 'warn') ]),
E('span', { 'class': 'vt-chip-main' }, [
E('span', { 'class': 'vt-chip-title' }, [ title ]),
E('span', { 'class': 'vt-chip-reason' }, [ reason ])
])
]);
},
tile: function(ic, title, link, linkText, body) {
return E('section', { 'class': 'vt-tile vt-fg-tile', 'aria-label': title }, [
E('header', { 'class': 'vt-tile-head' }, [
E('h3', { 'class': 'vt-tile-title' }, [ icon(ic), E('span', {}, [ title ]) ]),
E('a', { 'class': 'vt-link', 'href': link }, [ linkText, icon('chevron') ])
])
].concat(body));
},
row: function(href, main, sub, side) {
return E('a', { 'class': 'vt-fg-row', 'href': href }, [
E('span', { 'class': 'vt-fg-row-main' }, [
E('span', { 'class': 'vt-fg-row-name' }, [ main ]),
E('span', { 'class': 'vt-fg-row-sub' }, sub)
]),
side || ''
]);
},
renderOpen: function(fw) {
var self = this, body = [];
var group = function(title, items) {
body.push(E('h4', { 'class': 'vt-fg-group' }, [ title ]));
body.push(E('div', { 'class': 'vt-fg-rows' }, items));
};
var to = function(text) {
return E('span', { 'class': 'vt-fg-to' }, [ icon('arrow'), E('span', { 'class': 'vt-mono' }, [ text ]) ]);
};
var item = function(i) {
var redirect = i.kind === 'redirect';
return self.row(page(redirect ? 'forwards' : 'rules'), named(i),
[ what(i), ' · ', E('span', { 'class': i.from.anywhere ? 'vt-fg-anywhere' : '' }, [ from(i) ]) ],
redirect ? to(target(i)) : i.dest ? to(zoneName(i.dest)) : null);
};
if (fw.open.length) group(_('On this router'), fw.open.map(item));
if (fw.forwards.length) group(_('Into your network'), fw.forwards.map(item));
if (!fw.open.length && !fw.forwards.length)
body.push(E('p', { 'class': 'vt-empty-inline' }, [ fw.warnings.length
? _('No rule or port forward opens anything; the warnings above say what does.')
: _('Nothing is open in your firewall rules.') ]));
if (fw.standard.length)
body.push(E('details', { 'class': 'vt-fg-std' }, [
E('summary', {}, [ N_(fw.standard.length, 'Standard housekeeping: %d rule', 'Standard housekeeping: %d rules').format(fw.standard.length) ]),
E('p', { 'class': 'vt-hint' }, [ _('Ping and other control messages, multicast, DHCP renewals and IPsec passed on to your network: what every router lets in.') ]),
E('div', { 'class': 'vt-fg-rows' }, fw.standard.map(function(i) {
return self.row(page('rules'), named(i), [ what(i) ]);
}))
]));
/* what the config cannot show */
if (fw.upnp)
body.push(E('a', { 'class': 'vt-fg-note', 'href': L.url('admin', 'services', 'upnp') }, [ icon('info'),
E('span', {}, [ _('UPnP is installed: devices on your network can open ports on their own while it runs, and those are not listed here.') ]) ]));
if (fw.includes.length)
body.push(E('p', { 'class': 'vt-fg-note' }, [ icon('info'),
E('span', {}, [ _('Custom rule files are configured (%s); rules in them are not listed here.').format(fw.includes.map(function(i) { return i.path || '?'; }).join(', ')) ]) ]));
/* otherwise still say what this page cannot see: UPnP without its
LuCI app, and nftables files the firewall picks up on its own */
if (!fw.upnp && !fw.includes.length)
body.push(E('p', { 'class': 'vt-hint vt-fg-foot' }, [ _('From the firewall configuration. Ports opened by UPnP or by custom rule files are not shown.') ]));
return this.tile('globe', _('Open from the internet'), page('rules'), _('Traffic Rules'), body);
},
renderZones: function(fw) {
var fallback = fw.defaults.forward === 'ACCEPT';
/* where a zone forwards: its forwardings, and with an accepting
default policy every other zone */
var dests = function(to, risky) {
var chips = to.map(function(d) { return E('span', { 'class': 'vt-fg-dest' + (risky ? ' is-risk' : '') }, [ zoneName(d) ]); });
if (fallback) chips.push(E('span', { 'class': 'vt-fg-dest' + (risky ? ' is-risk' : '') }, [ _('any zone (default policy)') ]));
return chips.length ? E('span', { 'class': 'vt-fg-dests' }, chips) : E('span', { 'class': 'vt-hint' }, [ _('forwards nowhere') ]);
};
var zone = function(name, internet, input, to, hint) {
return E('div', { 'class': 'vt-fg-zone' + (internet ? ' is-internet' : '') }, [
E('div', { 'class': 'vt-fg-zone-head' }, [
E('span', { 'class': 'vt-fg-zone-name' }, [ name ]),
internet ? E('span', { 'class': 'vt-fg-tag' }, [ _('internet') ]) : '',
E('span', { 'class': 'vt-fg-zone-in' }, [ _('Input'), ' ', polBadge(input, internet && input === 'ACCEPT') ])
]),
E('div', { 'class': 'vt-fg-zone-to' }, [ icon('arrow'), dests(to, internet) ]),
hint ? E('div', { 'class': 'vt-hint vt-fg-nets' }, [ hint ]) : ''
]);
};
var rows = fw.zones.map(function(z) {
return zone(z.name, z.internet, z.input, z.to, z.networks.length ? _('Networks: %s').format(z.networks.join(', ')) : '');
}).concat(fw.outside.map(function(o) {
return zone(o.family ? _('%s (IPv%d)').format(o.name, o.family) : o.name, true, fw.defaults.input, fw.anyTo,
o.family ? _('This address family is in no zone: the default policies apply.') : _('In no zone: the default policies apply.'));
}));
if (!rows.length) rows = [ E('p', { 'class': 'vt-empty-inline' }, [ _('No zones are configured.') ]) ];
return this.tile('zones', _('Zones'), page('zones'), _('General Settings'), [ E('div', { 'class': 'vt-fg-zones' }, rows) ]);
},
renderOff: function(fw) {
var self = this;
var rows = fw.off.concat(fw.ignored).map(function(i) {
/* under "Not in effect", what is merely off says so too */
var why = i.bad ? [ ' · ', ignoredWhy(i) ] : fw.ignored.length ? [ ' · ', _('switched off') ] : [];
if (i.kind === 'forwarding')
return self.row(page('zones'), _('Forwarding %s to %s').format(i.src || '?', i.dest || '?'), [ _('zone forwarding') ].concat(why));
if (i.kind === 'zone')
return self.row(page('zones'), _('Zone %s').format(i.name || '?'), [ _('zone') ].concat(why));
if (i.kind === 'include')
return self.row(page('custom'), i.path || '?', [ _('custom rule file') ].concat(why));
return self.row(page(i.kind === 'redirect' ? 'forwards' : 'rules'), named(i),
[ i.kind === 'redirect' ? _('port forward') : _('traffic rule'), ' · ', what(i) ].concat(why));
});
return this.tile('off', fw.ignored.length ? _('Not in effect') : _('Switched off'), page('rules'), _('Traffic Rules'), [
E('div', { 'class': 'vt-fg-rows' }, rows)
]);
}
});
@@ -335,7 +335,23 @@ function tileHead(ic, title, extra) {
]);
}
/* LuCI's menu tree (loaded with the view): a link to a page this device
lacks (Mount Points without block-mount) would only reach a 404. LuCI
keeps such pages in the tree with satisfied false and hides them, as
ui.menu.getChildren does; so every step of the path must be satisfied */
var menuTree = null;
function hasPage(page) {
if (!menuTree) return true;
var n = menuTree;
for (var i = 0; n && i < page.length; i++) {
n = n.children ? n.children[page[i]] : null;
if (n && !n.satisfied) return false;
}
return !!n;
}
function pageLink(page, label) {
if (!hasPage(page)) return '';
return E('a', { 'class': 'vt-link', 'href': L.url.apply(L, page) }, [ E('span', {}, [ label ]), icon('chevron') ]);
}
@@ -403,6 +419,23 @@ function writeLayouts(v) {
} catch (e) {}
}
/* The throughput chart style the viewer picked (series.CHART_STYLES),
in this browser */
var CHART_KEY = 'vantage.chart.v1';
function readChartStyle() {
var v = null;
try { v = window.localStorage.getItem(CHART_KEY); } catch (e) { v = null; }
return series.CHART_STYLES.indexOf(v) >= 0 ? v : series.CHART_STYLES[0];
}
function writeChartStyle(v) {
try {
if (v === series.CHART_STYLES[0]) window.localStorage.removeItem(CHART_KEY);
else window.localStorage.setItem(CHART_KEY, v);
} catch (e) {}
}
/* ================================================================= view */
return view.extend({
@@ -421,8 +454,10 @@ return view.extend({
L.resolveDefault(callMdns(), null),
this.fetch(),
/* reverse DNS is an optional grant: without it, skip it quietly */
L.resolveDefault(callAccess('access-group', 'luci-app-vantage-rdns', 'read'), false)
L.resolveDefault(callAccess('access-group', 'luci-app-vantage-rdns', 'read'), false),
L.resolveDefault(ui.menu.load(), null)
]).then(function(r) {
menuTree = r[7] || null;
return { board: r[1] || {}, canWrite: r[2] === true, mdns: r[4], raw: r[5], canRdns: r[6] === true };
});
},
@@ -488,13 +523,13 @@ return view.extend({
var net = Object.create(null);
if (stored.net && typeof stored.net === 'object')
Object.keys(stored.net).forEach(function(k) {
if (/^[A-Za-z0-9_.:-]{1,40}$/.test(k)) net[k] = series.valid(stored.net[k], 3, HIST_MAX, now);
if (/^[A-Za-z0-9_.:-]{1,40}$/.test(k)) net[k] = series.valid(stored.net[k], 3, HIST_MAX, now, model.MAX_BPS);
});
var clientHist = Object.create(null);
if (stored.clients && typeof stored.clients === 'object')
Object.keys(stored.clients).slice(0, 64).forEach(function(k) {
var mac = names.normMac(k);
if (mac) clientHist[mac] = series.valid(stored.clients[k], 4, 150, now);
if (mac) clientHist[mac] = series.valid(stored.clients[k], 4, 150, now, model.MAX_BPS);
});
this.board = data.board || {};
this.canWrite = data.canWrite;
@@ -515,6 +550,7 @@ return view.extend({
this.foldEls = {};
this.foldBtns = {};
this.layouts = readLayouts();
this.chartStyle = readChartStyle();
this.editing = false;
this.drag = null;
this.applyMdns(data.mdns);
@@ -598,6 +634,9 @@ return view.extend({
Object.keys(st.clientHist).forEach(function(mac) {
if (!p.state.firstSeen[mac]) delete st.clientHist[mac];
});
Object.keys(st.bestStd).forEach(function(mac) {
if (!p.state.firstSeen[mac]) delete st.bestStd[mac];
});
/* per-station history is kept for the session (60 points each,
stations seen in the last minutes only) */
@@ -653,6 +692,13 @@ return view.extend({
resolveMore: function(m) {
var st = this.st, self = this;
var ips = [];
/* answers are kept for the session, but only for addresses still
in use once there are many */
if (Object.keys(st.rdns).length > 1024) {
var keep = Object.create(null);
m.clients.forEach(function(c) { c.ips.forEach(function(ip) { if (ip in st.rdns) keep[ip] = st.rdns[ip]; }); });
st.rdns = keep;
}
m.clients.forEach(function(c) {
c.ips.forEach(function(ip) { if (!(ip in st.rdns) && ips.indexOf(ip) < 0 && ips.length < 64) ips.push(ip); });
});
@@ -984,14 +1030,38 @@ return view.extend({
else this.el.layoutBtn.focus();
},
/* the throughput chart style, picked under Customize */
buildChartPick: function() {
var self = this, names = {
'overlay': _('Overlaid'),
'log': _('Overlaid, log scale'),
'mirror': _('Mirrored'),
'mirror-split': _('Mirrored, scale per direction'),
'lanes': _('Separate lanes')
};
var pick = E('select', { 'class': 'vt-select', 'id': 'vt-chart-style', 'change': function(ev) { self.setChartStyle(ev.target.value); } },
series.CHART_STYLES.map(function(k) { return E('option', { 'value': k }, [ names[k] ]); }));
pick.value = this.chartStyle;
return E('div', { 'class': 'vt-edit-field' }, [ E('label', { 'for': 'vt-chart-style' }, [ _('Throughput chart') ]), pick ]);
},
setChartStyle: function(v) {
if (series.CHART_STYLES.indexOf(v) < 0 || v === this.chartStyle) return;
this.chartStyle = v;
writeChartStyle(v);
this.chartHide();
this.paintChart();
},
buildEditBar: function() {
var self = this, el = this.el;
el.editHint = E('span', { 'class': 'vt-hint' });
el.editHidden = E('div', { 'class': 'vt-edit-hidden' });
el.editSay = E('span', { 'class': 'vt-sr', 'role': 'status', 'aria-live': 'polite' });
el.editBar = E('div', { 'class': 'vt-editbar', 'role': 'region', 'aria-label': _('Arrange the dashboard'), 'hidden': 'hidden' }, [
E('div', { 'class': 'vt-editbar-text' }, [ E('strong', {}, [ _('Arrange the dashboard') ]), el.editHint ]),
el.editBar = E('div', { 'class': 'vt-editbar', 'role': 'region', 'aria-label': _('Customize the dashboard'), 'hidden': 'hidden' }, [
E('div', { 'class': 'vt-editbar-text' }, [ E('strong', {}, [ _('Customize the dashboard') ]), el.editHint ]),
el.editHidden,
this.buildChartPick(),
E('div', { 'class': 'vt-editbar-actions' }, [
E('button', { 'type': 'button', 'class': 'vt-btn', 'click': function() {
var l = self.lay;
@@ -1023,10 +1093,10 @@ return view.extend({
ev.preventDefault();
if (!act.disabled) act.click();
});
o.left = btn('vt-ov-col', 'left', _('Move %s left').format(name), function() {
o.left = btn('vt-ov-col vt-ov-lr', 'left', _('Move %s left').format(name), function() {
self.editTile(id, function(l) { return layout.shift(l, id, -1, self.lay.tier); }, _('%s moved left').format(name));
});
o.right = btn('vt-ov-col vt-ov-flip', 'left', _('Move %s right').format(name), function() {
o.right = btn('vt-ov-col vt-ov-lr vt-ov-flip', 'left', _('Move %s right').format(name), function() {
self.editTile(id, function(l) { return layout.shift(l, id, 1, self.lay.tier); }, _('%s moved right').format(name));
});
o.up = btn('', 'up', _('Move %s up').format(name), function() {
@@ -1177,8 +1247,8 @@ return view.extend({
el.liveText = E('span', { 'class': 'vt-live-text' })
]),
el.layoutBtn = E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-ghost vt-layout-btn', 'aria-pressed': 'false',
'title': _('Arrange the dashboard'), 'click': L.bind(function() { this.setEditing(!this.editing); }, this) }, [
icon('layout'), E('span', {}, [ _('Layout') ])
'title': _('Customize the dashboard'), 'click': L.bind(function() { this.setEditing(!this.editing); }, this) }, [
icon('layout'), E('span', {}, [ _('Customize') ])
])
]);
},
@@ -1223,7 +1293,9 @@ return view.extend({
if (c.client)
return E('button', { 'type': 'button', 'class': 'vt-chip vt-chip-' + c.level, 'role': 'listitem', 'title': c.detail || c.reason,
'click': function() { self.openDrawer('client', c.client); } }, body);
return E('a', { 'class': 'vt-chip vt-chip-' + c.level, 'role': 'listitem', 'title': c.detail || c.reason, 'href': L.url.apply(L, c.page) }, body);
/* without its page (no block-mount), a check links to the status overview */
var to = hasPage(c.page) ? c.page : [ 'admin', 'status', 'overview' ];
return E('a', { 'class': 'vt-chip vt-chip-' + c.level, 'role': 'listitem', 'title': c.detail || c.reason, 'href': L.url.apply(L, to) }, body);
}));
},
@@ -1749,7 +1821,7 @@ return view.extend({
el.chartOver = E('div', { 'class': 'vt-mult-over', 'aria-hidden': 'true' }, [ el.chartCursor, el.chartTip ]);
el.chartX = E('div', { 'class': 'vt-mult-x', 'aria-hidden': 'true' });
var plot = E('div', { 'class': 'vt-mult-wrap', 'tabindex': '0', 'role': 'group',
'aria-label': _('Throughput per link, download above the line and upload below; arrow keys read the values over time') }, [ el.chartRows, el.chartOver, el.chartX ]);
'aria-label': _('Throughput per link; arrow keys read the values over time') }, [ el.chartRows, el.chartOver, el.chartX ]);
plot.addEventListener('pointermove', function(ev) { self.chartPointer(ev); });
plot.addEventListener('pointerleave', function() { if (!self.ui.hover || self.ui.hover.x != null) self.chartHide(); });
plot.addEventListener('keydown', function(ev) { self.chartKey(ev); });
@@ -1772,12 +1844,11 @@ return view.extend({
]));
},
/* one small multiple per link: the uplink, then each radio. Download
(towards the clients) rises above the middle line, upload hangs
below it, each in its direction's colour; each half has its own y
scale (upload is often a hundredth of download, and a busy uplink
would flatten an idle radio), labelled with its unit, on a shared
time axis and crosshair. Columns of a
/* one small multiple per link: the uplink, then each radio, each with
its own y scale (a busy uplink would flatten an idle radio), on a
shared time axis and crosshair. Download (towards the clients) and
upload each keep their direction's colour; how they share the plot
is the viewer's chart style (series.chartGeom). Columns of a
history point [ t, rx, tx ]: the uplink receives what clients
download, a radio sends it. */
chartSeries: function() {
@@ -1788,7 +1859,7 @@ return view.extend({
},
paintChart: function() {
var el = this.el, net = this.st.hist.net, W = 600, H = 100, M = H / 2, now = Date.now();
var el = this.el, net = this.st.hist.net, W = 600, H = 100, now = Date.now(), style = this.chartStyle;
var list = this.chartSeries();
/* sample times follow the device's clock (model.sampleClock) and
can run ahead of the browser's: the axis ends at the newest one */
@@ -1804,35 +1875,47 @@ return view.extend({
var d = (net[s.key] || []).filter(function(p) { return p[0] >= dm.t0 - POLL_S * 1000; });
var sd = series.stats(d, s.dn, dm.t0), su = series.stats(d, s.up, dm.t0);
var last = d.length ? d[d.length - 1] : null, fresh = !!(last && now - last[0] < POLL_S * 3000);
var vd = series.niceCeil(Math.max((sd.peak || 0) * 1.05, 1000)), vu = series.niceCeil(Math.max((su.peak || 0) * 1.05, 1000));
var g = series.chartGeom(style, sd.peak, su.peak);
samples = Math.max(samples, sd.n);
var svg = svgRoot('vt-mult-svg', '0 0 ' + W + ' ' + H);
svg.setAttribute('preserveAspectRatio', 'none');
svg.appendChild(svgPath('M0 0.5H' + W + 'M0 ' + (H - 0.5) + 'H' + W, 'vt-chart-grid'));
/* lines keep half a unit inside the box, or the edge ones get cut */
var yAt = function(f) { return Math.max(0.5, Math.min(H - 0.5, f * H)).toFixed(1); };
svg.appendChild(svgPath(g.grid.map(function(f) { return 'M0 ' + yAt(f) + 'H' + W; }).join(''), 'vt-chart-grid'));
ticks.forEach(function(k) {
var x = (1 - k * 60000 / dm.span) * W;
svg.appendChild(svgPath('M' + x.toFixed(1) + ' 0V' + H, 'vt-chart-vgrid'));
});
/* download: y in [0, M] with 0 at the line; upload mirrored below */
var segD = series.segments(d, s.dn, dm.t0, dm.t1, vd, W, M, POLL_S * 3500);
var segU = series.segments(d, s.up, dm.t0, dm.t1, vu, W, M, POLL_S * 3500).map(function(seg) {
return seg.map(function(pt) { return [ pt[0], H - pt[1] ]; });
});
[ [ segD, false, '' ], [ segU, true, ' vt-dir-up' ] ].forEach(function(h) {
var area = svgPath(series.areaPath(h[0], M), 'vt-series-area' + h[2]);
area.setAttribute('fill', fadeFill(svg, h[1], h[2].trim()));
var segD = series.trace(d, s.dn, dm.t0, dm.t1, W, H, POLL_S * 3500, g.dn.frac);
var segU = series.trace(d, s.up, dm.t0, dm.t1, W, H, POLL_S * 3500, g.up.frac);
[ [ segD, g.dn, '' ], [ segU, g.up, ' vt-dir-up' ] ].forEach(function(h) {
var area = svgPath(series.curveArea(h[0], h[1].base * H), 'vt-series-area' + h[2]);
area.setAttribute('fill', fadeFill(svg, h[1].flip, h[2].trim()));
svg.appendChild(area);
svg.appendChild(svgPath(series.linePath(h[0]), 'vt-series-line' + h[2]));
svg.appendChild(svgPath(series.curvePath(h[0]), 'vt-series-line' + h[2]));
});
svg.appendChild(svgPath('M0 ' + M + 'H' + W, 'vt-chart-mid'));
svg.appendChild(svgPath(g.axis.map(function(f) { return 'M0 ' + yAt(f) + 'H' + W; }).join(''), 'vt-chart-mid'));
function tailDot(segs, cls) {
var tail = segs.length ? segs[segs.length - 1] : null, pt = tail && tail[tail.length - 1];
return pt && fresh ? pos(E('span', { 'class': 'vt-sk-dot' + cls }), pt[0] / W * 100, pt[1] / H * 100) : '';
}
function scaleLabel(l) {
var lab = E('span', { 'class': 'vt-mult-max vt-tn' + (l.below ? ' vt-mult-max-below' : '') },
(l.dir ? [ dirArrow(l.dir), ' ' ] : []).concat([ bitsLabel(l.v) ]));
lab.style.top = (l.at * 100).toFixed(1) + '%';
return lab;
}
/* stretches without samples, so a gap reads as missing data */
function gapBand(gp) {
var b = E('span', { 'class': 'vt-mult-gap' }, gp[1] - gp[0] > .12 ? [ E('span', {}, [ _('no data') ]) ] : []);
b.style.left = (gp[0] * 100).toFixed(2) + '%';
b.style.width = ((gp[1] - gp[0]) * 100).toFixed(2) + '%';
return b;
}
var dotD = E('span', { 'class': 'vt-mult-dot', 'hidden': 'hidden' }), dotU = E('span', { 'class': 'vt-mult-dot vt-dir-up', 'hidden': 'hidden' });
rows.push({ s: s, d: d, vd: vd, vu: vu, dotD: dotD, dotU: dotU });
rows.push({ s: s, d: d, g: g, dotD: dotD, dotU: dotU });
return E('div', { 'class': 'vt-mult' }, [
E('div', { 'class': 'vt-mult-head' }, [
E('span', { 'class': 'vt-mult-name' }, [ s.label ]),
@@ -1844,15 +1927,14 @@ return view.extend({
E('span', {}, [ _('peak'), ' ', E('b', {}, [ dirArrow('dn'), ' ' + fmt.bitsShort(sd.peak) ]), ' ', E('b', {}, [ dirArrow('up'), ' ' + fmt.bitsShort(su.peak) ]) ])
] : [ E('span', {}, [ _('measuring…') ]) ])
]),
E('div', { 'class': 'vt-mult-plot' }, [
svg,
sd.n ? E('span', { 'class': 'vt-mult-max vt-tn' }, [ dirArrow('dn'), ' ' + bitsLabel(vd) ]) : '',
sd.n ? E('span', { 'class': 'vt-mult-max vt-mult-max-up vt-tn' }, [ dirArrow('up'), ' ' + bitsLabel(vu) ]) : '',
E('div', { 'class': 'vt-mult-plot vt-chart-' + style }, [
svg
].concat(sd.n ? g.labels.map(scaleLabel) : [], series.gaps(d, dm.t0, dm.t1, POLL_S * 3500).map(gapBand), [
sd.n < 2 ? E('span', { 'class': 'vt-sk-note' }, [ _('collecting…') ]) : '',
tailDot(segD, ''),
tailDot(segU, ' vt-dir-up'),
dotD, dotU
])
]))
]);
}));
@@ -1904,8 +1986,8 @@ return view.extend({
var vd = p ? p[r.s.dn] : null, vu = p ? p[r.s.up] : null;
r.dotD.hidden = vd == null;
r.dotU.hidden = vu == null;
if (vd != null) pos(r.dotD, x, (1 - Math.min(1, vd / r.vd)) * 50);
if (vu != null) pos(r.dotU, x, 50 + Math.min(1, vu / r.vu) * 50);
if (vd != null) pos(r.dotD, x, r.g.dn.frac(vd) * 100);
if (vu != null) pos(r.dotU, x, r.g.up.frac(vu) * 100);
var bd = vd == null ? fmt.DASH : fmt.bits(vd), bu = vu == null ? fmt.DASH : fmt.bits(vu);
said.push(r.s.label + ': ↓ ' + bd + ' ↑ ' + bu);
return E('div', { 'class': 'vt-tip-link' }, [
@@ -2558,9 +2640,11 @@ return view.extend({
},
/* Checked here for a quick answer, then sent to luci.vantage
set_alias, which checks again, keeps one section per MAC and writes
/etc/config/vantage directly (no staged uci changes, so LuCI's
apply/rollback lock does not block it). The names are reloaded after
set_alias, which checks again, keeps one section per MAC and commits
/etc/config/vantage at once, outside LuCI's Save & Apply (so LuCI's
apply/rollback lock does not block it; libuci's change store is
shared, so changes to that config still pending in LuCI go in with
it). The names are reloaded after
every attempt, failed or not, so the next one starts from what the
device holds.
-> Promise<true | false | string (error text)> */
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -9,5 +9,17 @@
"depends": {
"acl": [ "luci-app-vantage" ]
}
},
"admin/network/firewall/overview": {
"title": "Overview",
"order": 5,
"action": {
"type": "view",
"path": "vantage/firewall"
},
"depends": {
"acl": [ "luci-app-firewall" ],
"uci": { "firewall": true }
}
}
}
@@ -296,8 +296,12 @@ function set_alias(args) {
op = 'add';
}
if (!uci.commit(CONFIG))
if (!uci.commit(CONFIG)) {
// a failed commit leaves the change in libuci's delta, where the
// next commit of this config would apply it: drop it
uci.revert(CONFIG);
return { ok: false, error: 'failed' };
}
return { ok: true, op, mac };
}
-39
View File
@@ -1,39 +0,0 @@
#
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.16
PKG_RELEASE:=2
LUCI_TITLE:=LuCI support for vnt
LUCI_DEPENDS:=+kmod-tun +luci-compat
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-vnt
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ -f /etc/config/vnt ] ; then
echo "备份vnt配置文件/etc/config/vnt到/tmp/vnt_backup"
echo "不重启设备之前再次安装luci-app-vnt 配置不丢失,不用重新配置"
mv -f /etc/config/vnt /tmp/vnt_backup
fi
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
chmod +x /etc/init.d/vnt
if [ -f /tmp/vnt_backup ] ; then
echo "发现vnt备份配置文件/tmp/vnt_backup,开始恢复到/etc/config/vnt"
mv -f /tmp/vnt_backup /etc/config/vnt
echo "请前往 VPN - VNT 界面进行重启插件"
fi
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
-241
View File
@@ -1,241 +0,0 @@
module("luci.controller.vnt", package.seeall)
function index()
if not nixio.fs.access("/etc/config/vnt") then
return
end
entry({"admin", "vpn", "vnt"}, alias("admin", "vpn", "vnt", "vnt"),_("VNT"), 44).dependent = true
entry({"admin", "vpn", "vnt", "vnt"}, cbi("vnt"),_("VNT"), 45).leaf = true
entry({"admin", "vpn", "vnt", "vnt_log"}, form("vnt_log"),_("客户端日志"), 46).leaf = true
entry({"admin", "vpn", "vnt", "get_log"}, call("get_log")).leaf = true
entry({"admin", "vpn", "vnt", "clear_log"}, call("clear_log")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_log2"}, form("vnt_log2"),_("服务端日志"), 47).leaf = true
entry({"admin", "vpn", "vnt", "get_log2"}, call("get_log2")).leaf = true
entry({"admin", "vpn", "vnt", "clear_log2"}, call("clear_log2")).leaf = true
entry({"admin", "vpn", "vnt", "status"}, call("act_status")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_info"}, call("vnt_info")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_all"}, call("vnt_all")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_list"}, call("vnt_list")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_route"}, call("vnt_route")).leaf = true
entry({"admin", "vpn", "vnt", "vnt_cmd"}, call("vnt_cmd")).leaf = true
end
function act_status()
local e = {}
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("vnt", "vnts", "web_port"))
e.port = (port or 29870)
local web = tonumber(uci:get_first("vnt", "vnts", "web"))
e.web = (web or 0)
e.crunning = luci.sys.call("pgrep vnt-cli >/dev/null") == 0
e.srunning = luci.sys.call("pgrep vnts >/dev/null") == 0
local tagfile = io.open("/tmp/vnt_time", "r")
if tagfile then
local tagcontent = tagfile:read("*all")
tagfile:close()
if tagcontent and tagcontent ~= "" then
os.execute("start_time=$(cat /tmp/vnt_time) && time=$(($(date +%s)-start_time)) && day=$((time/86400)) && [ $day -eq 0 ] && day='' || day=${day}天 && time=$(date -u -d @${time} +'%H小时%M分%S秒') && echo $day $time > /tmp/command_vnt 2>&1")
local command_output_file = io.open("/tmp/command_vnt", "r")
if command_output_file then
e.vntsta = command_output_file:read("*all")
command_output_file:close()
end
end
end
local command2 = io.popen('test ! -z "`pidof vnt-cli`" && (top -b -n1 | grep -E "$(pidof vnt-cli)" 2>/dev/null | grep -v grep | awk \'{for (i=1;i<=NF;i++) {if ($i ~ /vnt-cli/) break; else cpu=i}} END {print $cpu}\')')
e.vntcpu = command2:read("*all")
command2:close()
local command3 = io.popen("test ! -z `pidof vnt-cli` && (cat /proc/$(pidof vnt-cli | awk '{print $NF}')/status | grep -w VmRSS | awk '{printf \"%.2f MB\", $2/1024}')")
e.vntram = command3:read("*all")
command3:close()
local stagfile = io.open("/tmp/vnts_time", "r")
if stagfile then
local stagcontent = stagfile:read("*all")
stagfile:close()
if stagcontent and stagcontent ~= "" then
os.execute("start_time=$(cat /tmp/vnts_time) && time=$(($(date +%s)-start_time)) && day=$((time/86400)) && [ $day -eq 0 ] && day='' || day=${day}天 && time=$(date -u -d @${time} +'%H小时%M分%S秒') && echo $day $time > /tmp/command_vnts 2>&1")
local command_output_file2 = io.open("/tmp/command_vnts", "r")
if command_output_file2 then
e.vntsta2 = command_output_file2:read("*all")
command_output_file2:close()
end
end
end
local command5 = io.popen('test ! -z "`pidof vnts`" && (top -b -n1 | grep -E "$(pidof vnts)" 2>/dev/null | grep -v grep | awk \'{for (i=1;i<=NF;i++) {if ($i ~ /vnts/) break; else cpu=i}} END {print $cpu}\')')
e.vntscpu = command5:read("*all")
command5:close()
local command6 = io.popen("test ! -z `pidof vnts` && (cat /proc/$(pidof vnts | awk '{print $NF}')/status | grep -w VmRSS | awk '{printf \"%.2f MB\", $2/1024}')")
e.vntsram = command6:read("*all")
command6:close()
local command7 = io.popen("([ -s /tmp/vnt.tag ] && cat /tmp/vnt.tag ) || (echo `$(uci -q get vnt.@vnt-cli[0].clibin) -h |grep 'version:'| awk -F 'version:' '{print $2}'` > /tmp/vnt.tag && cat /tmp/vnt.tag)")
e.vnttag = command7:read("*all")
command7:close()
local command8 = io.popen("([ -s /tmp/vntnew.tag ] && cat /tmp/vntnew.tag ) || ( curl -L -k -s --connect-timeout 3 --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' https://api.github.com/repos/vnt-dev/vnt/releases/latest | grep tag_name | sed 's/[^0-9.]*//g' >/tmp/vntnew.tag && cat /tmp/vntnew.tag )")
e.vntnewtag = command8:read("*all")
command8:close()
local command9 = io.popen("([ -s /tmp/vnts.tag ] && cat /tmp/vnts.tag ) || ( echo `$(uci -q get vnt.@vnts[0].vntsbin) -V | awk -F 'version: ' '{print $2}'` > /tmp/vnts.tag && cat /tmp/vnts.tag && [ ! -s /tmp/vnts.tag ] && echo '?' >> /tmp/vnts.tag && cat /tmp/vnts.tag )")
e.vntstag = command9:read("*all")
command9:close()
local command0 = io.popen("([ -s /tmp/vntsnew.tag ] && cat /tmp/vntsnew.tag ) || ( curl -L -k -s --connect-timeout 3 --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' https://api.github.com/repos/vnt-dev/vnts/releases/latest | grep tag_name | sed 's/[^0-9.]*//g' >/tmp/vntsnew.tag ; cat /tmp/vntsnew.tag )")
e.vntsnewtag = command0:read("*all")
command0:close()
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_log()
local log = ""
local files = {"/tmp/vnt-cli.log"}
for i, file in ipairs(files) do
if luci.sys.call("[ -f '" .. file .. "' ]") == 0 then
log = log .. luci.sys.exec("cat " .. file)
end
end
luci.http.write(log)
end
function clear_log()
luci.sys.call("rm -rf /tmp/vnt-cli*.log")
end
function get_log2()
local log2 = ""
local files = {"/tmp/vnts.log"}
for i, file in ipairs(files) do
if luci.sys.call("[ -f '" .. file .. "' ]") == 0 then
log2 = log2 .. luci.sys.exec("cat " .. file)
end
end
luci.http.write(log2)
end
function clear_log2()
luci.sys.call("rm -rf /tmp/vnts*.log")
end
function vnt_info()
os.execute("rm -rf /tmp/vnt-cli_info")
local info = luci.sys.exec("$(uci -q get vnt.@vnt-cli[0].clibin) --info 2>&1")
info = info:gsub("Connection status", "连接状态")
info = info:gsub("Virtual ip", "虚拟IP")
info = info:gsub("Virtual gateway", "虚拟网关")
info = info:gsub("Virtual netmask", "虚拟网络掩码")
info = info:gsub("NAT type", "NAT类型")
info = info:gsub("Relay server", "服务器地址")
info = info:gsub("Public ips", "外网IP")
info = info:gsub("Local addr", "WAN口IP")
luci.http.prepare_content("application/json")
luci.http.write_json({ info = info })
end
function vnt_all()
os.execute("rm -rf /tmp/vnt-cli_all")
local all = luci.sys.exec("$(uci -q get vnt.@vnt-cli[0].clibin) --all 2>&1")
all = all:gsub("Virtual Ip", "虚拟IP")
all = all:gsub("NAT Type", "NAT类型")
all = all:gsub("Public Ips", "外网IP")
all = all:gsub("Local Ip", "WAN口IP")
local rows = {}
for line in all:gmatch("[^\r\n]+") do
local cols = {}
for col in line:gmatch("%S+") do
table.insert(cols, col)
end
table.insert(rows, cols)
end
local html_table = "<table>"
for i, row in ipairs(rows) do
html_table = html_table .. "<tr>"
for j, col in ipairs(row) do
local colors = {"#FFA500", "#800000", "#0000FF", "#3CB371", "#00BFFF", "#DAA520", "#48D1CC", "#6600CC", "#2F4F4F"}
local color = colors[(j % 9) + 1]
html_table = html_table .. "<td><font color='" .. color .. "'>" .. col .. "</font></td>"
end
html_table = html_table .. "</tr>"
end
html_all = html_table .. "</table>"
luci.http.prepare_content("application/json")
luci.http.write_json({ all = html_all })
end
function vnt_route()
os.execute("rm -rf /tmp/vnt-cli_route")
local route = luci.sys.exec("$(uci -q get vnt.@vnt-cli[0].clibin) --route 2>&1")
route = route:gsub("Next Hop", "下一跳地址")
route = route:gsub("Interface", "连接地址")
local rows = {}
for line in route:gmatch("[^\r\n]+") do
local cols = {}
for col in line:gmatch("%S+") do
table.insert(cols, col)
end
table.insert(rows, cols)
end
local html_table = "<table>"
for i, row in ipairs(rows) do
html_table = html_table .. "<tr>"
for j, col in ipairs(row) do
local colors = {"#FFA500", "#800000", "#0000FF", "#3CB371", "#00BFFF"}
local color = colors[(j % 5) + 1]
html_table = html_table .. "<td><font color='" .. color .. "'>" .. col .. "</font></td>"
end
html_table = html_table .. "</tr>"
end
html_route = html_table .. "</table>"
luci.http.prepare_content("application/json")
luci.http.write_json({ route = html_route })
end
function vnt_list()
os.execute("rm -rf /tmp/vnt-cli_list")
local list = luci.sys.exec("$(uci -q get vnt.@vnt-cli[0].clibin) --list 2>&1")
list = list:gsub("Virtual Ip", "虚拟IP")
local rows = {}
for line in list:gmatch("[^\r\n]+") do
local cols = {}
for col in line:gmatch("%S+") do
table.insert(cols, col)
end
table.insert(rows, cols)
end
local html_table = "<table>"
for i, row in ipairs(rows) do
html_table = html_table .. "<tr>"
for j, col in ipairs(row) do
local colors = {"#FFA500", "#800000", "#0000FF", "#3CB371", "#00BFFF"}
local color = colors[(j % 5) + 1]
html_table = html_table .. "<td><font color='" .. color .. "'>" .. col .. "</font></td>"
end
html_table = html_table .. "</tr>"
end
html_table = html_table .. "</table>"
luci.http.prepare_content("application/json")
luci.http.write_json({ list = html_table })
end
function vnt_cmd()
os.execute("rm -rf /tmp/vnt*_cmd")
local html_cmd= luci.sys.exec("echo $(cat /proc/$(pidof vnt-cli)/cmdline | awk '{print $1}') 2>&1")
html_cmd = html_cmd:gsub("--no", "-不")
html_cmd = html_cmd:gsub("--dns", "dns服务")
html_cmd = html_cmd:gsub("--ip", "--地址")
html_cmd = html_cmd:gsub("--nic", "网卡名")
html_cmd = html_cmd:gsub("--use", "直连")
html_cmd = html_cmd:gsub("-channel", "方式")
luci.http.prepare_content("application/json")
luci.http.write_json({ cmd = html_cmd })
end
-600
View File
@@ -1,600 +0,0 @@
local http = luci.http
local nixio = require "nixio"
m = Map("vnt")
m.description = translate('vnt是一个简便高效的异地组网、内网穿透工具。<br>官网:<a href="http://rustvnt.com/">rustvnt.com</a>&nbsp;&nbsp;项目地址:<a href="https://github.com/vnt-dev/vnt">github.com/vnt-dev/vnt</a>&nbsp;&nbsp;安卓端、GUI:<a href="https://github.com/lmq8267/VntApp">VntApp</a>&nbsp;&nbsp;<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=o3Rr9xUWwAAnV9TkU_Nyj3yHNLs9k5F5&authKey=l1FKvqk7%2F256SK%2FHrw0PUhs%2Bar%2BtKYx0pLb7aiwBN9%2BKBCY8sOzWWEqtl4pdXAT7&noverify=0&group_code=1034868233">QQ群</a>')
-- vnt-cli
m:section(SimpleSection).template = "vnt/vnt_status"
s = m:section(TypedSection, "vnt-cli", translate("vnt-cli 客户端设置"))
s.anonymous = true
s:tab("general", translate("基本设置"))
s:tab("privacy", translate("高级设置"))
s:tab("infos", translate("连接信息"))
s:tab("upload", translate("上传程序"))
switch = s:taboption("general",Flag, "enabled", translate("Enable"))
switch.rmempty = false
btncq = s:taboption("general", Button, "btncq", translate("重启"))
btncq.inputtitle = translate("重启")
btncq.description = translate("在没有修改参数的情况下快速重新启动一次")
btncq.inputstyle = "apply"
btncq:depends("enabled", "1")
btncq.write = function()
os.execute("/etc/init.d/vnt restart ")
end
token = s:taboption("general", Value, "token", translate("Token"),
translate("这是必填项!一个虚拟局域网的标识,连接同一服务器时,使用相同token的客户端设备才会组成一个局域网(这是 -k 参数)"))
token.optional = false
token.password = true
token.placeholder = "test"
token.datatype = "string"
token.maxlength = 63
token.minlength = 1
token.validate = function(self, value, section)
if value and #value >= 1 and #value <= 63 then
return value
else
return nil, translate("Token为必填项,可填1至63位字符")
end
end
switch.write = function(self, section, value)
if value == "1" then
token.rmempty = false
else
token.rmempty = true
end
return Flag.write(self, section, value)
end
mode = s:taboption("general",ListValue, "mode", translate("接口模式"),
translate("动态分配将由服务器随机分配一个未使用的ip地址,重启程序可能导致ip变化,建议手动指定ip并指定设备ID"))
mode:value("dhcp",translate("动态分配"))
mode:value("static",translate("手动指定"))
ipaddr = s:taboption("general",Value, "ipaddr", translate("接口IP地址"),
translate("每个vnt-cli客户端的接口IP不能相同"))
ipaddr.optional = false
ipaddr.datatype = "ip4addr"
ipaddr.placeholder = "10.26.0.5"
ipaddr:depends("mode", "static")
desvice_id = s:taboption("general",Value, "desvice_id", translate("设备ID"),
translate("每台设备的唯一标识,注意不要重复,每个vnt-cli客户端的设备ID不能相同"))
desvice_id.placeholder = "5"
localadd = s:taboption("general",DynamicList, "localadd", translate("本地网段"),
translate("每个vnt-cli客户端的内网lan网段不能相同,例如本机lanIP为192.168.1.1则填 192.168.1.0/24 "))
localadd.placeholder = "192.168.1.0/24"
peeradd = s:taboption("general",DynamicList, "peeradd", translate("对端网段"),
translate("格式为对端的lanIP网段加英文,对端的接口IP,例如对端lanIP为192.168.2.1接口IP10.26.0.3则填192.168.2.0/24,10.26.0.3"))
peeradd.placeholder = "192.168.2.0/24,10.26.0.3"
forward = s:taboption("general",Flag, "forward", translate("启用IP转发"),
translate("内置的IP代理较为简单,而且一般来说直接使用网卡NAT转发性能会更高,所以默认开启IP转发关闭内置的ip代理"))
forward.rmempty = false
allow_wg = s:taboption("general",Flag, "allow_wg", translate("允许WireGuard接入"),
translate("由于WireGuard是来自vnts转发的,如果vnts不受信任,这将会有安全隐患,所以默认不允许WireGuard流量访问本机"))
allow_wg.rmempty = false
log = s:taboption("general",Flag, "log", translate("启用日志"),
translate("运行日志在/tmp/vnt.log,可在上方客户端日志查看"))
log.rmempty = false
clibin = s:taboption("privacy", Value, "clibin", translate("vnt-cli程序路径"),
translate("自定义vnt-cli的存放路径,确保填写完整的路径及名称,若指定的路径可用空间不足将会自动移至/tmp/vnt-cli"))
clibin.placeholder = "/usr/bin/vnt-cli"
vntshost = s:taboption("privacy", Value, "vntshost", translate("vnts服务器地址"),
translate("相同的服务器,相同token的设备才会组成一个局域网<br>协议支持使用tcp://和ws://和wss://,默认为udp://"))
vntshost.placeholder = "tcp://域名:端口"
vntdns = s:taboption("privacy",DynamicList, "vntdns", translate("DNS服务器"),
translate("指定DNS服务器地址,可使用多个dns,不指定时使用系统解析"))
vntdns.placeholder = "8.8.8.8:53"
stunhost = s:taboption("privacy",DynamicList, "stunhost", translate("stun服务器地址"),
translate("使用stun服务探测客户端NAT类型,不同类型有不同的打洞策略,最多三个,超过将被忽略<br>已内置谷歌 QQ 可不填,一些<a href='https://github.com/heiher/natmap/issues/18#issue-1580804352' target='_blank'>免费stun服务器</a>"))
stunhost.placeholder = "stun.qq.com:3478"
local model = nixio.fs.readfile("/proc/device-tree/model") or ""
local hostname = nixio.fs.readfile("/proc/sys/kernel/hostname") or ""
model = model:gsub("\n", "")
hostname = hostname:gsub("\n", "")
local device_name = (model ~= "" and model) or (hostname ~= "" and hostname) or "OpenWrt"
device_name = device_name:gsub(" ", "_")
desvice_name = s:taboption("privacy", Value, "desvice_name", translate("设备名称"),
translate("本机设备名称,方便区分不同设备"))
desvice_name.placeholder = device_name
desvice_name.default = device_name
tunname = s:taboption("privacy",Value, "tunname", translate("虚拟网卡名称"),
translate("自定义虚拟网卡的名称,在多开时虚拟网卡名称不能相同,默认为 vnt-tun"))
tunname.placeholder = "vnt-tun"
relay = s:taboption("privacy",ListValue, "relay", translate("传输模式"),
translate("自动:根据当前网络环境,自动选择由服务器或客户端转发还是客户端之间直连<br>转发:仅中继转发,会禁止打洞/p2p直连,只使用服务器或客户端转发<br>p2p:仅直连模式,会禁止网络数据从服务器/客户端转发,只会使用服务器转发控制包<br>在网络环境很差时,不使用p2p只使用服务器中继转发效果可能更好(可以配合服务器的tcp协议一起使用)<br>tcp直连需要指定监听port,且防火墙需要放行第一个端口,才有几率tcp-p2p"))
relay:value("自动")
relay:value("转发")
relay:value("P2P")
client_port = s:taboption("privacy", Value, "client_port", translate("本地监听端口"),
translate("取值0~65535,指定本地监听的端口组,多个端口使用英文逗号分隔,多个端口可以分摊流量,增加并发,tcp会监听端口组的第一个端口,用于tcp直连<br>例1:12345,12346,12347 表示udp监听12345、12346、12347这三个端口,tcp监听12345端口<br>例2:0,0 表示udp监听两个未使用的端口,tcp监听一个未使用的端口"))
client_port.placeholder = "0,0"
mapping = s:taboption("privacy",DynamicList, "mapping", translate("端口映射"),
translate("端口映射,可以设置多个映射地址,例如 udp:0.0.0.0:80-10.26.0.10:80 和 tcp:0.0.0.0:80-10.26.0.11:81 <br>表示将本地udp 80端口的数据转发到10.26.0.10:80,将本地tcp 80端口的数据转发到10.26.0.11:81,转发的目的地址可以使用域名+端口"))
mapping.placeholder = "tcp:0.0.0.0:80-10.26.0.10:80"
mtu = s:taboption("privacy",Value, "mtu", translate("MTU"),
translate("设置虚拟网卡的mtu值,大多数情况下(留空)使用默认值效率会更高,也可根据实际情况进行微调,默认值:不加密1450,加密1410"))
mtu.datatype = "range(1,1500)"
mtu.placeholder = "1300"
punch = s:taboption("privacy",ListValue, "punch", translate("打洞模式"),
translate("选择只使用ipv4打洞或者只使用ipv6打洞,all都会使用,ipv6相对于ipv4速率可能会有所降低,ipv6更容易打通直连"))
punch:value("all",translate("都使用"))
punch:value("ipv4",translate("仅ipv4-tcp/udp"))
punch:value("ipv6",translate("仅ipv6-tcp/udp"))
punch:value("ipv4-tcp",translate("仅ipv4-tcp"))
punch:value("ipv6-tcp",translate("仅ipv6-tcp"))
punch:value("ipv4-udp",translate("仅ipv4-udp"))
punch:value("ipv6-udp",translate("仅ipv6-udp"))
comp = s:taboption("privacy",ListValue, "comp", translate("启用压缩"),
translate("启用压缩,默认仅支持lz4压缩,开启压缩后,如果数据包长度大于等于128,则会使用压缩,否则还是会按原数据发送<br>也支持zstd压缩,但是需要确认程序编译时是否添加支持zstd否则无法启动!编译参数--features zstd<br>如果宽度速度比较慢,可以考虑使用高级别的压缩"))
comp:value("OFF",translate("关闭"))
comp:value("lz4")
comp:value("zstd")
passmode = s:taboption("privacy",ListValue, "passmode", translate("加密模式"),
translate("默认off不加密,通常情况aes_gcm安全性高、aes_ecb性能更好,在低性能设备上aes_ecb、chacha20、chacha20_poly1305、xor速度最快<br>注意:xor为数据混淆,并不是一种强大的加密算法,易被破解,因此不适合用于真正的加密需求"))
passmode:value("off",translate("不加密"))
passmode:value("aes_ecb")
passmode:value("sm4_cbc")
passmode:value("aes_cbc")
passmode:value("aes_gcm")
passmode:value("chacha20")
passmode:value("chacha20_poly1305")
passmode:value("xor")
key = s:taboption("privacy",Value, "key", translate("加密密钥"),
translate("先开启上方的加密模式再填写密钥才能生效,使用相同密钥的客户端才能通信,服务端无法解密(包括中继转发数据)"))
key.placeholder = "wodemima"
key.password = true
key:depends("passmode", "aes_ecb")
key:depends("passmode", "sm4_cbc")
key:depends("passmode", "sm4_cbc")
key:depends("passmode", "aes_gcm")
key:depends("passmode", "chacha20")
key:depends("passmode", "chacha20_poly1305")
key:depends("passmode", "xor")
local sys = require("luci.sys")
local_dev = s:taboption("privacy", ListValue, "local_dev", translate("绑定出口网卡"),
translate("指定作为流量出口的网卡"))
local_dev.optional = false
-- 添加空白值作为默认选项
local_dev:value("", translate("不绑定"))
-- 获取通过 ifconfig 列出的网卡接口
local ifaces = sys.exec("ifconfig | grep -E '^[a-zA-Z0-9]+' | awk -F':' '{print $1}' | awk '{print $1}'")
for iface in string.gmatch(ifaces, "%S+") do
-- 使用 ip 命令获取网卡的 IP 地址
local ip_addr = sys.exec("ifconfig " .. iface .. " | grep 'inet ' | awk '{print $2}'")
ip_addr = ip_addr:gsub("%s+", "") -- 去除回车和空白字符
-- 如果没有找到 IP 地址,则继续下一个网卡
if ip_addr ~= "" then
-- 将网卡名作为值,IP地址作为显示名称
local_dev:value(iface, iface .. " (" .. ip_addr .. ")")
end
end
serverw = s:taboption("privacy",Flag, "serverw", translate("启用服务端客户端加密"),
translate("用服务端通信的数据加密,采用rsa+aes256gcm加密客户端和服务端之间通信的数据,可以避免token泄漏、中间人攻击,<br>上面的加密模式是客户端与客户端之间加密,这是服务器和客户端之间的加密,不是一个性质,无需选择加密模式"))
serverw.rmempty = false
finger = s:taboption("privacy",Flag, "finger", translate("启用数据指纹校验"),
translate("开启数据指纹校验,可增加安全性,如果服务端开启指纹校验,则客户端也必须开启,开启会损耗一部分性能。<br>注意:默认情况下服务端不会对中转的数据做校验,如果要对中转的数据做校验,则需要客户端、服务端都开启此参数"))
finger.rmempty = false
first_latency = s:taboption("privacy",Flag, "first_latency", translate("启用优化传输"),
translate("启用后优先使用低延迟通道,默认情况下优先使用p2p通道,某些情况下可能p2p比客户端中继延迟更高,可启用此参数进行优化传输"))
first_latency.rmempty = false
disable_stats = s:taboption("privacy",Flag, "disable_stats", translate("启用流量统计"),
translate("记录vnt使用的流量统计信息"))
disable_stats.rmempty = false
vnt_forward = s:taboption("privacy",MultiValue, "vnt_forward", translate("访问控制"),
translate("设置不同网络区域之间的流量允许规则"))
vnt_forward:value("vntfwlan", translate("允许从虚拟网络 VNT 到局域网 lan 的流量"))
vnt_forward:value("vntfwwan", translate("允许从虚拟网络 VNT 到广域网 wan 的流量"))
vnt_forward:value("lanfwvnt", translate("允许从局域网 lan 到虚拟网络 VNT 的流量"))
vnt_forward:value("wanfwvnt", translate("允许从广域网 wan 到虚拟网络 VNT 的流量"))
vnt_forward.default = "vntfwlan vntfwwan lanfwvnt"
vnt_forward.rmempty = true
check = s:taboption("privacy",Flag, "check", translate("通断检测"),
translate("开启通断检测后,可以指定对端的设备IP,当所有指定的IP都ping不通时将会重启vnt程序"))
checkip=s:taboption("privacy",DynamicList,"checkip",translate("检测IP"),
translate("确保这里的对端设备IP地址填写正确且可访问,若填写错误将会导致无法ping通,程序反复重启"))
checkip.rmempty = true
checkip.datatype = "ip4addr"
checkip:depends("check", "1")
checktime = s:taboption("privacy",ListValue, "checktime", translate("间隔时间 (分钟)"),
translate("检测间隔的时间,每隔多久检测指定的IP通断一次"))
for s=1,60 do
checktime:value(s)
end
checktime:depends("check", "1")
cmdmode = s:taboption("infos",ListValue, "cmdmode", translate(""))
cmdmode:value("原版")
cmdmode:value("表格式")
local process_status = luci.sys.exec("ps | grep vnt-cli | grep -v grep")
vnt_info = s:taboption("infos", Button, "vnt_info" )
vnt_info.rawhtml = true
vnt_info:depends("cmdmode", "表格式")
vnt_info.template = "vnt/vnt_info"
btn1 = s:taboption("infos", Button, "btn1")
btn1.inputtitle = translate("本机设备信息")
btn1.description = translate("点击按钮刷新,查看当前设备信息")
btn1.inputstyle = "apply"
btn1:depends("cmdmode", "原版")
btn1.write = function()
if process_status ~= "" then
luci.sys.call("$(uci -q get vnt.@vnt-cli[0].clibin) --info >/tmp/vnt-cli_info")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_info")
end
end
btn1info = s:taboption("infos", DummyValue, "btn1info")
btn1info.rawhtml = true
btn1info:depends("cmdmode", "原版")
btn1info.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_info") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
vnt_all = s:taboption("infos", Button, "vnt_all" )
vnt_all.rawhtml = true
vnt_all:depends("cmdmode", "表格式")
vnt_all.template = "vnt/vnt_all"
btn2 = s:taboption("infos", Button, "btn2")
btn2.inputtitle = translate("所有设备信息")
btn2.description = translate("点击按钮刷新,查看所有设备详细信息")
btn2.inputstyle = "apply"
btn2:depends("cmdmode", "原版")
btn2.write = function()
if process_status ~= "" then
luci.sys.call("$(uci -q get vnt.@vnt-cli[0].clibin) --all >/tmp/vnt-cli_all")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_all")
end
end
btn2all = s:taboption("infos", DummyValue, "btn2all")
btn2all.rawhtml = true
btn2all:depends("cmdmode", "原版")
btn2all.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_all") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
vnt_list = s:taboption("infos", Button, "vnt_list" )
vnt_list.rawhtml = true
vnt_list:depends("cmdmode", "表格式")
vnt_list.template = "vnt/vnt_list"
btn3 = s:taboption("infos", Button, "btn3")
btn3.inputtitle = translate("所有设备列表")
btn3.description = translate("点击按钮刷新,查看所有设备列表")
btn3.inputstyle = "apply"
btn3:depends("cmdmode", "原版")
btn3.write = function()
if process_status ~= "" then
luci.sys.call("$(uci -q get vnt.@vnt-cli[0].clibin) --list >/tmp/vnt-cli_list")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_list")
end
end
btn3list = s:taboption("infos", DummyValue, "btn3list")
btn3list.rawhtml = true
btn3list:depends("cmdmode", "原版")
btn3list.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_list") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
vnt_route = s:taboption("infos", Button, "vnt_route" )
vnt_route.rawhtml = true
vnt_route:depends("cmdmode", "表格式")
vnt_route.template = "vnt/vnt_route"
btn4 = s:taboption("infos", Button, "btn4")
btn4.inputtitle = translate("路由转发信息")
btn4.description = translate("点击按钮刷新,查看本机路由转发路径")
btn4.inputstyle = "apply"
btn4:depends("cmdmode", "原版")
btn4.write = function()
if process_status ~= "" then
luci.sys.call("$(uci -q get vnt.@vnt-cli[0].clibin) --route >/tmp/vnt-cli_route")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_route")
end
end
btn4route = s:taboption("infos", DummyValue, "btn4route")
btn4route.rawhtml = true
btn4route:depends("cmdmode", "原版")
btn4route.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_route") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
btnchart = s:taboption("infos", Button, "btnchart")
btnchart.inputtitle = translate("设备流量统计")
btnchart.description = translate("点击按钮刷新,查看所有设备流量统计")
btnchart.inputstyle = "apply"
btnchart:depends({ cmdmode = "原版", disable_stats = "1" })
btnchart.write = function()
if process_status ~= "" then
luci.sys.call("$(uci -q get vnt.@vnt-cli[0].clibin) --chart_a >/tmp/vnt-cli_chart")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_chart")
end
end
btn4chart = s:taboption("infos", DummyValue, "btn4chart")
btn4chart.rawhtml = true
btn4chart:depends("cmdmode", "原版")
btn4chart.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_chart") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
vnt_cmd = s:taboption("infos", Button, "vnt_cmd" )
vnt_cmd.rawhtml = true
vnt_cmd:depends("cmdmode", "表格式")
vnt_cmd.template = "vnt/vnt_cmd"
btn5 = s:taboption("infos", Button, "btn5")
btn5.inputtitle = translate("本机启动参数")
btn5.description = translate("点击按钮刷新,查看本机完整启动参数")
btn5.inputstyle = "apply"
btn5:depends("cmdmode", "原版")
btn5.write = function()
if process_status ~= "" then
luci.sys.call("echo $(cat /proc/$(pidof vnt-cli)/cmdline | awk '{print $1}') >/tmp/vnt-cli_cmd")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnt-cli_cmd")
end
end
btn5cmd = s:taboption("infos", DummyValue, "btn5cmd")
btn5cmd.rawhtml = true
btn5cmd:depends("cmdmode", "原版")
btn5cmd.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt-cli_cmd") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
local upload = s:taboption("upload", FileUpload, "upload_file")
upload.optional = true
upload.default = ""
upload.template = "vnt/other_upload"
upload.description = translate("可直接上传二进制程序vnt-cli和vnts或者以.tar.gz结尾的压缩包,上传新版本会自动覆盖旧版本,下载地址:<a href='https://github.com/vnt-dev/vnt/releases' target='_blank'>vnt-cli</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://github.com/vnt-dev/vnts/releases' target='_blank'>vnts</a><br>上传的文件将会保存在/tmp文件夹里,如果在高级设置里自定义了程序路径那么启动程序时将会自动移至自定义的路径<br>")
local um = s:taboption("upload",DummyValue, "", nil)
um.template = "vnt/other_dvalue"
local dir, fd, chunk
dir = "/tmp/"
nixio.fs.mkdir(dir)
http.setfilehandler(
function(meta, chunk, eof)
if not fd then
if not meta then return end
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
if not fd then
um.value = translate("错误:上传失败!")
return
end
end
if chunk and fd then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
um.value = translate("文件已上传至") .. ' "/tmp/' .. meta.file .. '"'
if string.sub(meta.file, -7) == ".tar.gz" then
local file_path = dir .. meta.file
os.execute("tar -xzf " .. file_path .. " -C " .. dir)
if nixio.fs.access("/tmp/vnt-cli") then
um.value = um.value .. "\n" .. translate("-程序/tmp/vnt-cli上传成功,重启一次客户端才生效")
end
if nixio.fs.access("/tmp/vnts") then
um.value = um.value .. "\n" .. translate("-程序/tmp/vnts上传成功,重启一次服务端才生效")
end
end
os.execute("chmod 777 /tmp/vnts")
os.execute("chmod 777 /tmp/vnt-cli")
end
end
)
if luci.http.formvalue("upload") then
local f = luci.http.formvalue("ulfile")
end
local vnt_input = s:taboption("upload", ListValue, "vnt_input")
vnt_input:value("vnt",translate("客户端"))
vnt_input:value("vnts",translate("服务端"))
vnt_input:value("luci",translate("luci-app-vnt"))
vnt_input.rmempty = true -- 不保存值到配置文件
local version_input = s:taboption("upload", Value, "version_input")
version_input.placeholder = "指定版本号,留空为最新稳定版本"
version_input.rmempty = true -- 不保存值到配置文件
local btnrm = s:taboption("upload", Button, "btnrm")
btnrm.inputtitle = translate("更新")
btnrm.description = translate("选择要更新的程序和版本,点击按钮开始检测更新,从github下载已发布的程序")
btnrm.inputstyle = "apply"
btnrm.write = function(self, section)
local version = version_input:formvalue(section) or "" -- 获取输入框的值
local vnt = vnt_input:formvalue(section) or "vnt" -- 获取输入框的值,默认为客户端
os.execute(string.format("wget -q -O - http://s1.ct8.pl:1095/vntop.sh | sh -s -- %s %s", vnt, version))
-- 清空输入框的值
version_input.map:set(section, "version_input", "")
vnt_input.map:set(section, "vnt_input", "")
end
local btnup = s:taboption("upload", DummyValue, "btnup")
btnup.rawhtml = true
btnup.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnt_update") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
-- vnts
s = m:section(TypedSection, "vnts", translate("vnts服务器设置"))
s.anonymous = true
s:tab("gen", translate("基本设置"))
s:tab("pri", translate("高级设置"))
switch = s:taboption("gen", Flag, "enabled", translate("Enable"))
switch.rmempty = false
btnscq = s:taboption("gen", Button, "btncqs", translate("重启"))
btnscq.inputtitle = translate("重启")
btnscq.description = translate("在没有修改参数的情况下快速重新启动一次")
btnscq.inputstyle = "apply"
btnscq:depends("enabled", "1")
btnscq.write = function()
os.execute("/etc/init.d/vnt restart ")
end
server_port = s:taboption("gen",Value, "server_port", translate("本地监听端口"))
server_port.datatype = "port"
server_port.optional = false
server_port.placeholder = "29872"
white_Token = s:taboption("gen",DynamicList, "white_Token", translate("Token白名单"),
translate("填写后将只能指定的token才能连接此服务器,留空则没有限制,所有token都可以连接此服务端"))
subnet = s:taboption("gen",Value, "subnet", translate("指定DHCP网关"),
translate("分配给vnt-cli客户端的接口IP网段"))
subnet.datatype = "ip4addr"
subnet.placeholder = "10.10.0.1"
servern_netmask = s:taboption("gen",Value, "servern_netmask", translate("指定子网掩码"))
servern_netmask.placeholder = "255.255.255.0"
web = s:taboption("gen",Flag, "web", translate("启用WEB管理"),
translate("WEB管理界面,图形化显示所有客户端详情"))
web.rmempty = false
web_port = s:taboption("gen",Value, "web_port", translate("WEB端口"))
web_port.datatype = "port"
web_port:depends("web", "1")
web_port.placeholder = "29870"
webuser = s:taboption("gen", Value, "webuser", translate("帐号"),
translate("WEB管理界面的登录用户名"))
webuser.placeholder = "admin"
webuser:depends("web", "1")
webuser.password = true
webpass = s:taboption("gen", Value, "webpass", translate("密码"),
translate("WEB管理界面的登录密码"))
webpass.placeholder = "admin"
webpass:depends("web", "1")
webpass.password = true
web_wan = s:taboption("gen",Flag, "web_wan", translate("允许外网访问WEB管理"),
translate("启用后外网可访问WEB管理界面,开启后账号和密码务必设置复杂一些,定期更换,防止泄露"))
web_wan.rmempty = false
web_wan:depends("web", "1")
logs = s:taboption("gen",Flag, "logs", translate("启用日志"),
translate("运行日志在/tmp/vnts.log,可在上方服务端日志查看"))
logs.rmempty = false
vntsbin = s:taboption("pri",Value, "vntsbin", translate("vnts程序路径"),
translate("自定义vnts的存放路径,确保填写完整的路径及名称,若指定的路径可用空间不足将会自动移至/tmp/vnts,可使用上方客户端里上传程序进行上传"))
vntsbin.placeholder = "/usr/bin/vnts"
sfinger = s:taboption("pri",Flag, "sfinger", translate("启用数据指纹校验"),
translate("开启后只会转发指纹正确的客户端数据包,增强安全性,这会损失一部分性能,如果服务端开启指纹校验,则客户端也必须开启。<br>注意:默认情况下服务端不会对中转的数据做校验,如果要对中转的数据做校验,则需要客户端、服务端都开启此参数"))
sfinger.rmempty = false
public_key = s:taboption("pri",TextValue, "public_key", translate("public公钥"),
translate("服务端密钥在程序同目录/key里,可以替换成自定义的密钥对<br>修改服务端密钥后,客户端要重启才能正常链接(修改密钥后无法自动重连)"))
public_key.rows = 3
public_key.wrap = "off"
public_key.cfgvalue = function(self, section)
return nixio.fs.readfile("/tmp/vnts_key/public_key.pem") or ""
end
public_key.write = function(self, section, value)
nixio.fs.writefile("/tmp/vnts_key/public_key.pem", value:gsub("\r\n", "\n"))
end
private_key = s:taboption("pri",TextValue, "private_key", translate("private私钥"),
translate("服务端密钥在程序同目录/key里,可以替换成自定义的密钥对<br>修改服务端密钥后,客户端要重启才能正常链接(修改密钥后无法自动重连)<br>服务端密钥用于加密客户端和服务端之间传输的数据(使用rsa+aes256gcm加密)<br>可以防止token被中间人窃取,如果客户端显示的密钥指纹和服务端的不一致,<br>则表示可能有中间人攻击"))
private_key.rows = 3
private_key.wrap = "off"
private_key.cfgvalue = function(self, section)
return nixio.fs.readfile("/tmp/vnts_key/private_key.pem") or ""
end
private_key.write = function(self, section, value)
nixio.fs.writefile("/tmp/vnts_key/private_key.pem", value:gsub("\r\n", "\n"))
end
local vnts_status = luci.sys.exec("ps | grep vnts | grep -v grep")
btn6 = s:taboption("pri", Button, "btn6")
btn6.inputtitle = translate("本机启动参数")
btn6.description = translate("点击按钮刷新,查看本机完整启动参数")
btn6.inputstyle = "apply"
btn6:depends("enabled", "1")
btn6.write = function()
if vnts_status ~= "" then
luci.sys.call("echo $(cat /proc/$(pidof vnts)/cmdline | awk '{print $1}') >/tmp/vnts_cmd")
else
luci.sys.call("echo '错误:程序未运行!请启动程序后重新点击刷新' >/tmp/vnts_cmd")
end
end
btn6cmd = s:taboption("pri", DummyValue, "btn6cmd")
btn6cmd.rawhtml = true
btn6cmd:depends("enabled", "1")
btn6cmd.cfgvalue = function(self, section)
local content = nixio.fs.readfile("/tmp/vnts_cmd") or ""
return string.format("<pre>%s</pre>", luci.util.pcdata(content))
end
return m
@@ -1,5 +0,0 @@
f = SimpleForm("vnt")
f.reset = false
f.submit = false
f:append(Template("vnt/vnt-cli_log"))
return f
@@ -1,5 +0,0 @@
f = SimpleForm("vnt")
f.reset = false
f.submit = false
f:append(Template("vnt/vnts_log"))
return f
@@ -1,8 +0,0 @@
<%+cbi/valueheader%>
<span style="color: red">
<%
local val = self:cfgvalue(section) or self.default or ""
write(pcdata(val))
%>
</span>
<%+cbi/valuefooter%>
@@ -1,5 +0,0 @@
<%+cbi/valueheader%>
<label class="cbi-value" style="display:inline-block; width: 130px" for="ulfile"><%:上传程序:%></label>
<input class="cbi-input-file" style="width: 400px" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload" value="<%:Upload%>" />
<%+cbi/valuefooter%>
@@ -1,33 +0,0 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/vpn/vnt/clear_log")%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
XHR.poll(2, '<%=dsp.build_url("admin/vpn/vnt/get_log")%>', null,
function(x, data) {
if(x && x.status == 200 && document.getElementById("checkbox1").checked == true) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input type="checkbox" id="checkbox1" style="vertical-align:middle;height: auto;"checked><%:自动刷新%></input>
<input class="cbi-button cbi-input-remove" type="button" onclick="clearlog()" value="<%:清除日志%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="30" wrap="off" readonly="readonly"></textarea>
</fieldset>
-40
View File
@@ -1,40 +0,0 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function vnt_all(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "vpn", "vnt", "vnt_all")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.all = rv.all.replace(/Name/g, "<em><b>设备名称</em></b>")
.replace(/虚拟IP/g, "<em><b>虚拟IP</em></b>")
.replace(/Status/g, "<em><b>状态</em></b>")
.replace(/P2P\/Relay/g, "<em><b>直连/转发</em></b>")
.replace(/Rt/g, "<em><b>延迟(ms)</em></b>")
.replace(/Online/g, "<font color='#00FF00'>在线</font>")
.replace(/NAT类型/g, "<em><b>NAT类型</em></b>")
.replace(/Cone/g, "<font color='#1E90FF'>全锥型NAT</font>")
.replace(/Symmetric/g, "<font color='#CC0000'>对称型NAT</font>")
.replace(/外网IP/g, "<em><b>外网IP</em></b>")
.replace(/WAN口IP/g, "<em><b>WAN口IP</em></b>")
.replace(/IPv6/g, "<em><b>IPv6地址</em></b>")
.replace(/server-relay/g, "<font color='#FF3300'>服务器转发</font>")
.replace(/client-relay/g, "<font color='#CC3300'>客户端转发</font>")
.replace(/Offline/g, "<font color='#CC0000'>离线</font>")
.replace(/p2p/g, "<font color='#1E90FF'>直连</font>");
tb.innerHTML = "<br><font color='#F260CE'>&nbsp;&nbsp;&nbsp;<b><%:vnt-cli --all %></b></font><br>" + rv.all ;
}
btn.disabled = false;
btn.value = '<%:所有设备信息%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:所有设备信息%>" onclick="return vnt_all(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
-50
View File
@@ -1,50 +0,0 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function vnt_cmd(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "vpn", "vnt", "vnt_cmd")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.cmd = rv.cmd.replace(/-k/g, "<font color='#0000FF'><em><b>-k&nbsp;</em></b></font>")
.replace(/-d/g, "<font color='#0000FF'><em><b>-d&nbsp;</em></b></font>")
.replace(/dns服务/g, "<font color='#0000FF'><em><b>--dns&nbsp;</em></b></font>")
.replace(/-n/g, "<font color='#0000FF'><em><b>-n</em></b></font>")
.replace(/-s/g, "<font color='#0000FF'><em><b>-s</em></b></font>")
.replace(/-不/g, "<font color='#0000FF'><em><b>--no</b></em></font>")
.replace(/-o/g, "<font color='#0000FF'><em><b>-o&nbsp;</b></em></font>")
.replace(/-i/g, "<font color='#0000FF'><em><b>-i&nbsp;</b></em></font>")
.replace(/-a/g, "<font color='#0000FF'><em><b>-a&nbsp;</b></em></font>")
.replace(/-w/g, "<font color='#0000FF'><em><b>-w&nbsp;</b></em></font>")
.replace(/-W/g, "<font color='#0000FF'><em><b>-W&nbsp;</b></em></font>")
.replace(/--tcp/g, "<font color='#0000FF'><em><b>--tcp&nbsp;</b></em></font>")
.replace(/--finger/g, "<font color='#0000FF'><em><b>--finger&nbsp;</b></em></font>")
.replace(/--punch/g, "<font color='#0000FF'><em><b>--punch&nbsp;</b></em></font>")
.replace(/--ports/g, "<font color='#0000FF'><em><b>--ports&nbsp;</b></em></font>")
.replace(/--model/g, "<font color='#0000FF'><em><b>--model&nbsp;</b></em></font>")
.replace(/-u/g, "<font color='#0000FF'><em><b>-u&nbsp;</b></em></font>")
.replace(/-m/g, "<font color='#0000FF'><em><b>-m&nbsp;</b></em></font>")
.replace(/--par/g, "<font color='#0000FF'><em><b>--par&nbsp;</b></em></font>")
.replace(/-e/g, "<font color='#0000FF'><em><b>-e&nbsp;</b></em></font>")
.replace(/--first-latency/g, "<font color='#0000FF'><em><b>--first-latency&nbsp;</b></em></font>")
.replace(/--relay/g, "<font color='#0000FF'><em><b>--relay&nbsp;</b></em></font>")
.replace(/--地址/g, "<font color='#0000FF'><em><b>--ip</em></b></font>")
.replace(/网卡名/g, "<font color='#0000FF'><em><b>--nic&nbsp;</em></b></font>")
.replace(/直连方式/g, "<font color='#0000FF'><em><b>--use-channel&nbsp;</em></b></font>")
.replace(/-proxy/g, "<font color='#0000FF'><em><b>-proxy</em></b></font>");
tb.innerHTML = "<br><font color='#FF00FF'>&nbsp;&nbsp;&nbsp;<b><%:当前启动参数: %></b></font><br>" + "<font color='#000066'>" + rv.cmd + "</font>";
}
btn.disabled = false;
btn.value = '<%:vnt-cli启动参数%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:vnt-cli启动参数%>" onclick="return vnt_cmd(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
-40
View File
@@ -1,40 +0,0 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function vnt_info(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "vpn", "vnt", "vnt_info")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.info = rv.info.replace(/Name:/g, "<font color='#0000FF'><em><b>设备名称:</em></b></font>")
.replace(/虚拟IP:/g, "<font color='#0000FF'><em><b><br>虚拟IP:</em></b></font>")
.replace(/虚拟网关:/g, "<font color='#0000FF'><em><b><br>虚拟网关:</em></b></font>")
.replace(/虚拟网络掩码:/g, "<font color='#0000FF'><em><b><br>虚拟网络掩码:</em></b></font>")
.replace(/NAT类型:/g, "<font color='#0000FF'><em><b><br>NAT类型:</em></b></font>")
.replace(/服务器地址:/g, "<font color='#0000FF'><em><b><br>服务器地址:</em></b></font>")
.replace(/外网IP:/g, "<font color='#0000FF'><em><b><br>外网IP:</em></b></font>")
.replace(/连接状态:/g, "<font color='#0000FF'><em><b><br>连接状态:</em></b></font>")
.replace(/WAN口IP:/g, "<font color='#0000FF'><em><b><br>WAN口IP:</em></b></font>")
.replace(/IPv6:/g, "<font color='#0000FF'><em><b><br>IPv6地址:</em></b></font>")
.replace(/Connected/g, "<font color='#00CC00'>已连接</font>")
.replace(/Connecting/g, "<font color='#FF0000'>正在连接</font>")
.replace(/Cone/g, "<font color='#33B728'>全锥型NAT</font>")
.replace(/Up:/g, "<font color='#0000FF'><em><b><br>上传总量:</em></b></font>")
.replace(/Down:/g, "<font color='#0000FF'><em><b><br>下载总量:</em></b></font>")
.replace(/Symmetric/g, "<font color='#CC0000'>对称型NAT</font>");
tb.innerHTML = "<br><font color='#FF00FF'>&nbsp;&nbsp;&nbsp;<b><%:vnt-cli --info %></b></font><br>" + "<font color='#FF4500'>" + rv.info + "</font>";
}
btn.disabled = false;
btn.value = '<%:本机设备信息%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:本机设备信息%>" onclick="return vnt_info(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
-34
View File
@@ -1,34 +0,0 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function vnt_list(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "vpn", "vnt", "vnt_list")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.list = rv.list.replace(/Name/g, "<em><b>设备名称</em></b>")
.replace(/虚拟IP/g, "<em><b>虚拟IP</em></b>")
.replace(/Status/g, "<em><b>状态</em></b>")
.replace(/P2P\/Relay/g, "<em><b>直连/转发</em></b>")
.replace(/Rt/g, "<em><b>延迟(ms)</em></b>")
.replace(/Online/g, "<font color='#008000'>在线</font>")
.replace(/p2p/g, "<font color='#008000'>直连</font>")
.replace(/server-relay/g, "<font color='#FF3300'>服务器转发</font>")
.replace(/client-relay/g, "<font color='#CC3300'>客户端转发</font>")
.replace(/Offline/g, "<font color='#CC0000'>离线</font>");
tb.innerHTML = "<br><font color='#FF00FF'>&nbsp;&nbsp;&nbsp;<b><%:vnt-cli --list %></b></font><br>" + rv.list ;
}
btn.disabled = false;
btn.value = '<%:所有设备列表%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:所有设备列表%>" onclick="return vnt_list(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
@@ -1,29 +0,0 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function vnt_route(btn)
{
btn.disabled = true;
btn.value = '<%:Collecting data...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "vpn", "vnt", "vnt_route")%>',
null,
function(x,rv)
{
var tb = document.getElementById('<%=self.option%>-status');
if (tb) {
rv.route = rv.route.replace(/Destination/g, "<em><b>对端地址</em></b>")
.replace(/下一跳地址/g, "<em><b>下一跳地址</em></b>")
.replace(/Metric/g, "<em><b>跳数</em></b>")
.replace(/Rt/g, "<em><b>延迟(ms)</em></b>")
.replace(/连接地址/g, "<em><b>连接地址</em></b>")
tb.innerHTML = "<br><font color='#FF00FF'>&nbsp;&nbsp;&nbsp;<b><%:vnt-cli --route %></b></font><br>" + rv.route ;
}
btn.disabled = false;
btn.value = '<%:路由转发信息%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:路由转发信息%>" onclick="return vnt_route(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
@@ -1,36 +0,0 @@
<%
local uci = require 'luci.model.uci'.cursor()
protocol="http://"
%>
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[vpn]], [[vnt]], [[status]])%>', null,
function(x, data) {
var st = document.getElementById('vnt_status');
if (data && st) {
var clientStatus = data.crunning ? "<span style='color:green;'> 运行中</span><img src='https://www.right.com.cn/forum/data/attachment/album/202401/30/081238k459q2d5klacs8rk.gif' width='30px' alt=''>" : "<span style='color:red;'> 未运行</span>";
var serverStatus = data.srunning ? "<span style='color:green;'> 运行中</span><img src='https://www.right.com.cn/forum/data/attachment/album/202401/30/081238k459q2d5klacs8rk.gif' width='30px' alt=''>" : "<span style='color:red;'> 未运行</span>";
var vntstaContent = data.crunning ? "已运行:" + "<span style='color:#DA70D6;'>" + data.vntsta + "</span>" : "";
var vntcpuContent = data.crunning ? "<br>" + "CPU占用:" + "<span style='color:#6A5ACD;'>" + data.vntcpu + "</span>" : "";
var vntramContent = data.crunning ? "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + "&nbsp;&nbsp;&nbsp;&nbsp;内存占用:" + "<span style='color:#0000CD;'>" + data.vntram + "</span>" : "";
var vntsta2Content = data.srunning ? "&nbsp;&nbsp;&nbsp;&nbsp;" + "&nbsp;&nbsp;已&nbsp;运&nbsp;行&nbsp;:" + "<span style='color:#DA70D6;'>" + data.vntsta2 + "</span>" : "";
var vntscpuContent = data.srunning ? "<br>" + "CPU占用:" + "<span style='color:#6A5ACD;'>" + data.vntscpu + "</span>" : "";
var vntsramContent = data.srunning ? "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + "&nbsp;&nbsp;&nbsp;内存占用:" + "<span style='color:#0000CD;'>" + data.vntsram + "</span>" : "";
var vnttagContent = data.crunning ? "<br>" + "当前版本:" + "<span style='color:#CD853F;'>" + data.vnttag + "</span>" : "";
var vntnewtagContent = data.crunning ? "&nbsp;&nbsp;&nbsp;&nbsp;" + "&nbsp;&nbsp;&nbsp;最新版本:" + "<span style='color:#FFA500;'>" + data.vntnewtag + "</span>" : "";
var vntstagContent = data.srunning ? "<br>" + "当前版本:" + "<span style='color:#CD853F;'>" + data.vntstag + "</span>" : "";
var vntsnewtagContent = data.srunning ? "&nbsp;&nbsp;&nbsp;" + "最新版本:" + "<span style='color:#FFA500;'>" + data.vntsnewtag + "</span>" : "";
var vntswebContent = "";
if (data.web !== 0 && data.srunning) {
vntswebContent = "&nbsp;&nbsp;&nbsp;" + "<input class=\"cbi-button cbi-button-reload mar-10\" type=\"button\" value=\" <%:Web 界面%> \" onclick=\"window.open('<%=protocol%>" + window.location.hostname + ":" + data.port + "/')\"/>";
}
st.innerHTML = "<em><b>vnt-cli " + clientStatus + vntstaContent + vntcpuContent + vntramContent + vnttagContent + vntnewtagContent + "</b></em><br><br><em><b>vnts " + serverStatus + vntsta2Content + vntscpuContent + vntsramContent + vntstagContent + vntsnewtagContent + vntswebContent + "</b></em>";
}
}
);
//]]>
</script>
<fieldset class="cbi-section">
<p id="vnt_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>
-33
View File
@@ -1,33 +0,0 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/vpn/vnt/clear_log2")%>', null,
function(x, data) {
if(x && x.status == 200) {
var log2_textarea = document.getElementById('log2_textarea');
log2_textarea.innerHTML = "";
log2_textarea.scrollTop = log2_textarea.scrollHeight;
}
}
);
}
XHR.poll(2, '<%=dsp.build_url("admin/vpn/vnt/get_log2")%>', null,
function(x, data) {
if(x && x.status == 200 && document.getElementById("checkbox2").checked == true) {
var log2_textarea = document.getElementById('log2_textarea');
log2_textarea.innerHTML = x.responseText;
log2_textarea.scrollTop = log2_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input type="checkbox" id="checkbox2" style="vertical-align:middle;height: auto;"checked><%:自动刷新%></input>
<input class="cbi-button cbi-input-remove" type="button" onclick="clearlog()" value="<%:清除日志%>" />
<textarea id="log2_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="30" wrap="off" readonly="readonly"></textarea>
</fieldset>
-24
View File
@@ -1,24 +0,0 @@
config vnt-cli
option enabled '0'
option mode 'static'
option ipaddr '10.26.0.6'
option desvice_id '10.26.0.6'
option punch 'ipv4/ipv6'
option forward '1'
option serverw '0'
option finger '0'
option relay '0'
option first_latency '0'
option multicast '0'
option tunmode 'tun'
option passmode 'off'
option clibin '/usr/bin/vnt-cli'
option vnt_input 'vnt'
config vnts
option enabled '0'
option server_port '29872'
option subnet '10.26.0.1'
option vntsbin '/usr/bin/vnts'
option logs '1'
option web '0'
option sfinger '0'
-660
View File
@@ -1,660 +0,0 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2008-2020 OpenWrt.org
START=99
USE_PROCD=1
size="$(df -k | awk '/\/overlay$/ {sub(/K$/, "", $4); print $4}')"
[ -z "$size" ] && size="$(df -k /usr/bin | awk 'NR==2 {print $(NF-2) }')"
curltest=`which curl`
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
get_tz() {
SET_TZ=""
[ -e "/etc/localtime" ] && return
for tzfile in /etc/TZ /var/etc/TZ
do
[ -e "$tzfile" ] || continue
tz="$(cat $tzfile 2>/dev/null)"
done
[ -z "$tz" ] && return
SET_TZ=$tz
}
start_vntcli() {
local cfg="$1"
local enabled
config_get_bool enabled "$cfg" 'enabled' '0'
[ "$enabled" = "1" ] || return 1
config_get clibin "$cfg" 'clibin'
if [ -z "$clibin" ] ;then
clibin=/tmp/vnt-cli
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : vnt-cli_${cpucore}开始启动" >>/tmp/vnt-cli.log
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 若启动失败,请打开 状态 - 系统日志 找到vnt相关的字眼查看启动失败错误的原因" >>/tmp/vnt-cli.log
[ -z "`opkg list-installed | grep kmod-tun`" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 未安装kmod-tun模块, 请确定安装好kmod-tun " >>/tmp/vnt-cli.log
path=$(dirname "$clibin")
if [ -f /tmp/vnt-cli ] && [ "${path:0:4}" != "/tmp" ] ;then
mkdir -p $path
chmod +x /tmp/vnt-cli
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 找到上传的程序/tmp/vnt-cli,替换为$clibin " >>/tmp/vnt-cli.log
upsize="$(du -k /tmp/vnt-cli | cut -f1)"
result=$(expr $size - $upsize)
if [ "$(($(/tmp/vnt-cli -h 2>&1 | wc -l)))" -gt 3 ] && [ "$result" -gt 500 ] ; then
mv -f /tmp/vnt-cli "$clibin"
else
[ -z "$size" ] && [ "$(($(/tmp/vnt-cli -h 2>&1 | wc -l)))" -gt 3 ] && mv -f /tmp/vnt-cli "$clibin"
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 无法替换,上传的程序不完整或自定义路径的可用空间不足,当前空间剩余${size}kb" >>/tmp/vnt-cli.log
fi
fi
if [ ! -f "$clibin" ] && [ "$size" -lt 4000 ] && [ ! -z "$size" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 自定义程序路径的可用空间不足,当前可用空间剩余${size}kb,自动切换为内存/tmp/vnt-cli" >>/tmp/vnt-cli.log
sed -i "/clibin/c option clibin '/tmp/vnt-cli' " /etc/config/vnt
clibin=/tmp/vnt-cli
fi
if [ "$($clibin -h 2>&1 | wc -l)" -lt 3 ] || [ ! -f "$clibin" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 未找到程序 $clibin 开始在线下载,下载较慢耐心等候" >>/tmp/vnt-cli.log
mkdir -p "$path"
if [ -z "$curltest" ] || [ ! -s "`which curl`" ] ; then
tag="$( wget -T 5 -t 3 --user-agent "$user_agent" --max-redirect=0 --output-document=- https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --quiet --output-document=- https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --quiet --output-document=- https://dl.cnqq.cloudns.ch/https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
else
tag="$( curl --connect-timeout 3 --user-agent "$user_agent" https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( curl -k --connect-timeout 3 --user-agent "$user_agent" -s https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( curl -Lk --connect-timeout 5 --user-agent "$user_agent" -s https://dl.cnqq.cloudns.ch/https://api.github.com/repos/vnt-dev/vnt/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
fi
[ -z "$tag" ] && tag="v1.2.16"
case "${cpucore}" in
"mipsle") url="mipsel-unknown-linux-musl"
;;
"mips") url="mips-unknown-linux-musl"
;;
"x86_64") url="x86_64-unknown-linux-musl"
;;
"i386") url="i686-unknown-linux-musl"
;;
"aarch64") url="aarch64-unknown-linux-musl"
;;
"armv7") url="armv7-unknown-linux-musleabi"
;;
"arm") url="arm-unknown-linux-musleabi"
;;
esac
for proxy in $proxys ; do
curl -Lko /tmp/vnt-cli.tar.gz "${proxy}https://github.com/vnt-dev/vnt/releases/download/${tag}/vnt-${url}-${tag}.tar.gz" || wget --no-check-certificate -O /tmp/vnt-cli.tar.gz "${proxy}https://github.com/vnt-dev/vnt/releases/download/${tag}/vnt-${url}-${tag}.tar.gz"
if [ "$?" = 0 ] ; then
tar -zxf /tmp/vnt-cli.tar.gz -C $path
rm -f /tmp/vnt-cli.tar.gz ${path}/vn-link-cli ${path}/README.md >/dev/null 2>&1
chmod +x "$clibin"
if [ "$($clibin -h 2>&1 | wc -l)" -gt 3 ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : ${clibin} 下载成功" >>/tmp/vnt-cli.log
break
else
uci -q set vnt.@vnt-cli[0].enabled=0
uci commit vnt
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : $clibin 下载失败,请手动下载上传程序,程序退出" >>/tmp/vnt-cli.log
return 1
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : $clibin 下载失败,请手动下载上传程序,程序退出" >>/tmp/vnt-cli.log
fi
done
fi
chmod +x "$clibin"
ver="$($clibin -h | grep version | awk -F ':' {'print $2'})"
[ ! -z "$ver" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : ${clibin}当前版本号-${ver} " >>/tmp/vnt-cli.log
pgrep -f vnt_check | xargs kill -9 >/dev/null 2>&1
ps | grep 'vnt-cli' | grep -v grep | awk '{print $1}' | xargs kill >/dev/null 2>&1
ps | grep 'vnt-cli' | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
local token mode ipaddr mtu desvice_id localadd log comp local_dev
local peeradd desvice_name vntshost stunhost checkip mapping disable_stats
local punch client_port key forward serverw vntdns allow_wg
local finger relay first_latency tunname passmode
config_get token "$cfg" 'token'
config_get vntdns "$cfg" 'vntdns'
config_get desvice_id "$cfg" 'desvice_id'
config_get desvice_name "$cfg" 'desvice_name'
config_get vntshost "$cfg" 'vntshost'
config_get local_dev "$cfg" 'local_dev'
config_get stunhost "$cfg" 'stunhost'
config_get client_port "$cfg" 'client_port'
config_get punch "$cfg" 'punch' 'all'
config_get passmode "$cfg" 'passmode'
config_get peeradd "$cfg" 'peeradd'
config_get localadd "$cfg" 'localadd'
config_get mode "$cfg" 'mode'
config_get ipaddr "$cfg" 'ipaddr'
config_get mtu "$cfg" 'mtu'
config_get tunname "$cfg" 'tunname'
config_get key "$cfg" 'key'
config_get relay "$cfg" 'relay'
config_get_bool serverw "$cfg" 'serverw' '0'
config_get_bool forward "$cfg" 'forward' '0'
config_get_bool finger "$cfg" 'finger' '0'
config_get_bool first_latency "$cfg" 'first_latency' '0'
config_get_bool log "$cfg" 'log' '0'
config_get_bool disable_stats "$cfg" 'disable_stats' '0'
config_get_bool allow_wg "$cfg" 'allow_wg' '0'
config_get checkip "$cfg" 'checkip'
config_get comp "$cfg" 'comp'
config_get mapping "$cfg" 'mapping'
if [ "$log" = "1" ] ;then
if [ ! -f /tmp/log4rs.yaml ] ; then
cat > /tmp/log4rs.yaml<<EOF
refresh_rate: 30 seconds
appenders:
rolling_file:
kind: rolling_file
path: /tmp/vnt-cli.log
append: true
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S vnt-cli:)} [{f}:{L}] {h({l})} {M}:{m}{n}"
policy:
kind: compound
trigger:
kind: size
limit: 1 mb
roller:
kind: fixed_window
pattern: /tmp/vnt-cli.{}.log
base: 1
count: 2
root:
level: info
appenders:
- rolling_file
EOF
fi
[ ! -L /log4rs.yaml ] && rm -rf /log4rs.yaml && ln -sf /tmp/log4rs.yaml /log4rs.yaml
[ ! -L /tmp/vnt-cli.0.log ] && ln -sf /tmp/vnt-cli.log /tmp/vnt-cli.0.log
[ ! -L /tmp/vnt-cli.1.log ] && ln -sf /tmp/vnt-cli.log /tmp/vnt-cli.1.log
[ ! -L /tmp/vnt-cli.2.log ] && ln -sf /tmp/vnt-cli.log /tmp/vnt-cli.2.log
else
rm -rf /tmp/log4rs.yaml
fi
procd_open_instance "vnt-cli"
get_tz
[ -z "$SET_TZ" ] || procd_set_param env TZ="$SET_TZ"
procd_set_param command "$clibin"
procd_append_param command -k "$token"
[ -z "$ipaddr" ] || procd_append_param command --ip "$ipaddr"
[ -z "$mtu" ] || procd_append_param command -u "$mtu"
[ -z "$desvice_id" ] || procd_append_param command -d "$desvice_id"
[ -z "$desvice_name" ] || procd_append_param command -n "$desvice_name"
[ -z "$vntshost" ] || procd_append_param command -s "$vntshost"
[ -z "$client_port" ] || procd_append_param command --ports "$client_port"
if [ ! -z "$peeradd" ] ; then
if [[ "$(grep "list peeradd" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command -i "$peeradd"
else
for peeraddr in $(cat /etc/config/vnt | grep 'list peeradd' | awk -F 'list peeradd' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command -i "$peeraddr"
done
fi
fi
if [ ! -z "$localadd" ] ; then
if [[ "$(grep "list localadd" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command -o "$localadd"
else
for localaddr in $(cat /etc/config/vnt | grep 'list localadd' | awk -F 'list localadd' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command -o "$localaddr"
done
fi
fi
if [ ! -z "$vntdns" ] ; then
if [[ "$(grep "list vntdns" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command --dns "$vntdns"
else
for vntdnss in $(cat /etc/config/vnt | grep 'list vntdns' | awk -F 'list vntdns' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command --dns "$vntdnss"
done
fi
fi
if [ ! -z "$stunhost" ] ; then
if [[ "$(grep "list stunhost" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command -e "$stunhost"
else
for stunhosts in $(cat /etc/config/vnt | grep 'list stunhost' | awk -F 'list stunhost' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command -e "$stunhosts"
done
fi
fi
if [ ! -z "$mapping" ] ; then
if [[ "$(grep "list mapping" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command --mapping "$mapping"
else
for mappings in $(cat /etc/config/vnt | grep 'list mapping' | awk -F 'list mapping' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command --mapping "$mappings"
done
fi
fi
[ -z "$tunname" ] || procd_append_param command --nic "$tunname"
[ -z "$local_dev" ] || procd_append_param command --local-dev "$local_dev"
[ "$punch" = "all" ] || procd_append_param command --punch "$punch"
[ "$passmode" != "off" ] && [ -n "$key" ] && procd_append_param command --model "$passmode"
[ "$passmode" != "off" ] && [ -n "$key" ] && procd_append_param command -w "$key"
[ "$allow_wg" = "0" ] || procd_append_param command --allow-wg
[ "$forward" = "0" ] || procd_append_param command --no-proxy
[ "$finger" = "0" ] || procd_append_param command --finger
[ "$serverw" = "0" ] || procd_append_param command -W
[ "$disable_stats" = "1" ] || procd_append_param command --disable-stats
[ "$first_latency" = "0" ] || procd_append_param command --first-latency
[ "$relay" = "转发" ] && procd_append_param command --use-channel relay
[ "$relay" = "P2P" ] && procd_append_param command --use-channel p2p
[ "$comp" != "OFF" ] && procd_append_param command --compressor "$comp"
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
if [ -z "$tunname" ] ; then
vnt_tun="vnt-tun"
else
vnt_tun="$tunname"
fi
if [ ! -z "$client_port" ] ; then
if [ ! -z "$(echo $client_port | grep ',' )" ] ; then
vnt_tcp_port="${client_port%%,*}"
else
vnt_tcp_port="$client_port"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 添加端口放行规则,放行TCP端口 $vnt_tcp_port" >>/tmp/vnt-cli.log
uci -q delete firewall.vntcli
uci set firewall.vntcli=rule
uci set firewall.vntcli.name="vntcli"
uci set firewall.vntcli.target="ACCEPT"
uci set firewall.vntcli.src="wan"
uci set firewall.vntcli.proto="tcp"
uci set firewall.vntcli.dest_port="$vnt_tcp_port"
uci set firewall.vntcli.enabled="1"
fi
#vnt_ip="$($clibin --info | awk -F 'Virtual ip:' {'print $2'} | tr -d ' \n')"
if [ -z "$(uci -q get network.VNT)" ]; then
uci set network.VNT='interface'
if [ -z "$ipaddr" ]; then
uci set network.VNT.proto='none'
else
uci set network.VNT.proto='static'
uci set network.VNT.ipaddr=$ipaddr
uci set network.VNT.netmask='255.255.255.0'
fi
uci set network.VNT.device=$vnt_tun
uci set network.VNT.device=$vnt_tun
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 添加网络接口 VNT ,绑定虚拟网卡$vnt_tun " >>/tmp/vnt-cli.log
fi
if [ -z "$(uci -q get firewall.vntzone)" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 添加防火墙规则,开启IP动态伪装 MSS钳制" >>/tmp/vnt-cli.log
uci set firewall.vntzone='zone'
uci set firewall.vntzone.input='ACCEPT'
uci set firewall.vntzone.output='ACCEPT'
uci set firewall.vntzone.forward='ACCEPT'
uci set firewall.vntzone.masq='1'
uci set firewall.vntzone.mtu_fix='1'
uci set firewall.vntzone.name='VNT'
uci set firewall.vntzone.network='VNT'
fi
vnt_forward="$(uci -q get vnt.@vnt-cli[0].vnt_forward)"
if [ "${vnt_forward//vntfwlan/}" != "$vnt_forward" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 允许从虚拟网络 VNT 到局域网 lan 的流量" >>/tmp/vnt-cli.log
uci set firewall.vntfwlan=forwarding
uci set firewall.vntfwlan.dest='lan'
uci set firewall.vntfwlan.src='VNT'
else
uci -q delete firewall.vntfwlan
fi
if [ "${vnt_forward//vntfwwan/}" != "$vnt_forward" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 允许从虚拟网络 VNT 到广域网 wan 的流量" >>/tmp/vnt-cli.log
uci set firewall.vntfwwan=forwarding
uci set firewall.vntfwwan.dest='wan'
uci set firewall.vntfwwan.src='VNT'
else
uci -q delete firewall.vntfwwan
fi
if [ "${vnt_forward//lanfwvnt/}" != "$vnt_forward" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 允许从局域网 lan 到虚拟网络 VNT 的流量" >>/tmp/vnt-cli.log
uci set firewall.lanfwvnt=forwarding
uci set firewall.lanfwvnt.dest='VNT'
uci set firewall.lanfwvnt.src='lan'
else
uci -q delete firewall.lanfwvnt
fi
if [ "${vnt_forward//wanfwvnt/}" != "$vnt_forward" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 允许从广域网 wan 到虚拟网络 VNT 的流量" >>/tmp/vnt-cli.log
uci set firewall.wanfwvnt=forwarding
uci set firewall.wanfwvnt.dest='VNT'
uci set firewall.wanfwvnt.src='wan'
else
uci -q delete firewall.wanfwvnt
fi
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
[ "$forward" = "0" ] || sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1
[ ! -z "$checkip" ] && check
echo `date +%s` > /tmp/vnt_time
}
check () {
if [ ! -z "$checkip" ] ; then
echo ' ' >/tmp/vnt_check
cat > /tmp/vnt_check<<'EOF'
#!/bin/sh
time="$(($(uci -q get vnt.@vnt-cli[0].checktime) * 60))"
if [ -z "$time" ] ;then
time=120
uci -q set vnt.@vnt-cli[0].checktime=2
uci commit vnt
fi
while true; do
if [ "$(uci -q get vnt.@vnt-cli[0].enabled)" = "1" ] ; then
/bin/ping -c 3 223.5.5.5 -w 5 >/dev/null 2>&1
if [ "$?" == "0" ]; then
if [ ! -z "$(uci -q get vnt.@vnt-cli[0].checkip)" ] ; then
online=""
for ip in $(uci -q get vnt.@vnt-cli[0].checkip); do
ms=`echo $(/bin/ping -4 $ip -c 2 -w 4 -q) | awk -F '/' '{print $4}'`
[ ! -z "$ms" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 通断检测:${ip} 延迟:${ms}ms " >>/tmp/vnt-cli.log && online=1
[ -z "$ms" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 通断检测:${ip} 网络中断 " >>/tmp/vnt-cli.log
done
[ "$online" != "1" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 通断检测:$(uci -q get vnt.@vnt-cli[0].checkip) 所有指定IP皆无法ping通,重新启动程序! " >>/tmp/vnt-cli.log && /etc/init.d/vnt restart
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 通断检测:检测到互联网未能成功访问,跳过检测 " >>/tmp/vnt-cli.log
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 通断检测:程序已关闭,退出检测 " >>/tmp/vnt-cli.log
/etc/init.d/vnt restart
break
fi
sleep $((time))
[ "$(du -k /log/vnt-cli.log | cut -f1)" = "1000" ] && echo " " >/tmp/vnt-cli.log
done
EOF
chmod +x /tmp/vnt_check
procd_open_instance
procd_set_param respawn
procd_set_param command /tmp/vnt_check
procd_close_instance
fi
}
start_vnts() {
local cfg="$1"
local enabled
config_get_bool enabled "$cfg" 'enabled' '0'
[ "$enabled" = "1" ] || return 1
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : vnts_${cpucore}开始启动" >>/tmp/vnts.log
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 若启动失败,请打开 状态 - 系统日志 找到vnt相关的字眼查看启动失败错误的原因" >>/tmp/vnts.log
config_get vntsbin "$cfg" 'vntsbin'
if [ -z "$vntsbin" ] ;then
vntsbin=/tmp/vnts
fi
if [ -f /tmp/vnts ] && [ "${path:0:4}" != "/tmp" ] ;then
chmod +x /tmp/vnts
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 找到上传的程序/tmp/vnts,替换为$vntsbin " >>/tmp/vnts.log
upsize="$(du -k /tmp/vnts | cut -f1)"
result=$(expr $size - $upsize)
if [ "$(($(/tmp/vnts -h 2>&1 | wc -l)))" -gt 3 ] && [ "$result" -gt 500 ] ; then
mv -f /tmp/vnts "$vntsbin"
else
[ -z "$size" ] && [ "$(($(/tmp/vnts -h 2>&1 | wc -l)))" -gt 3 ] && mv -f /tmp/vnts "$vntsbin"
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 无法替换,上传的程序不完整或自定义路径的可用空间不足,当前空间剩余${size}kb" >>/tmp/vnts.log
fi
fi
if [ ! -f "$vntsbin" ] && [ "$size" -lt 10240 ] && [ ! -z "$size" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 自定义程序路径的可用空间不足,当前可用空间剩余${size}kb,自动切换为内存/tmp/vnts" >>/tmp/vnts.log
uci -q set vnt.@vnts[0].vntsbin=/tmp/vnts
uci commit
vntsbin=/tmp/vnts
fi
path=$(dirname "$vntsbin")
key_path="${path}/key"
log_path="${path}/log"
if [ "$($vntsbin -h 2>&1 | wc -l)" -lt 3 ] || [ ! -f "$vntsbin" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : ${vntsbin} 不存在,开始在线下载,下载较慢耐心等候..." >>/tmp/vnts.log
mkdir -p "$path"
if [ -z "$curltest" ] || [ ! -s "`which curl`" ] ; then
tag="$( wget -T 5 -t 3 --user-agent "$user_agent" --max-redirect=0 --output-document=- https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --quiet --output-document=- https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( wget --no-check-certificate -T 5 -t 3 --user-agent "$user_agent" --quiet --output-document=- https://dl.cnqq.cloudns.ch/https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
else
tag="$( curl --connect-timeout 3 --user-agent "$user_agent" https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( curl -k --connect-timeout 3 --user-agent "$user_agent" -s https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
[ -z "$tag" ] && tag="$( curl -Lk --connect-timeout 5 --user-agent "$user_agent" -s https://dl.cnqq.cloudns.ch/https://api.github.com/repos/vnt-dev/vnts/releases/latest 2>&1 | grep 'tag_name' | cut -d\" -f4 )"
fi
[ -z "$tag" ] && tag=v1.2.12
case "${cpucore}" in
"mipsle") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-mipsel-unknown-linux-musl-${tag}.tar.gz"
;;
"mips") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-mips-unknown-linux-musl-${tag}.tar.gz"
;;
"x86_64") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-x86_64-unknown-linux-musl-${tag}.tar.gz"
;;
"i386") echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 不支持的架构,程序退出" >>/tmp/vnts.log
return 1
;;
"arm") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-arm-unknown-linux-musleabi-${tag}.tar.gz"
;;
"armv7") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-armv7-unknown-linux-musleabi-${tag}.tar.gz"
;;
"aarch64") vntsurl="https://github.com/vnt-dev/vnts/releases/download/${tag}/vnts-aarch64-unknown-linux-musl-${tag}.tar.gz"
;;
esac
for proxy in $proxys ; do
curl -Lko /tmp/vnts.tar.gz "${proxy}${vntsurl}" || wget --no-check-certificate -O /tmp/vnts.tar.gz "${proxy}${vntsurl}"
if [ "$?" = 0 ] ; then
tar zxf /tmp/vnts.tar.gz -C /tmp/
chmod +x /tmp/vnts
rm -rf /tmp/vnts.tar.gz
if [ "$(($(/tmp/vnts -h 2>&1 | wc -l)))" -gt 3 ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : /tmp/vnts 下载成功" >>/tmp/vnts.log
mv -f /tmp/vnts "$vntsbin"
break
else
uci -q set vnt.@vnts[0].enabled=0
uci commit vnt
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : $vntsbin 下载失败,请手动下载上传程序,程序退出" >>/tmp/vnts.log
return 1
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : $vntsbin 下载失败,请手动下载上传程序,程序退出" >>/tmp/vnts.log
fi
done
fi
chmod +x "$vntsbin"
ps | grep 'vnts' | grep -v grep | awk '{print $1}' | xargs kill >/dev/null 2>&1
ps | grep 'vnts' | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
local server_port white_Token subnet servern_netmask logs sfinger web_port webuser webpass web web_wan
config_get server_port "$cfg" 'server_port'
config_get white_Token "$cfg" 'white_Token'
config_get subnet "$cfg" 'subnet'
config_get servern_netmask "$cfg" 'servern_netmask'
config_get web_port "$cfg" 'web_port'
config_get webuser "$cfg" 'webuser'
config_get webpass "$cfg" 'webpass'
config_get_bool web "$cfg" 'web' '0'
config_get_bool web_wan "$web_wan" 'web_wan' '0'
config_get_bool logs "$cfg" 'logs' '0'
config_get_bool sfinger "$cfg" 'sfinger' '0'
procd_open_instance "vnts"
get_tz
[ -z "$SET_TZ" ] || procd_set_param env TZ="$SET_TZ"
procd_set_param command "$vntsbin"
procd_append_param command --port "$server_port"
if [ ! -z "$white_Token" ] ; then
if [[ "$(grep "list white_Token" /etc/config/vnt | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
procd_append_param command --white-token "$white_Token"
else
for white_Tokens in $(cat /etc/config/vnt | grep 'list white_Token' | awk -F 'list white_Token' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
procd_append_param command --white-token "$white_Tokens"
done
fi
fi
[ -z "$subnet" ] || procd_append_param command --gateway "$subnet"
[ -z "$servern_netmask" ] || procd_append_param command --netmask "$servern_netmask"
[ "$sfinger" = "0" ] || procd_append_param command --finger
[ "$logs" = "1" ] || procd_append_param command --log-path /dev/null
if [ ! -f "${log_path}/log4rs.yaml" ] ; then
mkdir -p ${log_path}
cat > "${log_path}/log4rs.yaml"<<EOF
refresh_rate: 30 seconds
appenders:
rolling_file:
kind: rolling_file
path: /tmp/vnts.log
append: true
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S vnts:)} [{f}:{L}] {h({l})} {M}:{m}{n}"
policy:
kind: compound
trigger:
kind: size
limit: 1 mb
roller:
kind: fixed_window
pattern: /tmp/vnts.{}.log
base: 1
count: 2
root:
level: info
appenders:
- rolling_file
EOF
fi
if [ "$web" = "1" ] && [ "$cpucore" != "mips" ] ; then
[ -z "$web_port" ] || procd_append_param command --web-port "$web_port"
[ -z "$webuser" ] || procd_append_param command --username "$webuser"
[ -z "$webpass" ] || procd_append_param command --password "$webpass"
[ -z "$web_port" ] && web_port=29870
else
[ "$cpucore" = "mips" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : mips架构不支持web界面" >>/tmp/vnt-cli.log
[ "$cpucore" != "mips" ] && procd_append_param command --web-port 0
fi
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 防火墙放行服务器服务端口$server_port" >>/tmp/vnt-cli.log
uci -q delete firewall.vnts
uci set firewall.vnts=rule
uci set firewall.vnts.name="vnts"
uci set firewall.vnts.target="ACCEPT"
uci set firewall.vnts.src="wan"
uci set firewall.vnts.proto="tcp udp icmp igmp"
uci set firewall.vnts.dest_port="$server_port"
uci set firewall.vnts.enabled="1"
if [ "$web" = "1" ] && [ "$web_wan" = "1" ] ; then
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 防火墙放行服务器WEB端口$web_port" >>/tmp/vnt-cli.log
uci -q delete firewall.vntsweb
uci set firewall.vntsweb=rule
uci set firewall.vntsweb.name="vnts_web"
uci set firewall.vntsweb.target="ACCEPT"
uci set firewall.vntsweb.src="wan"
uci set firewall.vntsweb.proto="tcp"
uci set firewall.vntsweb.dest_port="$web_port"
uci set firewall.vntsweb.enabled="1"
fi
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
[ "$(uci -q get vnt.@vnts[0].logs)" = "0" ] && rm -rf /tmp/vnts*.log
if [ "$logs" = "1" ] ;then
sed -i 's|limit: 10 mb|limit: 1 mb|g' ${log_path}/log4rs.yaml
sed -i 's|count: 5|count: 2|g' ${log_path}/log4rs.yaml
logyaml=$(cat ${log_path}/log4rs.yaml |grep path: | awk -F'path: ' '{print $2}')
logyaml2=$(cat ${log_path}/log4rs.yaml | grep pattern: | awk -F'pattern: ' '{print $2}')
if [ "$logyaml" != "/tmp/vnts.log" ] ; then
sed -i "s|${logyaml}|/tmp/vnts.log|g" ${log_path}/log4rs.yaml
sed -i "s|${logyaml2}|/tmp/vnts.{}.log|g" ${log_path}/log4rs.yaml
fi
[ ! -L /tmp/vnts.0.log ] && ln -sf /tmp/vnts.log /tmp/vnts.0.log
[ ! -L /tmp/vnts.1.log ] && ln -sf /tmp/vnts.log /tmp/vnts.1.log
[ ! -L /tmp/vnts.2.log ] && ln -sf /tmp/vnts.log /tmp/vnts.2.log
fi
mkdir -p /tmp/vnts_key
[ ! -L /tmp/vnts_key/public_key.pem ] && ln -sf ${key_path}/public_key.pem /tmp/vnts_key/public_key.pem
[ ! -L /tmp/vnts_key/private_key.pem ] && ln -sf ${key_path}/private_key.pem /tmp/vnts_key/private_key.pem
echo `date +%s` > /tmp/vnts_time
}
stop_cli() {
local cfg="$1"
pgrep -f vnt_check | xargs kill -9 >/dev/null 2>&1
ps | grep 'vnt-cli' | grep -v grep | awk '{print $1}' | xargs kill >/dev/null 2>&1
ps | grep 'vnt-cli' | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
uci -q delete network.VNT
uci -q delete firewall.vntzone
uci -q delete firewall.vntfwlan
uci -q delete firewall.vntfwwan
uci -q delete firewall.lanfwvnt
uci -q delete firewall.wanfwvnt
uci -q delete firewall.vntcli
[ -n "$(uci changes network)" ] && uci commit network && /etc/init.d/network reload >/dev/null 2>&1
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
rm -rf /tmp/vnt-cli_info /tmp/vnt-cli_all /tmp/vnt-cli_list /tmp/vnt-cli_route
rm -rf /tmp/vnt-cli.log /tmp/vnt-cli*.log
rm -rf /tmp/vntnew.tag /tmp/vnt.tag
echo "$(date '+%Y-%m-%d %H:%M:%S') vnt-cli : 停止运行" >>/tmp/vnt-cli.log
}
stop_vnts() {
local cfg="$1"
ps | grep 'vnts' | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
uci -q delete firewall.vnts
uci -q delete firewall.vntsweb
[ -n "$(uci changes firewall)" ] && uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
rm -rf /tmp/vnts.log /tmp/vnts*.log
rm -rf /tmp/vntsnew.tag /tmp/vnts.tag
echo "$(date '+%Y-%m-%d %H:%M:%S') vnts : 停止运行" >>/tmp/vnts.log
}
start_service() {
config_load 'vnt'
cputype=$(uname -ms | tr ' ' '_' | tr '[A-Z]' '[a-z]')
[ -n "$(echo $cputype | grep -E "linux.*armv.*")" ] && cpucore="arm"
[ -n "$(echo $cputype | grep -E "linux.*armv7.*")" ] && [ -n "$(cat /proc/cpuinfo | grep vfp)" ] && cpucore="armv7"
[ -n "$(echo $cputype | grep -E "linux.*aarch64.*|linux.*armv8.*")" ] && cpucore="aarch64"
[ -n "$(echo $cputype | grep -E "linux.*86.*")" ] && cpucore="i386"
[ -n "$(echo $cputype | grep -E "linux.*86_64.*")" ] && cpucore="x86_64"
if [ -n "$(echo $cputype | grep -E "linux.*mips.*")" ] ; then
mipstype=$(echo -n I | hexdump -o 2>/dev/null | awk '{ print substr($2,6,1); exit}')
[ "$mipstype" = "0" ] && cpucore="mips" || cpucore="mipsle"
fi
proxys="https://github.moeyy.xyz/
https://gh.llkk.cc/
https://mirror.ghproxy.com/
https://ghproxy.net/
"
#[ -z "`opkg list-installed | grep curl`" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt : 未安装curl,请确定安装好curl " >>/tmp/vnt-cli.log
#[ -z "`opkg list-installed | grep luci-lib-fs`" ] && echo "$(date '+%Y-%m-%d %H:%M:%S') vnt : 未安装luci-lib-fs,请确定安装好luci-lib-fs " >>/tmp/vnt-cli.log
config_foreach start_vntcli 'vnt-cli'
config_foreach start_vnts 'vnts'
}
stop_service() {
[ "$(uci -q get vnt.@vnts[0].enabled)" = "0" ] && stop_vnts 'vnts'
[ "$(uci -q get vnt.@vnt-cli[0].enabled)" = "0" ] && stop_cli 'vnt-cli'
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "vnt"
procd_add_interface_trigger "interface.*.up" wan /etc/init.d/vnt reload
}
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more