diff --git a/clashoo/Makefile b/clashoo/Makefile index 77c66b06d..7ebbf6902 100644 --- a/clashoo/Makefile +++ b/clashoo/Makefile @@ -5,13 +5,13 @@ PKG_SHORT_SHA:=bc76c13 PKG_COMMIT_DATE:=2026.10.11 PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) -PKG_RELEASE:=11 +PKG_RELEASE:=12 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:=fac50d4bfa20e397dc765616578d92522b208e3a +PKG_SOURCE_VERSION:=38f826f2f89637be271fb441f2f10b0d66422b17 PKG_HASH:=skip PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) diff --git a/ifb-shaper/Makefile b/ifb-shaper/Makefile new file mode 100644 index 000000000..a68e72b21 --- /dev/null +++ b/ifb-shaper/Makefile @@ -0,0 +1,39 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=ifb-shaper +PKG_VERSION:=0.1.1 +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/ifb-shaper + SECTION:=net + CATEGORY:=Network + TITLE:=Per-client HTB traffic shaper (IFB) + DEPENDS:=+tc +ip +kmod-ifb + PKGARCH:=all +endef + +define Package/ifb-shaper/description + Per-client upload/download shaper. Download is shaped by HTB on the + LAN device egress, upload by HTB on an IFB device fed from the LAN + device ingress. Configured via /etc/config/ifb-shaper. +endef + +define Package/ifb-shaper/conffiles +/etc/config/ifb-shaper +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/ifb-shaper/install + $(INSTALL_DIR) $(1)/etc/init.d $(1)/etc/config + $(INSTALL_BIN) ./files/ifb-shaper.init $(1)/etc/init.d/ifb-shaper + $(INSTALL_CONF) ./files/ifb-shaper.config $(1)/etc/config/ifb-shaper +endef + +$(eval $(call BuildPackage,ifb-shaper)) diff --git a/ifb-shaper/README.md b/ifb-shaper/README.md new file mode 100644 index 000000000..0f726b5b3 --- /dev/null +++ b/ifb-shaper/README.md @@ -0,0 +1,91 @@ +# ifb-shaper + +Per-client traffic shaper for OpenWrt based on HTB and IFB, configured via UCI. + +- **Download**: HTB on the egress of the interface's device (e.g. `br-lan`), clients matched by destination address. +- **Upload**: device ingress is redirected to `ifb-`, HTB on the IFB, clients matched by source address. +- IPv4 and IPv6, several addresses per client. +- Each client gets its own class with five priority sub-classes (both directions), from highest to lowest priority: HTTP(S) (tcp/80, tcp/443), other TCP, other UDP, QUIC (udp/443), everything else (ICMP etc.). +- `fq_codel` on every leaf class by default (configurable). + +## How it works + +A `shaper` section is bound to a **logical netifd interface**. Its L3 device is resolved at run time and the IFB is created automatically as `ifb-` (max. 15 chars, so the device name may be up to 11 chars). The shaper is re-applied when the interface changes state or the config is changed. + +Class layout (per device and per IFB): + +| Class | Purpose | +|-------|---------| +| `1:1` | root, `total` | +| `1:10` | bypass (local) networks, `local_rate` / `local_ceil` | +| `1:1000` | parent of all client classes, `down_total` / `up_total` | +| `1:9000` | default (unclassified), `default_rate` / `default_ceil` | +| `1:1` | a client, numbered automatically (max. 2048 per shaper) | +| `1:2`..`1:6` | client sub-classes: HTTP(S), TCP, UDP, QUIC, rest | + +Networks listed in `bypass` skip the client classes and go to `1:10`. Note that `1:10` is still capped by `local_ceil` and shared by all clients; set it equal to `total` for effectively unrestricted local traffic. + +## Configuration + +`/etc/config/ifb-shaper`: + +``` +config shaper 'lan' + option interface 'lan' + list bypass '192.168.1.0/24' + +config client 'laptop' + option shaper 'lan' + list address '192.168.1.100' + list address '2001:db8:1::100' + option down_rate '1Mbit' + option down_ceil '50Mbit' + option up_rate '1Mbit' + option up_ceil '20Mbit' +``` + +### `shaper` + +| Option | Default | Description | +|--------|---------|-------------| +| `enabled` | `1` | Enable/disable this shaper | +| `interface` | – | Logical interface to shape (required) | +| `bypass` | – | List of networks (IPv4/IPv6) not subject to client limits | +| `leaf_qdisc` | `fq_codel limit 1024 flows 128` | qdisc attached to every client sub-class (`tc` arguments); `none` disables it | +| `total` | `1000Mbit` | Root class rate | +| `local_rate` / `local_ceil` | `50Mbit` / `500Mbit` | Bypass class | +| `down_total` / `up_total` | `400Mbit` / `300Mbit` | Aggregate rate of all clients | +| `default_rate` / `default_ceil` | `1Mbit` / `15Mbit` | Unclassified traffic | + +### `client` + +| Option | Default | Description | +|--------|---------|-------------| +| `shaper` | – | Logical interface name of the shaper (required) | +| `address` | – | Host or network, IPv4 or IPv6; may be a list (required) | +| `down_rate` / `down_ceil` | `1Mbit` / – | Guaranteed / maximum download (`down_ceil` required) | +| `up_rate` / `up_ceil` | `1Mbit` / – | Guaranteed / maximum upload (`up_ceil` required) | + +Rates use `tc` notation (`kbit`, `Mbit`, ...). Clients are matched in file order, so put specific hosts **before** wide subnets that contain them. + +## Notes + +- The ingress hook of the device may be shared with other tools (`clsact` from qosify etc.). It is never deleted or replaced: if there is no `ingress`/`clsact` qdisc, `clsact` is created, and only the shaper's own redirect filters (prio 4900 for IPv4, 4901 for IPv6) are managed. After `stop` an empty `clsact` stays on the device. +- Software/hardware flow offloading can bypass tc for offloaded flows. + +## Usage + +``` +/etc/init.d/ifb-shaper enable +/etc/init.d/ifb-shaper start|stop|reload +/etc/init.d/ifb-shaper dump # print the tc commands without applying +logread -e ifb-shaper # errors reported by tc +``` + +## Dependencies + +`tc`, `ip`, `kmod-ifb` (pulls in `kmod-sched-core`). + +## Todo + +- Luci app. diff --git a/ifb-shaper/files/ifb-shaper.config b/ifb-shaper/files/ifb-shaper.config new file mode 100644 index 000000000..4d9764be0 --- /dev/null +++ b/ifb-shaper/files/ifb-shaper.config @@ -0,0 +1,33 @@ +# Example, disabled by default. +# +# 'interface' logical netifd interface to shape (e.g. lan). Its device +# (e.g. br-lan) is found automatically; download = device +# egress, upload = device ingress redirected to an IFB +# named ifb- that is created automatically. +# Re-applied whenever the interface changes state. +# 'bypass' networks that go to the local class instead of the +# per-client classes (IPv4 and IPv6 allowed). +# +# Optional class sizing (defaults shown): +# total 1000Mbit, local_rate 50Mbit, local_ceil 500Mbit, +# down_total 400Mbit, up_total 300Mbit, +# default_rate 1Mbit, default_ceil 15Mbit +# leaf_qdisc 'fq_codel limit 1024 flows 128' ('none' to disable) +# +# Clients refer to a shaper by its logical interface name +# ("option shaper"). Clients are matched in file order: put specific +# hosts before wide subnets. + +config shaper 'lan' + option enabled '0' + option interface 'lan' + list bypass '192.168.1.0/24' + +config client 'laptop' + option shaper 'lan' + list address '192.168.1.100' + list address '2001:db8:1::100' + option down_rate '1Mbit' + option down_ceil '50Mbit' + option up_rate '1Mbit' + option up_ceil '20Mbit' diff --git a/ifb-shaper/files/ifb-shaper.init b/ifb-shaper/files/ifb-shaper.init new file mode 100755 index 000000000..0cb3eac10 --- /dev/null +++ b/ifb-shaper/files/ifb-shaper.init @@ -0,0 +1,238 @@ +#!/bin/sh /etc/rc.common +# Per-client HTB shaper on top of IFB. +# A shaper is bound to a logical (netifd) interface; its L3 device +# (e.g. br-man) is resolved at run time and paired with ifb-. +# download: HTB on egress, match on client dst +# upload: ingress -> ifb-, HTB on it, match on client src + +START=50 +USE_PROCD=1 + +NAME=ifb-shaper +. /lib/functions/network.sh +TC=/sbin/tc +IP=/sbin/ip + +extra_command "dump" "Print the tc commands that would be applied" + +log() { logger -s -t "$NAME" "$@"; } + +emit() { echo "$*" >> "$BATCH_ADD"; } +emit_del() { echo "$*" >> "$BATCH_DEL"; } + +# set per-family filter parameters for address $1 +fam() { + case "$1" in + *:*) F_PROTO=ipv6; F_M=ip6; F_P=10; F_ICMP=58 ;; + *) F_PROTO=ip; F_M=ip; F_P=0; F_ICMP=1 ;; + esac +} + +# root qdisc + service classes; $1 = dev, $2 = rate of the provider class +base_classes() { + emit qdisc add dev $1 root handle 1: htb default 9000 r2q 100 + emit class add dev $1 parent 1: classid 1:1 htb rate $S_TOTAL burst 15k quantum 200000 + # local traffic + emit class add dev $1 parent 1:1 classid 1:10 htb rate $S_LRATE ceil $S_LCEIL burst 15k + # traffic to/from provider; client classes live here + emit class add dev $1 parent 1:1 classid 1:1000 htb rate $2 burst 15k quantum 200000 + # unclassified + emit class add dev $1 parent 1:1 classid 1:9000 htb rate $S_DRATE ceil $S_DCEIL +} + +# One direction of one client; uses $n and $addr from client_add. +# $1 dev, $2 rate, $3 ceil, $4 sub-class rate, $5 dst|src, $6 sport|dport +# Sub-classes, highest priority first: +# 2 HTTP(S) 3 other TCP 4 other UDP 5 QUIC (udp/443) 6 the rest +client_dir() { + local dev="$1" r="$2" c="$3" sr="$4" dir="$5" port="$6" i a + + emit class add dev $dev parent 1:1000 classid 1:${n}1 htb rate $r ceil $c + for i in 2 3 4 5 6; do + emit class add dev $dev parent 1:${n}1 classid 1:${n}$i htb \ + rate $sr ceil $c prio $((i - 1)) quantum 1514 + [ -n "$S_LEAF" ] && emit qdisc add dev $dev parent 1:${n}$i $S_LEAF + done + + for a in $addr; do + fam $a + # HTTP(S) + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((1 + F_P)) u32 \ + match $F_M $dir $a match $F_M protocol 6 0xff match $F_M $port 80 0xffff flowid 1:${n}2 + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((1 + F_P)) u32 \ + match $F_M $dir $a match $F_M protocol 6 0xff match $F_M $port 443 0xffff flowid 1:${n}2 + # QUIC, must precede the generic UDP rule + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((2 + F_P)) u32 \ + match $F_M $dir $a match $F_M protocol 17 0xff match $F_M $port 443 0xffff flowid 1:${n}5 + # other TCP + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((3 + F_P)) u32 \ + match $F_M $dir $a match $F_M protocol 6 0xff flowid 1:${n}3 + # other UDP + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((4 + F_P)) u32 \ + match $F_M $dir $a match $F_M protocol 17 0xff flowid 1:${n}4 + # the rest (icmp etc.) + emit filter add dev $dev protocol $F_PROTO parent 1:0 prio $((5 + F_P)) u32 \ + match $F_M $dir $a flowid 1:${n}6 + done +} + +client_add() { + local cfg="$1" sh addr dr dc ur uc n + + config_get sh "$cfg" shaper + [ "$sh" = "$S_NAME" ] || return 0 # clients refer to the shaper section by name + + config_get addr "$cfg" address + config_get dr "$cfg" down_rate 1Mbit + config_get dc "$cfg" down_ceil + config_get ur "$cfg" up_rate 1Mbit + config_get uc "$cfg" up_ceil + if [ -z "$addr" ] || [ -z "$dc" ] || [ -z "$uc" ]; then + log "client $cfg: address, down_ceil and up_ceil are required, skipped" + return 0 + fi + + S_N=$((S_N + 1)) + if [ $S_N -gt 2048 ]; then + log "client $cfg: too many clients (max 2048 per shaper), skipped" + return 0 + fi + n=$(printf %x $((S_N + 255))) # 100, 101, ... 8ff -> classes 1:1 .. 1:6 + + client_dir $DEV $dr $dc 33kbit dst sport # download + client_dir $IFB $ur $uc 1kbit src dport # upload +} + +shaper_gen() { + local cfg="$1" en net iface K HOOK + + config_get iface "$cfg" interface + if [ -z "$iface" ]; then + log "shaper $cfg: option interface is required, skipped" + return 0 + fi + network_flush_cache + DEV= + network_get_device DEV "$iface" + if [ -z "$DEV" ]; then + log "shaper $cfg: interface $iface has no device (down?), skipped" + return 0 + fi + IFB="ifb-$DEV" + if [ ${#IFB} -gt 15 ]; then + log "shaper $cfg: name $IFB is longer than 15 chars, skipped" + return 0 + fi + append IFB_LIST "$IFB" + + # The ingress hook (clsact/ingress) may be shared with other tools, so it + # is never deleted or replaced: only our own filters (prio 4900/4901) are. + K=$($TC qdisc show dev $DEV 2>/dev/null | sed -n 's/^qdisc \(ingress\|clsact\) ffff:.*/\1/p') + HOOK=ingress + [ "$K" = ingress ] && HOOK="parent ffff:" + + emit_del filter del dev $DEV $HOOK prio 4900 + emit_del filter del dev $DEV $HOOK prio 4901 + emit_del qdisc del dev $DEV root + emit_del qdisc del dev $IFB root + [ -n "$STOP" ] && return 0 + + config_get_bool en "$cfg" enabled 1 + [ "$en" = 1 ] || return 0 + + if [ -z "$DUMP" ]; then + if [ ! -d /sys/class/net/$DEV ]; then + log "shaper $cfg: $DEV does not exist, skipped" + return 0 + fi + [ -d /sys/class/net/$IFB ] || $IP link add name $IFB type ifb || { + log "shaper $cfg: cannot create $IFB (kmod-ifb?), skipped" + return 0 + } + $IP link set dev $IFB up + fi + + config_get S_TOTAL "$cfg" total 1000Mbit + config_get S_LRATE "$cfg" local_rate 50Mbit + config_get S_LCEIL "$cfg" local_ceil 500Mbit + config_get S_DRATE "$cfg" default_rate 1Mbit + config_get S_DCEIL "$cfg" default_ceil 15Mbit + config_get S_DOWN "$cfg" down_total 400Mbit + config_get S_UP "$cfg" up_total 300Mbit + config_get S_BYPASS "$cfg" bypass + config_get S_LEAF "$cfg" leaf_qdisc "fq_codel limit 1024 flows 128" + [ "$S_LEAF" = none ] && S_LEAF= + S_NAME=$cfg + S_N=0 + + emit "# shaper $cfg: $iface = $DEV <-> $IFB" + # everything coming from clients goes to the IFB + [ -n "$K" ] || emit qdisc add dev $DEV clsact + emit filter add dev $DEV $HOOK protocol ip prio 4900 u32 match u32 0 0 \ + action mirred egress redirect dev $IFB + emit filter add dev $DEV $HOOK protocol ipv6 prio 4901 u32 match u32 0 0 \ + action mirred egress redirect dev $IFB + + base_classes $DEV $S_DOWN + base_classes $IFB $S_UP + + # local networks go to the local class + for net in $S_BYPASS; do + fam $net + emit filter add dev $IFB protocol $F_PROTO parent 1: prio $((1 + F_P)) u32 \ + match $F_M dst $net flowid 1:10 + emit filter add dev $DEV protocol $F_PROTO parent 1: prio $((1 + F_P)) u32 \ + match $F_M src $net flowid 1:10 + done + + config_foreach client_add client +} + +shaper_run() { + local out l + BATCH_ADD=/tmp/$NAME.$$.add + BATCH_DEL=/tmp/$NAME.$$.del + : > $BATCH_ADD + : > $BATCH_DEL + IFB_LIST= + + config_load $NAME + config_foreach shaper_gen shaper + + if [ -n "$DUMP" ]; then + cat $BATCH_DEL $BATCH_ADD + else + $TC -force -batch $BATCH_DEL >/dev/null 2>&1 + if [ -n "$STOP" ]; then + for out in $IFB_LIST; do + $IP link del dev $out 2>/dev/null + done + fi + if [ -s $BATCH_ADD ]; then + out=$($TC -force -batch $BATCH_ADD 2>&1) + case "$out" in + *failed*) + log "tc reported errors:" + echo "$out" | grep -v '^Warning' | while read -r l; do log "$l"; done + ;; + esac + fi + fi + rm -f $BATCH_ADD $BATCH_DEL +} + +start_service() { shaper_run; } +stop_service() { STOP=1 shaper_run; } +dump() { DUMP=1 shaper_run; } + +_iface_trigger() { + local iface + config_get iface "$1" interface + [ -n "$iface" ] && procd_add_reload_interface_trigger "$iface" +} + +service_triggers() { + procd_add_reload_trigger "$NAME" + config_load "$NAME" + config_foreach _iface_trigger shaper +} diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel index 49f33a8c6..8ab67f2e7 100644 --- a/luci-app-adguardhome/.official_channel +++ b/luci-app-adguardhome/.official_channel @@ -1 +1 @@ -both +snapshot diff --git a/luci-app-adguardhome/.official_version_snapshot b/luci-app-adguardhome/.official_version_snapshot index 6fa48e434..af617f252 100644 --- a/luci-app-adguardhome/.official_version_snapshot +++ b/luci-app-adguardhome/.official_version_snapshot @@ -1 +1 @@ -26.284.67820~acda1ad +26.285.40306~d12d553 diff --git a/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml b/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml index a32a4d07a..7d78aeb87 100644 --- a/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml +++ b/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml @@ -105,6 +105,7 @@ querylog: - orirainbow.cc^ - orirainwe.edu.kg^ - coriseay.edu.kg^ + - xzbzcoriseay.win^ - google.com^ interval: 7d size_memory: 1000 @@ -117,6 +118,7 @@ statistics: - orirainbow.cc^ - orirainwe.edu.kg^ - coriseay.edu.kg^ + - xzbzcoriseay.win^ - google.com^ interval: 7d enabled: true @@ -135,8 +137,6 @@ user_rules: - '@@||ntfy.sh^' - '@@||pubnub.com^' - '@@||clarity.ms^$important' - - '@@||sh-gsg.com^$important' - - '@@||sh-mtgc.com^$important' - '@@||taobao.com^$important' - '@@||tmall.com^$important' - '@@||jd.com^important' @@ -164,6 +164,7 @@ user_rules: - '@@||miwifi.com^' - '@@||tracker.ai.xiaomi.com^$important' - '@@||tracking.miui.com^$important' + - '@@||xiaomiyoupin.com^' - '#解除 Apple 及其所有子域名的封锁;' - '@@||apple.com^' - '@@||apple.com.cn^' @@ -195,9 +196,6 @@ user_rules: - '@@||app-measurement.com^$important' - '@@||tmdb.org^' - '@@||themoviedb.org^' - - /googleads.$~script,domain=~googleads.github.io - - /pagead/lvz? - - '||google.com/pagead/' - '||static.doubleclick.net^$domain=youtube.com' - '||youtube.com/get_midroll_' - '||5hz.org^' @@ -232,7 +230,27 @@ user_rules: - '@@||1ptba.com^' - '@@||ptcafe.club^' - '||nbpicture.xyz^' - - "" + - '! ===== Gemini & NotebookLM & Google Labs 核心 API 放行 =====' + - '@@||notebooklm.google.com^' + - '@@||labs.google.com^' + - '@@||gemini.google.com^' + - '@@||alkalimakersuite-pa.googleapis.com^' + - '@@||generativelanguage.googleapis.com^' + - '! ===== Google 基础前端脚本与 API 通讯保障 =====' + - '@@||googleapis.com^' + - '@@||gstatic.com^' + - '@@||googleusercontent.com^' + - '@@||apis.google.com^' + - '@@||clients1.google.com^' + - '@@||clients2.google.com^' + - '@@||clients3.google.com^' + - '@@||clients4.google.com^' + - '@@||clients5.google.com^' + - '@@||clients6.google.com^' + - '! ===== 仅对 Gemini 域名排除以下广告拦截/过滤规则的影响 =====' + - /googleads.$~script,domain=~googleads.github.io&~gemini.google.com + - /pagead/lvz?$domain=~gemini.google.com + - '||google.com/pagead/^$domain=~gemini.google.com' dhcp: enabled: false interface_name: "" diff --git a/luci-app-appflow/Makefile b/luci-app-appflow/Makefile index ec48e2035..4c03d25f2 100644 --- a/luci-app-appflow/Makefile +++ b/luci-app-appflow/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-appflow -PKG_VERSION:=1.1.2 -PKG_RELEASE:=2 +PKG_VERSION:=1.1.3 +PKG_RELEASE:=3 PKG_LICENSE:=Apache-2.0 PKG_LICENSE_FILES:=LICENSE PKG_MAINTAINER:=Bob diff --git a/luci-app-appflow/README.md b/luci-app-appflow/README.md index 1e0a8dae2..0077e141f 100644 --- a/luci-app-appflow/README.md +++ b/luci-app-appflow/README.md @@ -18,8 +18,8 @@ One file, every device. The package is `noarch` — only JavaScript and ucode inside — and the same `.apk` is verified installing on both aarch64 and arm_cortex-a15 hardware. - wget -O luci-app-appflow-1.1.2-r1.apk https://github.com/VolanticSystems/luci-app-appflow/releases/download/v1.1.2/luci-app-appflow-1.1.2-r1.apk - apk add --allow-untrusted ./luci-app-appflow-1.1.2-r1.apk + wget -O luci-app-appflow-1.1.3-r1.apk https://github.com/VolanticSystems/luci-app-appflow/releases/download/v1.1.3/luci-app-appflow-1.1.3-r1.apk + apk add --allow-untrusted ./luci-app-appflow-1.1.3-r1.apk /etc/init.d/appflowd enable && /etc/init.d/appflowd start Needs OpenWrt 25.12 or newer and `netifyd`. `--allow-untrusted` is required @@ -381,7 +381,14 @@ red, written before the assertion. Details in ## Requirements - OpenWrt with `netifyd` available (developed and tested against **OpenWrt - 25.12.5** with **netifyd 4.4.7**). + 25.12.5** with **netifyd 4.4.7**, the version OpenWrt packages). +- **netifyd 5.x** works from 1.1.3, tested on x86 with Netify's own 5.2 + packages. It needs two of its plugins switched on, which ship off: + `netifyd --enable-plugin proc-core`, `netifyd --enable-plugin sink-socket`, + and in `/etc/netifyd/netify-proc-core.json` set + `sinks.sink-socket.default.enable` to `true`. appflow finds the 5.x socket + by itself. If it is connected but nothing arrives for a minute, the status + page says so and the log says what to switch on. - Enough headroom to run DPI. Measured on the test device under light load: **netifyd** around 17 MB VSZ, **appflowd** around 2 MB VSZ, negligible CPU for both. netifyd is the expensive component; appflow adds little on top. diff --git a/luci-app-appflow/docs/DESIGN.md b/luci-app-appflow/docs/DESIGN.md index ca8c32e04..d3257d040 100644 --- a/luci-app-appflow/docs/DESIGN.md +++ b/luci-app-appflow/docs/DESIGN.md @@ -982,3 +982,41 @@ flow table. It is more revealing than either list alone, since it says which device used which application, and it is still far short of `flows`: no remote address, no port, no per-connection record. **`flows` remains unbound and ungranted.** + +## 16. Shipped in 1.1.3: netifyd 5.x, and a Router-row loss on every restart + +Found with a Docker test on 2026-10-12: OpenWrt 25.12.5 for x86 with Netify's +own netifyd 5.2 packages, and the 4.4.7 from OpenWrt's feed as a control. + +**On 5.x, appflow looked healthy and received nothing.** 5.x moved the JSON +export out of the agent into the `netify-sink-socket` plugin, which listens on +`/var/run/netifyd/netify-sink.sock`. The old path still exists but is a +different socket that accepts a connection and sends nothing, so status read +"connected, 0 events": a quiet router. appflowd now uses the 5.x socket when +the configured path is the 4.4 default and the 5.x socket exists, and flags a +connection that has delivered nothing for a minute (`socket.silent`, plus a log +line naming the plugins to enable). netifyd sends a status event every 15 s, so +a silent minute is never normal. + +**Other 5.x differences handled:** the category list moved to +`/etc/netifyd/netify-categories.json` (same format; without it every category +read unclassified); `flow_dpi_update` and `flow_dpi_complete` are accepted and +ignored rather than counted as unknown, since the `flow` event already carries +the classification and the first `flow_stats` covers the bytes from the start; +`agent_hello` now repeats every 15 s and is logged once per connection. + +**A shortfall leak.** Once in 133 updates on a slow, retransmitting flow, 5.x +reported `total_bytes` growth larger than `local_bytes + other_bytes`, and the +difference showed as `leaked`. The deltas are now scaled up to the growth in +the same direction ratio, mirroring the existing clamp for the opposite case. + +**The Router row lost flows after every restart, on 4.4.7 too.** §13's fix +kept a flow's ports on its record because `flow_stats` carries none. It +carries no addresses either, on both versions, though a comment said it did. +A router flow identified before the first LAN flow is re-checked against +conntrack later, from an update, with a key of empty address fields; it +missed, and the flow was shadowed for the rest of its life. The addresses are +now kept on the record too. The protocol suite's own fake events had put +addresses on every `flow_stats`, which real netifyd never does, and that is +why it stayed green; the new `netify5` group sends what netifyd actually +sends, and all five of its checks fail against the previous daemon. diff --git a/luci-app-appflow/root/etc/config/appflow b/luci-app-appflow/root/etc/config/appflow index d6147497c..2d13bf79f 100644 --- a/luci-app-appflow/root/etc/config/appflow +++ b/luci-app-appflow/root/etc/config/appflow @@ -1,5 +1,8 @@ config appflow 'main' # Netify Agent JSON export socket (netifyd.conf: [socket] listen_path[0]). + # netifyd 5.x serves it from the sink-socket plugin instead, at + # /var/run/netifyd/netify-sink.sock; with this left at the 4.4 default, + # appflowd uses that one automatically when it exists. option socket_path '/var/run/netifyd/netifyd.sock' # Maximum concurrently tracked flows. The flow table is the daemon's only diff --git a/luci-app-appflow/root/usr/sbin/appflowd b/luci-app-appflow/root/usr/sbin/appflowd index 33ae3b0e4..e50a0c7ea 100755 --- a/luci-app-appflow/root/usr/sbin/appflowd +++ b/luci-app-appflow/root/usr/sbin/appflowd @@ -58,7 +58,21 @@ const LOG_REPEAT_MS = 60000; // identical-message log suppression window const RECONNECT_MIN_MS = 1000; const RECONNECT_MAX_MS = 30000; -const CATEGORY_FILE = "/etc/netify.d/netify-categories.json"; +// netifyd 4.4 keeps its category list under /etc/netify.d; 5.x moved it to +// /etc/netifyd, same format. Without the second path every category on 5.x +// read "unclassified" (Docker test, 2026-10-12). +const CATEGORY_FILES = [ "/etc/netify.d/netify-categories.json", + "/etc/netifyd/netify-categories.json" ]; + +// netifyd 5.x moved the JSON export out of the agent into the +// netify-sink-socket plugin, which listens here. The old path still exists on +// 5.x but is a different socket that accepts a connection and sends nothing, +// so appflowd looked connected and idle while receiving nothing at all. When +// the configured path is the 4.4 default and the 5.x socket exists, use the +// 5.x one; an explicitly configured path is always used as given. +const LEGACY_SOCKET = "/var/run/netifyd/netifyd.sock"; +const SINK_SOCKET = "/var/run/netifyd/netify-sink.sock"; +const SILENT_MS = 60000; // connected with no events this long: warn const LEASE_FILE = "/tmp/dhcp.leases"; const STATUS_FILE = "/var/run/netifyd/status.json"; const CONNTRACK_FILE = "/proc/net/nf_conntrack"; @@ -101,6 +115,10 @@ let sock_fd = null; // uloop fd handle let rxbuf = ""; // partial line carry-over let connected = false; let ever_connected = false; // distinguishes the first connect from reconnects +let hello_logged = false; // agent_hello logged on this connection +let active_path = null; // the socket actually connected to +let connected_ms = 0; // when this connection was made +let silent = false; // connected, but nothing has arrived let backoff_ms = RECONNECT_MIN_MS; let retry_timer = null; @@ -664,14 +682,20 @@ function display_name(tag) { } function load_categories() { - let raw; - try { - raw = fs.readfile(CATEGORY_FILE); - } catch (e) { - raw = null; + let raw, path; + for (let p in CATEGORY_FILES) { + try { + raw = fs.readfile(p); + } catch (e) { + raw = null; + } + if (raw) { + path = p; + break; + } } if (!raw) { - logonce("cats", "warn", "category names unavailable: %s", CATEGORY_FILE); + logonce("cats", "warn", "category names unavailable: %s", join(" or ", CATEGORY_FILES)); return; } @@ -679,7 +703,7 @@ function load_categories() { try { obj = json(raw); } catch (e) { - logmsg("warn", "%s: parse error: %s", CATEGORY_FILE, e); + logmsg("warn", "%s: parse error: %s", path, e); return; } @@ -943,8 +967,9 @@ function ct_load(now, force) { function ct_nat_twin(f, now, fr) { // THE PORTS COME FROM THE FLOW RECORD, NOT THE EVENT, AND THAT IS THE WHOLE // FIX. netifyd puts local_port/other_port on the initial `flow` event and - // on nothing afterwards: a `flow_stats` payload carries addresses and byte - // counters only. This key was built straight off the event, so every call + // on nothing afterwards: a `flow_stats` payload carries byte counters + // only. (This comment once said it carried addresses too. It does not, on + // 4.4.7 or 5.x, so the addresses now come from the record as well.) This key was built straight off the event, so every call // made from an update produced "...|93.184.216.34|192.168.72.10||" with two // empty fields, matched nothing, and returned null. // @@ -959,7 +984,9 @@ function ct_nat_twin(f, now, fr) { // missing on the bench. See tests/protocol-suite.sh `router`. let lp = (fr != null && fr.local_port != null) ? fr.local_port : f.local_port; let op = (fr != null && fr.other_port != null) ? fr.other_port : f.other_port; - let key = (f.other_ip ?? "") + "|" + (f.local_ip ?? "") + "|" + + let lip = (fr != null && fr.local_ip != null) ? fr.local_ip : f.local_ip; + let oip = (fr != null && fr.other_ip != null) ? fr.other_ip : f.other_ip; + let key = (oip ?? "") + "|" + (lip ?? "") + "|" + (op ?? "") + "|" + (lp ?? ""); ct_load(now, false); let v = conntrack.entries ? conntrack.map[key] : null; @@ -1266,6 +1293,14 @@ function flow_identify(fr, ev, f, now) { // ct_nat_twin(). if (f.local_port != null) fr.local_port = f.local_port; if (f.other_port != null) fr.other_port = f.other_port; + // The addresses too, for the same reason. A `flow_stats` carries neither + // ports nor addresses (checked on 4.4.7 and 5.x captures, 2026-10-12), so + // a router flow re-checked against conntrack from an update built a key of + // empty fields, missed, and was shadowed for the rest of its life. Only + // router flows identified before the first LAN flow are re-checked, which + // is why the loss was small and showed up after every restart. + if (f.local_ip != null) fr.local_ip = f.local_ip; + if (f.other_ip != null) fr.other_ip = f.other_ip; let lm = lc(f.local_mac ?? ""), om = lc(f.other_mac ?? ""); let rl = is_router(lm, f.local_ip), ro = is_router(om, f.other_ip); @@ -1478,10 +1513,23 @@ function flow_new(digest, ev, f, now, late) { function ev_hello(ev) { agent.version = ev.agent_version ?? agent.version; + // 4.4 sends this once per connection; 5.x repeats it with every status + // update, every 15 s, which filled the log (57 lines in 16 minutes). Log + // it once per connection. + if (hello_logged) + return; + hello_logged = true; logmsg("info", "netify agent %s connected (%s)", agent.version ?? "?", ev.build_version ?? ""); } +// netifyd 5.x sends these as a flow's classification firms up. The `flow` +// event already carries the final classification in every case captured, and +// the first flow_stats covers the bytes from the flow's start, so they are +// accepted and ignored rather than counted as unknown traffic. +function ev_ignore(ev) { +} + // In normal operation `names` is bounded by nDPI's catalogue (a few thousand // ids from the definitions event). These caps make that bound explicit rather // than incidental, so a buggy or hostile agent cannot grow the maps without @@ -1578,6 +1626,14 @@ function flow_update(fr, f, now, final) { // Directional deltas exceed the cumulative growth: clamp them down. dl = (sum > 0) ? int(dl * dt / sum) : 0; do_ = dt - dl; + } else if (sum > 0 && sum < dt) { + // The opposite: the cumulative counter grew by more than the two + // directional deltas add up to. Seen once in 133 updates on a slow, + // retransmitting flow under netifyd 5.2 (2026-10-12, 13,254 bytes), + // and it showed as `leaked`. Scale up in the same direction ratio, + // mirroring the clamp above, so the total is what netifyd reported. + dl = int(dl * dt / sum); + do_ = dt - dl; } else if (sum == 0 && dt > 0) { // The cumulative counter advanced but this update carries no // directional bytes. This is the flow_purge case on 4.4.7: @@ -1670,7 +1726,9 @@ function ev_purge(ev, f, now) { const HANDLERS = { agent_hello: ev_hello, definitions: ev_definitions, - agent_status: ev_agent_status + agent_status: ev_agent_status, + flow_dpi_update: ev_ignore, + flow_dpi_complete: ev_ignore }; const FLOW_HANDLERS = { @@ -1836,18 +1894,25 @@ schedule_reconnect = function() { }; connect = function() { + active_path = cfg.socket_path; + if (active_path == LEGACY_SOCKET && fs.stat(SINK_SOCKET) != null) + active_path = SINK_SOCKET; + try { - sock = socket.connect(cfg.socket_path); + sock = socket.connect(active_path); } catch (e) { sock = null; } if (!sock) { stats.connect_fail++; - logonce("noconn", "warn", "waiting for netifyd on %s", cfg.socket_path); + logonce("noconn", "warn", "waiting for netifyd on %s", active_path); schedule_reconnect(); return; } + hello_logged = false; + silent = false; + connected_ms = now_ms(); sock_fd = uloop.handle(sock, on_readable, uloop.ULOOP_READ); // Count reconnects, not the first connect, so `socket.reconnects` reads as @@ -1864,7 +1929,7 @@ connect = function() { // stream re-establishes it from its own events. saw_internal = false; router_bootstrap(); - logmsg("info", "connected to %s", cfg.socket_path); + logmsg("info", "connected to %s", active_path); }; /* ------------------------------------------------------------------ * @@ -1920,6 +1985,15 @@ prune = function(now, force) { function housekeeping() { let now = now_ms(); stats.sweeps++; + + // Connected but silent. netifyd sends a status update every 15 s, so a + // minute with nothing at all means this socket is not the data stream; + // on 5.x that is the old path while the netify-sink-socket plugin is off. + // Without this it read as a healthy, idle router. + let last = (stats.last_event_ms > connected_ms) ? stats.last_event_ms : connected_ms; + silent = connected && (now - last) > SILENT_MS; + if (silent) + logonce("silent", "warn", "connected to %s but nothing has arrived for %d s; on netifyd 5.x enable the proc-core and sink-socket plugins", active_path, int((now - last) / 1000)); let slot = int(now / SERIES_MS), dslot = int(now / DEVSER_MS); prune(now, false); @@ -2396,7 +2470,7 @@ function m_status(req) { r.version = VERSION; r.uptime = int((mono_ms() - start_mono) / 1000); r.started_at = start_ms; - r.socket = { path: cfg.socket_path, connected: connected, reconnects: stats.reconnects, + r.socket = { path: active_path ?? cfg.socket_path, connected: connected, silent: silent, reconnects: stats.reconnects, connect_failures: stats.connect_fail }; r.config = { flow_max: cfg.flow_max, rate_window: cfg.rate_window, top_n: cfg.top_n, idle_timeout: cfg.idle_timeout, diff --git a/luci-app-appflow/tests/protocol-suite.sh b/luci-app-appflow/tests/protocol-suite.sh index 1efda4dd3..b1f9fde2a 100755 --- a/luci-app-appflow/tests/protocol-suite.sh +++ b/luci-app-appflow/tests/protocol-suite.sh @@ -114,6 +114,7 @@ ORIG_FLOWMAX="" head2() { printf '\n=== %s ===\n' "$*"; } ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$*"; } bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$*"; } +skip() { printf ' SKIP %s\n' "$*"; } note() { printf ' %s\n' "$*"; } # chk @@ -1232,6 +1233,103 @@ test_router_origin() { rm -f "$CT" } +test_netify5() { + head2 "WHAT REAL NETIFYD SENDS: NO ADDRESSES ON UPDATES, NEW 5.x MESSAGE TYPES" + + # THE DEFECT, found 2026-10-12 in a Docker test against netifyd 4.4.7 and + # 5.2. A flow_stats event carries the digest and the byte counters and + # nothing else: no ports, no addresses. The fake events everywhere else in + # this suite put local_ip/other_ip on every flow_stats, which real netifyd + # never does, and that is why the suite stayed green while router flows + # identified before the first LAN flow were silently shadowed: reshadow() + # re-asked conntrack with a key built from the update's (absent) addresses. + local RIP RMAC CT + CT=/tmp/appflow-ct-fixture5 + RIP=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.addr[0]' 2>/dev/null) + case "$RIP" in + *.*) : ;; + *) RIP=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.addr[1]' 2>/dev/null) ;; + esac + RMAC=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.mac' 2>/dev/null) + + if [ -n "$RIP" ] && [ -n "$RMAC" ]; then + printf 'ipv4 2 tcp 6 431999 ESTABLISHED src=%s dst=93.184.216.34 sport=40005 dport=443 packets=4 bytes=400 src=93.184.216.34 dst=%s sport=443 dport=40005 packets=4 bytes=400 [ASSURED] mark=0 zone=0 use=2\n' \ + "$RIP" "$RIP" > "$CT" + uci set appflow.main.conntrack_path="$CT" + uci commit appflow + + # Order is the point: the router's flow is identified BEFORE any LAN + # flow, so saw_internal is still false and nothing is decided then. + # The LAN flow arrives, saw_internal goes true, and the router flow's + # later updates, carrying no addresses, are what re-check conntrack. + { + hello + printf '{"type":"flow","internal":false,"interface":"wan","flow":{"digest":"N5R00001","local_ip":"%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_port":40005,"other_port":443,"ip_protocol":6,"detected_application":0,"detected_application_name":"","detected_protocol":91,"detected_protocol_name":"TLS","category":{"application":10,"protocol":20},"local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' "$RIP" "$RMAC" + flow_ev N5L00001 1001 lan-client 400 600 1000 true 02:00:00:00:00:aa + printf '{"type":"flow_stats","internal":false,"interface":"wan","flow":{"digest":"N5R00001","local_bytes":700,"other_bytes":1300,"total_bytes":2000}}\n' + printf '{"type":"flow_stats","internal":false,"interface":"wan","flow":{"digest":"N5R00001","local_bytes":700,"other_bytes":1300,"total_bytes":4000}}\n' + } > "$EV" + feed_and_settle + + # SABOTAGE: delete the two `fr.local_ip`/`fr.other_ip` assignments in + # flow_identify(), or make ct_nat_twin() key off f.local_ip again. The + # router row then reads MISSING, which is what 1.1.2 did. + chk "a router flow seen before any LAN flow still counts" "4000" "$(dev_bytes router)" + uci delete appflow.main.conntrack_path 2>/dev/null + uci commit appflow + rm -f "$CT" + else + skip "no WAN address/mac in netifyd status; cannot forge a router flow" + fi + + # THE SHORTFALL, seen once in 133 updates on netifyd 5.2: total_bytes grew + # by more than local_bytes + other_bytes. The difference showed as leaked. + # + # first update local 400 other 600 total 1000 + # second update local 100 other 100 total 1500 (grew 500, deltas 200) + # + # Expected: the device gets 1500, the cumulative total, and nothing leaks. + # SABOTAGE: delete the `sum > 0 && sum < dt` branch in flow_update(). The + # device then reads 1200 and leaked reads 300. + { + hello + flow_ev N5S00001 1001 lan-client 400 600 1000 true 02:00:00:00:00:bb + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"N5S00001","local_bytes":100,"other_bytes":100,"total_bytes":1500}}\n' + } > "$EV" + feed_and_settle + chk "a shortfall update still credits the full growth" "1500" "$(dev_bytes 02:00:00:00:00:bb)" + chk "and nothing is reported as leaked" "0" "$(field bytes leaked)" + + # 5.x sends flow_dpi_update / flow_dpi_complete as classification firms + # up. They carry nothing appflow needs; they must not count as unknown. + # SABOTAGE: drop them from HANDLERS. unknown then reads 2. + { + hello + flow_ev N5D00001 1001 lan-client 400 600 1000 true 02:00:00:00:00:cc + printf '{"type":"flow_dpi_update","flow":{"digest":"N5D00001"}}\n' + printf '{"type":"flow_dpi_complete","flow":{"digest":"N5D00001"}}\n' + } > "$EV" + feed_and_settle + chk "5.x classification events are not counted as unknown" "0" "$(field events unknown)" + + # Connected but silent: on 5.x the old socket path accepts a connection + # and sends nothing, and that read as a healthy idle router. A minute with + # no events at all must raise the flag. One hello, then nothing; the + # housekeeping sweep runs every 15 s, so wait past 60 + 15. + # SABOTAGE: never set `silent` in housekeeping(). The flag then reads false. + # + # The fake hangs up after HOLD_S, appflowd reconnects and the fake replays + # the hello, which is not silence. The first run of this check failed that + # way, against a daemon that was right. Hold the line past the wait here. + local HOLD_S=150 + hello > "$EV" + feed_and_settle + note "waiting 80 s for the silence to register" + sleep 80 + chk "a connection that sends nothing is flagged silent" "true" \ + "$(st | jsonfilter -e '@.socket.silent' 2>/dev/null)" +} + test_ai_breakout() { head2 "AI BREAKOUT (hostname matching, and the things it must NOT match)" @@ -1860,12 +1958,13 @@ case "$GROUP" in acl) test_acl ;; ai) test_ai_breakout ;; router) test_router_origin ;; + netify5) test_netify5 ;; hostmap) test_hostmap ;; drilldown) test_drilldown ;; all) test_conservation; test_protocol; test_bounds test_attribution; test_hostile; test_lifecycle; test_acl test_ai_breakout; test_hostmap; test_drilldown - test_router_origin ;; + test_router_origin; test_netify5 ;; *) printf "unknown group '%s'\n" "$GROUP"; exit 2 ;; esac diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index 90ba4aacc..0c6818414 100644 --- a/luci-app-open-box/Makefile +++ b/luci-app-open-box/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-open-box -PKG_VERSION:=0.1.317 -PKG_RELEASE:=26 +PKG_VERSION:=0.1.320 +PKG_RELEASE:=27 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later @@ -104,7 +104,7 @@ define Package/luci-app-open-box/postinst #!/bin/sh [ -n "$${IPKG_INSTROOT}" ] || { rm -rf /tmp/luci-*cache* 2>/dev/null || true - /etc/init.d/rpcd restart >/dev/null 2>&1 || true + /etc/init.d/rpcd reload >/dev/null 2>&1 || true /etc/init.d/openbox-panel restart >/dev/null 2>&1 || true } exit 0 diff --git a/luci-app-open-box/root/etc/config/openbox b/luci-app-open-box/root/etc/config/openbox deleted file mode 100644 index f9ca30b71..000000000 --- a/luci-app-open-box/root/etc/config/openbox +++ /dev/null @@ -1,6 +0,0 @@ -config openbox 'main' - option enabled '1' - option port '3036' - option channel 'mirror' - option mirror_prefix 'https://ghfast.top' - option auto_restart '1' diff --git a/luci-app-open-box/root/etc/init.d/openbox b/luci-app-open-box/root/etc/init.d/openbox deleted file mode 100755 index f73bb2b10..000000000 --- a/luci-app-open-box/root/etc/init.d/openbox +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/sh /etc/rc.common -# Copyright (C) 2026 OpenWrt.org -# Open-Box Core Service (sing-box transparent proxy) - -START=96 -STOP=09 -USE_PROCD=1 - -INSTALL_ROOT="/opt/open-box" - -openbox_cleanup() { - if command -v uci >/dev/null 2>&1; then - local changed=0 - for rule in openbox_dns openbox_tun_forward openbox_tun_input openbox_v6block; do - if [ -n "$(uci -q get firewall.$rule 2>/dev/null)" ]; then - uci -q delete firewall.$rule - changed=1 - fi - done - for _ob_rule in $(uci -q show firewall 2>/dev/null | sed -n 's/^firewall\.\(openbox_srv_[A-Za-z0-9_]*\)=rule$/\1/p'); do - uci -q delete "firewall.$_ob_rule" || true - changed=1 - done - if [ "$changed" = "1" ] && [ -n "$(uci -q changes firewall)" ]; then - uci -q commit firewall - /etc/init.d/firewall reload >/dev/null 2>&1 || true - fi - fi - - if command -v nft >/dev/null 2>&1; then - nft delete table inet openbox 2>/dev/null || true - fi -} - -start_service() { - if [ -f "$INSTALL_ROOT/openwrt/initd/openbox" ] && [ ! -L "$INSTALL_ROOT/openwrt/initd/openbox" ]; then - "$INSTALL_ROOT/openwrt/initd/openbox" start - return $? - fi - - if [ ! -d "$INSTALL_ROOT" ]; then - echo "[openbox] 核心组件尚未安装在 $INSTALL_ROOT,请先安装或下载核心组件。" >&2 - return 1 - fi - - local deploy_cli="$INSTALL_ROOT/panel/server/cli/deploy.mjs" - local node_bin="$INSTALL_ROOT/node/bin/node" - local core_bin="$INSTALL_ROOT/bin/sing-box" - - if [ -x "$node_bin" ] && [ -f "$deploy_cli" ]; then - OPENBOX_ROOT="$INSTALL_ROOT" ZASHBOARD_DB_PATH="$INSTALL_ROOT/data/openbox.sqlite" \ - OPENSSL_CONF=/dev/null LD_LIBRARY_PATH="$INSTALL_ROOT/node/lib" \ - "$node_bin" "$deploy_cli" >/dev/null 2>&1 - return $? - elif [ -x "$core_bin" ] && [ -f "$INSTALL_ROOT/etc/config.json" ]; then - procd_open_instance openbox - procd_set_param command "$core_bin" run -c "$INSTALL_ROOT/etc/config.json" -D "$INSTALL_ROOT" - procd_set_param respawn - procd_set_param limits core="unlimited" - procd_close_instance - else - echo "[openbox] 核心服务未就绪,请在面板完成订阅或分流配置后启动。" >&2 - return 1 - fi -} - -stop_service() { - if [ -f "$INSTALL_ROOT/openwrt/initd/openbox" ] && [ ! -L "$INSTALL_ROOT/openwrt/initd/openbox" ]; then - "$INSTALL_ROOT/openwrt/initd/openbox" stop - return $? - fi - - if [ -x "$INSTALL_ROOT/bin/sing-box" ]; then - killall -9 sing-box 2>/dev/null || true - fi - openbox_cleanup -} - -restart() { - stop - sleep 1 - start -} - -status() { - if [ -x "$INSTALL_ROOT/bin/sing-box" ] && pidof sing-box >/dev/null; then - echo "openbox is running (pid $(pidof sing-box))" - return 0 - fi - echo "openbox is not running" - return 1 -} diff --git a/luci-app-open-box/root/etc/init.d/openbox-panel b/luci-app-open-box/root/etc/init.d/openbox-panel deleted file mode 100755 index 2e71e35c2..000000000 --- a/luci-app-open-box/root/etc/init.d/openbox-panel +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/sh /etc/rc.common -# Copyright (C) 2026 OpenWrt.org -# Open-Box Panel Web Service - -START=95 -STOP=10 -USE_PROCD=1 - -INSTALL_ROOT="/opt/open-box" - -openbox_get_port() { - local p="" - [ -f "$INSTALL_ROOT/data/panel-port" ] && p=$(cat "$INSTALL_ROOT/data/panel-port" 2>/dev/null | tr -dc '0-9') - if [ -z "$p" ]; then - config_load openbox - config_get p main port 3036 - fi - [ -n "$p" ] || p=3036 - echo "$p" -} - -ensure_firewall_rule() { - local port="$1" - command -v uci >/dev/null 2>&1 || return 0 - - local cur_port=$(uci -q get firewall.openbox_panel.dest_port 2>/dev/null) - if [ "$cur_port" != "$port" ]; then - uci -q delete firewall.openbox_panel 2>/dev/null || true - uci -q set firewall.openbox_panel=rule - uci -q set firewall.openbox_panel.name='Open-Box Panel' - uci -q set firewall.openbox_panel.src='lan' - uci -q set firewall.openbox_panel.proto='tcp' - uci -q set firewall.openbox_panel.dest_port="$port" - uci -q set firewall.openbox_panel.target='ACCEPT' - uci -q commit firewall - /etc/init.d/firewall reload >/dev/null 2>&1 || true - fi -} - -start_service() { - if [ -f "$INSTALL_ROOT/openwrt/initd/openbox-panel" ] && [ ! -L "$INSTALL_ROOT/openwrt/initd/openbox-panel" ]; then - "$INSTALL_ROOT/openwrt/initd/openbox-panel" start - return $? - fi - - if [ ! -d "$INSTALL_ROOT" ] || [ ! -x "$INSTALL_ROOT/node/bin/node" ]; then - echo "[openbox-panel] 核心未安装在 $INSTALL_ROOT,请先执行 open-box install 或在 LuCI 页面下载安装核心。" >&2 - return 1 - fi - - local node_bin="$INSTALL_ROOT/node/bin/node" - local panel_app="$INSTALL_ROOT/panel/server/index.mjs" - [ -f "$panel_app" ] || panel_app="$INSTALL_ROOT/panel/server/index.js" - - local port=$(openbox_get_port) - mkdir -p "$INSTALL_ROOT/data" 2>/dev/null || true - echo "$port" > "$INSTALL_ROOT/data/panel-port" - ensure_firewall_rule "$port" - - if [ -f "$INSTALL_ROOT/openwrt/bin/open-box" ] && [ ! -e /usr/bin/open-box ]; then - ln -sf "$INSTALL_ROOT/openwrt/bin/open-box" /usr/bin/open-box 2>/dev/null || true - fi - - procd_open_instance openbox-panel - procd_set_param command "$node_bin" "$panel_app" - procd_set_param env OPENBOX_ROOT="$INSTALL_ROOT" OPENSSL_CONF=/dev/null LD_LIBRARY_PATH="$INSTALL_ROOT/node/lib" PORT="$port" - procd_set_param respawn 3600 5 0 - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_close_instance -} - -stop_service() { - if [ -f "$INSTALL_ROOT/openwrt/initd/openbox-panel" ] && [ ! -L "$INSTALL_ROOT/openwrt/initd/openbox-panel" ]; then - "$INSTALL_ROOT/openwrt/initd/openbox-panel" stop - return $? - fi - return 0 -} - -status() { - if pgrep -f "$INSTALL_ROOT/panel/server" >/dev/null 2>&1; then - local pid=$(pgrep -f "$INSTALL_ROOT/panel/server" | head -n 1) - local port=$(openbox_get_port) - echo "openbox-panel is running (pid $pid, port $port)" - return 0 - fi - echo "openbox-panel is not running" - return 1 -} - -service_triggers() { - procd_add_reload_trigger "openbox" -} diff --git a/luci-app-open-box/root/etc/uci-defaults/99-luci-app-openbox b/luci-app-open-box/root/etc/uci-defaults/99-luci-app-openbox deleted file mode 100644 index 1871a2b12..000000000 --- a/luci-app-open-box/root/etc/uci-defaults/99-luci-app-openbox +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -# Set default firewall rule for openbox-panel -uci -q get firewall.openbox_panel >/dev/null || { - uci -q set firewall.openbox_panel=rule - uci -q set firewall.openbox_panel.name='Open-Box Panel' - uci -q set firewall.openbox_panel.src='lan' - uci -q set firewall.openbox_panel.proto='tcp' - uci -q set firewall.openbox_panel.dest_port='3036' - uci -q set firewall.openbox_panel.target='ACCEPT' - uci -q commit firewall -} - -# Create open-box CLI link if available -[ -e /usr/bin/open-box ] || { - if [ -f /usr/share/openbox/open-box ]; then - ln -sf /usr/share/openbox/open-box /usr/bin/open-box - elif [ -f /opt/open-box/openwrt/bin/open-box ]; then - ln -sf /opt/open-box/openwrt/bin/open-box /usr/bin/open-box - fi -} - -# Clean LuCI cache -rm -rf /tmp/luci-*cache* 2>/dev/null || true - -exit 0 diff --git a/luci-app-open-box/root/usr/bin/open-box b/luci-app-open-box/root/usr/bin/open-box deleted file mode 100755 index 6522ccf8c..000000000 --- a/luci-app-open-box/root/usr/bin/open-box +++ /dev/null @@ -1,208 +0,0 @@ -#!/bin/sh -# Open-Box 命令行管理工具 -INSTALL_ROOT="/opt/open-box" -OPENBOX_PORT_FILE="$INSTALL_ROOT/data/panel-port" -OPENBOX_PORT_FALLBACK=2026 -OPENBOX_PORT_DEFAULT=3036 -OPENBOX_RESERVED_PORTS="9095 7853 7854 7891 53 22 80 443" - -if [ -f "$INSTALL_ROOT/openwrt/bin/open-box" ] && [ "$0" != "$INSTALL_ROOT/openwrt/bin/open-box" ] && [ ! -L "$INSTALL_ROOT/openwrt/bin/open-box" ]; then - exec "$INSTALL_ROOT/openwrt/bin/open-box" "$@" -fi - -openbox_port_problem() { - _p="$1" - case "$_p" in - ''|*[!0-9]*) echo "端口要填 1024-65535 的数字"; return 0 ;; - esac - if [ "$_p" -lt 1024 ] || [ "$_p" -gt 65535 ]; then - echo "端口要在 1024-65535 之间(1024 以下是系统保留端口)" - return 0 - fi - for _r in $OPENBOX_RESERVED_PORTS; do - if [ "$_p" = "$_r" ]; then - echo "$_p 是 Open-Box 自己或系统服务在用的端口(内核 API / DNS / SSH / HTTP 等),换一个" - return 0 - fi - done - _busy="" - if command -v ss >/dev/null 2>&1; then - _busy=$(ss -ltn 2>/dev/null | awk 'NR>1 {print $4}' | sed 's/.*[:.]//' | grep -x "$_p" | head -n 1) - elif command -v netstat >/dev/null 2>&1; then - _busy=$(netstat -ltn 2>/dev/null | awk '{print $4}' | sed 's/.*[:.]//' | grep -x "$_p" | head -n 1) - fi - if [ -n "$_busy" ]; then - echo "$_p 已经被别的程序占用(已在监听)" - return 0 - fi - return 1 -} - -openbox_current_port() { - _cp=$(cat "$OPENBOX_PORT_FILE" 2>/dev/null | tr -dc '0-9') - [ -n "$_cp" ] || _cp="$OPENBOX_PORT_DEFAULT" - printf '%s\n' "$_cp" -} - -openbox_get_lan_ip() { - LAN_IP="" - command -v uci >/dev/null 2>&1 && LAN_IP=$(uci -q get network.lan.ipaddr 2>/dev/null | tr " " "\n" | head -n 1 | cut -d/ -f1) - if [ -z "$LAN_IP" ] && command -v ip >/dev/null 2>&1; then - LAN_IP=$(ip -4 -o addr show br-lan 2>/dev/null | awk '{ print $4 }' | cut -d/ -f1 | head -n 1) - fi - [ -n "$LAN_IP" ] || LAN_IP="192.168.1.1" - printf '%s\n' "$LAN_IP" -} - -openbox_get_password() { - if [ -f "$INSTALL_ROOT/data/openbox.sqlite" ] && [ -x "$INSTALL_ROOT/node/bin/node" ]; then - _pw=$(OPENSSL_CONF=/dev/null LD_LIBRARY_PATH="$INSTALL_ROOT/node/lib" "$INSTALL_ROOT/node/bin/node" -e ' - try { - const sqlite3 = require("sqlite3"); - const db = new sqlite3.Database("'$INSTALL_ROOT'/data/openbox.sqlite", sqlite3.OPEN_READONLY, (err) => { - if (err) process.exit(1); - db.get("SELECT value FROM settings WHERE key = ?", ["password"], (e, row) => { - if (row && row.value) console.log(row.value); - db.close(); - }); - }); - } catch (_) {} - ' 2>/dev/null) - if [ -n "$_pw" ]; then - printf '%s\n' "$_pw" - return 0 - fi - fi - if [ -f "$INSTALL_ROOT/data/password" ]; then - cat "$INSTALL_ROOT/data/password" 2>/dev/null - return 0 - fi - echo "尚未设置(首次打开面板设置管理密码)" -} - -openbox_get_version() { - _ver=$(sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p' "$INSTALL_ROOT/meta.json" 2>/dev/null | head -n 1) - [ -n "$_ver" ] || _ver="未安装" - printf '%s\n' "$_ver" -} - -cmd="${1:-}" - -case "$cmd" in - password) - openbox_get_password - exit 0 - ;; - port) - if [ $# -ge 2 ]; then - new_port="$2" - _reason=$(openbox_port_problem "$new_port") && { - echo "面板端口不能用:$_reason" >&2 - exit 1 - } - mkdir -p "$INSTALL_ROOT/data" - printf '%s\n' "$new_port" > "$OPENBOX_PORT_FILE" - if command -v uci >/dev/null 2>&1; then - uci -q set openbox.main.port="$new_port" - uci -q set firewall.openbox_panel.dest_port="$new_port" - uci -q commit openbox 2>/dev/null || true - uci -q commit firewall 2>/dev/null || true - /etc/init.d/firewall reload >/dev/null 2>&1 || true - fi - echo "面板端口已修改为: $new_port" - /etc/init.d/openbox-panel restart >/dev/null 2>&1 || true - exit 0 - else - openbox_current_port - exit 0 - fi - ;; - check) - if [ -f "$INSTALL_ROOT/update.sh" ]; then - sh "$INSTALL_ROOT/update.sh" --probe direct - elif [ -f /usr/share/openbox/update.sh ]; then - sh /usr/share/openbox/update.sh --probe direct - else - echo "未找到更新脚本" - fi - exit 0 - ;; - update) - shift - if [ -f "$INSTALL_ROOT/update.sh" ]; then - sh "$INSTALL_ROOT/update.sh" "$@" - elif [ -f /usr/share/openbox/update.sh ]; then - sh /usr/share/openbox/update.sh "$@" - else - echo "未找到更新脚本" >&2 - exit 1 - fi - exit 0 - ;; - restart) - /etc/init.d/openbox restart - /etc/init.d/openbox-panel restart - echo "已重启 Open-Box 服务与面板" - exit 0 - ;; - uninstall) - shift - if [ -f "$INSTALL_ROOT/uninstall.sh" ]; then - sh "$INSTALL_ROOT/uninstall.sh" "$@" - elif [ -f /usr/share/openbox/uninstall.sh ]; then - sh /usr/share/openbox/uninstall.sh "$@" - else - /etc/init.d/openbox stop - /etc/init.d/openbox-panel stop - rm -rf "$INSTALL_ROOT" - echo "已停止并清理" - fi - exit 0 - ;; - install) - shift - if [ -f /usr/share/openbox/install.sh ]; then - sh /usr/share/openbox/install.sh "$@" - else - curl -fsSL https://raw.githubusercontent.com/liandu2024/Open-Box/main/scripts/install.sh | sh -s -- "$@" - fi - exit 0 - ;; - *) - VERSION=$(openbox_get_version) - echo "" - echo "Open-Box $VERSION" - echo " 1) 当前密码" - echo " 2) 重新启动" - echo " 3) 检查升级" - echo " 4) 卸载" - echo " 5) 退出" - printf '请选择 [1-5]: ' - read -r choice - case "$choice" in - 1) - port=$(openbox_current_port) - lan_ip=$(openbox_get_lan_ip) - pw=$(openbox_get_password) - echo "" - echo " 面板地址: http://$lan_ip:$port" - echo " 当前密码: $pw" - echo "" - ;; - 2) - /etc/init.d/openbox restart - /etc/init.d/openbox-panel restart - echo "服务已重启。" - ;; - 3) - sh "$0" update - ;; - 4) - sh "$0" uninstall - ;; - 5|*) - exit 0 - ;; - esac - ;; -esac diff --git a/luci-app-open-box/root/usr/share/luci/menu.d/luci-app-openbox.json b/luci-app-open-box/root/usr/share/luci/menu.d/luci-app-openbox.json deleted file mode 100644 index 7bc54201c..000000000 --- a/luci-app-open-box/root/usr/share/luci/menu.d/luci-app-openbox.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin/services/openbox": { - "title": "Open-Box", - "action": { - "type": "view", - "path": "openbox/main" - }, - "depends": { - "acl": [ "luci-app-openbox" ] - } - } -} diff --git a/luci-app-open-box/root/usr/share/openbox/install.sh b/luci-app-open-box/root/usr/share/openbox/install.sh deleted file mode 100755 index 11369f3ac..000000000 --- a/luci-app-open-box/root/usr/share/openbox/install.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/sh -# Open-Box 一键安装脚本 -set -eu -umask 022 - -REPO="liandu2024/Open-Box" -INSTALL_ROOT="/opt/open-box" -CHANNEL="mirror" -MIRROR_PREFIX="https://ghfast.top" -PANEL_PORT="3036" - -info() { echo "[open-box] $*"; } -warn() { echo "[open-box] 警告:$*" >&2; } -die() { echo "[open-box] 错误:$*" >&2; exit 1; } - -while [ $# -gt 0 ]; do - case "$1" in - --mirror) - CHANNEL="mirror" - shift - if [ $# -ge 1 ]; then - case "$1" in - --*) ;; - *) MIRROR_PREFIX="$1"; shift ;; - esac - fi - ;; - --direct) - CHANNEL="direct" - MIRROR_PREFIX="" - shift - ;; - --port) - shift - [ $# -ge 1 ] || die "--port 后面要跟端口号" - PANEL_PORT="$1" - shift - ;; - *) shift ;; - esac -done - -detect_arch() { - RAW_ARCH=$(uname -m 2>/dev/null || true) - case "$RAW_ARCH" in - x86_64) ARCH="x64" ;; - aarch64) ARCH="arm64" ;; - *) die "不支持的 CPU 架构:${RAW_ARCH:-未知}。Open-Box 目前仅支持 x86_64 与 aarch64(arm64)。" ;; - esac -} - -detect_arch - -if [ -f "/etc/config/openbox" ]; then - p=$(uci -q get openbox.main.port 2>/dev/null || true) - [ -n "$p" ] && PANEL_PORT="$p" -fi - -TMP_DL=$(mktemp -d /tmp/openbox-inst.XXXXXX 2>/dev/null || echo "/tmp/openbox-inst.$$") -mkdir -p "$TMP_DL" -trap 'rm -rf "$TMP_DL"' EXIT INT TERM - -ASSET="open-box-linux-${ARCH}.tar.gz" -ASSET_URL="https://github.com/$REPO/releases/latest/download/$ASSET" -SHA_URL="$ASSET_URL.sha256" - -build_url() { - url="$1" - if [ "$CHANNEL" = "mirror" ] && [ -n "$MIRROR_PREFIX" ]; then - case "$MIRROR_PREFIX" in - http://*|https://*) printf '%s/%s -' "${MIRROR_PREFIX%/}" "$url" ;; - *) printf 'https://%s/%s -' "${MIRROR_PREFIX%/}" "$url" ;; - esac - else - printf '%s -' "$url" - fi -} - -info "下载校验和与发布包: $ASSET ..." -curl -fsSL -o "$TMP_DL/$ASSET.sha256" "$(build_url "$SHA_URL")" 2>/dev/null || wget -q -O "$TMP_DL/$ASSET.sha256" "$(build_url "$SHA_URL")" || die "下载校验文件失败" -curl -fsSL -o "$TMP_DL/$ASSET" "$(build_url "$ASSET_URL")" 2>/dev/null || wget -q -O "$TMP_DL/$ASSET" "$(build_url "$ASSET_URL")" || die "下载发布包失败" - -info "校验 SHA256 ..." -( cd "$TMP_DL" && sha256sum -c "$ASSET.sha256" >/dev/null 2>&1 ) || die "安装包校验失败 (SHA256 不匹配)" - -info "解包到 $INSTALL_ROOT ..." -mkdir -p "$INSTALL_ROOT" -tar -xzf "$TMP_DL/$ASSET" -C "$INSTALL_ROOT" || die "解包失败" -chown -R 0:0 "$INSTALL_ROOT" 2>/dev/null || true - -mkdir -p "$INSTALL_ROOT/data" -printf '%s -' "$PANEL_PORT" > "$INSTALL_ROOT/data/panel-port" -if [ "$CHANNEL" = "mirror" ]; then - printf 'mirror -%s -' "$MIRROR_PREFIX" > "$INSTALL_ROOT/data/channel" -else - printf 'direct -' > "$INSTALL_ROOT/data/channel" -fi - -cp -f "$INSTALL_ROOT/openwrt/initd/openbox" /etc/init.d/openbox 2>/dev/null || true -cp -f "$INSTALL_ROOT/openwrt/initd/openbox-panel" /etc/init.d/openbox-panel 2>/dev/null || true -chmod +x /etc/init.d/openbox /etc/init.d/openbox-panel 2>/dev/null || true - -if [ -f "$INSTALL_ROOT/openwrt/bin/open-box" ]; then - chmod +x "$INSTALL_ROOT/openwrt/bin/open-box" 2>/dev/null || true - ln -sf "$INSTALL_ROOT/openwrt/bin/open-box" /usr/bin/open-box 2>/dev/null || true -fi - -if [ -d "$INSTALL_ROOT/openwrt/luci/htdocs/luci-static/resources/view/openbox" ]; then - mkdir -p /www/luci-static/resources/view/openbox - cp -f "$INSTALL_ROOT/openwrt/luci/htdocs/luci-static/resources/view/openbox/"*.js /www/luci-static/resources/view/openbox/ 2>/dev/null || true -fi - -if [ -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/luci/menu.d/luci-app-openbox.json" ]; then - mkdir -p /usr/share/luci/menu.d - cp -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/luci/menu.d/luci-app-openbox.json" /usr/share/luci/menu.d/ 2>/dev/null || true -fi - -if [ -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/rpcd/acl.d/luci-app-openbox.json" ]; then - mkdir -p /usr/share/rpcd/acl.d - cp -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/rpcd/acl.d/luci-app-openbox.json" /usr/share/rpcd/acl.d/ 2>/dev/null || true -fi - -rm -rf /tmp/luci-*cache* 2>/dev/null || true -/etc/init.d/rpcd restart >/dev/null 2>&1 || true - -/etc/init.d/openbox-panel enable >/dev/null 2>&1 || true -/etc/init.d/openbox-panel restart >/dev/null 2>&1 || true - -info "Open-Box 核心组件安装完成!" diff --git a/luci-app-open-box/root/usr/share/openbox/uninstall.sh b/luci-app-open-box/root/usr/share/openbox/uninstall.sh deleted file mode 100755 index b32993e2a..000000000 --- a/luci-app-open-box/root/usr/share/openbox/uninstall.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/sh -# Open-Box 卸载清理脚本 -set -eu - -INSTALL_ROOT="/opt/open-box" -STATUS_PATH="/tmp/openbox-uninstall.status" -PURGE=0 -DETACH=0 - -while [ $# -gt 0 ]; do - case "$1" in - --purge) PURGE=1; shift ;; - --detach) DETACH=1; shift ;; - *) shift ;; - esac -done - -if [ "$DETACH" = "1" ]; then - _args="" - [ "$PURGE" = "1" ] && _args="--purge" - nohup sh "$0" $_args >/dev/null 2>&1 & - echo "卸载已在后台启动" - exit 0 -fi - -{ - echo "pid=$$" - echo "stage=stopping" -} > "$STATUS_PATH" 2>/dev/null || true - -/etc/init.d/openbox stop >/dev/null 2>&1 || true -/etc/init.d/openbox-panel stop >/dev/null 2>&1 || true -/etc/init.d/openbox disable >/dev/null 2>&1 || true -/etc/init.d/openbox-panel disable >/dev/null 2>&1 || true - -{ - echo "pid=$$" - echo "stage=firewall" -} > "$STATUS_PATH" 2>/dev/null || true - -if command -v uci >/dev/null 2>&1; then - uci -q delete firewall.openbox_panel || true - uci -q delete firewall.openbox_dns || true - uci -q delete firewall.openbox_tun_forward || true - uci -q delete firewall.openbox_tun_input || true - uci -q delete firewall.openbox_v6block || true - for _ob_rule in $(uci -q show firewall 2>/dev/null | sed -n 's/^firewall\.\(openbox_srv_[A-Za-z0-9_]*\)=rule$/\1/p'); do - uci -q delete "firewall.$_ob_rule" || true - done - if [ -n "$(uci -q changes firewall)" ]; then - uci -q commit firewall - /etc/init.d/firewall reload >/dev/null 2>&1 || true - fi -fi - -{ - echo "pid=$$" - echo "stage=files" -} > "$STATUS_PATH" 2>/dev/null || true - -rm -f /etc/init.d/openbox /etc/init.d/openbox-panel -rm -f /www/luci-static/resources/view/openbox/main.js /www/luci-static/resources/view/openbox/status.js -rmdir /www/luci-static/resources/view/openbox 2>/dev/null || true -rm -f /usr/share/luci/menu.d/luci-app-openbox.json -rm -f /usr/share/rpcd/acl.d/luci-app-openbox.json -[ -L /usr/bin/open-box ] && rm -f /usr/bin/open-box -rm -rf /tmp/luci-*cache* 2>/dev/null || true - -if [ "$PURGE" = "1" ]; then - rm -rf "$INSTALL_ROOT" -else - for entry in "$INSTALL_ROOT"/*; do - [ -e "$entry" ] || continue - base=$(basename -- "$entry") - [ "$base" = "data" ] && continue - rm -rf "$entry" - done -fi - -{ - echo "pid=$$" - echo "stage=done" -} > "$STATUS_PATH" 2>/dev/null || true - -echo "Open-Box 卸载完成" diff --git a/luci-app-open-box/root/usr/share/openbox/update.sh b/luci-app-open-box/root/usr/share/openbox/update.sh deleted file mode 100755 index b60f24eba..000000000 --- a/luci-app-open-box/root/usr/share/openbox/update.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/bin/sh -# Open-Box 更新/回退脚本 -set -eu -umask 022 - -REPO="liandu2024/Open-Box" -INSTALL_ROOT="/opt/open-box" -STATUS_PATH="/tmp/openbox-update.status" -CANCEL_FLAG="/tmp/openbox-update.cancel" -UPDATE_LOG="/tmp/openbox-update.log" -STATUS_PID="$$" - -DETACH=0 -ROLLBACK_MODE=0 -CHANNEL="direct" -MIRROR_PREFIX="" -PROBE_CHANNEL="" -CANCEL_MODE=0 - -info() { echo "[open-box] $*"; } -warn() { echo "[open-box] 警告:$*" >&2; } -die() { - echo "[open-box] 错误:$*" >&2 - write_status failed "" "" "$*" - exit 1 -} - -write_status() { - [ -n "$STATUS_PID" ] || return 0 - _ws_tmp="$STATUS_PATH.$$.tmp" - { - echo "pid=$STATUS_PID" - echo "stage=$1" - echo "bytes=${2:-}" - echo "total=${3:-}" - echo "message=${4:-}" - } > "$_ws_tmp" 2>/dev/null && mv -f "$_ws_tmp" "$STATUS_PATH" 2>/dev/null || true -} - -check_cancel() { - if [ -e "$CANCEL_FLAG" ]; then - write_status cancelled "" "" "已取消更新" - exit 0 - fi -} - -while [ $# -gt 0 ]; do - case "$1" in - --detach) DETACH=1; shift ;; - --rollback) ROLLBACK_MODE=1; shift ;; - --cancel) CANCEL_MODE=1; shift ;; - --direct) CHANNEL="direct"; MIRROR_PREFIX=""; shift ;; - --mirror) - CHANNEL="mirror" - shift - if [ $# -ge 1 ]; then - case "$1" in - --*) ;; - *) MIRROR_PREFIX="$1"; shift ;; - esac - fi - ;; - --probe) - shift - PROBE_CHANNEL="$1" - shift - ;; - *) shift ;; - esac -done - -if [ "$CANCEL_MODE" = "1" ]; then - touch "$CANCEL_FLAG" 2>/dev/null || true - echo "requested" - exit 0 -fi - -if [ -n "$PROBE_CHANNEL" ]; then - start_time=$(date +%s%N 2>/dev/null || date +%s) - target="https://github.com/$REPO/releases/latest/download/open-box-linux-x64.tar.gz.sha256" - if [ "$PROBE_CHANNEL" != "direct" ]; then - case "$PROBE_CHANNEL" in - http://*|https://*) target="${PROBE_CHANNEL%/}/$target" ;; - *) target="https://${PROBE_CHANNEL%/}/$target" ;; - esac - fi - if curl -fsSL --connect-timeout 8 --max-time 15 -o /dev/null "$target" 2>/dev/null || wget -q --timeout=15 -O /dev/null "$target" 2>/dev/null; then - end_time=$(date +%s%N 2>/dev/null || date +%s) - ms=$(( (end_time - start_time) / 1000000 2>/dev/null || 100 )) - [ "$ms" -le 0 ] && ms=80 - echo "ok $ms ms" - else - echo "fail 连接超时或无法访问" - fi - exit 0 -fi - -if [ "$DETACH" = "1" ]; then - : > "$UPDATE_LOG" 2>/dev/null || true - write_status starting "" "" "" - rm -f "$CANCEL_FLAG" 2>/dev/null || true - _args="" - [ "$ROLLBACK_MODE" = "1" ] && _args="$_args --rollback" - if [ "$CHANNEL" = "mirror" ]; then - _args="$_args --mirror $MIRROR_PREFIX" - else - _args="$_args --direct" - fi - nohup sh "$0" $_args >"$UPDATE_LOG" 2>&1 /dev/null || true) -case "$RAW_ARCH" in - x86_64) ARCH="x64" ;; - aarch64) ARCH="arm64" ;; - *) die "不支持的 CPU 架构: $RAW_ARCH" ;; -esac - -ASSET="open-box-linux-${ARCH}.tar.gz" -ASSET_URL="https://github.com/$REPO/releases/latest/download/$ASSET" -SHA_URL="$ASSET_URL.sha256" - -build_url() { - url="$1" - if [ "$CHANNEL" = "mirror" ] && [ -n "$MIRROR_PREFIX" ]; then - case "$MIRROR_PREFIX" in - http://*|https://*) printf '%s/%s -' "${MIRROR_PREFIX%/}" "$url" ;; - *) printf 'https://%s/%s -' "${MIRROR_PREFIX%/}" "$url" ;; - esac - else - printf '%s -' "$url" - fi -} - -TMP_DL=$(mktemp -d /tmp/openbox-upd.XXXXXX 2>/dev/null || echo "/tmp/openbox-upd.$$") -mkdir -p "$TMP_DL" -trap 'rm -rf "$TMP_DL"' EXIT INT TERM - -write_status downloading 0 100000000 "开始下载" -check_cancel - -curl -fsSL -o "$TMP_DL/$ASSET.sha256" "$(build_url "$SHA_URL")" 2>/dev/null || wget -q -O "$TMP_DL/$ASSET.sha256" "$(build_url "$SHA_URL")" || die "下载校验和失败" -check_cancel - -curl -fsSL -o "$TMP_DL/$ASSET" "$(build_url "$ASSET_URL")" 2>/dev/null || wget -q -O "$TMP_DL/$ASSET" "$(build_url "$ASSET_URL")" || die "下载升级包失败" -check_cancel - -write_status verifying "" "" "" -( cd "$TMP_DL" && sha256sum -c "$ASSET.sha256" >/dev/null 2>&1 ) || die "校验失败 (SHA256 不匹配)" - -write_status extracting "" "" "" -check_cancel - -STAGE_DIR="$INSTALL_ROOT/.update-stage.$$" -rm -rf "$STAGE_DIR" -mkdir -p "$STAGE_DIR" -tar -xzf "$TMP_DL/$ASSET" -C "$STAGE_DIR" || die "解包失败" - -write_status committing "" "" "" -/etc/init.d/openbox-panel stop >/dev/null 2>&1 || true -/etc/init.d/openbox stop >/dev/null 2>&1 || true - -for comp in node panel bin openwrt; do - if [ -e "$STAGE_DIR/$comp" ]; then - rm -rf "$INSTALL_ROOT/$comp" - mv "$STAGE_DIR/$comp" "$INSTALL_ROOT/$comp" - fi -done -[ -e "$STAGE_DIR/meta.json" ] && mv "$STAGE_DIR/meta.json" "$INSTALL_ROOT/meta.json" -rm -rf "$STAGE_DIR" - -cp -f "$INSTALL_ROOT/openwrt/initd/openbox" /etc/init.d/openbox 2>/dev/null || true -cp -f "$INSTALL_ROOT/openwrt/initd/openbox-panel" /etc/init.d/openbox-panel 2>/dev/null || true -chmod +x /etc/init.d/openbox /etc/init.d/openbox-panel 2>/dev/null || true - -if [ -f "$INSTALL_ROOT/openwrt/bin/open-box" ]; then - ln -sf "$INSTALL_ROOT/openwrt/bin/open-box" /usr/bin/open-box 2>/dev/null || true -fi -if [ -d "$INSTALL_ROOT/openwrt/luci/htdocs/luci-static/resources/view/openbox" ]; then - cp -f "$INSTALL_ROOT/openwrt/luci/htdocs/luci-static/resources/view/openbox/"*.js /www/luci-static/resources/view/openbox/ 2>/dev/null || true -fi -if [ -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/luci/menu.d/luci-app-openbox.json" ]; then - cp -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/luci/menu.d/luci-app-openbox.json" /usr/share/luci/menu.d/ 2>/dev/null || true -fi -if [ -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/rpcd/acl.d/luci-app-openbox.json" ]; then - cp -f "$INSTALL_ROOT/openwrt/luci/root/usr/share/rpcd/acl.d/luci-app-openbox.json" /usr/share/rpcd/acl.d/ 2>/dev/null || true -fi - -rm -rf /tmp/luci-*cache* 2>/dev/null || true -/etc/init.d/rpcd restart >/dev/null 2>&1 || true - -/etc/init.d/openbox-panel start >/dev/null 2>&1 || true -/etc/init.d/openbox start >/dev/null 2>&1 || true - -write_status done "" "" "升级完成" -info "升级完成!" diff --git a/luci-app-open-box/root/usr/share/rpcd/acl.d/luci-app-openbox.json b/luci-app-open-box/root/usr/share/rpcd/acl.d/luci-app-openbox.json deleted file mode 100644 index 1e20e5936..000000000 --- a/luci-app-open-box/root/usr/share/rpcd/acl.d/luci-app-openbox.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "luci-app-openbox": { - "description": "Grant UCI and system execution access for luci-app-openbox", - "read": { - "cgi-io": [ "backup" ], - "file": { - "/opt/open-box/meta.json": [ "read" ], - "/opt/open-box/data/panel-port": [ "read" ], - "/tmp/openbox-update.status": [ "read" ], - "/tmp/openbox-update.log": [ "read" ], - "/tmp/openbox-uninstall.status": [ "read" ], - "/tmp/openbox-uninstall.log": [ "read" ] - }, - "uci": [ "network", "firewall", "openbox" ], - "ubus": { - "service": [ "list" ] - } - }, - "write": { - "file": { - "/etc/init.d/openbox": [ "exec" ], - "/etc/init.d/openbox-panel": [ "exec" ], - "/usr/bin/open-box": [ "exec" ], - "/opt/open-box/openwrt/bin/open-box": [ "exec" ], - "/opt/open-box/update.sh": [ "exec" ], - "/opt/open-box/uninstall.sh": [ "exec" ], - "/usr/share/openbox/install.sh": [ "exec" ], - "/usr/share/openbox/update.sh": [ "exec" ], - "/usr/share/openbox/uninstall.sh": [ "exec" ] - }, - "uci": [ "firewall", "openbox" ] - } - } -} diff --git a/luci-app-open-box/root/www/luci-static/resources/view/openbox/main.js b/luci-app-open-box/root/www/luci-static/resources/view/openbox/main.js deleted file mode 100644 index 119f7305b..000000000 --- a/luci-app-open-box/root/www/luci-static/resources/view/openbox/main.js +++ /dev/null @@ -1,456 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; -'require uci'; -'require rpc'; -'require poll'; - -var callServiceList = rpc.declare({ - object: 'service', - method: 'list', - params: [ 'name' ], - expect: { '': {} } -}); - -return view.extend({ - pollInterval: 3, - - load: function() { - return Promise.all([ - fs.read('/opt/open-box/meta.json').catch(function() { return null; }), - fs.read('/opt/open-box/data/panel-port').catch(function() { return null; }), - fs.exec_direct('/usr/bin/open-box', [ 'password' ]).catch(function() { return ''; }), - callServiceList('openbox'), - callServiceList('openbox-panel'), - uci.load('network').catch(function() { return null; }), - uci.load('openbox').catch(function() { return null; }) - ]); - }, - - render: function(data) { - var metaRaw = data[0]; - var portRaw = data[1]; - var password = (data[2] || '').trim(); - var openboxSvc = data[3] || {}; - var panelSvc = data[4] || {}; - - var meta = null; - if (metaRaw) { - try { meta = JSON.parse(metaRaw); } catch(e) {} - } - - var port = (portRaw || '').replace(/[^0-9]/g, ''); - if (!port) { - port = (uci.get('openbox', 'main', 'port') || '3036'); - } - - var lanIp = uci.get('network', 'lan', 'ipaddr'); - if (Array.isArray(lanIp)) lanIp = lanIp[0]; - if (lanIp && lanIp.indexOf('/') !== -1) lanIp = lanIp.split('/')[0]; - if (!lanIp) lanIp = window.location.hostname || '192.168.1.1'; - - var panelRunning = (panelSvc['openbox-panel'] && panelSvc['openbox-panel']['instances'] && Object.keys(panelSvc['openbox-panel']['instances']).length > 0); - var coreRunning = (openboxSvc['openbox'] && openboxSvc['openbox']['instances'] && Object.keys(openboxSvc['openbox']['instances']).length > 0); - - var panelUrl = 'http://' + lanIp + ':' + port; - - var viewRoot = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, [ _('Open-Box') + ' ' + (meta && meta.version ? 'v' + meta.version : '') ]), - E('div', { 'class': 'cbi-map-descr' }, [ - _('Open-Box 是路由器上的一体化透明代理方案,内置 sing-box 内核、Node.js 运行时与 Web 管理面板。本页面为 LuCI 兜底管理页,可用于启停服务、修改端口、找回密码或执行维护操作。') - ]) - ]); - - // 1. 面板直达与密码卡片 - var accessCard = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, [ _('Open-Box 管理面板') ]), - E('div', { 'class': 'cbi-section-descr' }, [ - _('日常代理节点选择、分流规则配置、订阅管理及流量统计请在独立 Web 面板中进行。') - ]), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'style': 'width:30%' }, [ E('strong', {}, _('面板访问地址')) ]), - E('div', { 'class': 'td' }, [ - E('a', { 'href': panelUrl, 'target': '_blank', 'class': 'cbi-button cbi-button-action' }, [ - _('打开管理面板') + ' (' + panelUrl + ')' - ]) - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ E('strong', {}, _('面板当前密码')) ]), - E('div', { 'class': 'td' }, [ - E('span', { 'style': 'font-family: monospace; font-size: 1.1em; color: #2c3e50; font-weight: bold;' }, [ - password || _('尚未设置(首次打开面板设置管理密码)') - ]), - password ? E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'style': 'margin-left: 1em;', - 'click': function() { - navigator.clipboard.writeText(password).then(function() { - ui.addNotification(null, E('p', {}, _('密码已复制到剪贴板。')), 'info'); - }); - } - }, [ _('复制密码') ]) : E('span') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ E('strong', {}, _('面板监听端口')) ]), - E('div', { 'class': 'td' }, [ - E('span', { 'style': 'font-family: monospace; margin-right: 1em;' }, [ port ]), - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'click': ui.createHandlerFn(this, 'handlePortChange', port) - }, [ _('修改端口') ]) - ]) - ]) - ]) - ]); - viewRoot.appendChild(accessCard); - - // 2. 服务运行状态卡片 - var statusCard = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, [ _('服务运行状态') ]), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'style': 'width:30%' }, [ E('strong', {}, _('面板服务 (openbox-panel)')) ]), - E('div', { 'class': 'td' }, [ - E('span', { 'class': 'badge ' + (panelRunning ? 'badge-success' : 'badge-danger') }, [ - panelRunning ? _('运行中 (Running)') : _('已停止 (Stopped)') - ]), - E('div', { 'class': 'btn-group', 'style': 'display: inline-block; margin-left: 1.5em;' }, [ - E('button', { - 'class': 'cbi-button ' + (panelRunning ? 'cbi-button-reset' : 'cbi-button-action'), - 'click': ui.createHandlerFn(this, 'handleServiceAction', 'openbox-panel', panelRunning ? 'stop' : 'start') - }, [ panelRunning ? _('停止面板') : _('启动面板') ]), - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'style': 'margin-left: 0.5em;', - 'click': ui.createHandlerFn(this, 'handleServiceAction', 'openbox-panel', 'restart') - }, [ _('重启面板') ]) - ]) - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ E('strong', {}, _('内核代理服务 (openbox)')) ]), - E('div', { 'class': 'td' }, [ - E('span', { 'class': 'badge ' + (coreRunning ? 'badge-success' : 'badge-danger') }, [ - coreRunning ? _('运行中 (Running)') : _('已停止 (Stopped)') - ]), - E('div', { 'class': 'btn-group', 'style': 'display: inline-block; margin-left: 1.5em;' }, [ - E('button', { - 'class': 'cbi-button ' + (coreRunning ? 'cbi-button-reset' : 'cbi-button-action'), - 'click': ui.createHandlerFn(this, 'handleServiceAction', 'openbox', coreRunning ? 'stop' : 'start') - }, [ coreRunning ? _('紧急停止内核 (恢复直连)') : _('启动内核') ]), - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'style': 'margin-left: 0.5em;', - 'click': ui.createHandlerFn(this, 'handleServiceAction', 'openbox', 'restart') - }, [ _('按配置重载/重启') ]) - ]) - ]) - ]) - ]) - ]); - viewRoot.appendChild(statusCard); - - // 3. 核心组件与更新维护卡片 - var hasCore = (meta !== null); - var maintenanceCard = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, [ _('组件与更新维护') ]), - E('div', { 'class': 'cbi-section-descr' }, [ - hasCore ? _('当前已安装 Open-Box 核心组件(安装目录:/opt/open-box)。') : - _('检测到核心组件尚未安装或 /opt/open-box 不完整。您可以直接使用下方一键下载安装功能完成初始化。') - ]), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'style': 'width:30%' }, [ E('strong', {}, _('核心组件状态')) ]), - E('div', { 'class': 'td' }, [ - hasCore ? E('span', { 'class': 'badge badge-success' }, [ - _('已就绪') + ' (' + (meta.arch || 'unknown') + ', ' + (meta.version || 'v' + meta.version) + ')' - ]) : E('span', { 'class': 'badge badge-danger' }, [ _('尚未就绪') ]), - !hasCore ? E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'margin-left: 1em;', - 'click': ui.createHandlerFn(this, 'handleInstallCore') - }, [ _('一键安装核心组件') ]) : E('span') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ E('strong', {}, _('更新加速镜像')) ]), - E('div', { 'class': 'td' }, [ - E('select', { 'id': 'openbox_mirror_select', 'class': 'cbi-input-select' }, [ - E('option', { 'value': 'direct' }, _('GitHub 直连 (官方)')), - E('option', { 'value': 'https://ghfast.top', 'selected': 'selected' }, 'ghfast.top (' + _('内置推荐') + ')'), - E('option', { 'value': 'https://gh-proxy.com' }, 'gh-proxy.com'), - E('option', { 'value': 'https://gh.llkk.cc' }, 'gh.llkk.cc') - ]), - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'style': 'margin-left: 1em;', - 'click': ui.createHandlerFn(this, 'handleTestLatency') - }, [ _('测速/检测可用性') ]), - E('span', { 'id': 'openbox_probe_result', 'style': 'margin-left: 1em; font-weight: bold;' }, '') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ E('strong', {}, _('系统升级与维护')) ]), - E('div', { 'class': 'td' }, [ - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'click': ui.createHandlerFn(this, 'handleUpdate', false) - }, [ _('检查并升级到最新版') ]), - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'style': 'margin-left: 0.5em;', - 'click': ui.createHandlerFn(this, 'handleUpdate', true) - }, [ _('回退到上个版本 (Rollback)') ]), - E('button', { - 'class': 'cbi-button cbi-button-reset', - 'style': 'margin-left: 1.5em;', - 'click': ui.createHandlerFn(this, 'handleUninstall') - }, [ _('卸载 Open-Box') ]) - ]) - ]) - ]) - ]); - viewRoot.appendChild(maintenanceCard); - - return viewRoot; - }, - - handleServiceAction: function(service, action) { - ui.showModal(_('正在处理'), [ - E('p', { 'class': 'spinning' }, _('正在执行操作,请稍候...')) - ]); - return fs.exec('/etc/init.d/' + service, [ action ]).then(function() { - window.location.reload(); - }).catch(function(e) { - ui.addNotification(null, E('p', {}, _('操作失败: ') + e.message), 'error'); - ui.hideModal(); - }); - }, - - handlePortChange: function(currentPort) { - var input = E('input', { - 'type': 'number', - 'class': 'cbi-input-text', - 'min': 1024, - 'max': 65535, - 'value': currentPort - }); - ui.showModal(_('修改面板端口'), [ - E('p', {}, _('请输入 1024 至 65535 之间的可用端口号(修改后将自动重启面板并放行防火墙):')), - input, - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'click': ui.hideModal - }, [ _('取消') ]), - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'style': 'margin-left: 0.5em;', - 'click': function() { - var newPort = input.value.trim(); - if (!newPort || isNaN(newPort) || newPort < 1024 || newPort > 65535) { - ui.addNotification(null, E('p', {}, _('请输入合法的端口号 (1024-65535)')), 'error'); - return; - } - ui.showModal(_('保存中'), [ E('p', { 'class': 'spinning' }, _('正在应用新端口并重启面板...')) ]); - fs.exec_direct('/usr/bin/open-box', [ 'port', newPort ]).then(function() { - window.location.reload(); - }).catch(function(e) { - ui.addNotification(null, E('p', {}, _('修改失败: ') + e.message), 'error'); - ui.hideModal(); - }); - } - }, [ _('保存并重启') ]) - ]) - ]); - }, - - handleTestLatency: function() { - var select = document.getElementById('openbox_mirror_select'); - var mirror = select ? select.value : 'direct'; - var resultSpan = document.getElementById('openbox_probe_result'); - if (resultSpan) { - resultSpan.textContent = _('正在检测...'); - resultSpan.style.color = '#3498db'; - } - - var args = [ '--probe', mirror ]; - var execPath = '/opt/open-box/update.sh'; - fs.stat(execPath).catch(function() { - return fs.stat('/usr/share/openbox/update.sh').then(function() { - execPath = '/usr/share/openbox/update.sh'; - }); - }).then(function() { - return fs.exec_direct(execPath, args); - }).then(function(out) { - if (resultSpan) { - var text = (out || '').trim(); - if (text.startsWith('ok')) { - resultSpan.textContent = '✓ ' + text.replace('ok', _('可用') + ': '); - resultSpan.style.color = '#27ae60'; - } else { - resultSpan.textContent = '✗ ' + text.replace('fail', _('失败') + ': '); - resultSpan.style.color = '#e74c3c'; - } - } - }).catch(function(err) { - if (resultSpan) { - resultSpan.textContent = '✗ ' + (err.message || _('检测出错')); - resultSpan.style.color = '#e74c3c'; - } - }); - }, - - handleInstallCore: function() { - var select = document.getElementById('openbox_mirror_select'); - var mirror = select ? select.value : 'https://ghfast.top'; - var args = []; - if (mirror !== 'direct') { - args = [ '--mirror', mirror ]; - } - - ui.showModal(_('安装核心组件'), [ - E('p', { 'class': 'spinning' }, _('正在下载并部署 Open-Box 完整核心组件(约需 1-3 分钟,视网络速度而定)...')) - ]); - - var script = '/usr/share/openbox/install.sh'; - fs.exec(script, args).then(function() { - ui.addNotification(null, E('p', {}, _('核心组件安装成功!')), 'info'); - window.location.reload(); - }).catch(function(e) { - ui.addNotification(null, E('p', {}, _('安装失败: ') + e.message), 'error'); - ui.hideModal(); - }); - }, - - handleUpdate: function(isRollback) { - var select = document.getElementById('openbox_mirror_select'); - var mirror = select ? select.value : 'direct'; - var args = [ '--detach' ]; - if (isRollback) args.push('--rollback'); - if (mirror === 'direct') { - args.push('--direct'); - } else { - args.push('--mirror'); - args.push(mirror); - } - - var execPath = '/opt/open-box/update.sh'; - fs.stat(execPath).catch(function() { - execPath = '/usr/share/openbox/update.sh'; - }).then(function() { - return fs.exec_direct(execPath, args); - }).then(function() { - var progressBar = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, [ E('div', { 'style': 'width: 0%' }) ]); - var statusText = E('p', {}, _('任务已派发,正在准备下载...')); - var logPre = E('pre', { 'style': 'max-height: 200px; overflow-y: scroll; font-size: 11px; background: #f8f9fa; padding: 8px;' }, ''); - - ui.showModal(isRollback ? _('正在执行版本回退') : _('正在升级 Open-Box'), [ - statusText, - progressBar, - E('p', { 'style': 'margin-top: 1em;' }, [ E('strong', {}, _('实时日志:')) ]), - logPre, - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-reset', - 'click': function() { - fs.exec_direct(execPath, [ '--cancel' ]); - ui.hideModal(); - } - }, [ _('取消更新') ]) - ]) - ]); - - var timer = window.setInterval(function() { - Promise.all([ - fs.read('/tmp/openbox-update.status').catch(function() { return ''; }), - fs.read('/tmp/openbox-update.log').catch(function() { return ''; }) - ]).then(function(res) { - var st = res[0] || ''; - var lg = res[1] || ''; - logPre.textContent = lg; - logPre.scrollTop = logPre.scrollHeight; - - var stage = (st.match(/stage=([^\n]+)/) || [])[1]; - var bytes = parseInt((st.match(/bytes=([0-9]+)/) || [])[1] || 0); - var total = parseInt((st.match(/total=([0-9]+)/) || [])[1] || 0); - var msg = (st.match(/message=([^\n]*)/) || [])[1] || ''; - - if (stage === 'downloading') { - if (total > 0) { - var pct = Math.floor((bytes / total) * 100); - progressBar.firstElementChild.style.width = pct + '%'; - progressBar.setAttribute('title', pct + '%'); - statusText.textContent = _('正在下载发布包: ') + (bytes / 1048576).toFixed(1) + 'MB / ' + (total / 1048576).toFixed(1) + 'MB (' + pct + '%)'; - } else { - statusText.textContent = _('正在下载发布包: ') + (bytes / 1048576).toFixed(1) + 'MB'; - } - } else if (stage === 'extracting') { - progressBar.firstElementChild.style.width = '80%'; - statusText.textContent = _('正在解包并校验文件完整性...'); - } else if (stage === 'committing') { - progressBar.firstElementChild.style.width = '90%'; - statusText.textContent = _('正在替换旧组件并重启服务...'); - } else if (stage === 'done') { - window.clearInterval(timer); - progressBar.firstElementChild.style.width = '100%'; - statusText.textContent = _('操作完成!') + (msg ? ' (' + msg + ')' : ''); - window.setTimeout(function() { window.location.reload(); }, 2000); - } else if (stage === 'failed' || stage === 'cancelled') { - window.clearInterval(timer); - statusText.textContent = (stage === 'failed' ? _('操作失败: ') : _('操作已取消: ')) + msg; - } - }); - }, 1000); - }).catch(function(e) { - ui.addNotification(null, E('p', {}, _('启动升级失败: ') + e.message), 'error'); - }); - }, - - handleUninstall: function() { - var checkPurge = E('input', { 'type': 'checkbox' }); - ui.showModal(_('卸载 Open-Box'), [ - E('p', {}, _('确定要从系统中卸载 Open-Box 吗?')), - E('label', { 'class': 'cbi-checkbox', 'style': 'display: block; margin: 1em 0;' }, [ - checkPurge, - ' ' + _('同时删除所有用户数据(/opt/open-box/data 下的节点订阅、规则集与数据库)') - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'click': ui.hideModal - }, [ _('取消') ]), - E('button', { - 'class': 'cbi-button cbi-button-reset', - 'style': 'margin-left: 0.5em;', - 'click': function() { - var purge = checkPurge.checked; - var args = [ '--detach' ]; - if (purge) args.push('--purge'); - - var execPath = '/opt/open-box/uninstall.sh'; - fs.stat(execPath).catch(function() { - execPath = '/usr/share/openbox/uninstall.sh'; - }).then(function() { - return fs.exec_direct(execPath, args); - }).then(function() { - ui.showModal(_('卸载中'), [ - E('p', { 'class': 'spinning' }, _('正在停止服务并清理残留文件...')) - ]); - window.setTimeout(function() { - window.location.href = window.location.pathname.replace(/\/admin\/services\/openbox.*/, '/admin/status/overview'); - }, 4000); - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('卸载失败: ') + err.message), 'error'); - ui.hideModal(); - }); - } - }, [ _('确认卸载') ]) - ]) - ]); - } -}); diff --git a/luci-app-openvpn-plus/Makefile b/luci-app-openvpn-plus/Makefile index ece494f50..cd11ffcf1 100755 --- a/luci-app-openvpn-plus/Makefile +++ b/luci-app-openvpn-plus/Makefile @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openvpn-plus PKG_VERSION:=1.0.0 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Manfred Jaider diff --git a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus index 78096be3c..99851053a 100755 --- a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus +++ b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus @@ -9,14 +9,9 @@ # luci-app-openvpn-plus : helper tools and background workers # /usr/libexec/luci-app-openvpn-plus -# Check if the user gave an argument -if [ -z "$1" ]; then - echo "Usage: $0 {getmac|lanip|getnetstat|bestcrypto|securepath|readfile|writefile|makedir|removefile|removedir|statfile|ovpnver|ovpnstart|ovpnrestart|ovpnstop|ovpnservice|firewallexecute|firewallservice|firewallnft|openssldgst|symlink|readstatus|iroute|cleanipdns|checkip|resolveip|checkport|checksroute|checkddns|publicip|wgetddns|ddnsservice|ucisortovpn|cleanup|keymeta|keygenclean|initkeys|keygen} [parameters]" - exit 1 -fi - # Get mac address of lan interface getmac() { + # no arguments (no shift) if [ -f "/sys/class/net/br-lan/address" ]; then cat /sys/class/net/br-lan/address else @@ -29,11 +24,12 @@ getmac() { lanip() { shift # Remove "lanip" from the arguments list local cidr="$1" - local dump=$(ubus call network.interface.lan status 2>/dev/null) # Try to extract the first IPv4 address and subnet mask from the JSON data - local ip=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) - local mask=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].mask' 2>/dev/null) + local dump ip mask + dump=$(ubus call network.interface.lan status 2>/dev/null) + ip=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) + mask=$(echo "$dump" | jsonfilter -e '@["ipv4-address"][0].mask' 2>/dev/null) # If no IPv4 address exists, try to get the first IPv6 address and prefix length if [ -z "$ip" ]; then @@ -72,10 +68,11 @@ getnetstat() { fi # Read statistics directly from sysfs files - local rxb=$(cat /sys/class/net/"$interface"/statistics/rx_bytes 2>/dev/null || echo 0) - local txb=$(cat /sys/class/net/"$interface"/statistics/tx_bytes 2>/dev/null || echo 0) - local rxp=$(cat /sys/class/net/"$interface"/statistics/rx_packets 2>/dev/null || echo 0) - local txp=$(cat /sys/class/net/"$interface"/statistics/tx_packets 2>/dev/null || echo 0) + local rxb txb rxp txp + rxb=$(cat /sys/class/net/"$interface"/statistics/rx_bytes 2>/dev/null || echo 0) + txb=$(cat /sys/class/net/"$interface"/statistics/tx_bytes 2>/dev/null || echo 0) + rxp=$(cat /sys/class/net/"$interface"/statistics/rx_packets 2>/dev/null || echo 0) + txp=$(cat /sys/class/net/"$interface"/statistics/tx_packets 2>/dev/null || echo 0) echo "$rxb|$txb|$rxp|$txp|true|$interface" return 0 @@ -83,6 +80,7 @@ getnetstat() { # Evaluates system hardware to determine the highest-performing cipher suite anchor bestcrypto() { + # no arguments (no shift) if grep -E -q 'aes|pmull' /proc/cpuinfo; then echo "AES" else @@ -91,11 +89,60 @@ bestcrypto() { return 0 } +# readstatus only allows access to: /proc/uptime, /var/run/openvpn.*.status, /tmp/run/openvpn.*.status, and /etc/openvpn/luci/*.conf +readstatus() { + shift # Remove "readstatus" from the arguments list + if [ "$#" -ne 1 ]; then + echo "ERROR: Invalid number of parameters." >&2 + return 1 + fi + + local requested_path="$1" + + # 1. Critical Guard: Block empty inputs or directory traversal hacks (..) + if [ -z "$requested_path" ] || echo "$requested_path" | grep -q '\.\.'; then + echo "ERROR: Structural security barrier rejection." >&2 + return 1 + fi + + # 2. Fix: If the path does not start with a slash, add it dynamically + case "$requested_path" in + /*) ;; + *) requested_path="/$requested_path" ;; + esac + + # 3. Strict White-Box Pattern Validation matching the openwrt/luci ACL expectations + case "$requested_path" in + /proc/uptime) + ;; + /var/run/openvpn.*.status) + ;; + /tmp/run/openvpn.*.status) + ;; + /etc/openvpn/luci/*.conf) + ;; + *) + echo "ERROR: Access denied. Protected path." >&2 + return 1 + ;; + esac + + # 4. Output the file content securely + if [ -e "$requested_path" ]; then + cat "$requested_path" + return 0 + else + # Return an explicit empty line for valid but empty status files + echo "" + return 0 + fi +} + # Central security gate to format and validate paths -securepath() { - shift # Remove "securepath" from the arguments list +internal_securepath() { + # internal (no shift) local target_path="$1" - local allow_write="$2" # Optional: "write" to block read-only folders + local block_access="$2" # Optional: "write" to block read-only folders # 1. Critical Guard: Block empty inputs or directory traversal hacks (..) if [ -z "$target_path" ] || echo "$target_path" | grep -q '\.\.'; then @@ -112,14 +159,14 @@ securepath() { case "$target_path" in /proc/uptime) # Strictly read-only /proc/uptime - [ "$allow_write" = "write" ] && return 1 + [ "$block_access" = "write" ] && return 1 echo "$target_path" return 0 ;; /proc/*) # Strictly read-only PID checks! # Validate that the path contains ONLY digits after /proc/ (e.g., /proc/1234) - [ "$allow_write" = "write" ] && return 1 + [ "$block_access" = "write" ] && return 1 if echo "$target_path" | grep -q -E '^/proc/[0-9]+$'; then echo "$target_path" return 0 @@ -128,13 +175,13 @@ securepath() { ;; /var/run/openvpn.keygen.lock|/var/run/fw4.state|/var/run/openvpn.*.status) # Read-only files! Block if write access is requested. - [ "$allow_write" = "write" ] && return 1 + [ "$block_access" = "write" ] && return 1 echo "$target_path" return 0 ;; /tmp/openvpn.keygen*|/tmp/run/openvpn.*.status) # Read-only files! Block if write access is requested. - [ "$allow_write" = "write" ] && return 1 + [ "$block_access" = "write" ] && return 1 echo "$target_path" return 0 ;; @@ -155,71 +202,73 @@ securepath() { esac } -# Securely read file from disk +# Securely read file from disk (Only called by administrative full-access scopes) readfile() { shift # Remove "readfile" from the arguments list + if [ "$#" -ne 1 ]; then + echo "ERROR: Invalid number of parameters." >&2 + return 1 + fi - # Get the validated absolute path from securepath - local filepath=$(securepath "securepath" "$1" "read") + # Get the validated absolute path from internal_securepath + local filepath + filepath=$(internal_securepath "$1" "read") if [ -z "$filepath" ]; then - echo "ERROR: Access denied." >&2 + echo "ERROR: Access denied. Protected path." >&2 return 1 fi if [ -e "$filepath" ]; then cat "$filepath" return 0 else - echo "problem: $filepath" >&2 + echo "" + return 0 fi - return 1 } # Securely write to disk with root-only access (0600) writefile() { shift # Remove "writefile" from the arguments list + if [ "$#" -ne 2 ]; then + echo "ERROR: Invalid number of parameters." + return 1 + fi # Get the validated absolute path from securepath with write permission check - local filepath=$(securepath "securepath" "$1" "write") + local filepath + filepath=$(internal_securepath "$1" "write") local content="$2" if [ -z "$filepath" ]; then - echo "ERROR: Access denied. Protected path." >&2 + echo "ERROR: Access denied. Protected path." return 1 fi # Ensure the target folder exists before writing files into it mkdir -p "$(dirname "$filepath")" - # Write the contents cleanly into the target destination path - echo -n "$content" > "$filepath" + # Restrict umask inline to block race conditions without subshell (prevents LuCI/rpcd process blocking and premature termination) + local old_umask + old_umask=$(umask) + umask 077 + printf '%s' "$content" > "$filepath" + umask "$old_umask" - # Restrict permissions immediately so only root can access it + # Restrict permissions permanently so only root can access it chmod 600 "$filepath" return 0 } -# Securely create directory paths with correct system permissions (0700) -makedir() { - shift # Remove "makedir" from the arguments list - - # Get the validated absolute path from securepath with write permission check - local dirpath=$(securepath "securepath" "$1" "write") - if [ -z "$dirpath" ]; then - echo "ERROR: Access denied. Protected path." >&2 - return 1 - fi - - # Create the folder layout securely (chmod 700 is root-only access) - mkdir -p "$dirpath" - chmod 700 "$dirpath" - return 0 -} - # Securely remove files from disk removefile() { shift # Remove "removefile" from the arguments list + if [ "$#" -ne 1 ]; then + echo "ERROR: Invalid number of parameters." >&2 + return 1 + fi # Get the validated absolute path from securepath with write permission check - local filepath=$(securepath "securepath" "$1" "write") + local filepath + filepath=$(internal_securepath "$1" "write") if [ -z "$filepath" ]; then echo "ERROR: Access denied. Protected path." >&2 return 1 @@ -230,29 +279,19 @@ removefile() { return 0 } -# Securely remove full directories from disk -removedir() { - shift # Remove "removedir" from the arguments list - - # Get the validated absolute path from securepath with write permission check - local dirpath=$(securepath "securepath" "$1" "write") - if [ -z "$dirpath" ] || [ "$dirpath" = "/" ]; then - echo "ERROR: Access denied or core root wipe blocked." >&2 - return 1 - fi - - # Delete the directory and all its contents safely - rm -rf "$dirpath" - return 0 -} - # Securely get file statistics statfile() { shift # Remove "statfile" from the arguments list + if [ "$#" -ne 1 ]; then + echo "ERROR: Invalid number of parameters." >&2 + return 1 + fi # Get the validated absolute path from securepath - local filepath=$(securepath "securepath" "$1" "read") + local filepath + filepath=$(internal_securepath "$1" "read") if [ -z "$filepath" ]; then + echo "ERROR: Access denied. Protected path." >&2 return 1 fi @@ -271,7 +310,8 @@ statfile() { fi # Get modification time - local mtime=$(date -r "$filepath" +%s 2>/dev/null || echo 0) + local mtime + mtime=$(date -r "$filepath" +%s 2>/dev/null || echo 0) # Print simulated JSON string echo "{\"type\":\"$file_type\",\"size\":$file_size,\"mtime\":$mtime}" @@ -282,12 +322,14 @@ statfile() { # OpenVPN version ovpnver() { + # no arguments (no shift) openvpn --version return 0; } # Central validator for sleep seconds parameter -validatesleep() { +internal_validatesleep() { + # internal (no shift) local sec="$1" # Check if empty, contains non-digits, is less than 1, or greater than 60 if [ -z "$sec" ] || ! echo "$sec" | grep -q '^[0-9]\+$' || [ "$sec" -lt 1 ] || [ "$sec" -gt 60 ]; then @@ -303,7 +345,7 @@ ovpnstart() { local sleepsec="$2" # Execute optional validated sleep delay before doing any work - if validatesleep "$sleepsec"; then + if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi @@ -321,7 +363,8 @@ ovpnstart() { else # B) GLOBAL MODE: Check all enabled profiles in the configuration uci -q show openvpn | grep "=openvpn" | while read -r line; do - local check_inst="${line##*.}" + local check_inst + check_inst="${line##*.}" check_inst="${check_inst%%=*}" if [ "$(uci -q get openvpn."${check_inst}".enabled)" = "1" ]; then @@ -352,7 +395,7 @@ ovpnrestart() { local sleepsec="$2" # Execute optional validated sleep delay before doing any work - if validatesleep "$sleepsec"; then + if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi @@ -374,7 +417,7 @@ ovpnstop() { local sleepsec="$2" # Execute optional validated sleep delay before doing any work - if validatesleep "$sleepsec"; then + if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi @@ -387,42 +430,14 @@ ovpnstop() { /etc/init.d/openvpn stop "$inst" >/dev/null 2>&1 } -# openvpn service stop/start (PID-guarded with direct background workers) -ovpnservice() { - shift # Remove "ovpnservice" from the arguments list - local action_cmd="$1" - local inst="$2" - local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" - - if [ -z "$(echo "$inst" | tr -d ' ')" ]; then - inst="" - fi - - if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then - # 1. Clean up any stale or previous ghost workers instantly - start-stop-daemon -K -q -p "$pid_file" 2>/dev/null - rm -f "$pid_file" - - # 2. Start the target command directly via the main script frame context (with sleep 3) - if [ "$action_cmd" = "start" ]; then - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnstart "$inst" 3 - elif [ "$action_cmd" = "restart" ]; then - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnrestart "$inst" 3 - elif [ "$action_cmd" = "stop" ]; then - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnstop "$inst" 3 - fi - fi - return 0 -} - # firewall stop/start/restart engine -firewallexecute () { - shift # Remove "firewallexecute" from the arguments list +firewallservice () { + shift # Remove "firewallservice" from the arguments list local action_cmd="$1" local sleepsec="$2" # 1. Execute optional validated sleep delay before doing any work - if validatesleep "$sleepsec"; then + if internal_validatesleep "$sleepsec"; then sleep "$sleepsec" fi @@ -433,23 +448,6 @@ firewallexecute () { return 0 } -# firewall service stop/start (PID-guarded with direct background workers) -firewallservice() { - shift # Remove "firewallservice" from the arguments list - local action_cmd="$1" - local pid_file="/var/run/luci-app-openvpn-plus-firewall-service.pid" - - if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then - # 1. Clean up any stale or previous ghost workers instantly - start-stop-daemon -K -q -p "$pid_file" 2>/dev/null - rm -f "$pid_file" - - # 2. Start the worker directly via the main script frame context (with sleep 3) - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallexecute "$action_cmd" 3 - fi - return 0 -} - # Secure helper to read and change firewall rules from LuCI without restrictions firewallnft() { shift # Remove "firewallnft" from the arguments list @@ -461,11 +459,11 @@ firewallnft() { /usr/sbin/nft list chain inet fw4 "$chain" 2>&1 } -# Secure helper to calculate cryptographic hashes using openssl dgst +# Secure helper to calculate cryptographic hashes using openssl dgst (openssldgst -sha256 test) openssldgst() { shift # Remove "openssldgst" from the arguments list - # We safely pipe the text inside the secure script layer - echo -n "$2" | /usr/bin/openssl dgst "$1" -r 2>&1 + # printf '%s' "test" | /usr/bin/openssl dgst "-sha256" -r 2>&1 (result: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08) + printf '%s' "$2" | /usr/bin/openssl dgst "$1" -r 2>&1 return 0 } @@ -504,7 +502,8 @@ symlink() { fi # Create a random crypt-token (9 random bytes as hex). - local secret_token=$(dd if=/dev/urandom bs=9 count=1 2>/dev/null | hexdump -e '"%02x"') + local secret_token + secret_token=$(dd if=/dev/urandom bs=9 count=1 2>/dev/null | hexdump -e '"%02x"') # Path to the temporary session folder. local session_dir="/www/ovpn-tmp-${secret_token}-${link_filename}" @@ -544,33 +543,18 @@ symlink() { return 0 } -# Read active multi-user server status logs securely bypassing rpcd nobody restrictions in LuCI -readstatus() { - shift # Remove the "readstatus" command word from the arguments list - local instance_id="$1" - - if [ -z "$instance_id" ]; then - echo "ERROR: Missing instance ID parameter." - return 1 - fi - - local status_path="/var/run/openvpn.${instance_id}.status" - - # Check if the targeted runtime status file exists and contains real data payload - if [ -s "$status_path" ]; then - cat "$status_path" - return 0 - else - # Return an explicit empty line to indicate a non-initialized routing socket channel - echo "" - return 0 - fi -} - # Validates connecting clients and applies dynamic routing topologies iroute() { shift # Remove "iroute" from the arguments list + # EXTERNAL ENVIRONMENT VARIABLES (Provided dynamically by OpenVPN Daemon): + # $common_name - Common Name (CN) of the connecting client certificate + # $trusted_ip - Real public WAN IP address of the remote client + # $proto - Protocol used by the client connection (tcp/udp) + local common_name="${common_name}" + local trusted_ip="${trusted_ip}" + local proto="${proto}" + case $proto in *tcp*) proto="tcp" ;; esac @@ -616,7 +600,7 @@ iroute() { # Clean any string from spaces, tabs, newlines, http(s)://, paths, ports and URL brackets cleanipdns() { - shift # Remove the command word from the arguments list + shift # Remove "cleanipdns" from the arguments list local input="$1" # 1. Delete spaces, tabs, and carriage returns @@ -653,7 +637,7 @@ cleanipdns() { # Validate and clean any given IPv4 or IPv6 address string checkip() { - shift # Remove the command word from the arguments list + shift # Remove "checkip" from the arguments list local ip="$1" # Delete all spaces, tabs and hidden lines from the text @@ -686,7 +670,7 @@ checkip() { # Resolve domain name to IP or validate raw IPv4/IPv6 input resolveip() { - shift # Remove the command word from the arguments list + shift # Remove "resolveip" from the arguments list local target="$1" # Use cleanipdns to fix the input string first @@ -741,7 +725,7 @@ resolveip() { # Check if TCP or UDP port is accessible from outside on Double NAT checkport() { - shift # Remove the word "checkport" from the arguments list + shift # Remove "checkport" from the arguments list local public_host="$1" # Public WAN IP or DDNS domain name local ext_port="$2" # External Port on WAN main modem (e.g. 58493) @@ -783,7 +767,8 @@ checkport() { fi # check and resolve ip from hostname - local resolved_ip=$(resolveip "resolveip" "$public_host") + local resolved_ip + resolved_ip=$(resolveip "resolveip" "$public_host") if [ -z "$resolved_ip" ]; then printf "ERROR: Invalid public hostname or ip: '%s'.%b" "$public_host" "$usage" return 1 @@ -862,7 +847,8 @@ checkport() { fi # 3. Read the nftables chain and verify if packets hit the internal counter of rule a) - local packet_count=$(nft list chain inet fw4 "$checkport_id" 2>/dev/null | grep "dport $int_port" | grep -oE "counter packets [0-9]+" | awk '{print $3}') + local packet_count + packet_count=$(nft list chain inet fw4 "$checkport_id" 2>/dev/null | grep "dport $int_port" | grep -oE "counter packets [0-9]+" | awk '{print $3}') [ -z "$packet_count" ] && packet_count=0 local scan_res="PORT_CLOSED" @@ -881,15 +867,16 @@ checkport() { echo "---" # 4. Clean up ALL old ghost firewall rules to keep the router fast and safe - local handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') + local handle_ids + handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') for handle in $handle_ids; do nft delete rule inet fw4 input handle "$handle" done nft delete chain inet fw4 "$checkport_id" else - - local handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') + local handle_ids + handle_ids=$(nft -a list chain inet fw4 input | grep "jump $checkport_id" | grep -oE "handle [0-9]+" | awk '{print $2}') for handle in $handle_ids; do nft delete rule inet fw4 input handle "$handle" 2>/dev/null done @@ -919,8 +906,9 @@ checksroute() { return 1 fi # Clean trailing slashes, prefixes or spaces - local clean_netip=$(echo "$remote_test_net" | cut -d'/' -f1 | tr -d ' ') - local clean_wanip=$(echo "$wan_router_ip" | cut -d'/' -f1 | tr -d ' ') + local clean_netip clean_wanip + clean_netip=$(echo "$remote_test_net" | cut -d'/' -f1 | tr -d ' ') + clean_wanip=$(echo "$wan_router_ip" | cut -d'/' -f1 | tr -d ' ') # Check if IPs contain only good characters (numbers, letters a-f, dots, colons) case "${clean_netip}:${clean_wanip}" in @@ -950,8 +938,9 @@ checksroute() { fi # Change dots (.) and colons (:) to underscores (_) to make the IDs - local netid=$(echo "$clean_netip" | tr '.:' '__') - local wanid=$(echo "$clean_wanip" | tr '.:' '__') + local netid wanid + netid=$(echo "$clean_netip" | tr '.:' '__') + wanid=$(echo "$clean_wanip" | tr '.:' '__') # create id for firewall table local checksroute_id="luci_app_openvpn_plus_checksroute_${netid}__${wanid}" @@ -965,7 +954,8 @@ checksroute() { ping_cmd="ping6" # Remove any trailing colons first - local base_v6=$(echo "$clean_netip" | sed 's/:*$//') + local base_v6 + base_v6=$(echo "$clean_netip" | sed 's/:*$//') # If the subnet ends with a double colon (e.g., fd00:1::) if echo "$clean_netip" | grep -q "::$"; then @@ -1033,8 +1023,8 @@ checksroute() { checkddns() { shift # Remove the command word from the arguments list local target="$1" - - local resolved_ip=$(resolveip "resolveip" "$target") + local resolved_ip + resolved_ip=$(resolveip "resolveip" "$target") [ -z "$resolved_ip" ] && return 1 # Live connection verification via native OpenWrt core ping utilities @@ -1058,6 +1048,8 @@ checkddns() { # Detect the real public WAN IP (IPv4 or IPv6) publicip() { + # no arguments (no shift) + local resolved_ip="" local valid_ip="" @@ -1256,7 +1248,8 @@ ddnsservice() { # Process evaluation logic if [ "$current_router_ip" != "$last_resolved_ip" ]; then - local current_uptime=$(awk '{print int($1)}' /proc/uptime) + local current_uptime + current_uptime=$(awk '{print int($1)}' /proc/uptime) local last_update_uptime=0 local last_url="" @@ -1281,15 +1274,17 @@ ddnsservice() { logger -t openvpn-luci "DDNS Service: ${wgetddns_response}" - local fresh_uptime=$(awk '{print int($1)}' /proc/uptime) + local fresh_uptime + fresh_uptime=$(awk '{print int($1)}' /proc/uptime) echo "$fresh_uptime" > "$status_file" echo "$url" >> "$status_file" fi else # Flash-safe evaluation window prevents loop locks on identical states if [ -f "$status_file" ]; then - local current_uptime=$(awk '{print int($1)}' /proc/uptime) - local last_update_uptime=$(sed -n '1p' "$status_file" | tr -d '[:space:]' | grep -E -o "^[0-9]+$") + local current_uptime last_update_uptime + current_uptime=$(awk '{print int($1)}' /proc/uptime) + last_update_uptime=$(sed -n '1p' "$status_file" | tr -d '[:space:]' | grep -E -o "^[0-9]+$") [ -z "$last_update_uptime" ] && last_update_uptime=0 local seconds_passed=$((current_uptime - last_update_uptime)) @@ -1306,9 +1301,11 @@ ddnsservice() { # This function dynamically check and sort if necessary sorts all openvpn sections ucisortovpn() { + # no arguments (no shift) # 1. Get the current raw list of instances from the file - local sections=$(uci -q show openvpn | grep "=openvpn" | cut -d. -f2 | cut -d= -f1) + local sections + sections=$(uci -q show openvpn | grep "=openvpn" | cut -d. -f2 | cut -d= -f1) [ -z "$sections" ] && return 0 local previous_num=-1 @@ -1317,7 +1314,8 @@ ucisortovpn() { # 2. Check if a smaller number comes after a bigger number for section_name in $sections; do # Extract only the digits from the name (e.g., "instance2" -> "2") - local current_num=$(echo "$section_name" | grep -oE "[0-9]+") + local current_num + current_num=$(echo "$section_name" | grep -oE "[0-9]+") [ -z "$current_num" ] && continue # If the current number is smaller than the last one, we must reorder! @@ -1333,7 +1331,8 @@ ucisortovpn() { local index=0 # Get the naturally sorted list (instance1, instance2) - local sorted_sections=$(echo "$sections" | sort -V) + local sorted_sections + sorted_sections=$(echo "$sections" | sort -V) for section_name in $sorted_sections; do [ -z "$section_name" ] && continue @@ -1484,6 +1483,7 @@ keymeta() { # Remove keygen_worker temporary keys keygenclean() { + shift # Remove "keygenclean" from the arguments list rm -f /tmp/openvpn.keygen.* return 0 } @@ -1530,7 +1530,7 @@ initkeys() { # Step 2: Files are missing, create lock file and set clean traps touch "$lock_file" - trap "rm -f $lock_file" EXIT INT TERM + trap 'rm -f "$lock_file"' EXIT INT TERM # Step 3: Check if openssl and openvpn are installed on this router if command -v openssl >/dev/null 2>&1 && command -v openvpn >/dev/null 2>&1; then @@ -1541,7 +1541,8 @@ initkeys() { if [ -z "$router_name" ]; then router_name="OpenWrt"; fi # Clean the router name string from bad characters and spaces - router_name=$(echo "$router_name" | tr -d '"'\''\\' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + # Using octal codes \042 (") \047 (') and \134 (\) to avoid single-quote escaping + router_name=$(echo "$router_name" | tr -d '\042\047\134' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') # Create temporary extension configuration files in RAM local ext_server_tmp="/tmp/openvpn.initkeys.server.ext.$$" @@ -1599,7 +1600,7 @@ initkeys() { openvpn --genkey tls-crypt-v2-server "$server_default_tls2_key" 2>/dev/null && \ \ # 4g) Derive OpenVPN static client keys - client_name=$(echo -n "$router_name (OpenVPN Client TLSv2)" | openssl enc -base64) && \ + client_name=$(printf '%s' "$router_name (OpenVPN Client TLSv2)" | openssl enc -base64) && \ openvpn --tls-crypt-v2 "$server_default_tls2_key" --genkey tls-crypt-v2-client "$client_default_tls2_key" "$client_name" # Save the status code of the complete command chain @@ -1637,7 +1638,7 @@ initkeys() { } # Asynchronous key and certificate generator -keygen_worker() { +keygen() { shift # Remove "keygen" from the arguments list local type="${1:-unknown}" local instance="${2:-default}" @@ -1871,7 +1872,8 @@ keygen_worker() { ( set +e echo "LOG: Invoking openvpn engine for asymmetric tls-crypt-v2 client key generation..." >> "$log_file" - local target_tls_name_base64=$(echo -n "$target_tls_name" | openssl enc -base64) + local target_tls_name_base64 + target_tls_name_base64=$(printf '%s' "$target_tls_name" | openssl enc -base64) echo "[CMD] openvpn --tls-crypt-v2 '$server_tls2_key' --genkey tls-crypt-v2-client '$active_out' '$target_tls_name_base64'" >> "$log_file" openvpn --tls-crypt-v2 "$server_tls2_key" --genkey tls-crypt-v2-client "$active_out" "$target_tls_name_base64" 2>> "$log_file" if [ -f "$active_out" ] && [ -s "$active_out" ]; then @@ -1891,82 +1893,165 @@ keygen_worker() { return 0 } +# async worker with start-stop-daemon + +ovpnservice_async() { + shift # Remove "ovpnservice_async" from the arguments list + local action_cmd="$1" + local inst="$2" + local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" + if [ -z "$(echo "$inst" | tr -d ' ')" ]; then + inst="" + fi + if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then + # 1. Clean up any stale or previous ghost workers instantly + start-stop-daemon -K -q -p "$pid_file" 2>/dev/null + rm -f "$pid_file" + + # 2. Start the target command via worker wrapper to handle absolute lifecycle traps + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnservice_worker "$pid_file" "$action_cmd" "$inst" + fi + return 0 +} +ovpnservice_worker() { + shift # Remove "ovpnservice_worker" from the arguments list + local pid_file="$1" + local cmd="$2" + local inst="$3" + trap 'rm -f "$pid_file"' EXIT INT TERM + if [ "$cmd" = "start" ]; then + ovpnstart "ovpnstart" "$inst" 3 + elif [ "$cmd" = "restart" ]; then + ovpnrestart "ovpnrestart" "$inst" 3 + elif [ "$cmd" = "stop" ]; then + ovpnstop "ovpnstop" "$inst" 3 + fi + rm -f "$pid_file" + return 0 +} + +firewallservice_async() { + shift # Remove "firewallservice_async" from the arguments list + local action_cmd="$1" + local pid_file="/var/run/luci-app-openvpn-plus-firewall-service.pid" + if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then + # 1. Clean up any stale or previous ghost workers instantly + start-stop-daemon -K -q -p "$pid_file" 2>/dev/null + rm -f "$pid_file" + + # 2. Start the worker directly via the main script frame context + # Inside the worker, we use a trap to cleanly remove the pid_file upon exit + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallservice_worker "$pid_file" "$action_cmd" + fi + return 0 +} +firewallservice_worker() { + shift # Remove "firewallservice_worker" from the arguments list + local pid_file="$1" + local cmd="$2" + trap 'rm -f "$pid_file"' EXIT INT TERM + firewallservice "firewallservice" "$cmd" 3 + rm -f "$pid_file" + return 0 +} + +initkeys_async() { + shift # Remove "initkeys_async" from the arguments list + # 1. Check if the key generator is running + if start-stop-daemon -K -q -p "/var/run/openvpn.initkeys.pid" -t 2>/dev/null; then + exit 0 + fi + # 2. Start the background worker and create the PID file automatically + start-stop-daemon -S -b -m -p "/var/run/openvpn.initkeys.pid" -x "$0" -- "initkeys_worker" "/var/run/openvpn.initkeys.pid" + return 0 +} +initkeys_worker() { + shift # Remove "initkeys_worker" from the arguments list + local pid_file="$1" + # 3. Secure the PID file lifecycle instantly with a trap framework + trap 'rm -f "$pid_file"' EXIT INT TERM + # 4. Run the key creation function quietly in the background + initkeys "initkeys" >/dev/null 2>&1 + rm -f "$pid_file" + return 0 +} + +keygen_async() { + shift # Remove "keygen" from the arguments list + local type="$1" + local instance="$2" + # 1. Start the background keygen worker and create the PID file automatically + start-stop-daemon -S -b -m -p "/var/run/openvpn.keygen-${instance}.pid" -x "$0" -- "keygen_worker" "/var/run/openvpn.keygen-${instance}.pid" "$@" + return 0 +} +keygen_worker() { + shift # Remove "keygen_worker" from the arguments list + local pid_file="$1" + shift # Remove "pid_file" from the arguments list + # 3. Secure the dynamic PID node tracking with a strict trap guard + trap 'rm -f "$pid_file"' EXIT INT TERM + # 4. Run the certificate engine pipeline with corrected original parameters ($@: type="$1", instance="$2") + keygen "keygen" "$@" + rm -f "$pid_file" + return 0 +} + +usage() { + local args="{getmac|lanip|getnetstat|bestcrypto|readstatus|readfile|writefile|removefile|statfile|ovpnver|ovpnstart|ovpnrestart|ovpnstop|ovpnservice|firewallservice|firewallnft|openssldgst|symlink|iroute|cleanipdns|checkip|resolveip|checkport|checksroute|checkddns|publicip|wgetddns|ddnsservice|ucisortovpn|cleanup|keymeta|keygenclean|initkeys|keygen} [parameters]" + # Check if the user gave an argument + if [ -z "$1" ]; then + echo "Usage: $0 $args" + else + echo "Error: Unknown command '$1'" + echo "Valid commands: $args" + fi + return 1 +} + # Main entry point case "$1" in - getmac) getmac ;; - lanip) lanip "$@" ;; - getnetstat) getnetstat "$@" ;; - bestcrypto) bestcrypto ;; - securepath) securepath "$@" ;; - readfile) readfile "$@" ;; - writefile) writefile "$@" ;; - makedir) makedir "$@" ;; - removefile) removefile "$@" ;; - removedir) removedir "$@" ;; - statfile) statfile "$@" ;; - ovpnver) ovpnver ;; - ovpnstart) ovpnstart "$@" ;; - ovpnrestart) ovpnrestart "$@" ;; - ovpnstop) ovpnstop "$@" ;; - ovpnservice) ovpnservice "$@" ;; - firewallexecute) firewallexecute "$@" ;; - firewallservice) firewallservice "$@" ;; - firewallnft) firewallnft "$@" ;; - openssldgst) openssldgst "$@" ;; - symlink) symlink "$@" ;; - readstatus) readstatus "$@" ;; - iroute) iroute "$@" ;; - cleanipdns) cleanipdns "$@" ;; - checkip) checkip "$@" ;; - resolveip) resolveip "$@" ;; - checkport) checkport "$@" ;; - checksroute) checksroute "$@" ;; - checkddns) checkddns "$@" ;; - publicip) publicip ;; - wgetddns) wgetddns "$@" ;; - ddnsservice) ddnsservice "$@" ;; - ucisortovpn) ucisortovpn ;; - cleanup) cleanup "$@" ;; - keymeta) keymeta "$@" ;; - keygenclean) keygenclean ;; + getmac) getmac ;; + lanip) lanip "$@" ;; + getnetstat) getnetstat "$@" ;; + bestcrypto) bestcrypto ;; + readstatus) readstatus "$@" ;; + readfile) readfile "$@" ;; + writefile) writefile "$@" ;; + removefile) removefile "$@" ;; + statfile) statfile "$@" ;; + ovpnver) ovpnver ;; + ovpnstart) ovpnstart "$@" ;; + ovpnrestart) ovpnrestart "$@" ;; + ovpnstop) ovpnstop "$@" ;; + firewallnft) firewallnft "$@" ;; + openssldgst) openssldgst "$@" ;; + symlink) symlink "$@" ;; + iroute) iroute "$@" ;; + cleanipdns) cleanipdns "$@" ;; + checkip) checkip "$@" ;; + resolveip) resolveip "$@" ;; + checkport) checkport "$@" ;; + checksroute) checksroute "$@" ;; + checkddns) checkddns "$@" ;; + publicip) publicip ;; + wgetddns) wgetddns "$@" ;; + ddnsservice) ddnsservice "$@" ;; + ucisortovpn) ucisortovpn ;; + cleanup) cleanup "$@" ;; + keymeta) keymeta "$@" ;; + keygenclean) keygenclean ;; - initkeys) - # 1. Check if the key generator is running - if start-stop-daemon -K -q -p "/var/run/openvpn.initkeys.pid" -t 2>/dev/null; then - exit 0 - fi - # 2. Start the background worker and create the PID file automatically - start-stop-daemon -S -b -m -p "/var/run/openvpn.initkeys.pid" -x "$0" -- "initkeys_worker" - exit 0 - ;; + # async worker with start-stop-daemon + firewallservice) firewallservice_async "$@" ;; + firewallservice_worker) firewallservice_worker "$@" ;; + ovpnservice) ovpnservice_async "$@" ;; + ovpnservice_worker) ovpnservice_worker "$@" ;; + initkeys) initkeys_async "$@" ;; + initkeys_worker) initkeys_worker "$@" ;; + keygen) keygen_async "$@" ;; + keygen_worker) keygen_worker "$@" ;; - initkeys_worker) - # 3. Run the key creation function quietly in the background - initkeys >/dev/null 2>&1 - # 4. Delete the PID file when the keys are finished - rm -f "/var/run/openvpn.initkeys.pid" - exit 0 - ;; - - keygen) - # 1. Start the background keygen worker and create the PID file automatically - start-stop-daemon -S -b -m -p "/var/run/openvpn.keygen-${2}.pid" -x "$0" -- "keygen_worker" "$@" - exit 0 - ;; - - keygen_worker) - # 2. Run the key creation function quietly in the background - shift # Remove "keygen_worker" from the arguments list to fix $@ - keygen_worker "$@" - # 3. Delete the PID file when keygen finished - rm -f "/var/run/openvpn.keygen-${2}.pid" - ;; - - *) - echo "Error: Unknown command '$1'" - echo "Valid commands: getmac lanip getnetstat bestcrypto securepath readfile writefile makedir removefile removedir statfile ovpnver ovpnstart ovpnrestart ovpnstop ovpnservice firewallexecute firewallservice firewallnft openssldgst symlink readstatus iroute cleanipdns checkip resolveip checkport checksroute checkddns publicip wgetddns ddnsservice ucisortovpn cleanup keymeta keygenclean initkeys keygen" - exit 1 - ;; + *) usage "$@" ;; esac # Global exit handler guarantees instant thread termination diff --git a/luci-app-openvpn-plus/root/usr/share/rpcd/acl.d/luci-app-openvpn-plus-status.json b/luci-app-openvpn-plus/root/usr/share/rpcd/acl.d/luci-app-openvpn-plus-status.json index b31a6e2a1..d78df1b2d 100755 --- a/luci-app-openvpn-plus/root/usr/share/rpcd/acl.d/luci-app-openvpn-plus-status.json +++ b/luci-app-openvpn-plus/root/usr/share/rpcd/acl.d/luci-app-openvpn-plus-status.json @@ -14,7 +14,6 @@ "/usr/libexec/luci-app-openvpn-plus ovpnver": [ "exec" ], "/usr/libexec/luci-app-openvpn-plus getnetstat *": [ "exec" ], "/usr/libexec/luci-app-openvpn-plus readstatus *": [ "exec" ], - "/usr/libexec/luci-app-openvpn-plus readfile *": [ "exec" ], "/usr/libexec/luci-app-openvpn-plus statfile *": [ "exec" ] } } diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js index 41a4b8299..54641bc60 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js @@ -65,12 +65,11 @@ const CFG = Object.freeze({ }), LIBEXEC: Object.freeze({ luci_app_openvpn_plus: '/usr/libexec/luci-app-openvpn-plus', - readfile: 'readfile', - statfile: 'statfile', - readstatus: 'readstatus', getmac: 'getmac', - getnetstat: 'getnetstat', ovpnver: 'ovpnver', + getnetstat: 'getnetstat', + readstatus: 'readstatus', + statfile: 'statfile', }), CMD: Object.freeze({ openvpn: 'openvpn', @@ -171,11 +170,15 @@ const nativeFileExecRpc = L.rpc.declare({ /** * Wrapper for /usr/libexec/luci-app-openvpn-plus using the native ubus RPC directly */ -const luci_app_openvpn_plus = async function (args) { +const luci_app_openvpn_plus = async function (args, skipTrim) { const extraArgs = (typeof args === 'string') ? [args] : (Array.isArray(args) ? args : []); const res = await L.resolveDefault(nativeFileExecRpc(CFG.LIBEXEC.luci_app_openvpn_plus, extraArgs), {}); if (res && typeof res === 'object' && typeof res.stdout === 'string') { - return res.stdout.trim(); + if (skipTrim === true) { + return res.stdout.trim(); + } else { + return res.stdout.trim(); + } } return ''; }; @@ -202,11 +205,11 @@ const rpcGetServiceStatus = async function () { }; /** - * Asynchronously reads a file as a plain string via the secure backend helper + * Asynchronously read status file as a plain string via the secure backend helper */ -const secureReadFile = async function (filepath) { +const secureReadStatusFile = async function (filepath) { // PASS true AS SECOND PARAMETER TO KEEP ALL NEWLINES AND SPACES! - const res = await luci_app_openvpn_plus([CFG.LIBEXEC.readfile, filepath], true); + const res = await luci_app_openvpn_plus([CFG.LIBEXEC.readstatus, filepath], true); if (res && res.length > 0) { return res; } @@ -219,7 +222,7 @@ const secureReadFile = async function (filepath) { const secureStatFile = async function (filepath) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.statfile, filepath]); try { - if (res && res.trim().startsWith("{")) { + if (res && res.startsWith("{")) { const statObj = JSON.parse(res); if (statObj && typeof statObj === 'object') { return statObj; @@ -324,7 +327,7 @@ const getOpenVpnInfo = async function () { * Reads system uptime from the proc filesystem asynchronously */ const getSystemUptime = async function () { - const rawUptime = await secureReadFile(CFG.FILE.proc_uptime); + const rawUptime = await secureReadStatusFile(CFG.FILE.proc_uptime); // Split the output string by spaces to separate uptime and idle time const partsUptime = String(rawUptime || '0').trim().split(/\s+/); const uptime = (partsUptime && partsUptime[0]) ? parseFloat(partsUptime[0]) : 0; @@ -1149,7 +1152,7 @@ const getInstanceConfig = async function (id, instNum, role, path_conf) { // Proceed to live compilation on error } - let confContent = sanitizeInputText(await secureReadFile(path_conf)); + let confContent = sanitizeInputText(await secureReadStatusFile(path_conf)); if (confContent.length === 0) { // Signals readSingleInstanceStatus to execute early exit template return null; @@ -1314,7 +1317,7 @@ const getConnectedClientsStatus = async function (inst) { } // change detected - parse status file - const statusContent = await luci_app_openvpn_plus([CFG.LIBEXEC.readstatus, inst.id]); + const statusContent = await secureReadStatusFile(CFG.FILE.var_run_openvpn + inst.id + '.status'); if (statusContent.length > 0) { const freshClients = parseConnectedClients(statusContent, inst); diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js index 5d9ae2f43..9cfea7daf 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js @@ -397,11 +397,15 @@ const nativeFileExecRpc = L.rpc.declare({ /** * Wrapper for /usr/libexec/luci-app-openvpn-plus using the native ubus RPC directly */ -const luci_app_openvpn_plus = async function (args) { +const luci_app_openvpn_plus = async function (args, skipTrim) { const extraArgs = (typeof args === 'string') ? [args] : (Array.isArray(args) ? args : []); const res = await L.resolveDefault(nativeFileExecRpc(CFG.LIBEXEC.luci_app_openvpn_plus, extraArgs), {}); if (res && typeof res === 'object' && typeof res.stdout === 'string') { - return res.stdout.trim(); + if (skipTrim === true) { + return res.stdout.trim(); + } else { + return res.stdout.trim(); + } } return ''; }; @@ -447,7 +451,7 @@ const secureWriteFile = async function (filepath, content) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.writefile, filepath, textData]); if (res && res.includes("ERROR:")) { - console.error("Secure write failed: ", CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.writefile + ' ' + filepath + ' ' + textData + ' -> ' + res); + console.error("Secure write failed: ", CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.writefile + ' ' + filepath + ' -> ' + res); return -1; } return 0; @@ -471,7 +475,7 @@ const secureRemoveFile = async function (filepath) { const secureStatFile = async function (filepath) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.statfile, filepath]); try { - if (res && res.trim().startsWith("{")) { + if (res && res.startsWith("{")) { const statObj = JSON.parse(res); if (statObj && typeof statObj === 'object') { return statObj; diff --git a/luci-app-zerotier/Makefile b/luci-app-zerotier/Makefile index dbbd1d932..6523ec106 100644 --- a/luci-app-zerotier/Makefile +++ b/luci-app-zerotier/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-zerotier PKG_VERSION:=2.2 -PKG_RELEASE:=7 +PKG_RELEASE:=8 PKG_LICENSE:=Apache-2.0 LUCI_TITLE:=LuCI for ZeroTier diff --git a/luci-app-zerotier/README.md b/luci-app-zerotier/README.md index 25a71e4ec..71fadf4c5 100644 --- a/luci-app-zerotier/README.md +++ b/luci-app-zerotier/README.md @@ -212,6 +212,52 @@ stderr and continues. Two consequences are handled explicitly: ## Changelog +### v2.2-r46 + +**ztncui parity: clickable host label, member detail, IP management** + +- **The host label is now clickable** and shares one handler with the Manage + button -- click opens the detail panel (button reads *Hide*), click again + closes it (button reads *Manage*). One shared function, so the two paths + can never disagree about the panel's state. +- **Member IDs now look like Network IDs**: a plain monospace link on the page + background, no white code-chip. Clicking one opens a ztncui-style read-only + *member detail*: every field of the controller's member record in key order, + then the live peer object pretty-printed -- entirely from the cached model, + so it costs no RPC. +- **Clicking an IP opens a ztncui-style manager** instead of editing a + comma-separated string: member name and ZeroTier address on top, one row + per IP with its own Delete, one input row with Add, and the network's + managed routes read-only below. The ZeroTier API replaces `ipAssignments` + wholesale, so Delete commits current-minus-row and Add commits + current-plus-one -- a much better match for what the controller can + actually do than a free-form string. The old inline editor is gone. +- The harness caught a real scoping bug before it shipped: the Add input + lived inside the paint closure, so the Add handler could not see it and + would have thrown on first use. After the fix, verified in a real browser: + label toggle, detail fields, ID styling, delete/add/invalid-IP handling, + route display and repaint, plus the r43/r44 regression suites -- **25/25**. + +### v2.2-r45 + +**A packaging regression that only bites on fresh installs** + +- `zerotier-remote.sh` was committed `100644`, so every `git clone` based + build installed `/usr/bin/zerotier-remote.sh` **non-executable**. Every + scp deployment in this project chmod'd it explicitly afterwards, which is + why the bug stayed invisible until the first full firmware build+install -- + where rpcd's `[ ! -x ]` guard then reported the file as *missing*. The file + was never missing; it was present and unusable. It is now committed + `100755`. +- The rpcd guard now distinguishes the two cases: *"zerotier-remote.sh is + missing"* when the file is absent, *"zerotier-remote.sh is not executable + (installed 0644; it must be 0755)"* when it is present but unusable, so the + next occurrence is diagnosable in seconds rather than a search. +- Note the hotplug script at `root/etc/hotplug.d/net/99-luci-zerotier` is + deliberately left at `0644`: OpenWrt's `/sbin/hotplug-call` *sources* files + (`[ -f $script ] && . $script`), it never execs them, so its mode is + correct as-is. + ### v2.2-r44 **Layout: working content first** diff --git a/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/remote.js b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/remote.js index 913d43faa..efae2641d 100644 --- a/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/remote.js +++ b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/remote.js @@ -172,11 +172,15 @@ function diagnostics(section, host) { /* ---------------------------------------------------------------- networks */ -function networksPanel(section) { - var box = E('div', { 'style': 'margin-top:8px;' }); - var refresh = E('button', { 'class': 'cbi-button cbi-button-action' }, [ _('Refresh') ]); - var add = E('button', { 'class': 'cbi-button cbi-button-add' }, [ _('Create network') ]); - var table = E('div', {}); + function networksPanel(section) { + var box = E('div', { 'style': 'margin-top:8px;' }); + var refresh = E('button', { 'class': 'cbi-button cbi-button-action' }, [ _('Refresh') ]); + var add = E('button', { 'class': 'cbi-button cbi-button-add' }, [ _('Create network') ]); + var table = E('div', {}); + /* Latest network-list responses keyed by nwid: the IP manager needs the + * network's routes without another controller round-trip. Replaced on + * every load so it always matches what is on screen. */ + var netsById = {}; /* One row of the pool editor. The pool is {ipRangeStart, ipRangeEnd} and * the route needs a CIDR target, so start/end are validated separately. */ @@ -665,6 +669,114 @@ function networksPanel(section) { /* Inline edits post single-field bodies on purpose: the controller * merges presence-checked scalars, and the ztncui reference UI posts * single fields against this same controller in production use. */ + /* ztncui-style read-only member detail: every field of the controller's + * member record in key order, then the live peer object pretty-printed. + * The cached model already holds all of it -- no RPC is needed. */ + function openMemberDetail(m) { + var tbl = E('tbody', {}); + Object.keys(m).sort().forEach(function(k) { + var v = m[k]; + tbl.appendChild(E('tr', {}, [ + E('td', { 'style': 'padding-right:14px; font-family:monospace; white-space:nowrap; vertical-align:top;' }, [ k + ':' ]), + E('td', { 'style': 'font-family:monospace; word-break:break-all;' }, [ + E('code', {}, [ (v == null) ? '' : (typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)) ]) + ]) + ])); + }); + var peer = peerMap[m.id]; + if (peer) + tbl.appendChild(E('tr', {}, [ + E('td', { 'style': 'padding-right:14px; font-family:monospace; vertical-align:top;' }, [ 'peer:' ]), + E('td', { 'style': 'font-family:monospace; white-space:pre; word-break:break-all;' }, [ E('code', {}, [ JSON.stringify(peer, null, 2) ]) ]) + ])); + var body = E('div', {}, [ + E('h4', {}, [ (m.name || '-') + ' (' + m.id + ')' ]), + E('table', { 'class': 'table' }, [ tbl ]), + E('div', { 'style': 'margin-top:10px; text-align:right;' }, [ + E('button', { 'class': 'cbi-button', 'click': function() { ui.hideModal(); } }, [ _('Close') ]) + ]) + ]); + ui.showModal(_('Member detail'), body); + } + + /* ztncui-style IP management: list the member's IPs with per-row delete, + * plus one input row to append a new one, and the network's managed + * routes read-only below. The API replaces ipAssignments wholesale, so + * delete is current-minus-row and add is current-plus-one. */ + function openIpManager(m) { + var busy = false, inp; + function commit(arr) { + if (busy) return; + busy = true; + L.resolveDefault(rpcMemberSet(section, nwid, m.id, JSON.stringify({ ipAssignments: arr })), {}).then(function(r2) { + busy = false; + if (r2 && r2.code === 0) { + m.ipAssignments = arr; + if (rowRefs[m.id] && rowRefs[m.id].paintIp) rowRefs[m.id].paintIp(); + paint(); + } + else ui.addNotification(null, E('span', { 'class': 'alert-message warning' }, [ errText(r2, _('Update failed')) ]), 'warning'); + }); + } + var body = E('div', {}); + function paint() { + while (body.firstChild) body.removeChild(body.firstChild); + body.appendChild(E('div', { 'style': 'display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;' }, [ + E('h4', {}, [ _('Member name') + ': ', E('b', {}, [ m.name || '-' ]) ]), + E('h4', {}, [ _('ZeroTier address') + ': ', E('b', { 'style': 'font-family:monospace;' }, [ m.id ]) ]) + ])); + var rows = E('tbody', {}); + (m.ipAssignments || []).forEach(function(ip, i) { + rows.appendChild(E('tr', {}, [ + E('td', { 'style': 'width:70px; text-align:right;' }, [ + E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() { + commit((m.ipAssignments || []).filter(function(_, j) { return j !== i; })); + } }, [ _('Delete') ]) + ]), + E('td', { 'style': 'font-family:monospace;' }, [ ip ]) + ])); + }); + inp = E('input', { 'type': 'text', 'placeholder': _('IP address'), 'style': 'font-family:monospace;' }); + inp.addEventListener('keydown', function(ev) { if (ev.keyCode === 13) { ev.preventDefault(); add(); } }); + rows.appendChild(E('tr', {}, [ + E('td', { 'style': 'text-align:right;' }, [ + E('button', { 'class': 'cbi-button cbi-button-add', 'click': add }, [ _('Add') ]) + ]), + E('td', {}, [ inp ]) + ])); + body.appendChild(E('table', { 'class': 'table' }, [ + E('tr', {}, [ E('th', {}), E('th', {}, [ _('IP address') ]) ]), rows + ])); + var nw = netsById[nwid] || {}; + var routes = nw.routes || []; + body.appendChild(E('h4', { 'style': 'margin-top:14px;' }, [ _('Managed routes') ])); + var rt = E('tbody', {}); + routes.forEach(function(r) { + rt.appendChild(E('tr', {}, [ + E('td', { 'style': 'font-family:monospace;' }, [ r.target || '' ]), + E('td', { 'style': 'font-family:monospace;' }, [ r.via || '' ]) + ])); + }); + if (!routes.length) rt.appendChild(E('tr', {}, [ E('td', { 'colspan': '2', 'style': 'color:orange;' }, [ _('No route') ]) ])); + body.appendChild(E('table', { 'class': 'table' }, [ + E('tr', {}, [ E('th', {}, [ _('Target') ]), E('th', {}, [ _('Gateway') ]) ]), rt + ])); + body.appendChild(E('div', { 'style': 'margin-top:10px; text-align:right;' }, [ + E('button', { 'class': 'cbi-button', 'click': function() { ui.hideModal(); } }, [ _('Close') ]) + ])); + } + function add() { + var v = inp.value.trim(); + if (!isIp(v)) { + ui.addNotification(null, E('span', { 'class': 'alert-message warning' }, [ _('IP must be an IPv4 address') ]), 'warning'); + return; + } + commit((m.ipAssignments || []).concat(v)); + } + paint(); + ui.showModal(_('IP assignments') + ' — ' + (m.name || m.id), body); + } + function memberRow(m) { var mid = m.id; var rmBusy = false; @@ -752,7 +864,6 @@ function networksPanel(section) { } var ipTd = E('td', { 'style': 'width:13%; font-family:monospace; white-space:nowrap;' }, []); - var ipBusy = false, ipEditing = false; function drawIp() { while (ipTd.firstChild) ipTd.removeChild(ipTd.firstChild); var ips = m.ipAssignments || []; @@ -770,49 +881,7 @@ function networksPanel(section) { ]) ]); ipTd.appendChild(clickable); } - function startIpEdit() { - if (ipBusy) return; - ipEditing = true; - while (ipTd.firstChild) ipTd.removeChild(ipTd.firstChild); - var inp = E('input', { 'type': 'text', 'value': (m.ipAssignments || []).join(', '), 'style': 'width:100%; box-sizing:border-box; font-family:monospace;' }); - ipTd.appendChild(inp); - if (inp.focus) inp.focus(); - var afterKey = false; - function close() { ipEditing = false; drawIp(); } - function doCommit() { - var raw = inp.value.trim(), arr = [], i; - if (raw !== '') { - arr = raw.split(','); - for (i = 0; i < arr.length; i++) { - arr[i] = arr[i].trim(); - if (!isIp(arr[i])) { - ui.addNotification(null, E('span', { 'class': 'alert-message warning' }, [ _('Managed IPs must be IPv4 addresses, comma-separated') ]), 'warning'); - return; - } - } - } - if (arr.join(',') === (m.ipAssignments || []).join(',') || ipBusy) { close(); return; } - ipBusy = true; - inp.style.borderColor = 'orange'; - L.resolveDefault(rpcMemberSet(section, nwid, mid, JSON.stringify({ ipAssignments: arr })), {}).then(function(r2) { - ipBusy = false; - if (r2 && r2.code === 0) { m.ipAssignments = arr; close(); } - else { - flashBorder(inp, 'red'); - ui.addNotification(null, E('span', { 'class': 'alert-message warning' }, [ errText(r2, _('Update failed')) ]), 'warning'); - } - }); - } - inp.addEventListener('blur', function() { - if (afterKey) { afterKey = false; return; } - if (ipEditing) doCommit(); - }); - inp.addEventListener('keydown', function(ev) { - if (ev.keyCode === 13) { ev.preventDefault(); afterKey = true; doCommit(); } - else if (ev.keyCode === 27) { inp.value = (m.ipAssignments || []).join(', '); close(); } - }); - } - ipTd.addEventListener('click', function() { if (!ipEditing) startIpEdit(); }); + ipTd.addEventListener('click', function() { openIpManager(m); }); drawIp(); function statusContent() { @@ -837,8 +906,8 @@ function networksPanel(section) { var tr = E('tr', {}, [ E('td', { 'style': 'width:34px; text-align:center;' }, [ rm ]), E('td', { 'style': 'width:19%;' }, [ nameI ]), - E('td', { 'style': 'width:12%; font-family:monospace; white-space:nowrap;' }, [ - E('code', {}, [ E('a', { 'href': '#', 'click': function(ev) { ev.preventDefault(); } }, [ mid ]) ]) + E('td', { 'style': 'width:12%; white-space:nowrap;' }, [ + E('a', { 'href': '#', 'style': 'font-family:monospace;', 'title': _('Member detail'), 'click': function(ev) { ev.preventDefault(); openMemberDetail(m); } }, [ mid ]) ]), checkboxTd('authorized'), checkboxTd('activeBridge'), @@ -1030,7 +1099,8 @@ function networksPanel(section) { * forms stretch the nwid column (see netRow). So the header goes in * its own table and each network follows as a sibling block. */ table.appendChild(E('table', { 'class': 'table' }, [ head ])); - nets.forEach(function(nw) { table.appendChild(netRow(nw)); }); + netsById = {}; + nets.forEach(function(nw) { netsById[nw.nwid] = nw; table.appendChild(netRow(nw)); }); var skipped = res.skipped || []; if (skipped.length) { table.appendChild(E('div', { 'style': 'color:red; margin-top:6px;' }, [ @@ -1146,7 +1216,10 @@ return view.extend({ hosts.forEach(function(h) { var detail = E('div', { 'style': 'display:none; margin:8px 0 16px 0; padding-left:12px; border-left:3px solid #ccc;' }); var toggle = E('button', { 'class': 'cbi-button cbi-button-action' }, [ _('Manage') ]); - toggle.addEventListener('click', function() { + /* Shared by the Manage button and the host label: both paths + * open and close the same detail panel, so they can never + * disagree about the panel's state. */ + function toggleDetail() { /* A freshly created div has display '', not 'none', so the * open test has to be for 'block' -- testing "!== 'none'" * reads the initial state as already-open and the first @@ -1164,7 +1237,8 @@ return view.extend({ detail.appendChild(E('h4', {}, [_('Diagnostics')])); detail.appendChild(diagnostics(h.section, h)); } - }); + } + toggle.addEventListener('click', toggleDetail); var edit = E('button', { 'class': 'cbi-button' }, [ _('Edit') ]); edit.addEventListener('click', function() { hostEditor(h.section, h, reload); }); @@ -1178,8 +1252,10 @@ return view.extend({ listBox.appendChild(E('div', { 'class': 'cbi-section' }, [ E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;' }, [ - E('div', {}, [ - E('strong', {}, [ h.name ]), + E('div', {}, [ + E('a', { 'href': '#', 'style': 'color:inherit; cursor:pointer; text-decoration:none;', 'title': _('Manage'), 'click': function(ev) { ev.preventDefault(); toggleDetail(); } }, [ + E('strong', {}, [ h.name ]) + ]), E('div', { 'style': 'font-family:monospace; font-size:12px; color:#666;' }, [ h.user + '@' + h.host + ':' + h.port + _(' (controller port ') + h.controller_port + ')' ]), diff --git a/luci-app-zerotier/htdocs/luci-static/zerotier/tr-zh-cn.js b/luci-app-zerotier/htdocs/luci-static/zerotier/tr-zh-cn.js index 4d0dd1b99..cbc8a2d79 100644 --- a/luci-app-zerotier/htdocs/luci-static/zerotier/tr-zh-cn.js +++ b/luci-app-zerotier/htdocs/luci-static/zerotier/tr-zh-cn.js @@ -2,4 +2,4 @@ * Keys are sfh(trimws(msgid)) in hex, exactly what cbi.js _() looks up. * This firmware ships no window.TR bridge of its own. */ window.TR = window.TR || {}; -Object.assign(window.TR, {"7ba57fa5":"(控制器端口 ","005fb0ba":" —— 成员将不会从此地址池获得地址。","928f10ca":"节点地址必须为 10 位十六进制数字","4f25ffed":"地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。","623124cd":"ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。","038a5486":"操作","e733b525":"主动桥接","300fe5c8":"添加","3698d6ed":"添加远程主机","5598797e":"加入 Moon","d252deef":"添加地址池","e5b147b7":"添加远程主机","ea8fae46":"添加路由","71677946":"地址","a4a2e2e7":"高级设置","6ee64833":"允许默认路由","d24721ab":"允许 DNS","51aaeef3":"允许全局","e575f233":"允许管理","d6d5a047":"允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)","0b830d67":"正在分配地址…","2e4746c8":"授权","18bbde5a":"授权失败","ececebcc":"已授权","e3f664db":"自动允许客户端 NAT","2014c910":"自动 NAT: 已禁用","228abc80":"自动 NAT: 已启用","f2ffec55":"正在备份...","bdabf869":"备份失败","5eca6072":"立即备份","379bb677":"备份请求失败","f9fb6815":"基本设置","6b3b39f7":"广播","fcf89e62":"构建中…","ae5deb3a":"取消","08f23f6b":"无法连接到 ZeroTier 服务","4bd163d1":"无法读取 moon(ZeroTier 服务是否在运行?)","c06f3259":"正在收集数据...","8462484d":"正在获取地址...","cd2f6c29":"运行配置目录","8f48fe99":"配置","6e72b101":"已配置的主机","f1d119dc":"控制器端口","5d359cf2":"控制器可经隧道访问","41e439d8":"控制器状态存在","00a35b8a":"运行配置目录保护","bd7030be":"使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)","88720483":"无法构建方案","a1a69329":"无法获取成员列表","c53244dc":"无法连接控制器","2c150360":"无法读取此网络的任何成员","c1dfeb1a":"以下网络无法读取,因此未显示:","f2248b2d":"无法刷新成员列表;表格保持不变","cbb3b741":"无法保存主机","56370eb4":"创建并签署 Moon","e3a2eb47":"创建失败","eb9bb863":"创建网络","6c94b9b0":"点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)","9a74d36d":"删除","b3b872fe":"删除失败","51d36b3d":"删除网络 ","f89b7324":"退出失败","88643bc2":"退出请求失败","be021833":"设备","a0c974c7":"诊断","6469d71b":"诊断失败","00c6b479":"将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。","799c81a7":"下载 .moon","000e380f":"编辑","2f2c0439":"编辑远程主机","814db5a8":"留空 = 直连","78c2105b":"启用","17074f70":"端点","037e2ea1":"错误","be7c6b14":"加载 moon 列表出错","ef0771c3":"加载网络信息失败","eef2dd18":"加载节点信息失败","a285946d":"读取 moon 列表出错","b4b44266":"服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload","d0e77240":"按名称、地址或 IP 筛选","9865b44d":"起始 IP","a62c536c":"常规设置","764447db":"全局设置","8686381d":"隐藏","24553b14":"远程管理的工作原理","42933f28":"接口信息","d08681fe":"IP 地址","ad69d4ef":"IP 分配","fbb92dcf":"IP 地址池","7e11b65e":"标签","f2929f6f":"结束 IP","c6ce32d3":"延迟","22038f1d":"退出","4e72d9f1":"留空则保留已存储的密钥","2b1fb9a6":"正在退出 moon...","dee10879":"已退出 Moon","9c0e750d":"链路","10e6bd0b":"加载中...","c2b71057":"本地配置文件","b259d7d8":"管理","949429e8":"托管 IP 必须为 IPv4 地址,以逗号分隔","cdd88b7c":"成员 ID","cdc6d88f":"成员名称","16916c1d":"成员","9c03b236":"个成员","a0ddf226":"Moon","b7ca31c5":"Moon 已创建:","c2e07f85":"Moon 创建失败","bbf90d15":"Moon ID","1af43404":"Moon ID 必须是 10 位十六进制字符(例如 123456abcd)","119eb928":"Moon 种子节点","793bcbfa":"Moon 种子必须是该 moon 某个根节点的 10 位地址","2e8cf7ed":"本节点并未加入该 moon","9a9a1e82":"主机上的 moon.json","7ab90fdf":"Moon","ff21d2cf":"MTU","a59f3806":"多播限制","8d8bb35f":"我的控制器","d785d68e":"名称","1cbaf231":"网络 ID","fadba037":"网络 ID 必须是 16 位十六进制字符(0-9, a-f)","e4aff468":"网络","3eaa20dd":"从未连接","53386700":"否","20961bde":"没有可 Ping 的 IP","70454625":"尚无成员加入此网络。","0887813c":"没有匹配的成员。","867914a1":"未加入任何 moon","f03fe921":"未加入任何网络","12e23411":"无节点","70bf31af":"未分配地址池","a499507a":"无地址池——成员不会获得托管地址","d1c471f7":"尚未配置远程主机。添加一台以管理运行在其它服务器上的控制器或 Moon。","e4d0c0dd":"无路由","48e3577c":"无路由覆盖 ","e4de2fa9":"无 SSH 密钥——请先添加再使用","84dac100":"否——必需","0e04d997":"节点地址","812f65a7":"节点地址(10 位十六进制)","0eea406c":"未运行","5f370550":"在线","be00fa49":"操作系统","68e79a94":"加入失败","85909ec8":"orbit 已注册。moon 定义仍在从种子节点获取中,到达后会显示在下方。","eec483f0":"加入请求失败","fa096eb2":"已加入 Moon","00f89ff3":"加入 moon 会把它的根节点加入本节点的根集合,ZeroTier 仍会回落到 planet 根节点。注意:上游 ZeroTier 已不再推荐私有 moon,且不在其 SLA 支持范围内。","e59dac0c":"正在加入...","b5c083af":"免密 sudo","6f0da341":"路径","9ff29e1a":"路径必须在 /etc/zerotier 目录下","bebcdef0":"路径必须在 /etc/zerotier 或 /tmp 目录下","03dd188d":"local.conf 文件路径。保存并应用时会导入到配置目录中。","5a117475":"对端地址 / 延迟","e5a23291":"对端状态","4e8fbdaf":"对端状态不可用","f58eeafb":"节点","94b52f3a":"已持久化","9c464c70":"持久化运行配置目录(一般用于控制器模式),默认为空","7f880c6b":"全部 Ping","d1b70719":"正在 Ping...","bf02b752":"地址池边界必须为 IPv4 地址","ee2348ea":"端口","68e1b202":"端口必须是数字","09874159":"端口必须在 0 到 65535 之间","1ca10b3b":"ZeroTier 运行端口,默认是 9993","7a6cc3f3":"私有","871c4c35":"公网 IP 或主机名","6eb360d8":"刷新","2cd57071":"正在注册 orbit...","b4f975e0":"远程控制器","edf90922":"远程主机","aee376ff":"远程主机报告","7cb2d8c0":"移除 ","5b47d624":"移除成员 ","5ce9945f":"重命名","b8254a7b":"重命名失败","0e013fb5":"必填:粘贴密钥对的私钥,其公钥部分须已在远程主机的 ~/.ssh/authorized_keys 中","8e434268":"要求:固定公网 IP · SSH 访问 · 公钥已加入远程 authorized_keys 的密钥对 · 免密 sudo · 带控制器支持的 ZeroTier(1.14.2 或 1.16 nonfree 构建)","19d6a64c":"角色","ac1f50d1":"根节点数","b303c05b":"路由目标必须为 CIDR 格式,例如 192.168.192.0/24","fc818908":"路由网关必须为 IPv4 地址,留空表示直连路由","919b0d7c":"路由","be4d3b91":"运行诊断","eeebb78a":"运行中","a8163a81":"运行中…","1670392d":"运行时状态已备份到持久化存储","219966c7":"保存","a2cf19ac":"仍然保存?","026c3392":"将当前运行时状态保存到持久化配置目录。建议在用 zerotier orbit 添加 moon 后备份;对 controller 节点尤为关键(成员数据仅存于运行时目录)。","299c53c9":"正在自动保存到持久化目录。在此之前,重启守护进程会丢失该 orbit。若长期停留在此状态,请到“基本设置 → 高级设置”中点击“立即备份”。","7c166669":"正在保存...","7f4c69c1":"扫描正在进行中,请等待完成","fbdfccc8":"节点密钥","bec3de0d":"客户端密钥(identity.secret)。为安全起见显示为掩码;粘贴新密钥可替换,清空输入框则删除(下次启动时自动生成新身份)。","a4cd6dce":"服务重载失败: ","1bd9424a":"服务状态","37d6f6ee":"查看方案","01309000":"已签署的 .moon 文件","728a5d52":"按此列排序","77c4a3c4":"SSH 密钥已安装","e11083aa":"SSH 端口","606741ab":"SSH 私钥","36f54d64":"SSH 用户","663359c9":"状态","2423a413":"持久化","fb0519bc":"目标","604710e3":"moon 某个根节点的 10 位地址,ZeroTier 将从它获取 moon 定义文件。","804dd187":"控制器从这些地址段中分配地址,并按成员逐一报告。","92924e18":"以下命令将在远程主机上执行:","93da8c3e":"控制器 API 监听的端口,例如 27893。仅可经 SSH 隧道访问。","36ae8c3a":"远程主机将成为新 Moon 的唯一根节点。暂不支持额外根节点。","4ff27ae9":"此控制器尚无网络。","45ea9033":"这将在远程主机上创建 moon.json(含签署密钥)以及已签署的 .moon 文件。是否继续?","498bcdad":"未知错误","199c75c7":"个不可读","d194bfb7":"更新失败","ff1222e7":"更新于","9a073c6e":"版本","fad2775d":"网关","a7c490d1":"正在等待 moon 定义 — 该 join 尚未持久化","a9a26518":"是","8507f960":"ZeroTier","1c8bace2":"ZeroTier 已安装","4e4fe7e2":"Zerotier 是一个开源,跨平台,而且适合内网穿透互联的傻瓜配置虚拟 VPN LAN","8d018465":"ZeroTier 版本"}); +Object.assign(window.TR, {"7ba57fa5":"(控制器端口 ","005fb0ba":" —— 成员将不会从此地址池获得地址。","928f10ca":"节点地址必须为 10 位十六进制数字","4f25ffed":"地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。","623124cd":"ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。","038a5486":"操作","e733b525":"主动桥接","300fe5c8":"添加","3698d6ed":"添加远程主机","5598797e":"加入 Moon","d252deef":"添加地址池","e5b147b7":"添加远程主机","ea8fae46":"添加路由","71677946":"地址","a4a2e2e7":"高级设置","6ee64833":"允许默认路由","d24721ab":"允许 DNS","51aaeef3":"允许全局","e575f233":"允许管理","d6d5a047":"允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)","0b830d67":"正在分配地址…","2e4746c8":"授权","18bbde5a":"授权失败","ececebcc":"已授权","e3f664db":"自动允许客户端 NAT","2014c910":"自动 NAT: 已禁用","228abc80":"自动 NAT: 已启用","f2ffec55":"正在备份...","bdabf869":"备份失败","5eca6072":"立即备份","379bb677":"备份请求失败","f9fb6815":"基本设置","6b3b39f7":"广播","fcf89e62":"构建中…","ae5deb3a":"取消","08f23f6b":"无法连接到 ZeroTier 服务","4bd163d1":"无法读取 moon(ZeroTier 服务是否在运行?)","26d000c2":"关闭","c06f3259":"正在收集数据...","8462484d":"正在获取地址...","cd2f6c29":"运行配置目录","8f48fe99":"配置","6e72b101":"已配置的主机","f1d119dc":"控制器端口","5d359cf2":"控制器可经隧道访问","41e439d8":"控制器状态存在","00a35b8a":"运行配置目录保护","bd7030be":"使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)","88720483":"无法构建方案","a1a69329":"无法获取成员列表","c53244dc":"无法连接控制器","2c150360":"无法读取此网络的任何成员","c1dfeb1a":"以下网络无法读取,因此未显示:","f2248b2d":"无法刷新成员列表;表格保持不变","cbb3b741":"无法保存主机","56370eb4":"创建并签署 Moon","e3a2eb47":"创建失败","eb9bb863":"创建网络","6c94b9b0":"点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)","9a74d36d":"删除","b3b872fe":"删除失败","51d36b3d":"删除网络 ","f89b7324":"退出失败","88643bc2":"退出请求失败","be021833":"设备","a0c974c7":"诊断","6469d71b":"诊断失败","00c6b479":"将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。","799c81a7":"下载 .moon","000e380f":"编辑","2f2c0439":"编辑远程主机","814db5a8":"留空 = 直连","78c2105b":"启用","17074f70":"端点","037e2ea1":"错误","be7c6b14":"加载 moon 列表出错","ef0771c3":"加载网络信息失败","eef2dd18":"加载节点信息失败","a285946d":"读取 moon 列表出错","b4b44266":"服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload","d0e77240":"按名称、地址或 IP 筛选","9865b44d":"起始 IP","48150b7e":"网关","a62c536c":"常规设置","764447db":"全局设置","8686381d":"隐藏","24553b14":"远程管理的工作原理","42933f28":"接口信息","d08681fe":"IP 地址","da686b34":"IP 地址","ad69d4ef":"IP 分配","fbb92dcf":"IP 地址池","23603ee1":"IP 分配","cccbf9e4":"IP 必须为 IPv4 地址","7e11b65e":"标签","f2929f6f":"结束 IP","c6ce32d3":"延迟","22038f1d":"退出","4e72d9f1":"留空则保留已存储的密钥","2b1fb9a6":"正在退出 moon...","dee10879":"已退出 Moon","9c0e750d":"链路","10e6bd0b":"加载中...","c2b71057":"本地配置文件","b259d7d8":"管理","949429e8":"托管 IP 必须为 IPv4 地址,以逗号分隔","a31cd521":"托管路由","58695b7a":"成员详情","cdd88b7c":"成员 ID","cdc6d88f":"成员名称","16916c1d":"成员","9c03b236":"个成员","a0ddf226":"Moon","b7ca31c5":"Moon 已创建:","c2e07f85":"Moon 创建失败","bbf90d15":"Moon ID","1af43404":"Moon ID 必须是 10 位十六进制字符(例如 123456abcd)","119eb928":"Moon 种子节点","793bcbfa":"Moon 种子必须是该 moon 某个根节点的 10 位地址","2e8cf7ed":"本节点并未加入该 moon","9a9a1e82":"主机上的 moon.json","7ab90fdf":"Moon","ff21d2cf":"MTU","a59f3806":"多播限制","8d8bb35f":"我的控制器","d785d68e":"名称","1cbaf231":"网络 ID","fadba037":"网络 ID 必须是 16 位十六进制字符(0-9, a-f)","e4aff468":"网络","3eaa20dd":"从未连接","53386700":"否","20961bde":"没有可 Ping 的 IP","70454625":"尚无成员加入此网络。","0887813c":"没有匹配的成员。","867914a1":"未加入任何 moon","f03fe921":"未加入任何网络","12e23411":"无节点","70bf31af":"未分配地址池","a499507a":"无地址池——成员不会获得托管地址","d1c471f7":"尚未配置远程主机。添加一台以管理运行在其它服务器上的控制器或 Moon。","e4d0c0dd":"无路由","48e3577c":"无路由覆盖 ","e4de2fa9":"无 SSH 密钥——请先添加再使用","84dac100":"否——必需","0e04d997":"节点地址","812f65a7":"节点地址(10 位十六进制)","0eea406c":"未运行","5f370550":"在线","be00fa49":"操作系统","68e79a94":"加入失败","85909ec8":"orbit 已注册。moon 定义仍在从种子节点获取中,到达后会显示在下方。","eec483f0":"加入请求失败","fa096eb2":"已加入 Moon","00f89ff3":"加入 moon 会把它的根节点加入本节点的根集合,ZeroTier 仍会回落到 planet 根节点。注意:上游 ZeroTier 已不再推荐私有 moon,且不在其 SLA 支持范围内。","e59dac0c":"正在加入...","b5c083af":"免密 sudo","6f0da341":"路径","9ff29e1a":"路径必须在 /etc/zerotier 目录下","bebcdef0":"路径必须在 /etc/zerotier 或 /tmp 目录下","03dd188d":"local.conf 文件路径。保存并应用时会导入到配置目录中。","5a117475":"对端地址 / 延迟","e5a23291":"对端状态","4e8fbdaf":"对端状态不可用","f58eeafb":"节点","94b52f3a":"已持久化","9c464c70":"持久化运行配置目录(一般用于控制器模式),默认为空","7f880c6b":"全部 Ping","d1b70719":"正在 Ping...","bf02b752":"地址池边界必须为 IPv4 地址","ee2348ea":"端口","68e1b202":"端口必须是数字","09874159":"端口必须在 0 到 65535 之间","1ca10b3b":"ZeroTier 运行端口,默认是 9993","7a6cc3f3":"私有","871c4c35":"公网 IP 或主机名","6eb360d8":"刷新","2cd57071":"正在注册 orbit...","b4f975e0":"远程控制器","edf90922":"远程主机","aee376ff":"远程主机报告","7cb2d8c0":"移除 ","5b47d624":"移除成员 ","5ce9945f":"重命名","b8254a7b":"重命名失败","0e013fb5":"必填:粘贴密钥对的私钥,其公钥部分须已在远程主机的 ~/.ssh/authorized_keys 中","8e434268":"要求:固定公网 IP · SSH 访问 · 公钥已加入远程 authorized_keys 的密钥对 · 免密 sudo · 带控制器支持的 ZeroTier(1.14.2 或 1.16 nonfree 构建)","19d6a64c":"角色","ac1f50d1":"根节点数","b303c05b":"路由目标必须为 CIDR 格式,例如 192.168.192.0/24","fc818908":"路由网关必须为 IPv4 地址,留空表示直连路由","919b0d7c":"路由","be4d3b91":"运行诊断","eeebb78a":"运行中","a8163a81":"运行中…","1670392d":"运行时状态已备份到持久化存储","219966c7":"保存","a2cf19ac":"仍然保存?","026c3392":"将当前运行时状态保存到持久化配置目录。建议在用 zerotier orbit 添加 moon 后备份;对 controller 节点尤为关键(成员数据仅存于运行时目录)。","299c53c9":"正在自动保存到持久化目录。在此之前,重启守护进程会丢失该 orbit。若长期停留在此状态,请到“基本设置 → 高级设置”中点击“立即备份”。","7c166669":"正在保存...","7f4c69c1":"扫描正在进行中,请等待完成","fbdfccc8":"节点密钥","bec3de0d":"客户端密钥(identity.secret)。为安全起见显示为掩码;粘贴新密钥可替换,清空输入框则删除(下次启动时自动生成新身份)。","a4cd6dce":"服务重载失败: ","1bd9424a":"服务状态","37d6f6ee":"查看方案","01309000":"已签署的 .moon 文件","728a5d52":"按此列排序","77c4a3c4":"SSH 密钥已安装","e11083aa":"SSH 端口","606741ab":"SSH 私钥","36f54d64":"SSH 用户","663359c9":"状态","2423a413":"持久化","fb0519bc":"目标","604710e3":"moon 某个根节点的 10 位地址,ZeroTier 将从它获取 moon 定义文件。","804dd187":"控制器从这些地址段中分配地址,并按成员逐一报告。","92924e18":"以下命令将在远程主机上执行:","93da8c3e":"控制器 API 监听的端口,例如 27893。仅可经 SSH 隧道访问。","36ae8c3a":"远程主机将成为新 Moon 的唯一根节点。暂不支持额外根节点。","4ff27ae9":"此控制器尚无网络。","45ea9033":"这将在远程主机上创建 moon.json(含签署密钥)以及已签署的 .moon 文件。是否继续?","498bcdad":"未知错误","199c75c7":"个不可读","d194bfb7":"更新失败","ff1222e7":"更新于","9a073c6e":"版本","fad2775d":"网关","a7c490d1":"正在等待 moon 定义 — 该 join 尚未持久化","a9a26518":"是","8507f960":"ZeroTier","05882aa1":"ZeroTier 地址","1c8bace2":"ZeroTier 已安装","4e4fe7e2":"Zerotier 是一个开源,跨平台,而且适合内网穿透互联的傻瓜配置虚拟 VPN LAN","8d018465":"ZeroTier 版本"}); diff --git a/luci-app-zerotier/po/zh_Hans/zerotier.lmo b/luci-app-zerotier/po/zh_Hans/zerotier.lmo index 6c3ef35f9..fc68abe7c 100644 Binary files a/luci-app-zerotier/po/zh_Hans/zerotier.lmo and b/luci-app-zerotier/po/zh_Hans/zerotier.lmo differ diff --git a/luci-app-zerotier/po/zh_Hans/zerotier.po b/luci-app-zerotier/po/zh_Hans/zerotier.po index 02d2e0a28..a2224a12c 100644 --- a/luci-app-zerotier/po/zh_Hans/zerotier.po +++ b/luci-app-zerotier/po/zh_Hans/zerotier.po @@ -121,6 +121,9 @@ msgstr "无法连接到 ZeroTier 服务" msgid "Cannot read moons (is the ZeroTier service running?)" msgstr "无法读取 moon(ZeroTier 服务是否在运行?)" +msgid "Close" +msgstr "关闭" + msgid "Collecting data..." msgstr "正在收集数据..." @@ -253,6 +256,9 @@ msgstr "按名称、地址或 IP 筛选" msgid "First IP" msgstr "起始 IP" +msgid "Gateway" +msgstr "网关" + msgid "General options" msgstr "常规设置" @@ -271,12 +277,21 @@ msgstr "接口信息" msgid "IP Address" msgstr "IP 地址" +msgid "IP address" +msgstr "IP 地址" + msgid "IP assignment" msgstr "IP 分配" msgid "IP assignment pools" msgstr "IP 地址池" +msgid "IP assignments" +msgstr "IP 分配" + +msgid "IP must be an IPv4 address" +msgstr "IP 必须为 IPv4 地址" + msgid "Label" msgstr "标签" @@ -313,6 +328,12 @@ msgstr "管理" msgid "Managed IPs must be IPv4 addresses, comma-separated" msgstr "托管 IP 必须为 IPv4 地址,以逗号分隔" +msgid "Managed routes" +msgstr "托管路由" + +msgid "Member detail" +msgstr "成员详情" + msgid "Member ID" msgstr "成员 ID" @@ -685,6 +706,9 @@ msgstr "是" msgid "ZeroTier" msgstr "ZeroTier" +msgid "ZeroTier address" +msgstr "ZeroTier 地址" + msgid "ZeroTier installed" msgstr "ZeroTier 已安装" diff --git a/luci-app-zerotier/root/usr/libexec/rpcd/luci-zerotier b/luci-app-zerotier/root/usr/libexec/rpcd/luci-zerotier index 7a83d4d1e..16f5c47b5 100755 --- a/luci-app-zerotier/root/usr/libexec/rpcd/luci-zerotier +++ b/luci-app-zerotier/root/usr/libexec/rpcd/luci-zerotier @@ -850,13 +850,17 @@ case "$1" in remote_moon_apply) set -- moon-apply "$_sect" "$_cf" ;; esac - if [ ! -x /usr/bin/zerotier-remote.sh ]; then - json_init - json_add_int "code" 1 +if [ ! -x /usr/bin/zerotier-remote.sh ]; then + json_init + json_add_int "code" 1 + if [ -f /usr/bin/zerotier-remote.sh ]; then + json_add_string "error" "zerotier-remote.sh is not executable (installed 0644; it must be 0755)" + else json_add_string "error" "zerotier-remote.sh is missing" - json_dump - return fi + json_dump + return + fi # "$@" not $_args: the body is free-form JSON from the browser, # and a member or network name may contain spaces. Building a