mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-15 21:04:21 +08:00
Compare commits
85
Commits
cb845a1f2d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5f70450b | ||
|
|
17d91bad15 | ||
|
|
4f589e6a41 | ||
|
|
d03b99505a | ||
|
|
c224e11cc7 | ||
|
|
578dfde655 | ||
|
|
17c94aacbb | ||
|
|
700d0d7e02 | ||
|
|
fa60b9df94 | ||
|
|
cf471bd833 | ||
|
|
2acd07fa2f | ||
|
|
90bee3aa55 | ||
|
|
d34867614a | ||
|
|
25adce2488 | ||
|
|
aaa3fd83ad | ||
|
|
ac76f4cbc9 | ||
|
|
a9e0557b71 | ||
|
|
1aa5f26196 | ||
|
|
7280abda02 | ||
|
|
7b7c6e31cd | ||
|
|
f6683ec9ac | ||
|
|
921c6ff346 | ||
|
|
7872f8755a | ||
|
|
cfa884d090 | ||
|
|
0bc6317028 | ||
|
|
127e9ab526 | ||
|
|
55f4afaae1 | ||
|
|
40019bd445 | ||
|
|
0dbf9b779b | ||
|
|
5a1af7b30a | ||
|
|
e1c9ca3f42 | ||
|
|
5fc27ca614 | ||
|
|
593e4e0ff8 | ||
|
|
1ffa1631e1 | ||
|
|
ed6695f0b6 | ||
|
|
8a76697528 | ||
|
|
d6d7df32a5 | ||
|
|
26d174538f | ||
|
|
d9f7c5c117 | ||
|
|
7614ee5c63 | ||
|
|
70d35bab31 | ||
|
|
bdd9cea5d7 | ||
|
|
795028f9ea | ||
|
|
cdcf288975 | ||
|
|
83bcd4708f | ||
|
|
07891fa751 | ||
|
|
5f6047bcba | ||
|
|
f56e31d6d8 | ||
|
|
84e13cad49 | ||
|
|
b5ce6b03b2 | ||
|
|
0ee799dbde | ||
|
|
91e0df6c86 | ||
|
|
d9c423b124 | ||
|
|
bea159edee | ||
|
|
9dcc7b440b | ||
|
|
e4a587bfc3 | ||
|
|
705f63c409 | ||
|
|
e895da1d6b | ||
|
|
87517f518f | ||
|
|
ca30fdbbbb | ||
|
|
81d5757031 | ||
|
|
18bc066e07 | ||
|
|
7b0b16dcd4 | ||
|
|
0e9e9c2496 | ||
|
|
3a12ffbcad | ||
|
|
e45467bc23 | ||
|
|
99e36fe9b0 | ||
|
|
f8f5792e57 | ||
|
|
a179224c4d | ||
|
|
302b45db1b | ||
|
|
b6b067e0ad | ||
|
|
b0cf524450 | ||
|
|
f4103e8c2a | ||
|
|
2d6112f2cd | ||
|
|
3d2cd8c73a | ||
|
|
629b00127c | ||
|
|
51ae3e2992 | ||
|
|
8ff254bfa0 | ||
|
|
6e956beb69 | ||
|
|
c05bdbe2bc | ||
|
|
7cef4bfa84 | ||
|
|
6348b68a14 | ||
|
|
95ded25b90 | ||
|
|
4b545cbffa | ||
|
|
65e584daaa |
@@ -0,0 +1,29 @@
|
||||
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
|
||||
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
|
||||
@@ -996,7 +996,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geoip-cn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -1004,7 +1004,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geosite-cn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-geolocation-cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -1012,7 +1012,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geosite-noncn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-geolocation-!cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -240,7 +240,7 @@
|
||||
@@ -825,7 +825,7 @@ for k, v in pairs(nodes_table) do
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/footer", {shunt_list = api.jsonc.stringify(shunt_list)})
|
||||
m:appendTemplate("/global/footer")
|
||||
-
|
||||
+m:appendTemplate("/global/status_bottom")
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
@@ -383,7 +383,7 @@ for k, v in pairs(nodes_table) do
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/footer", {shunt_list = api.jsonc.stringify(shunt_list)})
|
||||
m:appendTemplate("/global/footer")
|
||||
-
|
||||
+m:appendTemplate("/global/status_bottom")
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
--- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js
|
||||
+++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js
|
||||
@@ -32,8 +32,7 @@ return baseclass.extend({
|
||||
swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {};
|
||||
@@ -36,14 +36,14 @@ return baseclass.extend({
|
||||
available = Math.min(mem.total ?? Infinity, Math.max(0, available + (mem.buffered || 0)));
|
||||
|
||||
var fields = [
|
||||
- _('Total Available'), (mem.available) ? mem.available : (mem.total && mem.free && mem.buffered) ? mem.free + mem.buffered : null, mem.total,
|
||||
- _('Used'), (mem.total && mem.free) ? (mem.total - mem.free) : null, mem.total,
|
||||
+ _('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total,
|
||||
- _('Total Available'), available, mem.total,
|
||||
+ _('Used'), (mem.total && mem.available) ? (mem.total - available): null, mem.total,
|
||||
];
|
||||
|
||||
if (mem.buffered)
|
||||
@@ -43,7 +42,7 @@ return baseclass.extend({
|
||||
if (mem.cached)
|
||||
fields.push(_('Cached'), mem.cached, mem.total);
|
||||
|
||||
if (swap.total > 0)
|
||||
@@ -33,37 +31,16 @@
|
||||
|
||||
--- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js
|
||||
+++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js
|
||||
@@ -33,9 +33,9 @@ var callCPUInfo = rpc.declare({
|
||||
method: 'getCPUInfo'
|
||||
});
|
||||
@@ -36,7 +36,7 @@ var callCPUInfo = rpc.declare({
|
||||
|
||||
-var callCPUUsage = rpc.declare({
|
||||
+var callTempInfo = rpc.declare({
|
||||
var callCPUUsage = rpc.declare({
|
||||
object: 'luci',
|
||||
- method: 'getCPUUsage'
|
||||
+ method: 'getTempInfo'
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
@@ -48,7 +48,7 @@ return baseclass.extend({
|
||||
L.resolveDefault(callSystemInfo(), {}),
|
||||
L.resolveDefault(callCPUBench(), {}),
|
||||
L.resolveDefault(callCPUInfo(), {}),
|
||||
- L.resolveDefault(callCPUUsage(), {}),
|
||||
+ L.resolveDefault(callTempInfo(), {}),
|
||||
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
|
||||
L.resolveDefault(callGetUnixtime(), 0)
|
||||
]);
|
||||
@@ -59,7 +59,7 @@ return baseclass.extend({
|
||||
systeminfo = data[1],
|
||||
cpubench = data[2],
|
||||
cpuinfo = data[3],
|
||||
- cpuusage = data[4],
|
||||
+ tempinfo = data[4],
|
||||
luciversion = data[5],
|
||||
unixtime = data[6];
|
||||
|
||||
@@ -78,15 +78,9 @@ return baseclass.extend({
|
||||
@@ -85,10 +85,9 @@ return baseclass.extend({
|
||||
|
||||
var fields = [
|
||||
_('Hostname'), boardinfo.hostname,
|
||||
@@ -72,16 +49,11 @@
|
||||
+ _('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
|
||||
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
|
||||
- _('Firmware Version'), (L.isObject(boardinfo.release)
|
||||
- ? '%s%s / '.format(
|
||||
- boardinfo.release.description || '',
|
||||
- boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
- )
|
||||
- : '') + (luciversion || ''),
|
||||
+ _('Firmware Version'), (L.isObject(boardinfo.release) ? boardinfo.release.description + ' / ' : '') + (luciversion || ''),
|
||||
_('Kernel Version'), boardinfo.kernel,
|
||||
_('Local Time'), datestr,
|
||||
_('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null,
|
||||
@@ -94,10 +88,18 @@ return baseclass.extend({
|
||||
? '%s%s / '.format(
|
||||
boardinfo.release.description || '',
|
||||
boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
@@ -101,10 +100,18 @@ return baseclass.extend({
|
||||
systeminfo.load[0] / 65535.0,
|
||||
systeminfo.load[1] / 65535.0,
|
||||
systeminfo.load[2] / 65535.0
|
||||
@@ -90,7 +62,7 @@
|
||||
+ ) : null
|
||||
];
|
||||
|
||||
+ if (tempinfo.tempinfo) {
|
||||
+ if (cpuusage.tempinfo) {
|
||||
+ fields.splice(6, 0, _('Temperature'));
|
||||
+ fields.splice(7, 0, tempinfo.tempinfo);
|
||||
+ }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@@ -33,6 +33,13 @@ LUCI_DEPENDS:= \
|
||||
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
|
||||
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
|
||||
+resolveip +curl +nping +unzip +xz-utils +xz \
|
||||
+resolveip +bind-dig +curl +nping +unzip +xz-utils +xz \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
|
||||
|
||||
@@ -123,14 +123,14 @@ jobs:
|
||||
(
|
||||
git_clone https://github.com/sirpdboy/luci-app-advancedplus
|
||||
git_clone https://github.com/sirpdboy/luci-app-taskplan taskplan && mvdir taskplan
|
||||
git_clone https://github.com/sirpdboy/luci-app-lucky oplucky && mvdir oplucky
|
||||
# git_clone https://github.com/sirpdboy/luci-app-lucky oplucky && mvdir oplucky
|
||||
git_clone https://github.com/sirpdboy/luci-app-partexp partexp && mvdir partexp
|
||||
git_clone https://github.com/sirpdboy/netspeedtest speedtest && mvdir speedtest
|
||||
git_clone https://github.com/sirpdboy/luci-app-parentcontrol
|
||||
git_clone https://github.com/sirpdboy/luci-app-poweroffdevice poweroffdevice && mvdir poweroffdevice
|
||||
git_clone https://github.com/sirpdboy/luci-app-watchdog watchdog1 && mvdir watchdog1
|
||||
git_clone https://github.com/sirpdboy/luci-app-cupsd cupsd1 && mv -n cupsd1/{luci-app-cupsd,cups} ./ ; rm -rf cupsd1
|
||||
git_clone https://github.com/sirpdboy/luci-app-timecontrol timecontrol && mvdir timecontrol
|
||||
# git_clone https://github.com/sirpdboy/luci-app-timecontrol timecontrol && mvdir timecontrol
|
||||
git_clone https://github.com/sirpdboy/luci-theme-kucat
|
||||
git_clone https://github.com/sirpdboy/luci-app-kucat-config
|
||||
git_clone https://github.com/sirpdboy/luci-app-chatgpt-web
|
||||
@@ -152,6 +152,7 @@ jobs:
|
||||
git_clone https://github.com/Tencent-Cloud-Plugins/tencentcloud-openwrt-plugin-cos && mv -n tencentcloud-openwrt-plugin-cos/tencentcloud_cos ./luci-app-tencentcloud-cos; rm -rf tencentcloud-openwrt-plugin-cos
|
||||
git_clone https://github.com/doushang/luci-app-shortcutmenu luci-shortcutmenu && mv -n luci-shortcutmenu/luci-app-shortcutmenu ./ ; rm -rf luci-shortcutmenu
|
||||
git_clone https://github.com/aa65535/openwrt-dist-luci
|
||||
git_clone https://github.com/minicom365/openwrt-smart-reboot && mv -f openwrt-smart-reboot/*smart-reboot ./;rm -rf openwrt-smart-reboot
|
||||
# git_clone https://github.com/morytyann/OpenWrt-msd && mvdir OpenWrt-msd
|
||||
# git_clone https://github.com/messense/aliyundrive-webdav aliyundrive && mv -n aliyundrive/openwrt/* ./ ; rm -rf aliyundrive
|
||||
# git_clone https://github.com/sbilly/netmaker-openwrt && mv -n netmaker-openwrt/netmaker ./; rm -rf netmaker-openwrt
|
||||
@@ -165,6 +166,7 @@ jobs:
|
||||
git_clone https://github.com/linkease/nas-packages-luci && mv -n nas-packages-luci/luci/* ./; rm -rf nas-packages-luci
|
||||
git_clone https://github.com/linkease/istore && mv -n istore/luci/* ./; rm -rf istore
|
||||
#git_clone https://github.com/linkease/lcdsimple lcdsimple1 && mvdir lcdsimple1
|
||||
git_clone https://github.com/0x5826/openwrt-packages-extras 0x5826; rm -rf 0x5826/MAINTENANCE.md; mvdir 0x5826
|
||||
) &
|
||||
(
|
||||
# git_clone https://github.com/frainzy1477/luci-app-clash
|
||||
@@ -176,6 +178,7 @@ jobs:
|
||||
git_clone https://github.com/nikkinikki-org/OpenWrt-momo && mv -f OpenWrt-momo/{luci-app-momo,momo} ./;rm -rf OpenWrt-momo
|
||||
git_clone https://github.com/fcshark-org/openwrt-fchomo && mvdir openwrt-fchomo
|
||||
git clone https://github.com/fw876/helloworld -b dev && mvdir helloworld
|
||||
git_clone https://github.com/umk0/openwrt-luci-haproxy-manager && mv -f openwrt-luci-haproxy-manager/luci-app-haproxy-manager ./;rm -rf openwrt-luci-haproxy-manager
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/ZeaKyX/speedtest-web
|
||||
@@ -208,6 +211,7 @@ jobs:
|
||||
git_clone https://github.com/xptsp/luci-app-squid-adv
|
||||
git_clone https://github.com/xptsp/openwrt-e2guardian
|
||||
git_clone https://github.com/10000ge10000/luci-app-openclaw
|
||||
git_clone https://github.com/WROIATE/luci-app-hijpass
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/honwen/luci-app-shadowsocks-rust
|
||||
@@ -217,7 +221,7 @@ jobs:
|
||||
git_clone https://github.com/4IceG/luci-app-easyconfig-transfer easyconfig && mvdir easyconfig
|
||||
git_clone https://github.com/4IceG/luci-app-atcommands atcommands && mvdir atcommands
|
||||
git_clone https://github.com/wukongdaily/luci-app-run run1 && mvdir run1
|
||||
git_clone https://github.com/itv3/homeproxy && mv -f homeproxy luci-app-homeproxy
|
||||
git_clone https://github.com/szwjp/luci-app-homeproxy
|
||||
git_clone https://github.com/1andrevich/homeproxy-hiddify
|
||||
git_clone https://github.com/weicaixian86/luci-app-pingpacket
|
||||
) &
|
||||
@@ -242,6 +246,8 @@ jobs:
|
||||
git_sparse_clone master "https://github.com/coolsnowwolf/lede" package/lean package/network/services/shellsync package/qca/shortcut-fe
|
||||
git_clone https://github.com/linkease/istore-packages
|
||||
git clone https://github.com/laipeng668/luci-app-gecoosac gecoosac1 && mvdir gecoosac1
|
||||
git_clone https://github.com/eamonxg/luci-theme-shadcn
|
||||
git_sparse_clone dashboard-redesign "https://github.com/eamonxg/luci" modules/luci-mod-dashboard
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/muink/luci-app-dnsproxy
|
||||
@@ -388,8 +394,8 @@ jobs:
|
||||
(
|
||||
git_sparse_clone develop "https://github.com/Ysurac/openmptcprouter-feeds" \
|
||||
dsvpn glorytun-udp glorytun grpcurl ipcalc luci-app-dsvpn luci-app-glorytun-tcp luci-app-glorytun-udp luci-app-mail luci-app-mlvpn luci-app-mptcp luci-app-nginx-ha luci-app-sqm-autorate luci-app-packet-capture luci-app-iperf luci-theme-openmptcprouter sqm-autorate speedtestc mlvpn mptcp tcptraceroute tracebox tsping atinout z8102
|
||||
git_sparse_clone chawrt/24.10 "https://github.com/liudf0716/luci" applications/luci-app-yt-dlp applications/luci-app-apfree-wifidog applications/luci-app-ss-redir
|
||||
git_sparse_clone chawrt/24.10 "https://github.com/liudf0716/packages" net/ss-redir
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/luci" applications/luci-app-xkcptun applications/luci-app-apfree-wifidog
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/packages" net/xkcptun net/apfree-wifidog
|
||||
) &
|
||||
git_sparse_clone master "https://github.com/immortalwrt/packages" net/n2n net/qbittorrent \
|
||||
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
|
||||
@@ -465,7 +471,7 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.TOKEN_KIDDIN9 }}" \
|
||||
-X POST -d '{ "query": "query {repository(owner: \"'"$1"'\", name: \"'"$2"'\"){latestRelease{tagName tagCommit{oid}}refs(refPrefix:\"refs/tags/\",last:1,orderBy:{field:TAG_COMMIT_DATE,direction:ASC}){edges{node{name target{oid}}}}defaultBranchRef{target{...on Commit {oid}}}}}"}' https://api.github.com/graphql)
|
||||
}
|
||||
for pkg in $(ls !(luci-*|nikki|mihomo*|openwrt-einat-ebpf|qBittorrent-Enhanced-Edition|tuic-client|glorytun|dae|daed|shadowsocks-libev|mosdns|spotifyd|miniupnpd|openwrt-Toolkit|libdouble-conversion|wxbase|3proxy|ucode|mergerfs|openwrt-nezhav1|homebox|sub-web|tcptraceroute|frp|openwrt-caddy|mentohust|brlaser|rapidjson|smartdns|hysteria|gecoosac|libcryptopp|naiveproxy|rustdesk-server|shadowsocksr-libev|tuic-server|joker|ps3netsrv|natter|netmaker|openwrt-ympd|subconverter|sms-tool)/Makefile); do
|
||||
for pkg in $(ls !(luci-*|nikki|mihomo*|dockermanager|sing-box|kaiplus|fastnet|torrserver|baidudrive|openwrt-einat-ebpf|qBittorrent-Enhanced-Edition|tuic-client|glorytun|dae|daed|shadowsocks-libev|mosdns|spotifyd|miniupnpd|openwrt-Toolkit|libdouble-conversion|wxbase|3proxy|ucode|mergerfs|openwrt-nezhav1|homebox|sub-web|tcptraceroute|frp|openwrt-caddy|mentohust|brlaser|rapidjson|smartdns|hysteria|gecoosac|libcryptopp|naiveproxy|rustdesk-server|shadowsocksr-libev|tuic-server|joker|ps3netsrv|natter|netmaker|openwrt-ympd|subconverter|sms-tool)/Makefile); do
|
||||
repo="$(grep ^PKG_SOURCE_URL $pkg | grep github | cut -f 4-5 -d '/' | sed -e 's/.git//' | grep -E '[0-9a-zA-Z_-]+$')" || true
|
||||
if [ "$repo" ]; then
|
||||
owner="$(echo $repo | cut -f 1 -d '/')"
|
||||
@@ -534,6 +540,7 @@ jobs:
|
||||
sed -i 's/body.getAttribute("theme");if(o)switch(o){case"dark":case"light":return o}/documentElement.getAttribute("data-darkmode");if(o)switch(o){case"true":return"dark";case"false":return"light"}/g' luci-app-quickstart/htdocs/luci-static/quickstart/index.js
|
||||
sed -i 's/icon!=="baby"/icon!==""/' luci-app-quickstart/htdocs/luci-static/quickstart/index.js
|
||||
sed -i 's/+php8-cgi /+php8-cgi +php8-fpm /' luci-theme-spectra/Makefile
|
||||
sed -i 's/+luci-base/+luci-base +luci-app-oui/' luci-mod-status/Makefile
|
||||
|
||||
while [[ ! -f /tmp/ok || ! -f /tmp/ok2 || ! -f /tmp/ok3 ]]; do
|
||||
echo "wait"
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ShadowVPN
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/aa65535/openwrt-shadowvpn/releases/download/v$(PKG_VERSION)
|
||||
|
||||
+4
-4
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
|
||||
AGENTFLOW_ARCH_aarch64:=arm64
|
||||
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
|
||||
|
||||
AGENTFLOW_HASH_x86_64:=996ccb6580043a187e83a3558c6bb2f0ff503bafbe46b7c995cff7f77629c088
|
||||
AGENTFLOW_HASH_aarch64:=6d733ebc49b095bf8670a1b655a13bb8ad98c90ced19701b67d5cdd4fe9a3db4
|
||||
AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc
|
||||
AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231
|
||||
|
||||
PKG_NAME:=agentflow
|
||||
PKG_VERSION:=0.3.0
|
||||
PKG_RELEASE:=7
|
||||
PKG_VERSION:=0.3.3
|
||||
PKG_RELEASE:=11
|
||||
|
||||
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
|
||||
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# AgentFlow 共享 Runtime HOME 实施里程碑
|
||||
|
||||
## 目标架构
|
||||
|
||||
AgentFlow 的应用数据和设备级开发运行时分离:
|
||||
|
||||
- AgentFlow 数据目录:`<quickstart.conf_dir>/AgentFlow`
|
||||
- 共享 Runtime HOME:`<quickstart.conf_dir>/Runtime/home`
|
||||
|
||||
共享 Runtime HOME 由 `mise` 包提供公共 helper 初始化,AgentFlow 和后续运行时感知应用复用同一套 `HOME`、XDG、`MISE_*` 和 `PATH`。
|
||||
|
||||
## Milestone 1:公共 Runtime HOME 合约
|
||||
|
||||
目标:
|
||||
|
||||
- `mise` 包安装 `/etc/config/mise`。
|
||||
- `mise` 包安装 `/lib/functions/istore_runtime.sh`。
|
||||
- helper 自动从 `quickstart.main.conf_dir` 推导 `<conf_dir>/Runtime/home`。
|
||||
- helper 支持 `ISTORE_RUNTIME_CONF_DIR` 兜底,便于应用从自身 Configs 目录派生 Runtime。
|
||||
- helper 统一导出 `HOME`、`XDG_DATA_HOME`、`XDG_CACHE_HOME`、`XDG_CONFIG_HOME`、`XDG_STATE_HOME`、`MISE_DATA_DIR`、`MISE_CACHE_DIR`、`MISE_CONFIG_DIR`、`MISE_STATE_DIR` 和 `PATH`。
|
||||
|
||||
验收:
|
||||
|
||||
- 新安装 `mise` 后,存在 `/etc/config/mise` 和 `/lib/functions/istore_runtime.sh`。
|
||||
- 已配置 quickstart 时,初始化路径为 `<quickstart.conf_dir>/Runtime/home`。
|
||||
- 未配置 quickstart 但应用提供 `ISTORE_RUNTIME_CONF_DIR` 时,初始化路径为 `$ISTORE_RUNTIME_CONF_DIR/Runtime/home`。
|
||||
|
||||
## Milestone 2:AgentFlow 接入共享 Runtime HOME
|
||||
|
||||
目标:
|
||||
|
||||
- AgentFlow 不再使用 `$data_dir/global` 作为 `HOME` 或 mise shim 根。
|
||||
- AgentFlow 启动时调用 `istore_runtime_export_env`。
|
||||
- `AGENT_FLOW_DATA` 继续固定在 AgentFlow 数据目录下的 `data` 子目录。
|
||||
- AgentFlow 服务进程及其子进程继承共享 Runtime HOME。
|
||||
|
||||
验收:
|
||||
|
||||
- `/etc/init.d/agentflow start` 后,AgentFlow 进程环境中 `HOME=<conf_dir>/Runtime/home`。
|
||||
- `MISE_DATA_DIR=$HOME/.local/share/mise`。
|
||||
- `PATH` 以 `$MISE_DATA_DIR/shims:$HOME/.local/bin` 开头。
|
||||
- AgentFlow 数据库仍写入 `<conf_dir>/AgentFlow/data/db/db.sqlite`。
|
||||
|
||||
## Milestone 3:LuCI 可见性
|
||||
|
||||
目标:
|
||||
|
||||
- AgentFlow LuCI 页面继续配置 AgentFlow 数据目录。
|
||||
- 页面展示推导出的共享 Runtime HOME。
|
||||
- 如果 `mise.runtime_dir` 被显式设置,页面展示其对应的 `home`。
|
||||
|
||||
验收:
|
||||
|
||||
- quickstart `conf_dir=/mnt/vio3-1/Configs` 时,页面显示 `/mnt/vio3-1/Configs/Runtime/home`。
|
||||
- AgentFlow 数据目录仍显示 `/mnt/vio3-1/Configs/AgentFlow`。
|
||||
|
||||
## Milestone 4:迁移与兼容
|
||||
|
||||
目标:
|
||||
|
||||
- 新安装默认使用共享 Runtime HOME。
|
||||
- 老安装中已有 `$data_dir/global/.local/share/mise` 时,不自动移动大目录。
|
||||
- 后续提供显式迁移命令或 LuCI 操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 升级不会删除或搬移旧 runtime 数据。
|
||||
- 用户确认迁移后,旧 mise data 可迁移到 `<conf_dir>/Runtime/home/.local/share/mise`。
|
||||
- 迁移失败不会影响 AgentFlow 私有数据。
|
||||
|
||||
## Milestone 5:AgentFlow 后端项目环境集成
|
||||
|
||||
目标:
|
||||
|
||||
- OpenWrt 层面完成共享 HOME 后,再在 AgentFlow 后端引入项目环境 wrapper。
|
||||
- coding agent 进程使用 `mise exec -- <agent> ...` 应用项目 `mise.toml`。
|
||||
- 运行期设置 `MISE_EXEC_AUTO_INSTALL=0`,缺失工具只在准备阶段安装。
|
||||
|
||||
验收:
|
||||
|
||||
- 单 repo 项目中,agent 看到项目声明的 Node/Python/Go 版本。
|
||||
- 没有隐式运行期下载。
|
||||
- Codex/Claude/Kimi 等 agent 凭据继续来自共享 Runtime HOME。
|
||||
- 取消任务时 wrapper 和 agent 子进程都能退出。
|
||||
@@ -10,22 +10,6 @@ get_config() {
|
||||
config_get port "$1" port "9000"
|
||||
}
|
||||
|
||||
load_runtime_env() {
|
||||
local conf_dir
|
||||
|
||||
[ -n "$data_dir" ] || return 1
|
||||
conf_dir="$(dirname "$data_dir")"
|
||||
export ISTORE_RUNTIME_CONF_DIR="$conf_dir"
|
||||
|
||||
if [ -r /lib/functions/istore_runtime.sh ]; then
|
||||
. /lib/functions/istore_runtime.sh
|
||||
istore_runtime_export_env
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load agentflow
|
||||
config_foreach get_config agentflow
|
||||
@@ -35,11 +19,16 @@ start_service() {
|
||||
logger -t agentflow "missing executable: /usr/sbin/agentflow"
|
||||
return 1
|
||||
}
|
||||
mkdir -p "$data_dir" "$data_dir/data" || return 1
|
||||
if ! load_runtime_env; then
|
||||
logger -t agentflow "shared runtime home is not configured"
|
||||
[ -r /lib/functions/mise.sh ] || {
|
||||
logger -t agentflow "missing mise environment helper"
|
||||
return 1
|
||||
}
|
||||
. /lib/functions/mise.sh
|
||||
if ! istore_runtime_env; then
|
||||
logger -t agentflow "failed to initialize mise environment"
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$data_dir" "$data_dir/data" || return 1
|
||||
|
||||
logger -t agentflow "starting AgentFlow on $host:$port"
|
||||
procd_open_instance
|
||||
@@ -49,14 +38,6 @@ start_service() {
|
||||
"AGENT_FLOW_HOST=$host" \
|
||||
"AGENT_FLOW_PORT=$port" \
|
||||
"HOME=$HOME" \
|
||||
"XDG_DATA_HOME=$XDG_DATA_HOME" \
|
||||
"XDG_CACHE_HOME=$XDG_CACHE_HOME" \
|
||||
"XDG_CONFIG_HOME=$XDG_CONFIG_HOME" \
|
||||
"XDG_STATE_HOME=$XDG_STATE_HOME" \
|
||||
"MISE_DATA_DIR=$MISE_DATA_DIR" \
|
||||
"MISE_CACHE_DIR=$MISE_CACHE_DIR" \
|
||||
"MISE_CONFIG_DIR=$MISE_CONFIG_DIR" \
|
||||
"MISE_STATE_DIR=$MISE_STATE_DIR" \
|
||||
"PATH=$PATH"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
|
||||
|
||||
def read(relative):
|
||||
relative_path = Path(relative)
|
||||
package = relative_path.parts[0]
|
||||
app = package[9:] if package.startswith("luci-app-") else package
|
||||
remainder = Path(*relative_path.parts[1:])
|
||||
|
||||
candidates = [
|
||||
THIS_FILE.parents[index] / relative_path
|
||||
for index in range(2, min(len(THIS_FILE.parents), 6))
|
||||
]
|
||||
candidates.append(THIS_FILE.parents[3] / app / package / remainder)
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate.read_text()
|
||||
|
||||
searched = "\n".join(str(candidate) for candidate in candidates)
|
||||
raise FileNotFoundError(f"{relative} not found; searched:\n{searched}")
|
||||
|
||||
|
||||
class MiseRuntimeContractTest(unittest.TestCase):
|
||||
def test_mise_package_installs_shared_runtime_contract(self):
|
||||
makefile = read("mise/Makefile")
|
||||
config = read("mise/files/mise.config")
|
||||
defaults = read("mise/files/mise.uci-default")
|
||||
helper = read("mise/files/istore_runtime.sh")
|
||||
|
||||
self.assertIn("/etc/config/mise", makefile)
|
||||
self.assertIn("/lib/functions/istore_runtime.sh", makefile)
|
||||
self.assertIn("$(INSTALL_BIN) ./files/mise.uci-default", makefile)
|
||||
self.assertIn("[ -f /etc/uci-defaults/mise ]", makefile)
|
||||
|
||||
self.assertIn("config mise 'main'", config)
|
||||
self.assertIn("option runtime_dir ''", config)
|
||||
self.assertIn("option auto_discover '1'", config)
|
||||
|
||||
self.assertIn(". /lib/functions/istore_runtime.sh", defaults)
|
||||
self.assertIn("istore_runtime_init", defaults)
|
||||
|
||||
self.assertIn("config_get conf_dir main conf_dir", helper)
|
||||
self.assertIn('"$conf_dir" "$ISTORE_RUNTIME_DEFAULT_SUBDIR"', helper)
|
||||
self.assertIn("ISTORE_RUNTIME_HOME_SUBDIR=\"home\"", helper)
|
||||
self.assertIn("ISTORE_RUNTIME_CONF_DIR", helper)
|
||||
self.assertIn('export HOME="$runtime_home"', helper)
|
||||
self.assertIn('export XDG_DATA_HOME="$HOME/.local/share"', helper)
|
||||
self.assertIn('export MISE_DATA_DIR="$XDG_DATA_HOME/mise"', helper)
|
||||
self.assertIn('export PATH="$MISE_DATA_DIR/shims:$HOME/.local/bin:$PATH"', helper)
|
||||
|
||||
def test_agentflow_consumes_shared_runtime_home(self):
|
||||
init = read("agentflow/files/agentflow.init")
|
||||
cbi = read("luci-app-agentflow/luasrc/model/cbi/agentflow.lua")
|
||||
model = read("luci-app-agentflow/luasrc/model/agentflow.lua")
|
||||
translations = read("luci-app-agentflow/po/zh-cn/agentflow.po")
|
||||
|
||||
self.assertIn(". /lib/functions/istore_runtime.sh", init)
|
||||
self.assertIn("istore_runtime_export_env", init)
|
||||
self.assertIn('AGENT_FLOW_DATA=$data_dir/data', init)
|
||||
self.assertIn('HOME=$HOME', init)
|
||||
self.assertIn('MISE_DATA_DIR=$MISE_DATA_DIR', init)
|
||||
self.assertNotIn("$data_dir/global", init)
|
||||
self.assertNotIn(".local/share/mise/shims:$PATH", init)
|
||||
|
||||
self.assertIn('translate("Shared runtime home")', cbi)
|
||||
self.assertIn("agentflow_model.runtime_home", cbi)
|
||||
self.assertIn('return runtime_dir .. "/home"', model)
|
||||
self.assertIn('return conf_dir .. "/Runtime"', model)
|
||||
|
||||
self.assertIn('msgid "Shared runtime home"', translations)
|
||||
self.assertIn('msgstr "共享运行时 HOME"', translations)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=airconnect
|
||||
PKG_VERSION:=1.11.2
|
||||
PKG_VERSION:=1.11.3
|
||||
PKG_RELEASE=1
|
||||
|
||||
PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (C) 2018 Dengfeng Liu
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=apfree-wifidog
|
||||
PKG_VERSION:=9.09.2938
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_LICENSE_FILES:=COPYING
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
define Package/apfree-wifidog
|
||||
SUBMENU:=Captive Portals
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=+zlib +libjson-c +libevent2 +libevent2-openssl +libuci \
|
||||
+openssl-util +libnetfilter-queue +conntrack +libmosquitto \
|
||||
+libnftnl +libmnl +libbpf +aw-bpf
|
||||
TITLE:=Apfree's wireless captive portal solution
|
||||
URL:=https://github.com/liudf0716/apfree_wifidog
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/description
|
||||
The ApFree Wifidog project is a complete and embeddable captive portal
|
||||
solution for wireless community groups or individuals who wish to open a free
|
||||
Hotspot while still preventing abuse of their Internet connection.
|
||||
It's enhanced wifidog
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/conffiles
|
||||
/etc/config/wifidogx
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/wifidogx $(1)/usr/bin/wifidogx
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/wdctlx $(1)/usr/bin/wdctlx
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-msg.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html.front $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html.rear $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/authserver-offline.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/internet-offline.html $(1)/etc/
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/wifidogx.init $(1)/etc/init.d/wifidogx
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(CP) ./files/wifidogx.conf $(1)/etc/config/wifidogx
|
||||
$(INSTALL_DIR) $(1)/etc/wifidogx
|
||||
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/wifi-config $(1)/www/cgi-bin/
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/wifi-diag $(1)/www/cgi-bin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,apfree-wifidog))
|
||||
@@ -0,0 +1,125 @@
|
||||

|
||||
|
||||
|
||||
[![license][1]][2]
|
||||
[![PRs Welcome][3]][4]
|
||||
[![Issues Welcome][5]][6]
|
||||
[![Release Version][7]][8]
|
||||
[![OpenWrt][11]][12]
|
||||
[![Join the QQ Group][15]][16]
|
||||
|
||||
|
||||
[1]: https://img.shields.io/badge/license-GPLV3-brightgreen.svg?style=plastic
|
||||
[2]: https://github.com/liudf0716/apfree_wifidog/blob/master/COPYING
|
||||
[3]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=plastic
|
||||
[4]: https://github.com/liudf0716/apfree_wifidog/pulls
|
||||
[5]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=plastic
|
||||
[6]: https://github.com/liudf0716/apfree_wifidog/issues/new
|
||||
[7]: https://img.shields.io/badge/release-3.11.1716-red.svg?style=plastic
|
||||
[8]: https://github.com/liudf0716/apfree_wifidog/releases
|
||||
[11]: https://img.shields.io/badge/Platform-%20OpenWrt%7C%20LEDE%20-brightgreen.svg?style=plastic
|
||||
[12]: https://github.com/KunTengRom/kunteng-lede-17.01.4
|
||||
[13]: https://img.shields.io/badge/KunTeng-Inside-blue.svg?style=plastic
|
||||
[14]: https://www.kunteng.org.cn
|
||||
[15]: https://img.shields.io/badge/chat-qq%20group-brightgreen.svg
|
||||
[16]: https://jq.qq.com/?_wv=1027&k=4ADDSev
|
||||
|
||||
## ApFree WiFiDog: A high performance captive portal solution for HTTP(s)
|
||||
|
||||
ApFree WiFiDog is a high performance captive portal solution for HTTP(s), which mainly used in ([LEDE](https://github.com/lede-project/source) & [OpenWrt](https://github.com/openwrt/openwrt)) platform.
|
||||
|
||||
|
||||
**[中文介绍](https://github.com/liudf0716/apfree_wifidog/blob/master/README_ZH.md)**
|
||||
|
||||
## Enhancement of apfree-wifidog
|
||||
|
||||
In fact, the title should be why we choose apfree-wifidog, the reason was the following:
|
||||
|
||||
> Stable
|
||||
|
||||
apfree-wifidog was widely used in tens of thousands device, which were running in business scene. In order to improve its stable, we rewrite all iptables rule by api instead of fork call, which will easily cause deadlock in multithread-fork running environment. we also re-write the code and replace libhttpd (which is unmaintained for years) with libevent
|
||||
|
||||
> Performance
|
||||
|
||||
apfree-wifidog's http request-response is more quick, u can find statistic data in our test document
|
||||
|
||||
> HTTPs redirect
|
||||
|
||||
apfree-wifidog support https redirect, in current internet environment, captive portal solution without supporting https redirect will become unsuitable gradually
|
||||
|
||||
|
||||
> More features
|
||||
|
||||
apfree-wifidog support mac temporary-pass, ip,domain,pan-domain,white-mac,black-mac rule and etc. all these rules can be applied without restarting wifidog
|
||||
|
||||
> MQTT support
|
||||
|
||||
by enable mqtt support, u can remotely deliver such as trusted ip, domain and pan-domain rules to apfree wifidog
|
||||
|
||||
> Compilable with wifidog protocol
|
||||
|
||||
u don't need to modify your wifidog authentication server to adapt apfree-wifidog; if u have pression on server-side, apfree wifidog's improved protocol can greatly relieve it, which disabled by default
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
before starting apfree-wifidog, we must know how to configure it. apfree-wifidog use OpenWrt standard uci config system, all your apfree-wifidog configure information stored in `/etc/confg/wifidogx`, which will be parsed by `/etc/init.d/wifidogx` to /tmp/wifidog.conf, apfree-wifidog's real configure file is `/tmp/wifidog.conf`
|
||||
|
||||
The default apfree-wifidog UCI configuration file like this:
|
||||
|
||||
```
|
||||
config wifidog
|
||||
option gateway_interface 'br-lan'
|
||||
option auth_server_hostname 'wifidog.kunteng.org.cn'
|
||||
option auth_server_port 443
|
||||
option auth_server_path '/wifidog/'
|
||||
option check_interval 60
|
||||
option client_timeout 5
|
||||
option apple_cna 1
|
||||
option thread_number 5
|
||||
option wired_passed 0
|
||||
option enable 0
|
||||
```
|
||||
|
||||
> auth_server_hostname was apfree-wifidog auth server, it can be domain or ip; wifidog.kunteng.org.cn is a free auth server we provided, it was also [open source](https://github.com/wificoin-project/wwas)
|
||||
|
||||
> apple_cna 1 apple captive detect deceive; 2 apple captive detect deceive to disallow portal page appear
|
||||
|
||||
> wired_passed means whether LAN access devices need to auth or not, value 1 means no need to auth
|
||||
|
||||
> enable means whether start apfree-wifidog when we executed `/etc/init.d/wifidogx start`, if u wanted to start apfree-wifidog, you must set enable to 1 before executing `/etc/init.d/wifidogx start`
|
||||
|
||||
### How to support https redirect
|
||||
|
||||
In order to support https redirect, apfree-wifidog need x509 pem cert and private key, u can generate yourself like this:
|
||||
|
||||
```
|
||||
PX5G_BIN="/usr/sbin/px5g"
|
||||
OPENSSL_BIN="/usr/bin/openssl"
|
||||
APFREE_CERT="/etc/apfree.crt"
|
||||
APFREE_KEY="/etc/apfree.key"
|
||||
|
||||
generate_keys() {
|
||||
local days bits country state location commonname
|
||||
|
||||
# Prefer px5g for certificate generation (existence evaluated last)
|
||||
local GENKEY_CMD=""
|
||||
local UNIQUEID=$(dd if=/dev/urandom bs=1 count=4 | hexdump -e '1/1 "%02x"')
|
||||
[ -x "$OPENSSL_BIN" ] && GENKEY_CMD="$OPENSSL_BIN req -x509 -sha256 -outform pem -nodes"
|
||||
[ -x "$PX5G_BIN" ] && GENKEY_CMD="$PX5G_BIN selfsigned -pem"
|
||||
[ -n "$GENKEY_CMD" ] && {
|
||||
$GENKEY_CMD \
|
||||
-days ${days:-730} -newkey rsa:${bits:-2048} -keyout "${APFREE_KEY}.new" -out "${APFREE_CERT}.new" \
|
||||
-subj /C="${country:-CN}"/ST="${state:-localhost}"/L="${location:-Unknown}"/O="${commonname:-ApFreeWiFidog}$UNIQUEID"/CN="${commonname:-ApFreeWiFidog}"
|
||||
sync
|
||||
mv "${APFREE_KEY}.new" "${APFREE_KEY}"
|
||||
mv "${APFREE_CERT}.new" "${APFREE_CERT}"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
or when u start `/etc/init.d/wifidogx start`, it will generate it automatically
|
||||
|
||||
|
||||
For more information, please refer to the upstream [project page](https://github.com/liudf0716/apfree_wifidog)
|
||||
@@ -0,0 +1,39 @@
|
||||
config wifidogx 'common'
|
||||
option external_interface 'wan'
|
||||
option check_interval '60'
|
||||
option client_timeout '60'
|
||||
option wired_passed '1'
|
||||
option enabled '1'
|
||||
option log_level '0'
|
||||
option js_filter '1'
|
||||
option apple_cna '1'
|
||||
option auth_server_mode 'local'
|
||||
option enable_anti_nat '0'
|
||||
option ttl_values '64,128,255'
|
||||
option ap_device_id 'AW85844955CC7D742A339'
|
||||
option ap_mac_address '5C-C7-D7-42-A3-39'
|
||||
option ap_longitude '116.395000'
|
||||
option ap_latitude '039.911000'
|
||||
option disable_portal_auth '0'
|
||||
option enable_privileged_ops '0'
|
||||
option privileged_ops_secret 'chawrt@2026'
|
||||
option device_id 'AW58534605CC7D742A339'
|
||||
option local_portal 'https://qq.com'
|
||||
|
||||
config gateway 'apfree'
|
||||
option gateway_name 'br-lan'
|
||||
option gateway_channel 'apfree'
|
||||
option gateway_auth_enabled '1'
|
||||
option gateway_id '5CC7D742A336'
|
||||
option gateway_subnetv4 '192.168.8.1/24'
|
||||
|
||||
config auth 'local'
|
||||
option auth_server_hostname '192.168.8.109'
|
||||
option auth_server_port '8001'
|
||||
option auth_server_path '/wifidog/'
|
||||
|
||||
config longconn 'openclaw'
|
||||
option long_conn_mode 'ws'
|
||||
option ws_server_hostname '192.168.8.109'
|
||||
option ws_server_port '8001'
|
||||
option ws_server_path '/ws/wifidogx'
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2018 Dengfeng Liu
|
||||
|
||||
# After aw-bpf (START=98) so dns_ringbuf_portal is pinned for wildcard trust.
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
NAME=wifidogx
|
||||
PROG="/usr/bin/${NAME}"
|
||||
CONFIGFILE="/tmp/wifidogx.conf"
|
||||
|
||||
handle_gateway() {
|
||||
local section="$1"
|
||||
local gateway_name gateway_channel gateway_id
|
||||
local gateway_subnetv4
|
||||
local gateway_auth_enabled
|
||||
|
||||
config_get gateway_name "$section" gateway_name
|
||||
config_get gateway_channel "$section" gateway_channel
|
||||
config_get gateway_id "$section" gateway_id
|
||||
config_get gateway_auth_enabled "$section" gateway_auth_enabled 1
|
||||
|
||||
|
||||
if [ -z "$gateway_name" ] || [ -z "$gateway_channel" ] ; then
|
||||
echo "gateway_name and gateway_channel are required for $section" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
# Get gateway_id from gateway_name if not set
|
||||
if [ -z "$gateway_id" ]; then
|
||||
gateway_id=$(ifconfig "$gateway_name" | awk '/HWaddr/ {print toupper($5)}' | tr -d ':')
|
||||
if [ -z "$gateway_id" ]; then
|
||||
echo "Failed to get gateway_id for $gateway_name" >&2
|
||||
return
|
||||
fi
|
||||
uci set wifidogx."$section".gateway_id="$gateway_id"
|
||||
uci commit wifidogx
|
||||
fi
|
||||
# according to the gateway_name to get the subnetv4
|
||||
local gateway_ipv4=$(ifconfig "$gateway_name" | awk '/inet addr:/ {print $2}' | cut -d: -f2)
|
||||
local gateway_maskv4=$(ifconfig "$gateway_name" | awk '/Mask:/ {print $4}' | cut -d: -f2)
|
||||
[ -z "$gateway_ipv4" ] && echo "Failed to get gateway_ipv4 for $gateway_name" >&2 && return
|
||||
[ -z "$gateway_maskv4" ] && echo "Failed to get gateway_maskv4 for $gateway_name" >&2 && return
|
||||
|
||||
# change the gateway_ip4/gateway_maskv4 to CIDR format
|
||||
local mask_bits=0
|
||||
local mask_value=$(printf '%d' "0x$(echo $gateway_maskv4 | tr '.' ' ' | awk '{printf "%02x%02x%02x%02x",$1,$2,$3,$4}')")
|
||||
while [ $mask_value -ne 0 ]; do
|
||||
mask_bits=$((mask_bits + (mask_value & 1)))
|
||||
mask_value=$((mask_value >> 1))
|
||||
done
|
||||
gateway_subnetv4="$gateway_ipv4/$mask_bits"
|
||||
|
||||
uci set wifidogx."$section".gateway_subnetv4="$gateway_subnetv4"
|
||||
uci commit wifidogx
|
||||
|
||||
printf "GatewaySetting {\n\tGatewayAuthEnabled %s\n\tGatewayInterface %s\n\tGatewayChannel %s\n\tGatewayID %s\n\tGatewaySubnetV4 %s\n}\n" \
|
||||
"${gateway_auth_enabled}" "${gateway_name}" "${gateway_channel}" "${gateway_id}" "${gateway_subnetv4}" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
add_white_list_entries() {
|
||||
local list_type="$1"
|
||||
local uci_field="$2"
|
||||
local target_variable="$3"
|
||||
|
||||
list_type=$(uci get wifidogx.common."$list_type")
|
||||
for group in $list_type; do
|
||||
group_list=$(uci get wifidogx."$group"."$uci_field")
|
||||
if [ -n "$group_list" ]; then
|
||||
eval "$target_variable=\"\${$target_variable} \$group_list\""
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
prepare_common_settings() {
|
||||
printf "CheckInterval %s\nClientTimeout %s\nJsFilter %s\nWiredPassed %s\nBypassAppleCNA %s\n" \
|
||||
"$check_interval" "$client_timeout" "$js_filter" "$wired_passed" "$apple_cna" >> "$CONFIGFILE"
|
||||
|
||||
printf "EnableAntiNat %s\n" "$enable_anti_nat" >> "$CONFIGFILE"
|
||||
printf "TTLValues %s\n" "$ttl_values" >> "$CONFIGFILE"
|
||||
printf "DisablePortalAuth %s\n" "$disable_portal_auth" >> "$CONFIGFILE"
|
||||
printf "EnablePrivilegedOps %s\n" "$enable_privileged_ops" >> "$CONFIGFILE"
|
||||
[ -n "$privileged_ops_secret" ] && printf "PrivilegedOpsSecret %s\n" "$privileged_ops_secret" >> "$CONFIGFILE"
|
||||
|
||||
[ -n "$anti_nat_permit_macs" ] && printf "AntiNatPermitMACs %s\n" "$anti_nat_permit_macs" >> "$CONFIGFILE"
|
||||
|
||||
process_trusted_list() {
|
||||
local list="$1"
|
||||
local config_name="$2"
|
||||
|
||||
if [ -n "$list" ]; then
|
||||
# Clean up whitespace and remove duplicates
|
||||
list=$(echo "$list" | sed -e 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g' \
|
||||
| tr ' ' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
printf "%s %s\n" "$config_name" "$list" >> "$CONFIGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
process_trusted_list "$trusted_domains" "TrustedDomains"
|
||||
process_trusted_list "$trusted_macs" "TrustedMACList"
|
||||
process_trusted_list "$trusted_wildcard_domains" "TrustedWildcardDomains"
|
||||
}
|
||||
|
||||
prepare_device_info() {
|
||||
# Check if any device info fields are set
|
||||
if [ -n "$ap_device_id" ] || [ -n "$ap_mac_address" ] || [ -n "$ap_longitude" ] || [ -n "$ap_latitude" ] || [ -n "$location_id" ]; then
|
||||
printf "DeviceInfo {\n" >> "$CONFIGFILE"
|
||||
|
||||
[ -n "$ap_device_id" ] && printf "\tApDeviceId %s\n" "$ap_device_id" >> "$CONFIGFILE"
|
||||
[ -n "$ap_mac_address" ] && printf "\tApMacAddress %s\n" "$ap_mac_address" >> "$CONFIGFILE"
|
||||
[ -n "$ap_longitude" ] && printf "\tApLongitude %s\n" "$ap_longitude" >> "$CONFIGFILE"
|
||||
[ -n "$ap_latitude" ] && printf "\tApLatitude %s\n" "$ap_latitude" >> "$CONFIGFILE"
|
||||
[ -n "$location_id" ] && printf "\tLocationId %s\n" "$location_id" >> "$CONFIGFILE"
|
||||
|
||||
printf "}\n" >> "$CONFIGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_auth_server_settings() {
|
||||
# 获取选中的认证服务器配置
|
||||
local selected_auth_server
|
||||
selected_auth_server=$(uci get wifidogx.common.selected_auth_server 2>/dev/null)
|
||||
|
||||
# 如果是云认证或旁路模式,需要验证选中的认证服务器
|
||||
if [ "$auth_server_mode" = "cloud" ] || [ "$auth_server_mode" = "bypass" ]; then
|
||||
if [ -z "$selected_auth_server" ]; then
|
||||
echo "Error: selected_auth_server is required for $auth_server_mode mode" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local auth_server_hostname auth_server_port auth_server_path
|
||||
|
||||
uci_validate_section "$NAME" "auth" "$selected_auth_server" \
|
||||
'auth_server_hostname:string:192.168.1.1' \
|
||||
'auth_server_port:port:80' \
|
||||
'auth_server_path:string:/wifidog/'
|
||||
|
||||
export auth_server_hostname
|
||||
export auth_server_port
|
||||
export auth_server_path
|
||||
fi
|
||||
|
||||
case "$auth_server_mode" in
|
||||
cloud|bypass)
|
||||
printf "AuthServerMode 0\n" >> "$CONFIGFILE"
|
||||
printf "DeviceID %s\nAuthServer {\n\tHostname %s\n\tHTTPPort %s\n\tPath %s\n}\n" \
|
||||
"$device_id" "$auth_server_hostname" "$auth_server_port" "$auth_server_path" >> "$CONFIGFILE"
|
||||
;;
|
||||
local)
|
||||
printf "AuthServerMode 2\n" >> "$CONFIGFILE"
|
||||
[ -n "$device_id" ] && printf "DeviceID %s\n" "$device_id" >> "$CONFIGFILE"
|
||||
[ -n "$auth_server_offline_file" ] && printf "AuthServerOfflineFile %s\n" "$auth_server_offline_file" >> "$CONFIGFILE"
|
||||
[ -n "$local_portal" ] && printf "LocalPortal %s\n" "$local_portal" >> "$CONFIGFILE"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "$internet_offline_file" ] && printf "InternetOfflineFile %s\n" "$internet_offline_file" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
prepare_longconn_settings() {
|
||||
local long_conn_mode ws_server_hostname ws_server_port ws_server_path
|
||||
local mqtt_server_hostname mqtt_server_port mqtt_username mqtt_password
|
||||
local selected_long_conn
|
||||
|
||||
selected_long_conn=$(uci get wifidogx.common.selected_long_conn 2>/dev/null)
|
||||
[ -z "$selected_long_conn" ] && return
|
||||
|
||||
if ! uci -q get wifidogx."$selected_long_conn" >/dev/null; then
|
||||
echo "Long connection: selected section $selected_long_conn does not exist" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
uci_validate_section "$NAME" "longconn" "$selected_long_conn" \
|
||||
'long_conn_mode:or("ws","wss","mqtt","mqtts"):ws' \
|
||||
'ws_server_hostname:string' \
|
||||
'ws_server_port:port:443' \
|
||||
'ws_server_path:string:/ws/wifidogx' \
|
||||
'mqtt_server_hostname:string' \
|
||||
'mqtt_server_port:port:1883' \
|
||||
'mqtt_username:string' \
|
||||
'mqtt_password:string'
|
||||
|
||||
case "$long_conn_mode" in
|
||||
ws|wss)
|
||||
if [ -z "$ws_server_hostname" ]; then
|
||||
echo "Long connection: ws_server_hostname is required" >&2
|
||||
return
|
||||
fi
|
||||
local ws_ssl
|
||||
ws_ssl=$([ "$long_conn_mode" = "wss" ] && echo 1 || echo 0)
|
||||
printf "WebSocket {\n\tWSServer %s\n\tWSServerPort %s\n\tWSServerPath %s\n\tWSServerSSL %s\n}\n" \
|
||||
"$ws_server_hostname" "$ws_server_port" "$ws_server_path" "$ws_ssl" >> "$CONFIGFILE"
|
||||
;;
|
||||
mqtt|mqtts)
|
||||
if [ -z "$mqtt_server_hostname" ]; then
|
||||
echo "Long connection: mqtt_server_hostname is required" >&2
|
||||
return
|
||||
fi
|
||||
local mqtt_ssl
|
||||
mqtt_ssl=$([ "$long_conn_mode" = "mqtts" ] && echo 1 || echo 0)
|
||||
printf "mqtt {\n\tserveraddr %s\n\tserverport %s\n\tmqttUseSSL %s\n\tmqttUsername %s\n\tmqttPassword %s\n}\n" \
|
||||
"$mqtt_server_hostname" "$mqtt_server_port" "$mqtt_ssl" "${mqtt_username:-}" "${mqtt_password:-}" >> "$CONFIGFILE"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
prepare_external_interface() {
|
||||
[ -z "$external_interface" ] && echo "No ExternalInterface " >&2 && return
|
||||
local external_interface_name
|
||||
|
||||
if [ "$external_interface" = "wwan" ]; then
|
||||
external_interface_name=$(ubus call network.interface."$external_interface" status | jsonfilter -e '@.device')
|
||||
else
|
||||
external_interface_name=$(uci get network."$external_interface".device)
|
||||
fi
|
||||
|
||||
[ -z "$external_interface_name" ] && echo "Failed to get device name for $external_interface" >&2 && return
|
||||
|
||||
printf "ExternalInterface %s\n" "$external_interface_name" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
prepare_wifidog_conf() {
|
||||
[ -f "$CONFIGFILE" ] && rm -f "$CONFIGFILE"
|
||||
local auth_server_mode_value='"cloud", "bypass", "local"'
|
||||
|
||||
# 获取选中的认证服务器
|
||||
local selected_auth_server
|
||||
selected_auth_server=$(uci get wifidogx.common.selected_auth_server 2>/dev/null)
|
||||
|
||||
|
||||
uci_validate_section "$NAME" "$NAME" common \
|
||||
'enabled:bool:0' \
|
||||
"auth_server_mode:or($auth_server_mode_value)" \
|
||||
'selected_auth_server:string' \
|
||||
'selected_long_conn:string' \
|
||||
'log_level:integer:7' \
|
||||
'device_id:string' \
|
||||
'check_interval:integer:60' \
|
||||
'client_timeout:integer:5' \
|
||||
'wired_passed:bool:1' \
|
||||
'apple_cna:bool:0' \
|
||||
'trusted_domains:list(host)' \
|
||||
'trusted_wildcard_domains:list(string)' \
|
||||
'trusted_macs:list(string)' \
|
||||
'app_white_list:list(string)' \
|
||||
'mac_white_list:list(string)' \
|
||||
'wildcard_white_list:list(string)' \
|
||||
'js_filter:bool:1' \
|
||||
'auth_server_offline_file:string' \
|
||||
'internet_offline_file:string' \
|
||||
'local_portal:string' \
|
||||
'external_interface:string' \
|
||||
'enable_anti_nat:bool:0' \
|
||||
'enable_privileged_ops:bool:0' \
|
||||
'privileged_ops_secret:string:chawrt@2026' \
|
||||
'ttl_values:string:64,128' \
|
||||
'anti_nat_permit_macs:string' \
|
||||
'disable_portal_auth:bool:1' \
|
||||
'ap_device_id:string' \
|
||||
'ap_mac_address:string' \
|
||||
'ap_longitude:string' \
|
||||
'ap_latitude:string' \
|
||||
'location_id:string'
|
||||
|
||||
[ -n "$app_white_list" ] && add_white_list_entries "app_white_list" "domain_name" "trusted_domains"
|
||||
[ -n "$mac_white_list" ] && add_white_list_entries "mac_white_list" "mac_address" "trusted_macs"
|
||||
[ -n "$wildcard_white_list" ] && add_white_list_entries "wildcard_white_list" "wildcard_domain" "trusted_wildcard_domains"
|
||||
|
||||
prepare_external_interface
|
||||
prepare_auth_server_settings
|
||||
prepare_longconn_settings
|
||||
prepare_device_info
|
||||
config_foreach handle_gateway gateway
|
||||
prepare_common_settings
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "wifidogx"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$NAME"
|
||||
|
||||
prepare_wifidog_conf
|
||||
|
||||
if [ "$enabled" -eq 0 ]; then
|
||||
echo "wifidogx is disabled, exit..." >&2
|
||||
return
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" -c "$CONFIGFILE" -s -f -d "$log_level"
|
||||
procd_set_param respawn
|
||||
procd_set_param file /etc/config/wifidogx
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
status_service() {
|
||||
/usr/bin/wdctlx status
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
+2
-2
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=baidudrive
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=19
|
||||
PKG_VERSION:=1.0.6
|
||||
PKG_RELEASE:=21
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=brook
|
||||
PKG_VERSION:=20260101.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=20270101
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/txthinking/brook/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2021 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudflared-next
|
||||
PKG_VERSION:=2026.9.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),x86_64)
|
||||
CF_ARCH:=amd64
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),arm64)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),armv8)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
CF_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),armv7)
|
||||
CF_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),i386)
|
||||
CF_ARCH:=386
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=cloudflared-linux-$(CF_ARCH)-$(PKG_VERSION)
|
||||
PKG_SOURCE_URL:=https://github.com/cloudflare/cloudflared/releases/download/$(PKG_VERSION)/cloudflared-linux-$(CF_ARCH)?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/cloudflared-next
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=Cloudflare Tunnel client Next - Pre-built Binary
|
||||
URL:=https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/
|
||||
DEPENDS:=@(x86_64||aarch64||arm||i386) +ca-bundle
|
||||
PROVIDES:=cloudflared
|
||||
CONFLICTS:=cloudflared
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/description
|
||||
Contains the command-line client for Cloudflare Tunnel, a tunneling
|
||||
daemon that proxies traffic from the Cloudflare network to your origins.
|
||||
This package contains official pre-built static binaries.
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/conffiles
|
||||
/etc/config/cloudflared
|
||||
/etc/cloudflared/
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) $(DL_DIR)/$(PKG_SOURCE) $(PKG_BUILD_DIR)/cloudflared
|
||||
chmod 0755 $(PKG_BUILD_DIR)/cloudflared
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/cloudflared $(1)/usr/bin/cloudflared
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/cloudflared/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/sample_config.yml $(1)/etc/cloudflared/config.yml
|
||||
$(INSTALL_DIR) $(1)/etc/config/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/cloudflared.config $(1)/etc/config/cloudflared
|
||||
$(INSTALL_DIR) $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) $(CURDIR)/files/cloudflared.init $(1)/etc/init.d/cloudflared
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,cloudflared-next))
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
config cloudflared 'config'
|
||||
option enabled '0'
|
||||
option token ''
|
||||
# option config '/etc/cloudflared/config.yml'
|
||||
# option origincert '/etc/cloudflared/cert.pem'
|
||||
option edge_bind_address ''
|
||||
option edge_ip_version ''
|
||||
option grace_period ''
|
||||
option protocol 'auto'
|
||||
option region ''
|
||||
option retries ''
|
||||
option tag ''
|
||||
option metrics ''
|
||||
option loglevel 'info'
|
||||
option logfile '/var/log/cloudflared.log'
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2021 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="cloudflared"
|
||||
PROG="/usr/bin/cloudflared"
|
||||
|
||||
append_param_arg() {
|
||||
local value
|
||||
|
||||
config_get value "config" "$1" $2
|
||||
[ -n "$value" ] && procd_append_param command "--${1//_/-}" "$value"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
local token
|
||||
|
||||
config_get_bool enabled "config" "enabled"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param command "$PROG" "tunnel"
|
||||
procd_append_param command "--no-autoupdate"
|
||||
|
||||
append_param_arg "edge_bind_address"
|
||||
append_param_arg "edge_ip_version"
|
||||
append_param_arg "grace_period"
|
||||
append_param_arg "protocol"
|
||||
append_param_arg "region"
|
||||
append_param_arg "retries"
|
||||
append_param_arg "tag"
|
||||
append_param_arg "metrics"
|
||||
append_param_arg "loglevel"
|
||||
append_param_arg "logfile"
|
||||
|
||||
config_get token "config" "token"
|
||||
if [ -z "$token" ]; then
|
||||
# Locally-managed tunnels
|
||||
append_param_arg "config" "/etc/cloudflared/config.yml"
|
||||
append_param_arg "origincert" "/etc/cloudflared/cert.pem"
|
||||
fi
|
||||
|
||||
procd_append_param command "run"
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
# Remotely-managed tunnel (recommended by Cloudflare)
|
||||
procd_append_param command "--token" "$token"
|
||||
fi
|
||||
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
procd_add_interface_trigger "interface.*.up" "wan" /etc/init.d/cloudflared restart
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/configuration-file/
|
||||
|
||||
# NOTICE
|
||||
# Cloudflare recommends setting up a remotely-managed tunnel.
|
||||
# Remotely-managed configurations are stored on Cloudflare,
|
||||
# which allows you to manage the tunnel from any machine
|
||||
# using the dashboard, API, or Terraform.
|
||||
|
||||
#tunnel: <Tunnel-UUID>
|
||||
#credentials-file: /etc/cloudflared/<Tunnel-UUID>.json
|
||||
#
|
||||
#ingress:
|
||||
# - hostname: luci.example.com
|
||||
# service: http://localhost:80
|
||||
# - hostname: ssh.example.com
|
||||
# service: ssh://localhost:22
|
||||
# - service: http_status:404
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
cloudflared --version | grep "$PKG_VERSION"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 自动更新 OpenWrt cloudflared-next 包的版本和 Hash
|
||||
# 依赖: curl, grep, sed, sha256sum (或 shasum)
|
||||
|
||||
set -e
|
||||
|
||||
PKG_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
MAKEFILE="$PKG_DIR/Makefile"
|
||||
|
||||
if [ ! -f "$MAKEFILE" ]; then
|
||||
echo "错误: 找不到 Makefile 文件 ($MAKEFILE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 获取当前本地版本
|
||||
CURRENT_VERSION=$(grep 'PKG_VERSION:=' "$MAKEFILE" | cut -d'=' -f2 | tr -d ' ')
|
||||
echo "当前本地版本: $CURRENT_VERSION"
|
||||
|
||||
# 2. 获取 GitHub 上的最新版本号
|
||||
# 注意: cloudflared 的 tag 通常是 2024.x.y 这种格式,无需特殊处理
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/cloudflare/cloudflared/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^v//')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "错误: 无法从 GitHub 获取最新版本号。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub 最新版本: $LATEST_VERSION"
|
||||
|
||||
# 3. 比较版本
|
||||
if [ "$CURRENT_VERSION" == "$LATEST_VERSION" ]; then
|
||||
echo "版本已经是最新,无需更新。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "发现新版本! 正在准备更新..."
|
||||
|
||||
# 4. 下载新版本的源码包并计算 Hash
|
||||
SOURCE_URL="https://codeload.github.com/cloudflare/cloudflared/tar.gz/$LATEST_VERSION"
|
||||
echo "正在从 $SOURCE_URL 下载并计算 Hash..."
|
||||
|
||||
# 探测可用的哈希计算工具
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
NEW_HASH=$(curl -sL "$SOURCE_URL" | sha256sum | cut -d' ' -f1)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
NEW_HASH=$(curl -sL "$SOURCE_URL" | shasum -a 256 | cut -d' ' -f1)
|
||||
else
|
||||
echo "错误: 系统中未找到 sha256sum 或 shasum 命令。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_HASH" ] || [ ${#NEW_HASH} -ne 64 ]; then
|
||||
echo "错误: 计算得到的 Hash 值无效: $NEW_HASH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "新 Hash 值: $NEW_HASH"
|
||||
|
||||
# 5. 更新 Makefile
|
||||
# 使用特定的正则表达式确保精准替换
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
sed -i '' "s/PKG_HASH:=.*/PKG_HASH:=$NEW_HASH/" "$MAKEFILE"
|
||||
else
|
||||
sed -i "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
sed -i "s/PKG_HASH:=.*/PKG_HASH:=$NEW_HASH/" "$MAKEFILE"
|
||||
fi
|
||||
|
||||
echo "Makefile 更新成功: $CURRENT_VERSION -> $LATEST_VERSION"
|
||||
|
||||
# 6. 可选: 提交更改 (如果是在 Git 仓库中且您希望自动提交)
|
||||
# cd "$PKG_DIR/.."
|
||||
# if [ -d ".git" ]; then
|
||||
# git add cloudflared-next/Makefile
|
||||
# git commit -m "cloudflared-next: update to version $LATEST_VERSION"
|
||||
# echo "已在本地 Git 仓库中提交更新。"
|
||||
# fi
|
||||
+2
-2
@@ -6,11 +6,11 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudreve
|
||||
PKG_VERSION:=4.18.0
|
||||
PKG_RELEASE:=16
|
||||
PKG_RELEASE:=17
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git
|
||||
PKG_SOURCE_VERSION:=1c3fe4da3c4073e73a11ed93518b545bb8b8d5ce
|
||||
PKG_SOURCE_VERSION:=ade1866be54a3614bc643ab5f513e93a1e0dc82f
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=41
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=44
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.09.12-187058462a1f.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=52
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=55
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.08.28-73f01b995c34.tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.09.12-a0181f729855.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -1,8 +1,65 @@
|
||||
#!/bin/sh
|
||||
# daed-cleanup.sh — reaper for stale daed kernel state.
|
||||
#
|
||||
# Removes:
|
||||
# 1. Processes inside the `daens` netns (SIGTERM, then SIGKILL after 1s)
|
||||
# 2. The `daens` network namespace itself (ip netns del, with
|
||||
# umount+rm as fallback for zombie nsfs mounts)
|
||||
# 3. The `dae0` veth pair in the host netns
|
||||
#
|
||||
# daed itself owns TC clsact detach. This opkg-side helper only
|
||||
# reaps stale daens/dae0 state and never removes /sys/fs/bpf/daed.
|
||||
# The pin directory is created during normal startup, so it is not
|
||||
# a reliable leak indicator and must not block service lifecycle.
|
||||
#
|
||||
# The function stays sourceable by both init.d/daed and daed-guard.
|
||||
|
||||
daed_process_probe() {
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -f '^/usr/bin/daed([[:space:]]|$)' >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if command -v pidof >/dev/null 2>&1; then
|
||||
pidof daed >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
daed_cleanup_runtime() {
|
||||
local pid
|
||||
local pid rc=0 probe_rc
|
||||
|
||||
# If daed userspace is currently running, do not touch the
|
||||
# netns, the veth, or the eBPF dataplane. They are in active
|
||||
# use; removing them would make every connection through dae
|
||||
# hang.
|
||||
daed_process_probe
|
||||
probe_rc=$?
|
||||
case "$probe_rc" in
|
||||
0)
|
||||
if [ "${DAED_GUARD_CLEANUP:-start}" = "start" ]; then
|
||||
logger -t daed-init "cleanup: pre-start skipped because /usr/bin/daed is still running; refusing a second instance"
|
||||
return 1
|
||||
fi
|
||||
logger -t daed-init "cleanup: post-exit skipped because another /usr/bin/daed instance is still running"
|
||||
return 0
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
logger -t daed-init "cleanup: cannot determine whether daed is running; refusing to remove netns/veth"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 1. Kill processes inside daens.
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
@@ -14,14 +71,36 @@ daed_cleanup_runtime() {
|
||||
done
|
||||
fi
|
||||
|
||||
# 2. Remove the daens netns. ip netns del can fail if a
|
||||
# process still references it via /proc/<pid>/ns/net or
|
||||
# because the umount has already happened. Try the
|
||||
# umount/rm fallback.
|
||||
if ! ip netns del daens 2>/dev/null; then
|
||||
umount -l /run/netns/daens 2>/dev/null
|
||||
rm -f /run/netns/daens
|
||||
if [ -e /run/netns/daens ] && ! rm -f /run/netns/daens 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove /run/netns/daens (resource busy); a reboot may be required"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
ip link del dae0 2>/dev/null || true
|
||||
# 3. Remove the dae0 veth pair.
|
||||
if ip link show dae0 >/dev/null 2>&1; then
|
||||
if ! ip link del dae0 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove dae0 veth pair"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. The pin root is normal persistent state. Never remove it or
|
||||
# make its existence change the cleanup result.
|
||||
if [ -e /sys/fs/bpf/daed ]; then
|
||||
logger -t daed-init "cleanup: /sys/fs/bpf/daed exists; leaving normal pin root unchanged"
|
||||
fi
|
||||
|
||||
# Final verification covers only netns and veth state.
|
||||
[ ! -e /run/netns/daens ] || return 1
|
||||
! ip netns list 2>/dev/null | awk '$1 == "daens" { found=1 } END { exit !found }' || return 1
|
||||
! ip netns list 2>/dev/null | grep -Eq '^daens([[:space:]]|$)' || return 1
|
||||
! ip link show dae0 >/dev/null 2>&1 || return 1
|
||||
|
||||
return $rc
|
||||
}
|
||||
|
||||
+118
-16
@@ -1,29 +1,131 @@
|
||||
#!/bin/sh
|
||||
# Keep daed as a child so signals can be forwarded and stale netns/veth
|
||||
# state can be cleaned before start and after exit. daed owns TC detach;
|
||||
# /sys/fs/bpf/daed is normal persistent state and is never removed here.
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
# Pre-start cleanup refuses to remove runtime state while another
|
||||
# daed instance is active, and fails closed if that probe is broken.
|
||||
DAED_GUARD_CLEANUP=start
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: stale network state could not be removed" >&2
|
||||
echo "daed: stale /usr/bin/daed or netns state could not be verified or removed; refusing to start. Check process and netns state." >&2
|
||||
logger -t daed-init "pre-start cleanup failed: refusing to start daed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keep daed as a child so a panic or unexpected exit is followed by an
|
||||
# immediate teardown of all kernel/runtime state before procd can respawn us.
|
||||
# Keep daed as a child so post-exit cleanup runs before procd can
|
||||
# respawn it.
|
||||
child_pid=
|
||||
cleanup_child() {
|
||||
[ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null
|
||||
pending_signal=
|
||||
shutdown_signal=
|
||||
shutdown_elapsed=0
|
||||
forced_kill=0
|
||||
child_term_timeout=20
|
||||
|
||||
forward_signal() {
|
||||
local sig="$1"
|
||||
if [ -z "$child_pid" ]; then
|
||||
pending_signal="$sig"
|
||||
logger -t daed-init "signal $sig received before daed child started; launch cancelled"
|
||||
return 0
|
||||
fi
|
||||
case "$sig" in
|
||||
TERM|INT|QUIT)
|
||||
if [ -z "$shutdown_signal" ]; then
|
||||
shutdown_signal="$sig"
|
||||
shutdown_elapsed=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
kill -"$sig" "$child_pid" 2>/dev/null
|
||||
}
|
||||
trap cleanup_child TERM INT
|
||||
|
||||
/usr/bin/daed "$@" &
|
||||
child_pid=$!
|
||||
wait "$child_pid"
|
||||
status=$?
|
||||
child_pid=
|
||||
trap - TERM INT
|
||||
exit_with_signal() {
|
||||
local sig="$1"
|
||||
trap - TERM INT HUP QUIT
|
||||
kill -"$sig" "$$" 2>/dev/null
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
status=1
|
||||
child_is_running() {
|
||||
local state
|
||||
kill -0 "$child_pid" 2>/dev/null || return 1
|
||||
state=$(awk '{ print $3 }' "/proc/$child_pid/stat" 2>/dev/null)
|
||||
[ "$state" != "Z" ]
|
||||
}
|
||||
trap 'forward_signal TERM' TERM
|
||||
trap 'forward_signal INT' INT
|
||||
trap 'forward_signal HUP' HUP
|
||||
trap 'forward_signal QUIT' QUIT
|
||||
|
||||
start_child() {
|
||||
local sig
|
||||
# Keep this check inside the function as well as at the call site:
|
||||
# it closes the ordinary pre-start window, while the pending-signal
|
||||
# path below handles a signal arriving during the background fork.
|
||||
[ -z "$pending_signal" ] || return 125
|
||||
/usr/bin/daed "$@" &
|
||||
child_pid=$!
|
||||
if [ -n "$pending_signal" ]; then
|
||||
sig="$pending_signal"
|
||||
pending_signal=
|
||||
forward_signal "$sig"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$pending_signal" ]; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
exit "$status"
|
||||
|
||||
# Best-effort OOM preference; failure must not block startup.
|
||||
if ! echo -16 > /proc/self/oom_score_adj 2>/dev/null; then
|
||||
logger -t daed-init "warn: failed to set /proc/self/oom_score_adj; continuing without OOM preference"
|
||||
fi
|
||||
if ! start_child "$@"; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
if [ -n "$pending_signal" ]; then
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
status=0
|
||||
reaped=0
|
||||
while [ "$reaped" -eq 0 ]; do
|
||||
if [ -n "$shutdown_signal" ] && child_is_running; then
|
||||
if [ "$shutdown_elapsed" -ge "$child_term_timeout" ]; then
|
||||
if [ "$forced_kill" -eq 0 ]; then
|
||||
logger -t daed-init "daed did not exit within ${child_term_timeout}s after $shutdown_signal; sending KILL"
|
||||
kill -KILL "$child_pid" 2>/dev/null
|
||||
forced_kill=1
|
||||
fi
|
||||
else
|
||||
sleep 1
|
||||
shutdown_elapsed=$((shutdown_elapsed + 1))
|
||||
continue
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
wait "$child_pid" 2>/dev/null
|
||||
status=$?
|
||||
if ! child_is_running; then
|
||||
reaped=1
|
||||
fi
|
||||
done
|
||||
child_pid=
|
||||
trap - TERM INT HUP QUIT
|
||||
|
||||
# Post-exit cleanup runs after the child is reaped.
|
||||
DAED_GUARD_CLEANUP=post-exit
|
||||
cleanup_status=0
|
||||
daed_cleanup_runtime || cleanup_status=$?
|
||||
if [ "$cleanup_status" -ne 0 ]; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
logger -t daed-init "post-exit cleanup failed; check ip netns / ip link show"
|
||||
fi
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
exit "$cleanup_status"
|
||||
|
||||
+37
-13
@@ -1,5 +1,7 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
# daed-guard handles bounded child shutdown and post-exit netns/veth cleanup.
|
||||
# Keep the log file and a pre-stop state snapshot for diagnostics.
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
@@ -10,18 +12,28 @@ LOG="/var/log/daed/daed.log"
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
log() {
|
||||
logger -t daed-init "$@"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
log "start: begin"
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
if [ "$enabled" -ne "1" ]; then
|
||||
log "start: config disabled, exit"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local listen_addr log_maxbackups log_maxsize
|
||||
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "5"
|
||||
|
||||
log "start: listen=$listen_addr log_maxbackups=$log_maxbackups log_maxsize=$log_maxsize"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
@@ -33,25 +45,37 @@ start_service() {
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
# Avoid an endless crash/respawn loop which can repeatedly reattach dae's
|
||||
# data-plane hooks and make the router management plane unreachable.
|
||||
procd_set_param respawn 3600 5 5
|
||||
# daed-guard escalates its child after 20 seconds. Leave time for
|
||||
# reap and post-exit cleanup before procd kills the wrapper.
|
||||
procd_set_param term_timeout 30
|
||||
# procd_set_param respawn: arguments are (threshold, timeout, retry).
|
||||
# threshold = runtime that resets the short-lived exit counter
|
||||
# timeout = seconds to wait between retries
|
||||
# retry = maximum short-lived exits before procd gives up
|
||||
# Reset the counter after one hour of stable runtime; otherwise retry
|
||||
# after 5 seconds and stop after 10 failures.
|
||||
procd_set_param respawn 3600 5 10
|
||||
# daed-guard sets oom_score_adj before forking; procd has no
|
||||
# oom_adj/oom_score_adj parameter.
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
log "start: procd_open_instance done"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
rm -f "$LOG"
|
||||
daed_cleanup_runtime
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
sleep 1
|
||||
daed_cleanup_runtime
|
||||
start
|
||||
log "stop: begin"
|
||||
# Cleanup runs in daed-guard after its child exits. Record only a
|
||||
# pre-stop snapshot here; pin entries do not prove TC attachment.
|
||||
local pinned="" ns_left=""
|
||||
if [ -d /sys/fs/bpf/daed ]; then
|
||||
pinned=$(ls /sys/fs/bpf/daed 2>/dev/null | tr '\n' ' ')
|
||||
fi
|
||||
if ip netns list 2>/dev/null | grep -q '^daens'; then
|
||||
ns_left="daens"
|
||||
fi
|
||||
log "stop: pre-stop state — bpf_pin_entries=[${pinned:-none}] netns_left=[${ns_left:-none}]"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
|
||||
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_DOCKERMANAGER:=$(ARCH)
|
||||
|
||||
PKG_NAME:=dockermanager
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=7
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
|
||||
|
||||
PKG_NAME:=easytier-next
|
||||
PKG_VERSION:=2.6.4
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),mipsel)
|
||||
APP_ARCH:=mipsel
|
||||
endif
|
||||
ifeq ($(ARCH),mips)
|
||||
APP_ARCH:=mips
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
APP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(BOARD),kirkwood)
|
||||
APP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),armv7)
|
||||
APP_ARCH:=armv7
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),arm64)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),armv8)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),x86_64)
|
||||
APP_ARCH:=x86_64
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=easytier-linux-$(APP_ARCH)-v$(PKG_VERSION).zip
|
||||
PKG_SOURCE_URL:=https://github.com/EasyTier/EasyTier/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=VPN
|
||||
TITLE:=A simple, decentralized mesh VPN with WireGuard support (Next)
|
||||
URL:=https://github.com/EasyTier/EasyTier
|
||||
DEPENDS:=@(x86_64||arm||aarch64||mipsel||mips) +kmod-tun
|
||||
PROVIDES:=easytier
|
||||
CONFLICTS:=easytier
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
A simple, decentralized mesh VPN with WireGuard support.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
config EASYTIER_INCLUDE_WEBCONSOLE
|
||||
bool "Include Web Console (easytier-web)"
|
||||
depends on PACKAGE_$(PKG_NAME)
|
||||
depends on !(mips || mipsel)
|
||||
default y
|
||||
help
|
||||
Install the easytier-web web console.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
unzip -q -o $(DL_DIR)/$(PKG_SOURCE) -d $(PKG_BUILD_DIR)
|
||||
find $(PKG_BUILD_DIR) -mindepth 2 -type f -exec mv -f {} $(PKG_BUILD_DIR)/ \;
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-core $(1)/usr/bin/
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-cli $(1)/usr/bin/
|
||||
ifdef CONFIG_EASYTIER_INCLUDE_WEBCONSOLE
|
||||
if [ -f $(PKG_BUILD_DIR)/easytier-web-embed ]; then \
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-web-embed $(1)/usr/bin/easytier-web; \
|
||||
elif [ -f $(PKG_BUILD_DIR)/easytier-web ]; then \
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-web $(1)/usr/bin/easytier-web; \
|
||||
fi
|
||||
endif
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
+6
-8
@@ -9,15 +9,14 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_FASTNET:=$(ARCH)
|
||||
|
||||
PKG_NAME:=fastnet
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compatible
|
||||
PKG_SOURCE_DATE:=0.7.2
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=0.7.7
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_SOURCE_DATE).tar.gz
|
||||
PKG_SOURCE_URL:=http://dl.istoreos.com/binary/fastnet/
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/fastnet-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_SOURCE_DATE)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
@@ -30,7 +29,6 @@ define Package/$(PKG_NAME)
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=FastNet - network test Web UI
|
||||
DEPENDS:=@(arm||x86_64||aarch64)
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
@@ -57,7 +55,7 @@ endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/fastnet.$(PKG_ARCH_FASTNET) $(1)/usr/sbin/fastnet
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/FastNet.$(PKG_ARCH_FASTNET) $(1)/usr/sbin/FastNet
|
||||
$(INSTALL_CONF) ./files/fastnet.config $(1)/etc/config/fastnet
|
||||
$(INSTALL_BIN) ./files/fastnet.init $(1)/etc/init.d/fastnet
|
||||
$(INSTALL_BIN) ./files/fastnet.uci-default $(1)/etc/uci-defaults/fastnet
|
||||
|
||||
@@ -20,7 +20,7 @@ start_service() {
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param limits nofile="65535 65535"
|
||||
procd_set_param command /usr/sbin/fastnet web --addr "$addr" --no-open
|
||||
procd_set_param command /usr/sbin/FastNet web --addr "$addr" --no-open
|
||||
[ -n "$token" ] && procd_append_param command --token "$token"
|
||||
[ "$logger" = 1 ] && procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=frp
|
||||
PKG_VERSION:=0.71.0
|
||||
PKG_RELEASE:=6
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
config init
|
||||
option enabled '0'
|
||||
option stdout '1'
|
||||
option stderr '1'
|
||||
# Uncomment to run frpc as an existing user/group. Keep disabled by
|
||||
|
||||
+11
-4
@@ -912,6 +912,7 @@ service_triggers() {
|
||||
|
||||
start_service() {
|
||||
local init_cfg=
|
||||
local enabled=0
|
||||
local stdout=1
|
||||
local stderr=1
|
||||
local respawn=1
|
||||
@@ -919,14 +920,19 @@ start_service() {
|
||||
local run_group=
|
||||
local old_umask
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
config_load "$NAME"
|
||||
|
||||
config_foreach _find_init_section init
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_get_bool enabled "$init_cfg" enabled 0
|
||||
fi
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
old_umask="$(umask)"
|
||||
umask 077
|
||||
: > "$CONF_FILE" || {
|
||||
@@ -950,6 +956,7 @@ start_service() {
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_list_foreach "$init_cfg" conf_inc _append_conf_file
|
||||
[ "$_TOML_ERR" = "0" ] || return 1
|
||||
|
||||
config_get_bool stdout "$init_cfg" stdout 1
|
||||
config_get_bool stderr "$init_cfg" stderr 1
|
||||
|
||||
@@ -79,6 +79,7 @@ upgrade_init() {
|
||||
changed=1
|
||||
fi
|
||||
|
||||
set_if_empty "$init_section" enabled 1
|
||||
set_if_empty "$init_section" stdout 1
|
||||
set_if_empty "$init_section" stderr 1
|
||||
set_if_empty "$init_section" respawn 1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
config init
|
||||
option enabled '0'
|
||||
option stdout '1'
|
||||
option stderr '1'
|
||||
# Uncomment to run frps as an existing user/group. Keep disabled by
|
||||
@@ -18,7 +19,6 @@ config init
|
||||
config conf 'common'
|
||||
option bind_addr '0.0.0.0'
|
||||
option bind_port '7000'
|
||||
|
||||
option authentication_method 'token'
|
||||
# option token 'your_token'
|
||||
# Alternatively, load a token from a file or command. Exec token sources
|
||||
@@ -31,12 +31,10 @@ config conf 'common'
|
||||
# list token_source_exec_args '--format'
|
||||
# list token_source_exec_args 'raw'
|
||||
# list token_source_exec_env 'TOKEN_SERVICE=production'
|
||||
|
||||
option max_pool_count '5'
|
||||
option tcp_mux 'true'
|
||||
option tls_force 'false'
|
||||
option detailed_errors_to_client 'true'
|
||||
|
||||
# Web server is disabled when admin_port is empty or 0.
|
||||
# option admin_addr '127.0.0.1'
|
||||
# option admin_port '7500'
|
||||
@@ -47,15 +45,12 @@ config conf 'common'
|
||||
option enable_prometheus 'false'
|
||||
# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt'
|
||||
# option admin_tls_key_file '/etc/ssl/acme/example.com.key'
|
||||
|
||||
# Allow ports can be single ports or ranges.
|
||||
# list allow_ports '2000-3000'
|
||||
# list allow_ports '3001'
|
||||
|
||||
option log_file 'console'
|
||||
option log_level 'info'
|
||||
option log_max_days '3'
|
||||
|
||||
# List options with name "_" will be directly appended as raw TOML lines.
|
||||
# Use this only for options not covered by UCI options above.
|
||||
# Do not duplicate keys generated by UCI options above.
|
||||
@@ -68,7 +63,6 @@ config conf 'common'
|
||||
# option path '/handler'
|
||||
# list ops 'Login'
|
||||
# option tls_verify 'false'
|
||||
|
||||
#config http_plugin 'port_manager'
|
||||
# option name 'port-manager'
|
||||
# option addr '127.0.0.1:9001'
|
||||
|
||||
+11
-4
@@ -689,6 +689,7 @@ service_triggers() {
|
||||
|
||||
start_service() {
|
||||
local init_cfg=
|
||||
local enabled=0
|
||||
local stdout=1
|
||||
local stderr=1
|
||||
local respawn=1
|
||||
@@ -696,14 +697,19 @@ start_service() {
|
||||
local run_group=
|
||||
local old_umask
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
config_load "$NAME"
|
||||
|
||||
config_foreach _find_init_section init
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_get_bool enabled "$init_cfg" enabled 0
|
||||
fi
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
old_umask="$(umask)"
|
||||
umask 077
|
||||
: > "$CONF_FILE" || {
|
||||
@@ -727,6 +733,7 @@ start_service() {
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_list_foreach "$init_cfg" conf_inc _append_conf_file
|
||||
[ "$_TOML_ERR" = "0" ] || return 1
|
||||
|
||||
config_get_bool stdout "$init_cfg" stdout 1
|
||||
config_get_bool stderr "$init_cfg" stderr 1
|
||||
|
||||
@@ -90,6 +90,7 @@ upgrade_init() {
|
||||
changed=1
|
||||
fi
|
||||
|
||||
set_if_empty "$init_section" enabled 1
|
||||
set_if_empty "$init_section" stdout 1
|
||||
set_if_empty "$init_section" stderr 1
|
||||
set_if_empty "$init_section" respawn 1
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=frpc-next
|
||||
PKG_VERSION:=0.71.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),mipsel)
|
||||
FRP_ARCH:=mipsle
|
||||
endif
|
||||
ifeq ($(ARCH),mips)
|
||||
FRP_ARCH:=mips
|
||||
endif
|
||||
ifeq ($(ARCH),i386)
|
||||
FRP_ARCH:=386
|
||||
endif
|
||||
ifeq ($(ARCH),x86_64)
|
||||
FRP_ARCH:=amd64
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
FRP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
FRP_ARCH:=arm64
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=frp_$(PKG_VERSION)_linux_$(FRP_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/fatedier/frp/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/frp_$(PKG_VERSION)_linux_$(FRP_ARCH)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/frpc-next
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=A fast reverse proxy (Client) - Binary
|
||||
URL:=https://github.com/fatedier/frp
|
||||
DEPENDS:=@(i386||x86_64||arm||aarch64||mipsel||mips)
|
||||
PROVIDES:=frpc
|
||||
CONFLICTS:=frpc
|
||||
endef
|
||||
|
||||
define Package/frpc-next/description
|
||||
frp is a fast reverse proxy to help you expose a local server behind a
|
||||
NAT or firewall to the internet. This package contains the pre-built frpc client.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/frpc-next/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/frpc $(1)/usr/bin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,frpc-next))
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
frpc --version | grep "$PKG_VERSION"
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 自动更新 OpenWrt frpc-next 包的版本和 Hash
|
||||
# 依赖: curl, grep, sed, sha256sum (或 shasum)
|
||||
|
||||
set -e
|
||||
|
||||
PKG_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
MAKEFILE="$PKG_DIR/Makefile"
|
||||
|
||||
if [ ! -f "$MAKEFILE" ]; then
|
||||
echo "错误: 找不到 Makefile 文件 ($MAKEFILE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 获取当前本地版本
|
||||
CURRENT_VERSION=$(grep 'PKG_VERSION:=' "$MAKEFILE" | cut -d'=' -f2 | tr -d ' ')
|
||||
echo "当前本地版本: $CURRENT_VERSION"
|
||||
|
||||
# 2. 获取 GitHub 上的最新版本号
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^v//')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "错误: 无法从 GitHub 获取最新版本号。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub 最新版本: $LATEST_VERSION"
|
||||
|
||||
# 3. 比较版本
|
||||
if [ "$CURRENT_VERSION" == "$LATEST_VERSION" ]; then
|
||||
echo "版本已经是最新,无需更新。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "发现新版本! 正在准备更新..."
|
||||
|
||||
# 4. 更新 Makefile
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
else
|
||||
sed -i "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
fi
|
||||
|
||||
echo "Makefile 更新成功: $CURRENT_VERSION -> $LATEST_VERSION (Binary Mode)"
|
||||
+5
-3
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gecoosac
|
||||
PKG_VERSION:=2.2.20251015
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
@@ -43,8 +43,8 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
TITLE:=gecoosac server (version $(PKG_VERSION))
|
||||
URL:=http://www.cnrouter.com/
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util
|
||||
PROVIDES:=gecoosac-files
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util +coreutils-timeout
|
||||
PROVIDES:=gecoosac-files gecoosac-common
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
@@ -66,11 +66,13 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/gecoosac/tls
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_DIR) $(1)/usr/share/gecoosac
|
||||
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac
|
||||
$(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac
|
||||
$(INSTALL_DATA) ./files/usr/share/gecoosac/common.sh $(1)/usr/share/gecoosac/common.sh
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/procd.sh
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
@@ -10,13 +11,9 @@ START=90
|
||||
STOP=10
|
||||
|
||||
PROG=/usr/bin/gecoosac
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
DEFAULT_LANG=zh
|
||||
CERT_TIMEOUT=60
|
||||
CERT_RENEW_BEFORE=2592000
|
||||
|
||||
init_conf() {
|
||||
local section_type
|
||||
@@ -47,193 +44,6 @@ init_conf() {
|
||||
config_get "log" "config" "log" "0"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_port() {
|
||||
case "$1" in
|
||||
""|*[!0-9]*) return 1 ;;
|
||||
@@ -242,66 +52,6 @@ is_port() {
|
||||
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ]
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
local timeout pid i
|
||||
|
||||
timeout="$1"
|
||||
shift
|
||||
|
||||
"$@" >/dev/null 2>&1 &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
sleep 1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
run_with_timeout_output() {
|
||||
local timeout output pid i
|
||||
|
||||
timeout="$1"
|
||||
output="$2"
|
||||
shift 2
|
||||
|
||||
"$@" >"$output" 2>/dev/null &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
is_readable_regular_file() {
|
||||
local path="$1" tmp_dir status
|
||||
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-file.XXXXXX)" || return 1
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/check" \
|
||||
/bin/sh -c '[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ]' \
|
||||
gecoosac-file-check "$path" </dev/null
|
||||
status="$?"
|
||||
rm -rf "$tmp_dir"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
local value="$1"
|
||||
local part count
|
||||
@@ -328,48 +78,55 @@ san_has_entry() {
|
||||
local san="$1"
|
||||
local entry="$2"
|
||||
|
||||
echo "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
printf '%s\n' "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
}
|
||||
|
||||
cert_matches_san() {
|
||||
local cert_file="$1" cert_host="$2" cert_ip="$3"
|
||||
local san tmp_dir
|
||||
run_with_timeout_output() {
|
||||
local limit="$1" output="$2"
|
||||
shift 2
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/san" \
|
||||
openssl x509 -in "$cert_file" -noout -ext subjectAltName </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
san="$(cat "$tmp_dir/san" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
[ -n "$san" ] || return 1
|
||||
san_has_entry "$san" "DNS:${cert_host}" || return 1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || return 1
|
||||
|
||||
return 0
|
||||
# coreutils-timeout waits for completion directly and also bounds commands
|
||||
# which ignore SIGTERM; no polling delay or orphaned sleep process is needed.
|
||||
timeout -k 1 "$limit" "$@" >"$output" 2>/dev/null
|
||||
}
|
||||
|
||||
cert_matches_key() {
|
||||
local cert_file="$1" key_file="$2"
|
||||
local cert_pub key_pub tmp_dir
|
||||
run_with_timeout() {
|
||||
local limit="$1"
|
||||
shift
|
||||
run_with_timeout_output "$limit" /dev/null "$@"
|
||||
}
|
||||
|
||||
cert_pair_valid() {
|
||||
local cert_file="$1" key_file="$2" cert_host="$3" cert_ip="$4"
|
||||
local min_lifetime="${5:-0}" tmp_dir cert_pub key_pub san status
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
is_readable_regular_file "$key_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert.pub" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey </dev/null || \
|
||||
! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
status=1
|
||||
if run_with_timeout "$CERT_TIMEOUT" /bin/sh -c '
|
||||
[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] &&
|
||||
[ -f "$2" ] && [ -r "$2" ] && [ -s "$2" ]
|
||||
' gecoosac-file-check "$cert_file" "$key_file" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey -ext subjectAltName -checkend "$min_lifetime" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null && \
|
||||
run_with_timeout "$CERT_TIMEOUT" \
|
||||
openssl verify -partial_chain -trusted "$cert_file" "$cert_file" </dev/null; then
|
||||
# Trust the configured leaf only for the local validity-period check;
|
||||
# browser trust still depends on the certificate installed by the user.
|
||||
cert_pub="$(sed -n '/^-----BEGIN PUBLIC KEY-----$/,/^-----END PUBLIC KEY-----$/p' "$tmp_dir/cert")"
|
||||
key_pub="$(cat "$tmp_dir/key.pub")"
|
||||
if [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]; then
|
||||
status=0
|
||||
if [ -n "$cert_host" ]; then
|
||||
san="$(cat "$tmp_dir/cert")"
|
||||
san_has_entry "$san" "DNS:${cert_host}" || status=1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || status=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
cert_pub="$(cat "$tmp_dir/cert.pub" 2>/dev/null)"
|
||||
key_pub="$(cat "$tmp_dir/key.pub" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
[ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]
|
||||
return "$status"
|
||||
}
|
||||
|
||||
generate_default_cert() {
|
||||
@@ -389,7 +146,7 @@ generate_default_cert() {
|
||||
cert_ip="$(uci -q get network.lan.ipaddr)"
|
||||
is_ipv4 "$cert_ip" || cert_ip=""
|
||||
|
||||
if is_readable_regular_file "$DEFAULT_KEY_FILE" && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
if cert_pair_valid "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -423,9 +180,7 @@ generate_default_cert() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$tmp_crt" ] || [ ! -s "$tmp_key" ] || \
|
||||
! cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" || \
|
||||
! cert_matches_key "$tmp_crt" "$tmp_key" || \
|
||||
if ! cert_pair_valid "$tmp_crt" "$tmp_key" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE" || \
|
||||
! chmod 600 "$tmp_key" || ! chmod 644 "$tmp_crt"; then
|
||||
rm -f "$tmp_crt" "$tmp_key"
|
||||
logger -t gecoosac "failed to validate default HTTPS certificate"
|
||||
@@ -468,10 +223,8 @@ generate_default_cert() {
|
||||
old_key=1
|
||||
fi
|
||||
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && \
|
||||
mv "$tmp_crt" "$DEFAULT_CRT_FILE" && \
|
||||
cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && \
|
||||
cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
# Both temporary files were validated above; rename preserves their contents.
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && mv "$tmp_crt" "$DEFAULT_CRT_FILE"; then
|
||||
rm -rf "$backup_dir"
|
||||
return 0
|
||||
fi
|
||||
@@ -601,31 +354,6 @@ ensure_secure_dir_tree() {
|
||||
done
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_upload_storage_root() {
|
||||
local path="$1" root
|
||||
|
||||
@@ -677,10 +405,8 @@ prepare_service() {
|
||||
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
|
||||
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
|
||||
generate_default_cert || return 1
|
||||
fi
|
||||
|
||||
if ! cert_matches_key "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate and key do not match or cannot be read"
|
||||
elif ! cert_pair_valid "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate is not currently valid, or its private key cannot be read or does not match"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
changed=0
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
LEGACY_UPLOAD_DIR=/etc/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
OLD_CRT_FILE=/etc/gecoosac/tls/1.crt
|
||||
OLD_KEY_FILE=/etc/gecoosac/tls/1.key
|
||||
CONFIG_COMPAT=2
|
||||
@@ -26,192 +23,6 @@ set_default() {
|
||||
changed=1
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root line mount_path
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS= read -r line; do
|
||||
mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2
|
||||
[ -n "$mount_path" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
normalize_upload_dir() {
|
||||
local upload_dir normalized
|
||||
|
||||
@@ -229,38 +40,45 @@ normalize_upload_dir() {
|
||||
}
|
||||
|
||||
configured_path_in_legacy_upload() {
|
||||
local option path resolved
|
||||
local physical="$1" option role path
|
||||
|
||||
for option in db_dir piddir crt_file key_file; do
|
||||
path="$(uci -q get "gecoosac.config.${option}")"
|
||||
[ -n "$path" ] || continue
|
||||
path="$(normalize_path "$path")" || return 0
|
||||
is_path_in_dir "$path" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 0
|
||||
resolved="$(normalize_path "$resolved")" || return 0
|
||||
is_path_in_dir "$resolved" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
fi
|
||||
case "$option" in
|
||||
db_dir) role=db ;;
|
||||
piddir) role=pid ;;
|
||||
*) role="file" ;;
|
||||
esac
|
||||
protected_path_in_dir "$path" "$role" "$physical" "$LEGACY_UPLOAD_DIR" "$physical"
|
||||
case "$?" in
|
||||
1) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup_legacy_upload_dir() {
|
||||
local mount_state
|
||||
local physical mount_state
|
||||
|
||||
[ -e "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ] || return 0
|
||||
if [ ! -d "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
if configured_path_in_legacy_upload; then
|
||||
physical="$(readlink -f "$LEGACY_UPLOAD_DIR" 2>/dev/null)" || {
|
||||
logger -t gecoosac "unable to resolve legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
}
|
||||
physical="$(normalize_path "$physical")" || return 0
|
||||
if configured_path_in_legacy_upload "$physical"; then
|
||||
logger -t gecoosac "preserving legacy upload path referenced by configuration: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
path_has_mount "$LEGACY_UPLOAD_DIR"
|
||||
path_has_mount "$physical"
|
||||
mount_state="$?"
|
||||
case "$mount_state" in
|
||||
0)
|
||||
@@ -273,7 +91,13 @@ cleanup_legacy_upload_dir() {
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -rf "$LEGACY_UPLOAD_DIR" || {
|
||||
# Apply the same ownership and ancestor-symlink policy as RPC cleanup.
|
||||
# Relocated legacy directories are preserved for manual review.
|
||||
if ! is_secure_upload_dir "$LEGACY_UPLOAD_DIR" || [ "$physical" != "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
rm -rf "$physical" || {
|
||||
logger -t gecoosac "failed to remove legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 1
|
||||
}
|
||||
@@ -310,7 +134,7 @@ is_managed_cert_path() {
|
||||
|
||||
is_regular_file_or_absent() {
|
||||
[ ! -e "$1" ] && [ ! -L "$1" ] && return 0
|
||||
[ -f "$1" ] && [ ! -L "$1" ]
|
||||
[ -f "$1" ] && [ ! -L "$1" ] && is_secure_upload_dir "${1%/*}"
|
||||
}
|
||||
|
||||
migrate_default_certificates() {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
# Shared path policy for the service, configuration migration and LuCI RPC.
|
||||
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root mount_id parent_id device mount_root mount_path rest
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS=' ' read -r mount_id parent_id device mount_root mount_path rest; do
|
||||
[ -n "$mount_path" ] && [ -n "$rest" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# The checked root must be a resolved, validated directory. Return 0 for a
|
||||
# protected path, 1 for an unrelated path, or 2 when validation is impossible.
|
||||
# Keep the original spelling when resolving symlinks: link/../file and its
|
||||
# lexical normalization can refer to different files.
|
||||
protected_path_in_dir() {
|
||||
local raw_path="$1" role="$2" checked_root="$3"
|
||||
local live_logical="${4:-$3}" live_physical="${5:-$3}"
|
||||
local path="$1" physical real_path suffix mapped
|
||||
|
||||
[ -n "$path" ] || return 1
|
||||
|
||||
path="$(normalize_path "$path")" || return 2
|
||||
physical="$(managed_dir_path "$role" "$path")" || return 2
|
||||
checked_root="$(normalize_path "$checked_root")" || return 2
|
||||
live_logical="$(normalize_path "$live_logical")" || return 2
|
||||
live_physical="$(normalize_path "$live_physical")" || return 2
|
||||
|
||||
is_path_in_dir "$path" "$checked_root" && return 0
|
||||
is_path_in_dir "$physical" "$checked_root" && return 0
|
||||
real_path="$(readlink -f "$raw_path" 2>/dev/null)" || return 2
|
||||
[ -n "$real_path" ] || return 2
|
||||
real_path="$(normalize_path "$real_path")" || return 2
|
||||
is_path_in_dir "$real_path" "$checked_root" && return 0
|
||||
|
||||
[ "$checked_root" != "$live_physical" ] || {
|
||||
is_path_in_dir "$path" "$live_logical" && return 0
|
||||
is_path_in_dir "$physical" "$live_physical" && return 0
|
||||
if [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
suffix=
|
||||
if is_path_in_dir "$path" "$live_logical"; then
|
||||
suffix="${path#"$live_logical"}"
|
||||
elif is_path_in_dir "$physical" "$live_physical"; then
|
||||
suffix="${physical#"$live_physical"}"
|
||||
elif [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
suffix="${real_path#"$live_physical"}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
mapped="${checked_root%/}${suffix}"
|
||||
[ -e "$mapped" ] || [ -L "$mapped" ]
|
||||
}
|
||||
+1
-4
@@ -9,7 +9,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=glorytun
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=95fa5ead2154adb546bcdb454c56c876e9f60bb5
|
||||
PKG_SOURCE:=glorytun-$(PKG_VERSION).tar.gz
|
||||
@@ -50,13 +50,10 @@ endef
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/glorytun $(1)/usr/sbin/$(PKG_NAME)
|
||||
$(INSTALL_BIN) files/glorytun-irq-affinity.sh $(1)/usr/sbin/glorytun-irq-affinity.sh
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) init $(1)/etc/init.d/$(PKG_NAME)
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DATA) glorytun.config $(1)/etc/config/glorytun
|
||||
$(INSTALL_DIR) $(1)/etc/glorytun
|
||||
$(INSTALL_DATA) files/glorytun-sysctl.conf $(1)/etc/glorytun/glorytun-sysctl.conf
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set IRQ affinity for glorytun interfaces
|
||||
set_irq_affinity() {
|
||||
local irq=$1
|
||||
local cpu=$2
|
||||
local mask=$(printf "%x" $((1 << $cpu)))
|
||||
|
||||
echo "Setting IRQ $irq to CPU $cpu (mask: $mask)"
|
||||
echo $mask > /proc/irq/$irq/smp_affinity
|
||||
}
|
||||
|
||||
# Get interface IRQs
|
||||
get_interface_irqs() {
|
||||
local interface=$1
|
||||
grep -E "$interface" /proc/interrupts | awk '{print $1}' | tr -d ':'
|
||||
}
|
||||
|
||||
# Main function
|
||||
setup_irq_affinity() {
|
||||
# Get available CPU cores
|
||||
local cpu_count=$(grep -c processor /proc/cpuinfo)
|
||||
echo "Available CPU cores: $cpu_count"
|
||||
|
||||
# Determine tun interface
|
||||
local tun_interface=$(ip link | grep tun | awk -F': ' '{print $2}' | head -n1)
|
||||
if [ -z "$tun_interface" ]; then
|
||||
echo "No tun interface found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Found tun interface: $tun_interface"
|
||||
|
||||
# Get IRQs for tun interface
|
||||
local irqs=$(get_interface_irqs $tun_interface)
|
||||
if [ -z "$irqs" ]; then
|
||||
echo "No IRQs found for $tun_interface"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Distribute IRQs across available cores (except CPU0)
|
||||
local cpu=1
|
||||
for irq in $irqs; do
|
||||
set_irq_affinity $irq $cpu
|
||||
cpu=$(( (cpu + 1) % cpu_count ))
|
||||
if [ $cpu -eq 0 ]; then
|
||||
cpu=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Set RPS (Receive Packet Steering) for better packet distribution
|
||||
if [ -e /sys/class/net/$tun_interface/queues/rx-0/rps_cpus ]; then
|
||||
# Calculate mask for all CPUs except CPU0
|
||||
local rps_mask=$(printf "%x" $(( (1 << $cpu_count) - 2 )))
|
||||
echo "Setting RPS mask to $rps_mask for $tun_interface"
|
||||
echo $rps_mask > /sys/class/net/$tun_interface/queues/rx-0/rps_cpus
|
||||
fi
|
||||
|
||||
# Set RFS (Receive Flow Steering) limits if available
|
||||
if [ -e /proc/sys/net/core/rps_sock_flow_entries ]; then
|
||||
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
|
||||
if [ -e /sys/class/net/$tun_interface/queues/rx-0/rps_flow_cnt ]; then
|
||||
echo 32768 > /sys/class/net/$tun_interface/queues/rx-0/rps_flow_cnt
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set XPS (Transmit Packet Steering) if available
|
||||
if [ -e /sys/class/net/$tun_interface/queues/tx-0/xps_cpus ]; then
|
||||
# Use all CPUs except CPU0 for transmit
|
||||
local xps_mask=$(printf "%x" $(( (1 << $cpu_count) - 2 )))
|
||||
echo "Setting XPS mask to $xps_mask for $tun_interface"
|
||||
echo $xps_mask > /sys/class/net/$tun_interface/queues/tx-0/xps_cpus
|
||||
fi
|
||||
|
||||
# Increase network queue length for better performance
|
||||
if [ -e /sys/class/net/$tun_interface/tx_queue_len ]; then
|
||||
echo 1000 > /sys/class/net/$tun_interface/tx_queue_len
|
||||
fi
|
||||
|
||||
echo "IRQ affinity setup completed for $tun_interface"
|
||||
}
|
||||
|
||||
# Run the main function
|
||||
setup_irq_affinity
|
||||
|
||||
exit 0
|
||||
@@ -1,27 +0,0 @@
|
||||
# Glorytun-specific optimizations
|
||||
# Note: These settings complement the system-wide sysctl parameters
|
||||
|
||||
# Buffer size optimization for glorytun tunnels
|
||||
# Increase only if not already set in system configuration
|
||||
net.core.rmem_max = 16777216
|
||||
net.core.wmem_max = 16777216
|
||||
net.core.optmem_max = 16777216
|
||||
|
||||
# Enable BBR congestion control for better tunnel performance
|
||||
net.core.default_qdisc = fq
|
||||
|
||||
# RPS/RFS settings for tunnel interfaces
|
||||
# These are critical for multicore performance with glorytun
|
||||
net.core.rps_sock_flow_entries = 32768
|
||||
|
||||
# Network device tuning for tunnel interfaces
|
||||
net.core.netdev_max_backlog = 10000
|
||||
net.core.netdev_budget = 600
|
||||
net.core.netdev_budget_usecs = 10000
|
||||
|
||||
# Lower TCP timeout for faster recovery of tunnel connections
|
||||
net.ipv4.tcp_keepalive_intvl = 10
|
||||
net.ipv4.tcp_keepalive_probes = 6
|
||||
|
||||
# Disable slow start after idle for more consistent tunnel performance
|
||||
net.ipv4.tcp_slow_start_after_idle = 0
|
||||
@@ -11,6 +11,5 @@ config glorytun 'vpn'
|
||||
option localip '10.255.255.2'
|
||||
option remoteip '10.255.255.1'
|
||||
option multiqueue '1'
|
||||
option cpu_affinity '1'
|
||||
option label 'Default VPN'
|
||||
option timeout '10000'
|
||||
+6
-56
@@ -18,14 +18,6 @@ _err() {
|
||||
logger -p daemon.err -t ${PROG_NAME} "$@"
|
||||
}
|
||||
|
||||
# Apply system optimizations
|
||||
apply_sysctl_optimizations() {
|
||||
if [ -f /etc/glorytun/glorytun-sysctl.conf ]; then
|
||||
_log "Applying system optimizations"
|
||||
sysctl -q -p /etc/glorytun/glorytun-sysctl.conf
|
||||
fi
|
||||
}
|
||||
|
||||
validate_section() {
|
||||
uci_validate_section glorytun glorytun "${1}" \
|
||||
'enable:bool:0' \
|
||||
@@ -38,50 +30,13 @@ validate_section() {
|
||||
'timeout:uinteger:10000' \
|
||||
'chacha20:bool:0' \
|
||||
'multiqueue:bool:1' \
|
||||
'cpu_affinity:bool:1' \
|
||||
'proto:string' \
|
||||
'localip:string' \
|
||||
'remoteip:string'
|
||||
}
|
||||
|
||||
# New function for CPU core optimization
|
||||
optimize_cpu_cores() {
|
||||
local dev="$1"
|
||||
local multiqueue="$2"
|
||||
local cpu_affinity="$3"
|
||||
|
||||
if [ "$cpu_affinity" = "1" ]; then
|
||||
_log "Setting up CPU affinity for $dev"
|
||||
if [ -x /usr/sbin/glorytun-irq-affinity.sh ]; then
|
||||
/usr/sbin/glorytun-irq-affinity.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$multiqueue" = "1" ]; then
|
||||
_log "Enabling multiqueue for $dev"
|
||||
|
||||
# Get CPU count
|
||||
local cpu_count=$(grep -c processor /proc/cpuinfo)
|
||||
if [ "$cpu_count" -gt 1 ]; then
|
||||
# Use half of available CPUs but at least 2
|
||||
local queue_count=$(( cpu_count / 2 ))
|
||||
[ "$queue_count" -lt 2 ] && queue_count=2
|
||||
|
||||
# Set multiqueue parameters
|
||||
ip link set dev "$dev" multiqueue on
|
||||
|
||||
# Increase buffer sizes for better performance
|
||||
if [ -e /proc/sys/net/core/rmem_max ]; then
|
||||
echo 16777216 > /proc/sys/net/core/rmem_max
|
||||
echo 16777216 > /proc/sys/net/core/wmem_max
|
||||
echo 16777216 > /proc/sys/net/core/optmem_max
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_instance() {
|
||||
local enable key host port dev mptcp proto chacha20 mode multiqueue cpu_affinity timeout localip remoteip
|
||||
local enable key host port dev mptcp proto chacha20 mode multiqueue timeout localip remoteip
|
||||
local key_dir="/var/run/${PROG_NAME}"
|
||||
local key_file="${key_dir}/${1}.key"
|
||||
|
||||
@@ -91,9 +46,6 @@ start_instance() {
|
||||
}
|
||||
|
||||
[ "${enable}" = "1" ] || return 1
|
||||
|
||||
apply_sysctl_optimizations
|
||||
|
||||
[ "${proto}" = "tcp" ] || return 1
|
||||
[ -n "${key}" ] || {
|
||||
_err "Key empty"
|
||||
@@ -148,16 +100,14 @@ start_instance() {
|
||||
|
||||
procd_close_instance
|
||||
|
||||
# Apply CPU optimizations after the instance is started
|
||||
optimize_cpu_cores "$dev" "$multiqueue" "$cpu_affinity"
|
||||
|
||||
# Configure interface IP addresses
|
||||
# Configure the tunnel addresses once glorytun has created the device.
|
||||
# RPS/XPS steering for it is set by /etc/hotplug.d/net/20-rps-xps
|
||||
# (openmptcprouter package), which handles every interface.
|
||||
if [ -n "$localip" ] && [ -n "$remoteip" ]; then
|
||||
(
|
||||
# Wait for device to be created
|
||||
local count=0
|
||||
count=0
|
||||
while [ $count -lt 10 ]; do
|
||||
if ip link show "$dev" >/dev/null 2>&1; then
|
||||
if [ -d "/sys/class/net/$dev" ]; then
|
||||
ifconfig "$dev" "$localip" pointopoint "$remoteip" up && \
|
||||
_log "Configured $dev with IP $localip peer $remoteip"
|
||||
break
|
||||
|
||||
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=istoreenhance
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=0.7.17
|
||||
PKG_RELEASE:=7
|
||||
PKG_SOURCE_DATE:=0.8.0
|
||||
PKG_RELEASE:=8
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_HEXCODE=8664
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_kaiplus:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kaiplus
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=13
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kaiplus-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"standalone": {
|
||||
"entry": "index.html",
|
||||
"basePath": "/apps/kaiplus/"
|
||||
"basePath": "/apps/kaiplus/",
|
||||
"url": "/apps/kaiplus/"
|
||||
},
|
||||
"config": {
|
||||
"providerOrder": ["uci"],
|
||||
|
||||
@@ -8,3 +8,5 @@ config kaiplus
|
||||
option 'bind_addr' '0.0.0.0'
|
||||
option 'base_path' '/apps/kaiplus/'
|
||||
option 'system_role' 'istoreos'
|
||||
option 'release_channel' 'stable'
|
||||
option 'auth_mode' 'disabled'
|
||||
|
||||
@@ -13,6 +13,8 @@ get_config() {
|
||||
config_get bind_addr "$1" bind_addr "0.0.0.0"
|
||||
config_get base_path "$1" base_path "/apps/kaiplus/"
|
||||
config_get system_role "$1" system_role "istoreos"
|
||||
config_get release_channel "$1" release_channel "stable"
|
||||
config_get auth_mode "$1" auth_mode "disabled"
|
||||
}
|
||||
|
||||
mkdir_p() {
|
||||
@@ -32,10 +34,12 @@ start_service() {
|
||||
fi
|
||||
|
||||
mkdir_p "$data_dir"
|
||||
umask 077
|
||||
mkdir_p "$data_dir/workspace"
|
||||
mkdir_p "$data_dir/cache"
|
||||
mkdir_p "$data_dir/config"
|
||||
mkdir_p "$data_dir/state"
|
||||
chmod 0700 "$data_dir/config" "$data_dir/state"
|
||||
|
||||
helper_token_file="$data_dir/state/ssh-helper.token"
|
||||
if [ ! -s "$helper_token_file" ]; then
|
||||
@@ -58,6 +62,9 @@ start_service() {
|
||||
procd_set_param env \
|
||||
KAIPLUS_HOME="$data_dir" \
|
||||
KAIPLUS_SYSTEM_ROLE="$system_role" \
|
||||
KAIPLUS_RELEASE_CHANNEL="$release_channel" \
|
||||
KAIPLUS_AUTH_MODE="$auth_mode" \
|
||||
LINKEASE_AUTH_PROVIDER="$auth_mode" \
|
||||
KAIPLUS_LISTEN_MODE="tcp" \
|
||||
KAIPLUS_WORKSPACE_HELPER_DIR="/usr/share/kaiplus/helpers" \
|
||||
KAIPLUS_WORKSPACE_TOOL_INSTALL_DIR="/tmp/kaiplus_workspace_tool" \
|
||||
@@ -68,6 +75,9 @@ start_service() {
|
||||
procd_set_param env \
|
||||
KAIPLUS_HOME="$data_dir" \
|
||||
KAIPLUS_SYSTEM_ROLE="$system_role" \
|
||||
KAIPLUS_RELEASE_CHANNEL="$release_channel" \
|
||||
KAIPLUS_AUTH_MODE="$auth_mode" \
|
||||
LINKEASE_AUTH_PROVIDER="$auth_mode" \
|
||||
KAIPLUS_LISTEN_MODE="unix" \
|
||||
KAIPLUS_SOCKET_PATH="$socket_path" \
|
||||
KAIPLUS_WORKSPACE_HELPER_DIR="/usr/share/kaiplus/helpers" \
|
||||
@@ -88,6 +98,7 @@ start_service() {
|
||||
procd_append_param command --defaults-dir /usr/share/kaiplus/defaults
|
||||
procd_append_param command --base-path "$base_path"
|
||||
procd_append_param command --system-role "$system_role"
|
||||
procd_append_param command --release-channel "$release_channel"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2026 Dante <dante@seckv.com>
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=linkback
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Dante
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Routing and Redirection
|
||||
TITLE:=LinkBack gateway switch daemon
|
||||
DEPENDS:=+libuci +libubus +libubox +libblobmsg-json +ip-full
|
||||
MAINTAINER:=Dante
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
LinkBack: A highly efficient and lightweight C-based Multi-WAN link health monitoring
|
||||
and dynamic route metric failover/failback daemon. Supports Ping, DNS, and TCP probes.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/linkback
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/* $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
$(MAKE) -C $(PKG_BUILD_DIR) \
|
||||
CC="$(TARGET_CC)" \
|
||||
CFLAGS="$(TARGET_CFLAGS)" \
|
||||
LDFLAGS="$(TARGET_LDFLAGS)"
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
# Install C daemon binary
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/linkbackd $(1)/usr/sbin/linkbackd
|
||||
|
||||
# Install config and init files
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/linkback.config $(1)/etc/config/linkback
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/linkback.init $(1)/etc/init.d/linkback
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/linkback enable
|
||||
/etc/init.d/linkback restart
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
config global
|
||||
option enabled '0'
|
||||
option check_interval '5'
|
||||
option check_timeout '3'
|
||||
option recovery_delay '3'
|
||||
option failover_delay '2'
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
_get_global_enabled() {
|
||||
config_get_bool _GLOBAL_ENABLED "$1" "enabled" 0
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "linkback"
|
||||
|
||||
local _GLOBAL_ENABLED=0
|
||||
config_foreach _get_global_enabled "global"
|
||||
[ "$_GLOBAL_ENABLED" -eq 1 ] || return 0
|
||||
|
||||
procd_open_instance "main"
|
||||
procd_set_param command /usr/sbin/linkbackd
|
||||
procd_set_param respawn
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "linkback"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
TARGET := linkbackd
|
||||
OBJS := linkbackd.o
|
||||
|
||||
CFLAGS += -Wall -O2
|
||||
LIBS := -luci
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) $(CFLAGS) $(OBJS) -o $(TARGET) $(LDFLAGS) $(LIBS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f *.o $(TARGET)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,849 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <syslog.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <uci.h>
|
||||
#include "linkbackd.h"
|
||||
|
||||
// DNS Header struct for raw check
|
||||
struct dns_header {
|
||||
unsigned short id;
|
||||
unsigned short flags;
|
||||
unsigned short qdcount;
|
||||
unsigned short ancount;
|
||||
unsigned short nscount;
|
||||
unsigned short arcount;
|
||||
};
|
||||
|
||||
// Global daemon state
|
||||
static global_config_t global_cfg;
|
||||
static link_t links[MAX_LINKS];
|
||||
static int link_count = 0;
|
||||
static volatile bool keep_running = true;
|
||||
|
||||
// Prototypes
|
||||
static void restore_all_metrics(void);
|
||||
static bool validate_loaded_config(void);
|
||||
static void handle_signal(int sig) {
|
||||
syslog(LOG_INFO, "Received signal %d, exiting...", sig);
|
||||
keep_running = false;
|
||||
}
|
||||
|
||||
// Ubus popen JSON parser
|
||||
static bool get_interface_ubus_status(const char *ifname, char *device, int dev_len, char *gateway, int gw_len, bool *is_up) {
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "ubus call network.interface.%s status 2>/dev/null", ifname);
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) return false;
|
||||
|
||||
char buf[4096] = {0};
|
||||
int bytes_read = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
pclose(fp);
|
||||
|
||||
if (bytes_read <= 0) return false;
|
||||
|
||||
// Check if interface is up
|
||||
*is_up = false;
|
||||
char *p_up = strstr(buf, "\"up\":");
|
||||
if (p_up) {
|
||||
char *p_true = strstr(p_up, "true");
|
||||
// Ensure 'true' belongs to this property and isn't far away
|
||||
if (p_true && p_true - p_up < 10) {
|
||||
*is_up = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract physical device
|
||||
device[0] = '\0';
|
||||
char *p_dev = strstr(buf, "\"l3_device\":");
|
||||
int dev_key_len = 12;
|
||||
if (!p_dev) {
|
||||
p_dev = strstr(buf, "\"device\":");
|
||||
dev_key_len = 9;
|
||||
}
|
||||
if (p_dev) {
|
||||
char *p_start = strchr(p_dev + dev_key_len, '"');
|
||||
if (p_start) {
|
||||
char *p_end = strchr(p_start + 1, '"');
|
||||
if (p_end) {
|
||||
int len = p_end - p_start - 1;
|
||||
if (len >= dev_len) len = dev_len - 1;
|
||||
strncpy(device, p_start + 1, len);
|
||||
device[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract gateway/nexthop from route list
|
||||
gateway[0] = '\0';
|
||||
char *p_route = strstr(buf, "\"route\":");
|
||||
if (p_route) {
|
||||
char *p_nexthop = strstr(p_route, "\"nexthop\":");
|
||||
if (p_nexthop) {
|
||||
char *p_start = strchr(p_nexthop + 10, '"');
|
||||
if (p_start) {
|
||||
char *p_end = strchr(p_start + 1, '"');
|
||||
if (p_end) {
|
||||
int len = p_end - p_start - 1;
|
||||
if (len >= gw_len) len = gw_len - 1;
|
||||
strncpy(gateway, p_start + 1, len);
|
||||
gateway[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ping check
|
||||
static bool run_ping_check(const char *device, const char *target, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || target[0] == '\0') return false;
|
||||
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "ping -I %s -c 1 -W %d %s 2>/dev/null", device, timeout, target);
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) return false;
|
||||
|
||||
char line[128];
|
||||
bool ok = false;
|
||||
*rtt_ms = -1;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char *p = strstr(line, "time=");
|
||||
if (p) {
|
||||
ok = true;
|
||||
double rtt = atof(p + 5);
|
||||
*rtt_ms = (int)rtt;
|
||||
}
|
||||
}
|
||||
int status = pclose(fp);
|
||||
return ok && (WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
// DNS check
|
||||
static bool run_dns_check(const char *device, const char *dns_server, const char *domain, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || dns_server[0] == '\0' || domain[0] == '\0') return false;
|
||||
|
||||
struct timeval start, end;
|
||||
gettimeofday(&start, NULL);
|
||||
|
||||
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sockfd < 0) return false;
|
||||
|
||||
// Set non-blocking
|
||||
int flags = fcntl(sockfd, F_GETFL, 0);
|
||||
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Bind to device
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)) < 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in servaddr;
|
||||
memset(&servaddr, 0, sizeof(servaddr));
|
||||
servaddr.sin_family = AF_INET;
|
||||
servaddr.sin_port = htons(53);
|
||||
if (inet_pton(AF_INET, dns_server, &servaddr.sin_addr) <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format DNS packet
|
||||
unsigned char packet[512];
|
||||
memset(packet, 0, sizeof(packet));
|
||||
|
||||
struct dns_header *dns = (struct dns_header *)packet;
|
||||
dns->id = (unsigned short)htons(getpid());
|
||||
dns->flags = htons(0x0100);
|
||||
dns->qdcount = htons(1);
|
||||
|
||||
unsigned char *qname = packet + sizeof(struct dns_header);
|
||||
const char *src = domain;
|
||||
unsigned char *dst = qname;
|
||||
while (*src) {
|
||||
const char *next = strchr(src, '.');
|
||||
int len = next ? (next - src) : strlen(src);
|
||||
*dst++ = len;
|
||||
memcpy(dst, src, len);
|
||||
dst += len;
|
||||
src = next ? (next + 1) : (src + len);
|
||||
}
|
||||
*dst++ = 0;
|
||||
|
||||
unsigned short *qtype = (unsigned short *)dst;
|
||||
*qtype = htons(1); // A Record
|
||||
dst += 2;
|
||||
unsigned short *qclass = (unsigned short *)dst;
|
||||
*qclass = htons(1); // IN
|
||||
dst += 2;
|
||||
|
||||
int packet_len = dst - packet;
|
||||
|
||||
if (sendto(sockfd, packet, packet_len, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(sockfd, &readfds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int sel = select(sockfd + 1, &readfds, NULL, NULL, &tv);
|
||||
if (sel <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned char response[512];
|
||||
struct sockaddr_in from;
|
||||
socklen_t from_len = sizeof(from);
|
||||
int resp_len = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&from, &from_len);
|
||||
close(sockfd);
|
||||
|
||||
if (resp_len < 12) return false;
|
||||
|
||||
struct dns_header *resp_dns = (struct dns_header *)response;
|
||||
if (ntohs(resp_dns->id) != getpid()) return false;
|
||||
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TCP check
|
||||
static bool run_tcp_check(const char *device, const char *tcp_target, int port, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || tcp_target[0] == '\0' || port <= 0) return false;
|
||||
|
||||
struct timeval start, end;
|
||||
gettimeofday(&start, NULL);
|
||||
|
||||
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sockfd < 0) return false;
|
||||
|
||||
// Set non-blocking
|
||||
int flags = fcntl(sockfd, F_GETFL, 0);
|
||||
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Bind to device
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)) < 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, tcp_target, &addr.sin_addr) <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (rc < 0) {
|
||||
if (errno != EINPROGRESS) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
close(sockfd);
|
||||
return true;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(sockfd, &writefds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int sel = select(sockfd + 1, NULL, &writefds, NULL, &tv);
|
||||
if (sel <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
int optval;
|
||||
socklen_t optlen = sizeof(optval);
|
||||
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0 || optval != 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
close(sockfd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// UCI parser helper
|
||||
static bool load_config(void) {
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return false;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "linkback", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse global config
|
||||
global_cfg.enabled = false;
|
||||
|
||||
struct uci_section *global_sec = uci_lookup_section(ctx, pkg, "global");
|
||||
if (!global_sec) {
|
||||
struct uci_element *ge;
|
||||
uci_foreach_element(&pkg->sections, ge) {
|
||||
struct uci_section *s = uci_to_section(ge);
|
||||
if (strcmp(s->type, "global") == 0) {
|
||||
global_sec = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (global_sec) {
|
||||
const char *enabled = uci_lookup_option_string(ctx, global_sec, "enabled");
|
||||
global_cfg.enabled = (enabled && strcmp(enabled, "1") == 0);
|
||||
}
|
||||
|
||||
// Parse links
|
||||
link_count = 0;
|
||||
struct uci_element *e;
|
||||
uci_foreach_element(&pkg->sections, e) {
|
||||
struct uci_section *s = uci_to_section(e);
|
||||
if (strcmp(s->type, "link") != 0) continue;
|
||||
|
||||
const char *enabled = uci_lookup_option_string(ctx, s, "enabled");
|
||||
if (enabled && strcmp(enabled, "0") == 0) continue;
|
||||
|
||||
link_t *link = &links[link_count];
|
||||
memset(link, 0, sizeof(link_t));
|
||||
|
||||
const char *name = uci_lookup_option_string(ctx, s, "name");
|
||||
if (!name) continue;
|
||||
strncpy(link->name, name, MAX_NAME_LEN - 1);
|
||||
|
||||
link->enabled = true;
|
||||
|
||||
const char *priority = uci_lookup_option_string(ctx, s, "priority");
|
||||
int prio_val = priority ? atoi(priority) : 1;
|
||||
link->priority = (prio_val > 0) ? prio_val : 1;
|
||||
|
||||
link->metric = link->priority * 10;
|
||||
link->current_metric = link->metric;
|
||||
|
||||
// Parse ping targets (split by comma or space)
|
||||
const char *pings = uci_lookup_option_string(ctx, s, "ping_targets");
|
||||
if (pings) {
|
||||
char tmp[256];
|
||||
strncpy(tmp, pings, sizeof(tmp) - 1);
|
||||
tmp[sizeof(tmp) - 1] = '\0';
|
||||
char *token = strtok(tmp, ", \t");
|
||||
while (token && link->ping_target_count < MAX_TARGETS) {
|
||||
// Trim leading whitespace
|
||||
while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') {
|
||||
token++;
|
||||
}
|
||||
// Trim trailing whitespace
|
||||
char *end = token + strlen(token) - 1;
|
||||
while (end > token && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
if (*token != '\0') {
|
||||
strncpy(link->ping_targets[link->ping_target_count], token, MAX_IP_LEN - 1);
|
||||
link->ping_target_count++;
|
||||
}
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse DNS targets
|
||||
const char *dns_srv = uci_lookup_option_string(ctx, s, "dns_server");
|
||||
if (dns_srv) strncpy(link->dns_server, dns_srv, MAX_IP_LEN - 1);
|
||||
|
||||
const char *dns_dom = uci_lookup_option_string(ctx, s, "dns_domain");
|
||||
if (dns_dom) strncpy(link->dns_domain, dns_dom, MAX_DOMAIN_LEN - 1);
|
||||
|
||||
// Parse TCP targets
|
||||
const char *tcp_tgt = uci_lookup_option_string(ctx, s, "tcp_target");
|
||||
if (tcp_tgt) strncpy(link->tcp_target, tcp_tgt, MAX_IP_LEN - 1);
|
||||
|
||||
const char *tcp_p = uci_lookup_option_string(ctx, s, "tcp_port");
|
||||
if (tcp_p) link->tcp_port = atoi(tcp_p);
|
||||
|
||||
const char *interval = uci_lookup_option_string(ctx, s, "check_interval");
|
||||
link->check_interval = interval ? atoi(interval) : 5;
|
||||
|
||||
const char *timeout = uci_lookup_option_string(ctx, s, "check_timeout");
|
||||
link->check_timeout = timeout ? atoi(timeout) : 3;
|
||||
|
||||
const char *recovery = uci_lookup_option_string(ctx, s, "recovery_delay");
|
||||
link->recovery_delay = recovery ? atoi(recovery) : 3;
|
||||
|
||||
const char *failover = uci_lookup_option_string(ctx, s, "failover_delay");
|
||||
link->failover_delay = failover ? atoi(failover) : 2;
|
||||
|
||||
// Default runtime states
|
||||
link->healthy = true;
|
||||
link->is_up = false;
|
||||
link->last_checked = 0; // Force immediate check on startup
|
||||
|
||||
link_count++;
|
||||
if (link_count >= MAX_LINKS) break;
|
||||
}
|
||||
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
|
||||
if (!validate_loaded_config()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Startup-time defensive validation. LuCI-side checks can be bypassed by
|
||||
// direct UCI edits, so daemon must refuse unsafe/incomplete configs.
|
||||
static bool validate_loaded_config(void) {
|
||||
if (!global_cfg.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (link_count < 2) {
|
||||
syslog(LOG_ERR, "Invalid config: at least 2 enabled monitored links are required, got %d.", link_count);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *a = &links[i];
|
||||
|
||||
if (a->name[0] == '\0') {
|
||||
syslog(LOG_ERR, "Invalid config: link[%d] has empty interface name.", i);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->priority <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has invalid priority %d (must be > 0).", a->name, a->priority);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->metric <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has invalid metric %d (must be > 0).", a->name, a->metric);
|
||||
return false;
|
||||
}
|
||||
|
||||
// No duplicate priorities
|
||||
for (int j = i + 1; j < link_count; j++) {
|
||||
link_t *b = &links[j];
|
||||
if (a->priority == b->priority) {
|
||||
syslog(LOG_ERR, "Invalid config: duplicate priority %d on links %s and %s.", a->priority, a->name, b->name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool has_ping = (a->ping_target_count > 0);
|
||||
bool has_dns = (a->dns_server[0] != '\0' && a->dns_domain[0] != '\0');
|
||||
bool has_tcp = (a->tcp_target[0] != '\0' && a->tcp_port > 0);
|
||||
|
||||
int check_count = 0;
|
||||
if (has_ping) check_count++;
|
||||
if (has_dns) check_count++;
|
||||
if (has_tcp) check_count++;
|
||||
|
||||
if (check_count == 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has no complete health-check probe configured.", a->name);
|
||||
return false;
|
||||
}
|
||||
if (check_count > 1) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has multiple health-check probes configured. Only one check type is allowed.", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((a->dns_server[0] != '\0') != (a->dns_domain[0] != '\0')) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s DNS probe is incomplete (dns_server + dns_domain required).", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((a->tcp_target[0] != '\0') != (a->tcp_port > 0)) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s TCP probe is incomplete (tcp_target + tcp_port required).", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->check_interval <= 0 || a->check_timeout <= 0 ||
|
||||
a->recovery_delay <= 0 || a->failover_delay <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s timing values must be > 0.", a->name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve the real default route metric for a device from /proc/net/route
|
||||
static int get_system_route_metric(const char *device, int expected_metric) {
|
||||
if (device[0] == '\0') return -1;
|
||||
FILE *fp = fopen("/proc/net/route", "r");
|
||||
if (!fp) return -1;
|
||||
|
||||
char line[256];
|
||||
char iface[32];
|
||||
unsigned long dest;
|
||||
int metric = -1;
|
||||
int first_found_metric = -1;
|
||||
bool found_expected = false;
|
||||
|
||||
// Skip header line
|
||||
if (fgets(line, sizeof(line), fp)) {
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
// Destination is 2nd column, Metric is 7th column, dest is in hex.
|
||||
// Use %*s for Gateway and Flags to safely skip non-numeric characters.
|
||||
if (sscanf(line, "%31s %lx %*s %*s %*d %*d %d", iface, &dest, &metric) == 3) {
|
||||
if (strcmp(iface, device) == 0 && dest == 0) {
|
||||
if (first_found_metric == -1) {
|
||||
first_found_metric = metric;
|
||||
}
|
||||
if (metric == expected_metric) {
|
||||
found_expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
if (found_expected) {
|
||||
return expected_metric;
|
||||
}
|
||||
return first_found_metric;
|
||||
}
|
||||
|
||||
// Compare priority for sorting (lowest priority number is highest precedence)
|
||||
static int compare_links(const void *a, const void *b) {
|
||||
link_t *la = (link_t *)a;
|
||||
link_t *lb = (link_t *)b;
|
||||
return la->priority - lb->priority;
|
||||
}
|
||||
|
||||
// Dynamic route update using ip route command
|
||||
static void update_route_metric(link_t *link, int new_metric, int old_metric_to_delete) {
|
||||
if (link->device[0] == '\0') return;
|
||||
|
||||
char cmd[512];
|
||||
|
||||
// 1. Delete the specified old metric to prevent duplicate routes
|
||||
if (old_metric_to_delete != -1 && old_metric_to_delete != new_metric) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, old_metric_to_delete);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, old_metric_to_delete);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
// 2. Delete the current_metric if it is different from new_metric and old_metric_to_delete
|
||||
if (link->current_metric != new_metric && link->current_metric != old_metric_to_delete) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->current_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->current_metric);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
// 3. Add/replace with new_metric
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, new_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default dev %s metric %d 2>/dev/null",
|
||||
link->device, new_metric);
|
||||
}
|
||||
|
||||
syslog(LOG_INFO, "Applying route metric update on link %s (%s, priority %d): %d -> %d",
|
||||
link->name, link->device, link->priority, link->current_metric, new_metric);
|
||||
|
||||
int rc = system(cmd);
|
||||
if (rc == 0) {
|
||||
link->current_metric = new_metric;
|
||||
} else {
|
||||
syslog(LOG_ERR, "Failed to apply route update for %s (priority %d) using cmd: %s", link->name, link->priority, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default metrics on exit
|
||||
static void restore_all_metrics(void) {
|
||||
syslog(LOG_INFO, "Restoring all interface metrics on exit...");
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
if (link->enabled && link->is_up && link->device[0] != '\0') {
|
||||
char cmd[512];
|
||||
// Delete floated metric if it was in fault state
|
||||
if (link->current_metric != link->metric) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->current_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->current_metric);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->metric);
|
||||
}
|
||||
int rc = system(cmd);
|
||||
if (rc == 0) {
|
||||
syslog(LOG_INFO, "Successfully restored default metric %d for interface %s (priority %d)", link->metric, link->name, link->priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write status file to /var/run/linkback.json
|
||||
static void write_status_json(void) {
|
||||
FILE *fp = fopen(STATUS_FILE, "w");
|
||||
if (!fp) return;
|
||||
|
||||
fprintf(fp, "{\n");
|
||||
fprintf(fp, " \"enabled\": %s,\n", global_cfg.enabled ? "true" : "false");
|
||||
fprintf(fp, " \"check_interval\": 5,\n");
|
||||
|
||||
// Find current active gateway link (first healthy link ordered by priority)
|
||||
char active_link[MAX_NAME_LEN] = "none";
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
if (links[i].is_up && links[i].healthy) {
|
||||
strncpy(active_link, links[i].name, MAX_NAME_LEN - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
fprintf(fp, " \"active_link\": \"%s\",\n", active_link);
|
||||
fprintf(fp, " \"links\": [\n");
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
fprintf(fp, " {\n");
|
||||
fprintf(fp, " \"name\": \"%s\",\n", link->name);
|
||||
fprintf(fp, " \"priority\": %d,\n", link->priority);
|
||||
fprintf(fp, " \"metric\": %d,\n", link->metric);
|
||||
fprintf(fp, " \"current_metric\": %d,\n", link->current_metric);
|
||||
fprintf(fp, " \"healthy\": %s,\n", link->healthy ? "true" : "false");
|
||||
fprintf(fp, " \"is_up\": %s,\n", link->is_up ? "true" : "false");
|
||||
fprintf(fp, " \"device\": \"%s\",\n", link->device);
|
||||
fprintf(fp, " \"gateway\": \"%s\",\n", link->gateway);
|
||||
fprintf(fp, " \"score\": %d,\n", link->current_score);
|
||||
fprintf(fp, " \"threshold\": %d,\n", link->weight_threshold);
|
||||
fprintf(fp, " \"check_interval\": %d,\n", link->check_interval);
|
||||
fprintf(fp, " \"check_timeout\": %d,\n", link->check_timeout);
|
||||
fprintf(fp, " \"recovery_delay\": %d,\n", link->recovery_delay);
|
||||
fprintf(fp, " \"failover_delay\": %d,\n", link->failover_delay);
|
||||
const char *type = "none";
|
||||
if (link->ping_target_count > 0) type = "ping";
|
||||
else if (link->dns_server[0] != '\0') type = "dns";
|
||||
else if (link->tcp_target[0] != '\0') type = "tcp";
|
||||
|
||||
fprintf(fp, " \"check_type\": \"%s\",\n", type);
|
||||
fprintf(fp, " \"ping\": {\"ok\": %s, \"rtt\": %d},\n", link->ping_ok ? "true" : "false", link->ping_rtt_ms);
|
||||
fprintf(fp, " \"dns\": {\"ok\": %s, \"rtt\": %d},\n", link->dns_ok ? "true" : "false", link->dns_rtt_ms);
|
||||
fprintf(fp, " \"tcp\": {\"ok\": %s, \"rtt\": %d}\n", link->tcp_ok ? "true" : "false", link->tcp_rtt_ms);
|
||||
fprintf(fp, " }%s\n", (i == link_count - 1) ? "" : ",");
|
||||
}
|
||||
fprintf(fp, " ]\n");
|
||||
fprintf(fp, "}\n");
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// Setup syslog
|
||||
openlog("linkbackd", LOG_PID | LOG_NDELAY, LOG_DAEMON);
|
||||
syslog(LOG_INFO, "Starting LinkBack daemon...");
|
||||
|
||||
// Register signal handlers for clean exits and metric restoration
|
||||
signal(SIGTERM, handle_signal);
|
||||
signal(SIGINT, handle_signal);
|
||||
|
||||
// Load configuration
|
||||
if (!load_config()) {
|
||||
syslog(LOG_ERR, "Failed to load linkback config. Exiting.");
|
||||
closelog();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!global_cfg.enabled) {
|
||||
syslog(LOG_WARNING, "LinkBack is disabled globally in configuration. Exiting.");
|
||||
closelog();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sort links by priority (lowest priority number first)
|
||||
qsort(links, link_count, sizeof(link_t), compare_links);
|
||||
|
||||
syslog(LOG_INFO, "Loaded %d monitored interfaces. Starting health check scheduler.", link_count);
|
||||
|
||||
// Core check loop
|
||||
while (keep_running) {
|
||||
time_t now = time(NULL);
|
||||
bool any_checked = false;
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
|
||||
// Check if this interface is due for checking
|
||||
if (now - link->last_checked < link->check_interval) {
|
||||
continue;
|
||||
}
|
||||
link->last_checked = now;
|
||||
any_checked = true;
|
||||
|
||||
// 1. Fetch real-time netifd status
|
||||
char dev[MAX_NAME_LEN] = {0};
|
||||
char gw[MAX_IP_LEN] = {0};
|
||||
bool is_up = false;
|
||||
|
||||
get_interface_ubus_status(link->name, dev, sizeof(dev), gw, sizeof(gw), &is_up);
|
||||
link->is_up = is_up;
|
||||
strncpy(link->device, dev, MAX_NAME_LEN - 1);
|
||||
strncpy(link->gateway, gw, MAX_IP_LEN - 1);
|
||||
|
||||
if (!is_up || dev[0] == '\0') {
|
||||
// Interface is down in netifd, mark unhealthy immediately
|
||||
link->healthy = false;
|
||||
link->current_score = 0;
|
||||
link->ping_ok = false;
|
||||
link->dns_ok = false;
|
||||
link->tcp_ok = false;
|
||||
link->consecutive_success = 0;
|
||||
link->consecutive_failure = 0;
|
||||
|
||||
// If it still has low metric, float it
|
||||
if (link->current_metric == link->metric) {
|
||||
update_route_metric(link, 1000 + link->metric, -1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Perform health checks
|
||||
bool check_success = false;
|
||||
|
||||
if (link->ping_target_count > 0) {
|
||||
link->ping_ok = false;
|
||||
link->ping_rtt_ms = -1;
|
||||
for (int p = 0; p < link->ping_target_count; p++) {
|
||||
int rtt = -1;
|
||||
if (run_ping_check(link->device, link->ping_targets[p], link->check_timeout, &rtt)) {
|
||||
link->ping_ok = true;
|
||||
link->ping_rtt_ms = rtt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
check_success = link->ping_ok;
|
||||
}
|
||||
else if (link->dns_server[0] != '\0' && link->dns_domain[0] != '\0') {
|
||||
link->dns_ok = false;
|
||||
link->dns_rtt_ms = -1;
|
||||
int rtt = -1;
|
||||
if (run_dns_check(link->device, link->dns_server, link->dns_domain, link->check_timeout, &rtt)) {
|
||||
link->dns_ok = true;
|
||||
link->dns_rtt_ms = rtt;
|
||||
}
|
||||
check_success = link->dns_ok;
|
||||
}
|
||||
else if (link->tcp_target[0] != '\0' && link->tcp_port > 0) {
|
||||
link->tcp_ok = false;
|
||||
link->tcp_rtt_ms = -1;
|
||||
int rtt = -1;
|
||||
if (run_tcp_check(link->device, link->tcp_target, link->tcp_port, link->check_timeout, &rtt)) {
|
||||
link->tcp_ok = true;
|
||||
link->tcp_rtt_ms = rtt;
|
||||
}
|
||||
check_success = link->tcp_ok;
|
||||
}
|
||||
|
||||
link->current_score = check_success ? 1 : 0;
|
||||
|
||||
// 3. Evaluate health state changes (filtering and delay)
|
||||
|
||||
if (check_success) {
|
||||
link->consecutive_success++;
|
||||
link->consecutive_failure = 0;
|
||||
|
||||
if (!link->healthy && link->consecutive_success >= link->recovery_delay) {
|
||||
// Recovered! Failback!
|
||||
link->healthy = true;
|
||||
syslog(LOG_NOTICE, "Link %s (%s, priority %d) recovered to healthy after %d successes.",
|
||||
link->name, link->device, link->priority, link->consecutive_success);
|
||||
|
||||
// Restore original metric
|
||||
update_route_metric(link, link->metric, -1);
|
||||
}
|
||||
} else {
|
||||
link->consecutive_failure++;
|
||||
link->consecutive_success = 0;
|
||||
|
||||
if (link->healthy && link->consecutive_failure >= link->failover_delay) {
|
||||
// Failed! Failover!
|
||||
link->healthy = false;
|
||||
syslog(LOG_WARNING, "Link %s (%s, priority %d) went down after %d failures.",
|
||||
link->name, link->device, link->priority, link->consecutive_failure);
|
||||
|
||||
// Push metric out of choice range
|
||||
update_route_metric(link, 1000 + link->metric, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Active routing metric self-healing to prevent external/netifd interference
|
||||
if (link->device[0] != '\0') {
|
||||
int expected_metric = (link->is_up && link->healthy) ? link->metric : (1000 + link->metric);
|
||||
int real_metric = get_system_route_metric(link->device, expected_metric);
|
||||
if (real_metric != -1 && real_metric != expected_metric) {
|
||||
syslog(LOG_WARNING, "Route metric mismatch detected on %s (%s, priority %d): expected %d, got %d. Correcting...",
|
||||
link->name, link->device, link->priority, expected_metric, real_metric);
|
||||
update_route_metric(link, expected_metric, real_metric);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (any_checked) {
|
||||
// Write states to shared JSON file
|
||||
write_status_json();
|
||||
}
|
||||
|
||||
// High-responsiveness scheduler ticks every second
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// Terminating: clean up routes before exit
|
||||
restore_all_metrics();
|
||||
unlink(STATUS_FILE);
|
||||
syslog(LOG_INFO, "LinkBack daemon terminated successfully.");
|
||||
closelog();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef LINKBACKD_H
|
||||
#define LINKBACKD_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MAX_LINKS 16
|
||||
#define MAX_NAME_LEN 32
|
||||
#define MAX_TARGETS 8
|
||||
#define MAX_IP_LEN 64
|
||||
#define MAX_DOMAIN_LEN 128
|
||||
#define STATUS_FILE "/var/run/linkback.json"
|
||||
|
||||
typedef struct {
|
||||
char name[MAX_NAME_LEN];
|
||||
bool enabled;
|
||||
int priority;
|
||||
int metric;
|
||||
|
||||
// Ping config
|
||||
char ping_targets[MAX_TARGETS][MAX_IP_LEN];
|
||||
int ping_target_count;
|
||||
int ping_weight;
|
||||
|
||||
// DNS config
|
||||
char dns_server[MAX_IP_LEN];
|
||||
char dns_domain[MAX_DOMAIN_LEN];
|
||||
int dns_weight;
|
||||
|
||||
// TCP config
|
||||
char tcp_target[MAX_IP_LEN];
|
||||
int tcp_port;
|
||||
int tcp_weight;
|
||||
|
||||
// Thresholds
|
||||
int weight_threshold;
|
||||
int check_interval;
|
||||
int check_timeout;
|
||||
int recovery_delay;
|
||||
int failover_delay;
|
||||
|
||||
// Runtime state
|
||||
char device[MAX_NAME_LEN]; // physical interface name e.g., pppoe-wan, eth1
|
||||
char gateway[MAX_IP_LEN]; // gateway IP address
|
||||
bool is_up; // whether interface is reported up by netifd
|
||||
bool healthy; // daemon link health state
|
||||
int consecutive_success;
|
||||
int consecutive_failure;
|
||||
time_t last_checked; // Last health check timestamp
|
||||
|
||||
// Detailed health checks status
|
||||
bool ping_ok;
|
||||
int ping_rtt_ms;
|
||||
bool dns_ok;
|
||||
int dns_rtt_ms;
|
||||
bool tcp_ok;
|
||||
int tcp_rtt_ms;
|
||||
|
||||
int current_score;
|
||||
int current_metric; // actual system metric applied
|
||||
} link_t;
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
} global_config_t;
|
||||
|
||||
#endif // LINKBACKD_H
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease-common-bin
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=25
|
||||
PKG_RELEASE:=32
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=28
|
||||
PKG_RELEASE:=35
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.17
|
||||
PKG_RELEASE:=27
|
||||
PKG_SOURCE_DATE:=3.0.20
|
||||
PKG_RELEASE:=34
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7093bc8b705b7b239b8adde8e4d89928157de456
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=AgentFlow
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
@@ -45,37 +45,14 @@ agentflow.find_paths = function(blocks, home_dirs)
|
||||
return paths, default_path
|
||||
end
|
||||
|
||||
local dirname = function(path)
|
||||
path = (path or ""):match("^%s*(.-)%s*$")
|
||||
path = path:gsub("/+$", "")
|
||||
return path:match("^(.*)/[^/]+$") or ""
|
||||
end
|
||||
|
||||
agentflow.runtime_dir = function(data_dir, home_dirs)
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local configured = uci:get_first("mise", "mise", "runtime_dir", "")
|
||||
if configured ~= nil and configured ~= "" then
|
||||
return configured
|
||||
end
|
||||
|
||||
local conf_dir = dirname(data_dir)
|
||||
if conf_dir == "" then
|
||||
conf_dir = home_dirs["Configs"]
|
||||
end
|
||||
agentflow.runtime_dir = function(data_dir)
|
||||
data_dir = (data_dir or ""):match("^%s*(.-)%s*$"):gsub("/+$", "")
|
||||
local conf_dir = data_dir:match("^(.*)/[^/]+$")
|
||||
if conf_dir == nil or conf_dir == "" then
|
||||
return ""
|
||||
return nil
|
||||
end
|
||||
|
||||
return conf_dir .. "/Runtime"
|
||||
end
|
||||
|
||||
agentflow.runtime_home = function(data_dir, home_dirs)
|
||||
local runtime_dir = agentflow.runtime_dir(data_dir, home_dirs)
|
||||
if runtime_dir == nil or runtime_dir == "" then
|
||||
return ""
|
||||
end
|
||||
|
||||
return runtime_dir .. "/home"
|
||||
return conf_dir .. "/Runtime/home"
|
||||
end
|
||||
|
||||
return agentflow
|
||||
|
||||
@@ -34,15 +34,22 @@ for _, val in pairs(paths) do
|
||||
end
|
||||
data_dir.default = default_path
|
||||
|
||||
local runtime_home = s:option(DummyValue, "_runtime_home", translate("Shared runtime home"))
|
||||
runtime_home.description = translate("AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory.")
|
||||
function runtime_home.cfgvalue(self, section)
|
||||
local selected_data_dir = m.uci:get("agentflow", section, "data_dir") or default_path
|
||||
local path = agentflow_model.runtime_home(selected_data_dir, home)
|
||||
if path == "" then
|
||||
return translate("Not configured")
|
||||
function m.on_after_commit(self)
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local runtime_dir = uci:get("mise", "main", "runtime_dir")
|
||||
if runtime_dir ~= nil and runtime_dir ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
local saved_data_dir = uci:get_first("agentflow", "agentflow", "data_dir", "")
|
||||
runtime_dir = agentflow_model.runtime_dir(saved_data_dir)
|
||||
if runtime_dir == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:set("mise", "main", "runtime_dir", runtime_dir) then
|
||||
uci:commit("mise")
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local port = s:option(Value, "port", translate("Listen port"))
|
||||
|
||||
@@ -43,15 +43,6 @@ msgstr "请选择硬盘作为数据目录"
|
||||
msgid "Required. AgentFlow stores its configuration, database and workspace data under this directory."
|
||||
msgstr "必需。AgentFlow 在此目录中保存配置、数据库和工作区数据。"
|
||||
|
||||
msgid "Shared runtime home"
|
||||
msgstr "共享运行时 HOME"
|
||||
|
||||
msgid "AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory."
|
||||
msgstr "AgentFlow 和其他运行时感知应用共用此 HOME。除非 mise 显式设置了运行时目录,否则它会从所选 Configs 目录推导。"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未配置"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-ap-switch
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Dante
|
||||
|
||||
LUCI_TITLE:=LuCI support for one-click AP mode switching
|
||||
LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
var callSetMode = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'set_mode',
|
||||
params: ['mode']
|
||||
});
|
||||
|
||||
var callProbeIP = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'probe_ip'
|
||||
});
|
||||
|
||||
return L.view.extend({
|
||||
i18n: 'ap-switch',
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callGetStatus(),
|
||||
uci.load('ap-switch')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var status = data[0];
|
||||
var mode = status.mode || 'router';
|
||||
var lan_ip = status.lan_ip || 'N/A';
|
||||
var lan_proto = status.lan_proto || 'static';
|
||||
var lan_mac = status.lan_mac || 'N/A';
|
||||
|
||||
var changeModeRows = [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Switch to %s').format(mode === 'ap' ? _('Router Mode') : _('AP Mode'))),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(self, function() {
|
||||
return self.handleSwitch(mode === 'ap' ? 'router' : 'ap', status);
|
||||
})
|
||||
}, [ (mode === 'ap' ? _('Switch to Router Mode') : _('Switch to AP Mode')) ])
|
||||
])
|
||||
])
|
||||
];
|
||||
|
||||
var body = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('AP Switch Title') + ' - ' + _('Switch Mode')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Switch your system runtime configuration smoothly between standard Router and Access Point (AP) modes. Network interfaces and bridge rules will be adjusted automatically.')),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Current Status')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Operation Mode')),
|
||||
E('td', { 'class': 'td left' }, E('strong', {}, (mode === 'ap' ? _('Access Point (AP)') : _('Router'))))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Login URL')),
|
||||
E('td', { 'class': 'td left' }, lan_ip ? E('a', {
|
||||
'href': 'http://' + lan_ip,
|
||||
'target': '_blank',
|
||||
'style': 'text-decoration: underline; font-weight: bold; color: blue;'
|
||||
}, 'http://' + lan_ip) : _('N/A'))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('LAN IP Address')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('strong', { 'style': 'color: #2196F3;' }, lan_ip || _('Pending...')),
|
||||
E('span', { 'style': 'margin-left: 10px; color: #666;' }, '(' + lan_proto + ')')
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('LAN MAC Address')),
|
||||
E('td', { 'class': 'td left', 'id': 'lan-mac' }, lan_mac)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Change Mode')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, changeModeRows)
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
handleSwitch: function(target_mode, status) {
|
||||
var self = this;
|
||||
|
||||
var executeSwitch = function() {
|
||||
ui.showModal(null, [
|
||||
E('p', { 'class': 'spinning' }, _('Applying changes and restarting network...')),
|
||||
E('p', {}, _('The page will redirect or you may need to manually reconnect to the new IP address in a few moments.'))
|
||||
]);
|
||||
|
||||
return callSetMode(target_mode).then(function(res) {
|
||||
if (res && res.result === 'success') {
|
||||
setTimeout(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Mode switched successfully. Please check your network connection.')), 'info');
|
||||
}, 5000);
|
||||
} else {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to switch mode: %s').format(res.error || 'Unknown error')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error calling RPC: %s').format(err.message)), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
var showConfirmModal = function(title, messages, onConfirm) {
|
||||
var content = [];
|
||||
messages.forEach(function(msg) {
|
||||
content.push(E('p', { 'style': 'margin-bottom: 10px; line-height: 1.5;' }, msg));
|
||||
});
|
||||
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 20px;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-reset',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action cbi-button-apply',
|
||||
'style': 'margin-left: 10px;',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
onConfirm();
|
||||
}
|
||||
}, _('Confirm'))
|
||||
]));
|
||||
|
||||
ui.showModal(title, content);
|
||||
};
|
||||
|
||||
if (target_mode === 'ap') {
|
||||
ui.showModal(null, [
|
||||
E('p', { 'class': 'spinning' }, _('Auto-probing for future AP IP via DHCP on br-lan...')),
|
||||
E('p', {}, _('This ensures you can find your way back to the management panel after the switch.'))
|
||||
]);
|
||||
|
||||
return callProbeIP().then(function(res) {
|
||||
ui.hideModal();
|
||||
|
||||
var title = _('Confirm AP Mode Switch');
|
||||
var messages = [];
|
||||
|
||||
if (res && res.ip) {
|
||||
messages.push(E('span', {}, [
|
||||
_('Successfully pre-fetched future management IP: '),
|
||||
E('a', {
|
||||
'href': 'http://' + res.ip,
|
||||
'target': '_blank',
|
||||
'style': 'color: #2196F3; font-weight: bold; text-decoration: underline; word-break: break-all;'
|
||||
}, 'http://' + res.ip)
|
||||
]));
|
||||
messages.push(_('You can use this URL to access this dashboard after the switch.'));
|
||||
messages.push(_('Are you sure you want to switch to AP mode and restart network?'));
|
||||
} else {
|
||||
messages.push(_('WARNING: Failed to pre-fetch IP address via DHCP probe!'));
|
||||
messages.push(_('This may be because your LAN port is not connected to the main router.'));
|
||||
messages.push(_('If you proceed, you must find the new IP from your main router client list using MAC: %s').format(status.lan_mac || 'N/A'));
|
||||
messages.push(_('Do you still want to force the switch anyway?'));
|
||||
}
|
||||
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
|
||||
var title = _('Confirm AP Mode Switch');
|
||||
var messages = [
|
||||
_('WARNING: DHCP probe RPC error: %s').format(err.message),
|
||||
_('If you proceed, you must find the new IP from your main router client list using MAC: %s').format(status.lan_mac || 'N/A'),
|
||||
_('Do you still want to force the switch anyway?')
|
||||
];
|
||||
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
});
|
||||
} else {
|
||||
var title = _('Confirm Router Mode Switch');
|
||||
var messages = [
|
||||
_('Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?')
|
||||
];
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "AP Switch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch between Router and Access Point (AP) mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Point (AP)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Router"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAN IP Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Router Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "AP Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to Router Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to AP Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAN MAC Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to AP mode will bridge the WAN port into \"br-lan\". The \"br-lan\" interface will become a DHCP client to your main router."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pre-fetch AP IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Probing for DHCP IP on br-lan..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Successfully fetched IP: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to fetch IP. Ensure a LAN port is connected to your main router."
|
||||
msgstr ""
|
||||
|
||||
msgid "Probe IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not probed"
|
||||
msgstr ""
|
||||
|
||||
msgid "A valid IP was pre-fetched: %s. You can use this address to log in after the switch."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to proceed?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WARNING: No IP address was pre-fetched via DHCP probe!"
|
||||
msgstr ""
|
||||
|
||||
msgid "We strongly recommend connecting your LAN port to the main router and using \"Probe IP\" first."
|
||||
msgstr ""
|
||||
|
||||
msgid "If you proceed now, you must find the new IP from your main router using MAC: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Do you want to proceed anyway or cancel to plug in the cable and try again?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pending..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Login URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applying changes and restarting network..."
|
||||
msgstr ""
|
||||
|
||||
msgid "The page will redirect or you may need to manually reconnect to the new IP address in a few moments."
|
||||
msgstr ""
|
||||
|
||||
msgid "Mode switched successfully. Please check your network connection."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to switch mode: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error calling RPC: %s"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,169 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "AP Switch"
|
||||
msgstr "AP模式切换"
|
||||
|
||||
msgid "Switch between Router and Access Point (AP) mode."
|
||||
msgstr "在路由器模式和访问点 (AP) 模式之间切换。"
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "Operation Mode"
|
||||
msgstr "运行模式"
|
||||
|
||||
msgid "Access Point (AP)"
|
||||
msgstr "访问点 (AP)"
|
||||
|
||||
msgid "Router"
|
||||
msgstr "路由器"
|
||||
|
||||
msgid "LAN IP Address"
|
||||
msgstr "LAN IP 地址"
|
||||
|
||||
msgid "Change Mode"
|
||||
msgstr "更改模式"
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr "切换到 %s"
|
||||
|
||||
msgid "Router Mode"
|
||||
msgstr "路由器模式"
|
||||
|
||||
msgid "AP Mode"
|
||||
msgstr "AP 模式"
|
||||
|
||||
msgid "Switch to Router Mode"
|
||||
msgstr "切换到路由器模式"
|
||||
|
||||
msgid "Switch to AP Mode"
|
||||
msgstr "切换到 AP 模式"
|
||||
|
||||
msgid "LAN MAC Address"
|
||||
msgstr "LAN MAC 地址"
|
||||
|
||||
msgid "Switching to AP mode will bridge the WAN port into \"br-lan\". The \"br-lan\" interface will become a DHCP client to your main router."
|
||||
msgstr "切换到 AP 模式会将 WAN 口桥接到 \"br-lan\"。\"br-lan\" 接口将成为主路由器的 DHCP 客户端。"
|
||||
|
||||
msgid "Pre-fetch AP IP"
|
||||
msgstr "预获取 AP IP"
|
||||
|
||||
msgid "Probing for DHCP IP on br-lan..."
|
||||
msgstr "正在 br-lan 接口探测 DHCP IP..."
|
||||
|
||||
msgid "Successfully fetched IP: %s"
|
||||
msgstr "成功获取到 IP:%s"
|
||||
|
||||
msgid "Failed to fetch IP. Ensure a LAN port is connected to your main router."
|
||||
msgstr "获取 IP 失败。请确保 LAN 口已连接到主路由器。"
|
||||
|
||||
msgid "Probe IP"
|
||||
msgstr "开始探测"
|
||||
|
||||
msgid "Not probed"
|
||||
msgstr "未探测"
|
||||
|
||||
msgid "A valid IP was pre-fetched: %s. You can use this address to log in after the switch."
|
||||
msgstr "已预获取到有效的 IP:%s。切换后,您可以使用此地址登录。"
|
||||
|
||||
msgid "Are you sure you want to proceed?"
|
||||
msgstr "确定要继续吗?"
|
||||
|
||||
msgid "WARNING: No IP address was pre-fetched via DHCP probe!"
|
||||
msgstr "警告:未通过 DHCP 探测预获取到 IP 地址!"
|
||||
|
||||
msgid "We strongly recommend connecting your LAN port to the main router and using \"Probe IP\" first."
|
||||
msgstr "强烈建议先将 LAN 口连接到主路由器,并使用“预获取 AP IP”功能。"
|
||||
|
||||
msgid "If you proceed now, you must find the new IP from your main router using MAC: %s"
|
||||
msgstr "如果您现在继续,切换后必须通过 MAC 地址在主路由中查找新 IP:%s"
|
||||
|
||||
msgid "Do you want to proceed anyway or cancel to plug in the cable and try again?"
|
||||
msgstr "您是坚持继续,还是取消操作并在插上网线后重新尝试?"
|
||||
|
||||
msgid "Pending..."
|
||||
msgstr "等待中..."
|
||||
|
||||
msgid "Login URL"
|
||||
msgstr "登录地址"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不适用"
|
||||
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
msgid "Are you sure?"
|
||||
msgstr "确定要继续吗?"
|
||||
|
||||
msgid "Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?"
|
||||
msgstr "切换到路由器模式将恢复 WAN 口和本地 DHCP 服务。您的设备将使用之前的静态 IP 地址。确定要继续吗?"
|
||||
|
||||
msgid "Applying changes and restarting network..."
|
||||
msgstr "正在应用更改并重启网络..."
|
||||
|
||||
msgid "The page will redirect or you may need to manually reconnect to the new IP address in a few moments."
|
||||
msgstr "页面将会重定向,或者您可能需要在片刻后手动连接到新的 IP 地址。"
|
||||
|
||||
msgid "Mode switched successfully. Please check your network connection."
|
||||
msgstr "模式切换成功。请检查您的网络连接。"
|
||||
|
||||
msgid "Failed to switch mode: %s"
|
||||
msgstr "切换模式失败:%s"
|
||||
|
||||
msgid "Error calling RPC: %s"
|
||||
msgstr "调用 RPC 错误:%s"
|
||||
|
||||
msgid "Switch Mode"
|
||||
msgstr "模式切换"
|
||||
|
||||
msgid "Switch your system runtime configuration smoothly between standard Router and Access Point (AP) modes. Network interfaces and bridge rules will be adjusted automatically."
|
||||
msgstr "在标准路由器模式与 AP 桥接访问点模式之间平滑切换系统运行配置。切换后,网口物理桥接规则以及接口 DHCP 客户端行为将自动智能调整。"
|
||||
|
||||
msgid "AP Switch Title"
|
||||
msgstr "AP 模式切换"
|
||||
|
||||
msgid "Auto-probing for future AP IP via DHCP on br-lan..."
|
||||
msgstr "正在通过 br-lan 接口的 DHCP 自动探测未来的 AP 管理 IP 地址..."
|
||||
|
||||
msgid "This ensures you can find your way back to the management panel after the switch."
|
||||
msgstr "这可以确保您在切换模式后,能够顺利找到并登录新的设备管理后台。"
|
||||
|
||||
msgid "Successfully pre-fetched future management IP: "
|
||||
msgstr "成功预先获取到未来的管理 IP:"
|
||||
|
||||
msgid "You can use this URL to access this dashboard after the switch."
|
||||
msgstr "您可以使用此 URL 地址在切换后访问本控制台。"
|
||||
|
||||
msgid "Are you sure you want to switch to AP mode and restart network?"
|
||||
msgstr "您确定要切换到 AP 模式并重启网络吗?"
|
||||
|
||||
msgid "WARNING: Failed to pre-fetch IP address via DHCP probe!"
|
||||
msgstr "警告:通过 DHCP 探测预获取 IP 地址失败!"
|
||||
|
||||
msgid "This may be because your LAN port is not connected to the main router."
|
||||
msgstr "这可能是由于您设备的 LAN 物理端口尚未连接到上层主路由器。"
|
||||
|
||||
msgid "If you proceed, you must find the new IP from your main router client list using MAC: %s"
|
||||
msgstr "如果您继续,切换后必须通过 MAC 地址在上级主路由的客户端列表中查找新 IP:%s"
|
||||
|
||||
msgid "Do you still want to force the switch anyway?"
|
||||
msgstr "您是否仍要强制执行切换?"
|
||||
|
||||
msgid "WARNING: DHCP probe RPC error: %s"
|
||||
msgstr "警告:DHCP 探测 RPC 请求发生错误:%s"
|
||||
|
||||
msgid "Confirm AP Mode Switch"
|
||||
msgstr "确认切换至 AP 模式"
|
||||
|
||||
msgid "Confirm Router Mode Switch"
|
||||
msgstr "确认切换至路由器模式"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Confirm"
|
||||
msgstr "确认"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
config global 'settings'
|
||||
option mode 'router'
|
||||
|
||||
config backup 'router_state'
|
||||
option lan_proto 'static'
|
||||
option lan_ipaddr '192.168.1.1'
|
||||
option lan_netmask '255.255.255.0'
|
||||
option lan_gateway ''
|
||||
list lan_dns ''
|
||||
option dhcp_ignore '0'
|
||||
option wan_device ''
|
||||
option wan_disabled '0'
|
||||
option wan6_disabled '0'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$1" = "bound" ] || exit 0
|
||||
|
||||
echo "$ip" > /tmp/ap_switch_probed_ip
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/services/ap_switch": {
|
||||
"title": "AP Switch",
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "ap-switch/ap-switch"
|
||||
},
|
||||
"order": 90,
|
||||
"depends": {
|
||||
"acl": [ "luci-app-ap-switch" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"luci-app-ap-switch": {
|
||||
"description": "ACL for AP Switch",
|
||||
"read": {
|
||||
"uci": [ "ap-switch", "network", "dhcp" ],
|
||||
"ubus": {
|
||||
"luci.ap-switch": [ "get_status", "probe_ip" ],
|
||||
"network.interface": [ "dump" ],
|
||||
"network.device": [ "status" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "ap-switch", "network", "dhcp" ],
|
||||
"ubus": {
|
||||
"luci.ap-switch": [ "set_mode" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { connect } from 'ubus';
|
||||
import { readfile, unlink } from 'fs';
|
||||
|
||||
const UCI_CONFIG = 'ap-switch';
|
||||
const PROBE_FILE = '/tmp/ap_switch_probed_ip';
|
||||
|
||||
const methods = {
|
||||
get_status: {
|
||||
call: function() {
|
||||
let uci_cursor = cursor();
|
||||
let mode = uci_cursor.get(UCI_CONFIG, 'settings', 'mode') || 'router';
|
||||
|
||||
let lan_proto = uci_cursor.get('network', 'lan', 'proto') || 'static';
|
||||
let lan_ip = ''; // We will get the real-time IP
|
||||
|
||||
let ubus_conn = connect();
|
||||
let lan_mac = '';
|
||||
let wan_mac = '';
|
||||
let wan_ip = '';
|
||||
|
||||
if (ubus_conn) {
|
||||
// 1. Get real-time LAN IP and MAC
|
||||
let lan_status = ubus_conn.call('network.interface.lan', 'status');
|
||||
if (lan_status && type(lan_status['ipv4-address']) === 'array' && length(lan_status['ipv4-address']) > 0) {
|
||||
lan_ip = lan_status['ipv4-address'][0].address;
|
||||
}
|
||||
|
||||
let lan_dev = ubus_conn.call('network.device', 'status', { name: 'br-lan' });
|
||||
lan_mac = lan_dev?.macaddr || '';
|
||||
|
||||
// 2. Get current WAN interface status and its physical device MAC
|
||||
let wan_status = ubus_conn.call('network.interface.wan', 'status');
|
||||
let wan_device_name = wan_status?.device || '';
|
||||
if (wan_device_name) {
|
||||
let wan_dev = ubus_conn.call('network.device', 'status', { name: wan_device_name });
|
||||
wan_mac = wan_dev?.macaddr || '';
|
||||
}
|
||||
|
||||
// 3. Get current WAN IP
|
||||
if (wan_status && type(wan_status['ipv4-address']) === 'array' && length(wan_status['ipv4-address']) > 0) {
|
||||
wan_ip = wan_status['ipv4-address'][0].address;
|
||||
}
|
||||
ubus_conn.disconnect();
|
||||
}
|
||||
|
||||
let probed_ip = rtrim(readfile(PROBE_FILE) || '');
|
||||
|
||||
return {
|
||||
mode: mode,
|
||||
lan_proto: lan_proto,
|
||||
lan_ip: lan_ip,
|
||||
lan_mac: lan_mac,
|
||||
wan_mac: wan_mac,
|
||||
wan_ip: wan_ip,
|
||||
probed_ip: probed_ip
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
probe_ip: {
|
||||
call: function() {
|
||||
unlink(PROBE_FILE);
|
||||
// Run udhcpc on br-lan with a short timeout and our probe script
|
||||
// -n: exit if no lease
|
||||
// -q: quit after obtaining lease
|
||||
// -t 3: send 3 discover packets
|
||||
// -T 2: pause 2 seconds between packets
|
||||
let rc = system('udhcpc -i br-lan -n -q -t 3 -T 2 -s /usr/share/luci-app-ap-switch/dhcp-probe.sh >/dev/null 2>&1');
|
||||
|
||||
let ip = rtrim(readfile(PROBE_FILE) || '');
|
||||
return { success: (rc === 0), ip: ip };
|
||||
}
|
||||
},
|
||||
|
||||
set_mode: {
|
||||
args: { mode: 'string' },
|
||||
call: function(request) {
|
||||
let target_mode = request.args?.mode;
|
||||
if (target_mode !== 'router' && target_mode !== 'ap') {
|
||||
return { error: 'Invalid mode' };
|
||||
}
|
||||
|
||||
let uci_cursor = cursor();
|
||||
let current_mode = uci_cursor.get(UCI_CONFIG, 'settings', 'mode') || 'router';
|
||||
|
||||
if (target_mode === current_mode) {
|
||||
return { result: 'Already in target mode' };
|
||||
}
|
||||
|
||||
if (target_mode === 'ap') {
|
||||
// --- Switch to AP Mode (Full Lossless Snapshot) ---
|
||||
|
||||
// 1. Identify WAN device
|
||||
let wan_device = uci_cursor.get('network', 'wan', 'device');
|
||||
if (!wan_device) {
|
||||
wan_device = uci_cursor.get('network', 'wan', 'ifname');
|
||||
}
|
||||
|
||||
// 2. Full Snapshot Backup of Router Mode state
|
||||
let lan_proto = uci_cursor.get('network', 'lan', 'proto') || 'static';
|
||||
let lan_ipaddr = uci_cursor.get('network', 'lan', 'ipaddr') || '';
|
||||
let lan_netmask = uci_cursor.get('network', 'lan', 'netmask') || '';
|
||||
let lan_gateway = uci_cursor.get('network', 'lan', 'gateway') || '';
|
||||
let lan_dns = uci_cursor.get('network', 'lan', 'dns') || [];
|
||||
let dhcp_ignore = uci_cursor.get('dhcp', 'lan', 'ignore') || '0';
|
||||
let wan_disabled = uci_cursor.get('network', 'wan', 'disabled') || '0';
|
||||
let wan6_disabled = uci_cursor.get('network', 'wan6', 'disabled') || '0';
|
||||
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_proto', lan_proto);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_ipaddr', lan_ipaddr);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_netmask', lan_netmask);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_gateway', lan_gateway);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_dns', lan_dns);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'dhcp_ignore', dhcp_ignore);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan_device', wan_device || '');
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan_disabled', wan_disabled);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan6_disabled', wan6_disabled);
|
||||
uci_cursor.set(UCI_CONFIG, 'settings', 'mode', 'ap');
|
||||
|
||||
// 3. Bridge WAN device to LAN bridge
|
||||
if (wan_device) {
|
||||
uci_cursor.foreach('network', 'device', (s) => {
|
||||
if (s.name === 'br-lan') {
|
||||
let ports = s.ports || [];
|
||||
if (type(ports) === 'string') ports = [ports];
|
||||
let exists = false;
|
||||
for (let p in ports) if (p === wan_device) exists = true;
|
||||
if (!exists) {
|
||||
push(ports, wan_device);
|
||||
uci_cursor.set('network', s['.name'], 'ports', ports);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Set LAN to DHCP client
|
||||
uci_cursor.set('network', 'lan', 'proto', 'dhcp');
|
||||
uci_cursor.delete('network', 'lan', 'ipaddr');
|
||||
uci_cursor.delete('network', 'lan', 'netmask');
|
||||
uci_cursor.delete('network', 'lan', 'gateway');
|
||||
uci_cursor.delete('network', 'lan', 'dns');
|
||||
|
||||
// 5. Disable WAN interface
|
||||
uci_cursor.set('network', 'wan', 'disabled', '1');
|
||||
uci_cursor.set('network', 'wan6', 'disabled', '1');
|
||||
|
||||
// 6. Disable DHCP Server
|
||||
uci_cursor.set('dhcp', 'lan', 'ignore', '1');
|
||||
|
||||
} else {
|
||||
// --- Switch to Router Mode (Lossless State Restoration) ---
|
||||
|
||||
// 1. Read Snapshot
|
||||
let wan_device = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan_device');
|
||||
let lan_proto = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_proto') || 'static';
|
||||
let lan_ipaddr = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_ipaddr') || '192.168.1.1';
|
||||
let lan_netmask = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_netmask') || '';
|
||||
let lan_gateway = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_gateway') || '';
|
||||
let lan_dns = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_dns') || [];
|
||||
let dhcp_ignore = uci_cursor.get(UCI_CONFIG, 'router_state', 'dhcp_ignore') || '0';
|
||||
let wan_disabled = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan_disabled') || '0';
|
||||
let wan6_disabled = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan6_disabled') || '0';
|
||||
|
||||
uci_cursor.set(UCI_CONFIG, 'settings', 'mode', 'router');
|
||||
|
||||
// 2. Remove WAN device from LAN bridge
|
||||
if (wan_device) {
|
||||
uci_cursor.foreach('network', 'device', (s) => {
|
||||
if (s.name === 'br-lan') {
|
||||
let ports = s.ports || [];
|
||||
if (type(ports) === 'string') ports = [ports];
|
||||
let new_ports = [];
|
||||
for (let p in ports) if (p !== wan_device) push(new_ports, p);
|
||||
uci_cursor.set('network', s['.name'], 'ports', new_ports);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Restore LAN settings accurately without hardcoding
|
||||
uci_cursor.set('network', 'lan', 'proto', lan_proto);
|
||||
if (lan_ipaddr) uci_cursor.set('network', 'lan', 'ipaddr', lan_ipaddr);
|
||||
if (lan_netmask) uci_cursor.set('network', 'lan', 'netmask', lan_netmask);
|
||||
if (lan_gateway) uci_cursor.set('network', 'lan', 'gateway', lan_gateway);
|
||||
if (type(lan_dns) === 'array' && length(lan_dns) > 0) {
|
||||
uci_cursor.set('network', 'lan', 'dns', lan_dns);
|
||||
} else if (type(lan_dns) === 'string' && lan_dns !== '') {
|
||||
uci_cursor.set('network', 'lan', 'dns', lan_dns);
|
||||
}
|
||||
|
||||
// 4. Restore WAN / WAN6 states
|
||||
if (wan_disabled === '1') {
|
||||
uci_cursor.set('network', 'wan', 'disabled', '1');
|
||||
} else {
|
||||
uci_cursor.delete('network', 'wan', 'disabled');
|
||||
}
|
||||
if (wan6_disabled === '1') {
|
||||
uci_cursor.set('network', 'wan6', 'disabled', '1');
|
||||
} else {
|
||||
uci_cursor.delete('network', 'wan6', 'disabled');
|
||||
}
|
||||
|
||||
// 5. Restore DHCP Server state
|
||||
if (dhcp_ignore === '1') {
|
||||
uci_cursor.set('dhcp', 'lan', 'ignore', '1');
|
||||
} else {
|
||||
uci_cursor.delete('dhcp', 'lan', 'ignore');
|
||||
}
|
||||
}
|
||||
|
||||
uci_cursor.commit('ap-switch');
|
||||
uci_cursor.commit('network');
|
||||
uci_cursor.commit('dhcp');
|
||||
|
||||
// Apply changes in background
|
||||
system('/etc/init.d/network restart &');
|
||||
system('/etc/init.d/dnsmasq restart &');
|
||||
|
||||
return { success: true, result: 'success' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.ap-switch': methods };
|
||||
@@ -1,515 +0,0 @@
|
||||
#chartjs-tooltip {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, .7);
|
||||
color: white;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
transition: all .1s ease;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#chartjs-tooltip.above {
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
#chartjs-tooltip.above:before {
|
||||
border: solid;
|
||||
border-color: #111 transparent;
|
||||
border-color: rgba(0, 0, 0, .8) transparent;
|
||||
border-width: 8px 8px 0 8px;
|
||||
bottom: 1em;
|
||||
content: "";
|
||||
display: block;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.pie label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kpi li {
|
||||
margin: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kpi big {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.head {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.head .pie {
|
||||
/* min-width: 200px; */
|
||||
padding: 5px;
|
||||
flex: 1 1 30%;
|
||||
}
|
||||
|
||||
.cbi-tooltip .head .pie {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.head .kpi {
|
||||
padding: 5px;
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
flex: 1 0 33%;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.head .kpi ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.td.double > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cbi-tooltip {
|
||||
box-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.td.hide-xs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span {
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:first-child::before {
|
||||
content: "IPv4: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:last-child::before {
|
||||
content: "IPv6: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中 */
|
||||
margin-bottom: 15px; /* 项间距 */
|
||||
gap: 10px; /* Label 和 Input 间距 */
|
||||
}
|
||||
|
||||
/* 标签固定最小宽度 + 右对齐 */
|
||||
.form-label {
|
||||
flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.th-sort-asc::after {
|
||||
content: " ▲";
|
||||
}
|
||||
|
||||
.th-sort-desc::after {
|
||||
content: " ▼";
|
||||
}
|
||||
|
||||
.table .th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table .th:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* L7 View Specific Styles */
|
||||
.l7-view-container #l7-error-message { color: red; background-color: #ffefef; border: 1px solid red; padding: 10px; margin-bottom: 10px; display: none; }
|
||||
.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; }
|
||||
.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; }
|
||||
.l7-view-container .kpi-card-label { font-size: 0.9em; color: #666; }
|
||||
.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; }
|
||||
.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; }
|
||||
.l7-view-container .l7-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
color: white;
|
||||
}
|
||||
.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Display View Specific Styles */
|
||||
.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.display-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; }
|
||||
.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; }
|
||||
.display-view-container .kpi-card-label { font-size: 0.9em; color: #666; }
|
||||
.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.display-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; }
|
||||
.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; }
|
||||
.display-view-container .display-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Control Groups */
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 1.2em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-input {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-input:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-group {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.last-updated-text {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Enhanced Select Styling */
|
||||
.cbi-input-select {
|
||||
padding: 6px 30px 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 12px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cbi-input-select:hover {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.cbi-input-select:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Button Enhancements for Controls */
|
||||
.l7-controls .cbi-button,
|
||||
.display-controls .cbi-button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.cbi-button-action {
|
||||
background: rgba(255, 193, 7, 0.9);
|
||||
color: #333;
|
||||
border-color: rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-action:hover {
|
||||
background: #ffc107;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.cbi-button-positive {
|
||||
background: rgba(40, 167, 69, 0.9);
|
||||
color: white;
|
||||
border-color: rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-positive:hover {
|
||||
background: #28a745;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
|
||||
/* Responsive Controls */
|
||||
@media screen and (max-width: 768px) {
|
||||
.l7-controls,
|
||||
.display-controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.l7-controls-left,
|
||||
.l7-controls-right,
|
||||
.control-buttons {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Cell Icon Styles */
|
||||
.activity-indicator {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon, .btn-icon, .th-icon {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Cell Specific Styles */
|
||||
.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.speed-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.speed-cell.download {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.speed-cell.upload {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.volume-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.volume-cell.download {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.volume-cell.upload {
|
||||
color: #6610f2;
|
||||
}
|
||||
|
||||
.packet-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
color: #6c757d;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Data value with monospace font for better alignment */
|
||||
.data-value {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Table header enhancements */
|
||||
.table .th {
|
||||
font-weight: 600;
|
||||
padding: 10px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table .td {
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.protocol-icon.l7::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #5470c6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.protocol-icon.domain::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #91cc75;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Button Enhancements */
|
||||
.cbi-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cbi-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.cbi-button-edit {
|
||||
background-color: #ffc107;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.cbi-button-edit:hover {
|
||||
background-color: #e0a800;
|
||||
border-color: #e0a800;
|
||||
}
|
||||
|
||||
.cbi-button-remove {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-remove:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.cbi-button-add {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-add:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
.cbi-button-add:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Table Row Hover Effects */
|
||||
.table .tr:not(.table-titles):not(.placeholder):hover {
|
||||
background-color: #f8f9fa;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Active/Inactive Status Styles */
|
||||
.activity-indicator[title*="Active"] {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Responsive Icon Handling */
|
||||
@media screen and (max-width: 768px) {
|
||||
.icon, .btn-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-icon + span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -30,219 +30,6 @@ var callGetWanMac = rpc.declare({
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
// Generate Device ID: AW + 7 random digits + 12-char MAC (total 21 chars)
|
||||
function generateDeviceId(macAddress) {
|
||||
var macPart = macAddress.replace(/-/g, ''); // Remove hyphens from MAC
|
||||
var randomPart = '';
|
||||
for (var i = 0; i < 7; i++) {
|
||||
randomPart += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return 'AW' + randomPart + macPart;
|
||||
}
|
||||
|
||||
// Generate Location ID: 14 digits (AAAAAABBBCCCCC)
|
||||
// AAAAAA: Administrative division code (GB/T 2260) - using Beijing as default (110101)
|
||||
// BBB: Service type (100 for commercial internet service locations, positions 7-9)
|
||||
// CCCCC: 5-digit random sequence number (positions 10-14)
|
||||
function generateLocationId() {
|
||||
var adminCode = '110101'; // Default to Beijing Dongcheng District (positions 1-6)
|
||||
var serviceType = '100'; // Commercial internet service location (positions 7-9)
|
||||
var sequenceNum = '';
|
||||
|
||||
// Generate 5-digit random sequence number (positions 10-14)
|
||||
for (var i = 0; i < 5; i++) {
|
||||
sequenceNum += Math.floor(Math.random() * 10);
|
||||
}
|
||||
|
||||
return adminCode + serviceType + sequenceNum; // 6 + 3 + 5 = 14 digits
|
||||
}
|
||||
|
||||
// Auto-fill helper functions
|
||||
function showAutoFillModal() {
|
||||
ui.showModal(_('Auto Fill'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Getting device information and location data, please wait...')),
|
||||
E('div', { 'id': 'auto-fill-progress' }, [
|
||||
E('ul', { 'style': 'margin: 10px 0;' }, [
|
||||
E('li', { 'id': 'mac-status' }, _('Getting WAN MAC address...')),
|
||||
E('li', { 'id': 'device-id-status' }, _('Generating Device ID...')),
|
||||
E('li', { 'id': 'location-id-status' }, _('Generating Location ID...')),
|
||||
E('li', { 'id': 'location-status' }, _('Getting location coordinates...'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function updateMacStatus(macAddr, isSuccess, errorMsg, isFallback) {
|
||||
var statusElement = document.getElementById('mac-status');
|
||||
if (isSuccess) {
|
||||
var prefix = isFallback ? _('✓ WAN MAC address obtained (fallback): ') : _('✓ WAN MAC address obtained: ');
|
||||
var suffix = isFallback ? '' : _(' (interface: ') + arguments[4] + _(')');
|
||||
statusElement.innerHTML = prefix + macAddr + suffix;
|
||||
} else {
|
||||
statusElement.innerHTML = _('✗ Failed to get WAN MAC address: ') + (errorMsg || _('Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
function generateAndUpdateIds(macAddr) {
|
||||
var deviceId = generateDeviceId(macAddr);
|
||||
var locationId = generateLocationId();
|
||||
|
||||
document.getElementById('device-id-status').innerHTML = _('✓ Device ID generated: ') + deviceId;
|
||||
document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + locationId;
|
||||
|
||||
return { deviceId: deviceId, locationId: locationId };
|
||||
}
|
||||
|
||||
function getMacAddressPromise(results) {
|
||||
return callGetWanMac().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
var macAddr = response.mac.toUpperCase().replace(/:/g, '-');
|
||||
results.macAddress = macAddr;
|
||||
updateMacStatus(macAddr, true, null, false, response.interface);
|
||||
|
||||
var ids = generateAndUpdateIds(macAddr);
|
||||
results.deviceId = ids.deviceId;
|
||||
results.locationId = ids.locationId;
|
||||
} else {
|
||||
return getMacAddressFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
return getMacAddressFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
function getMacAddressFallback(results) {
|
||||
document.getElementById('mac-status').innerHTML = _('RPC failed, trying fallback...');
|
||||
|
||||
return fs.exec('/bin/sh', ['-c', 'ip route | grep default | awk \'{print $5}\' | head -n1 | xargs -I {} cat /sys/class/net/{}/address 2>/dev/null || echo "failed"']).then(function (response) {
|
||||
if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') {
|
||||
var macAddr = response.stdout.trim().toUpperCase().replace(/:/g, '-');
|
||||
results.macAddress = macAddr;
|
||||
updateMacStatus(macAddr, true, null, true);
|
||||
|
||||
var ids = generateAndUpdateIds(macAddr);
|
||||
results.deviceId = ids.deviceId;
|
||||
results.locationId = ids.locationId;
|
||||
} else {
|
||||
updateMacStatus(null, false, _('Unknown error'));
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID without MAC address');
|
||||
}
|
||||
}).catch(function (error) {
|
||||
updateMacStatus(null, false, error.message);
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID due to MAC error');
|
||||
});
|
||||
}
|
||||
|
||||
function formatCoordinate(coord) {
|
||||
var num = parseFloat(coord).toFixed(6);
|
||||
if (num >= 0) {
|
||||
return num.padStart(10, '0');
|
||||
} else {
|
||||
return '-' + Math.abs(num).toFixed(6).padStart(9, '0');
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationPromise(results) {
|
||||
return callGetLocation().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
var lat = formatCoordinate(response.lat);
|
||||
var lon = formatCoordinate(response.lon);
|
||||
|
||||
results.longitude = lon;
|
||||
results.latitude = lat;
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + lat + ', ' + lon;
|
||||
} else {
|
||||
var errorMsg = (response && response.message) ? response.message : _('Unknown error');
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg;
|
||||
}
|
||||
}).catch(function (error) {
|
||||
return getLocationFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
function getLocationFallback(results) {
|
||||
document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...');
|
||||
|
||||
return fs.exec('/bin/sh', ['-c', 'curl -s --connect-timeout 10 --max-time 30 "https://ipapi.co/json" 2>/dev/null || curl -s --connect-timeout 10 --max-time 30 "http://ipinfo.io/json" 2>/dev/null || echo "failed"']).then(function (response) {
|
||||
if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') {
|
||||
try {
|
||||
var data = JSON.parse(response.stdout);
|
||||
var lat, lon;
|
||||
|
||||
if (data.latitude && data.longitude) {
|
||||
lat = formatCoordinate(data.latitude);
|
||||
lon = formatCoordinate(data.longitude);
|
||||
} else if (data.loc) {
|
||||
var coords = data.loc.split(',');
|
||||
lat = formatCoordinate(coords[0]);
|
||||
lon = formatCoordinate(coords[1]);
|
||||
}
|
||||
|
||||
if (lat && lon) {
|
||||
results.longitude = lon;
|
||||
results.latitude = lat;
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained (fallback): ') + lat + ', ' + lon;
|
||||
} else {
|
||||
document.getElementById('location-status').innerHTML = _('✗ No valid coordinates found');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to parse location data');
|
||||
}
|
||||
} else {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location data');
|
||||
}
|
||||
}).catch(function (fallbackError) {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Error getting location: ') + fallbackError.message;
|
||||
});
|
||||
}
|
||||
|
||||
function fillFormFields(results) {
|
||||
var successCount = 0;
|
||||
var totalFields = 0;
|
||||
var messages = [];
|
||||
var fieldMappings = [
|
||||
{ result: 'macAddress', selector: 'ap_mac_address', label: _('MAC Address: ') },
|
||||
{ result: 'deviceId', selector: 'ap_device_id', label: _('Device ID: ') },
|
||||
{ result: 'locationId', selector: 'location_id', label: _('Location ID: ') },
|
||||
{ result: 'longitude', selector: 'ap_longitude', label: _('Longitude: ') },
|
||||
{ result: 'latitude', selector: 'ap_latitude', label: _('Latitude: ') }
|
||||
];
|
||||
|
||||
fieldMappings.forEach(function (mapping) {
|
||||
if (results[mapping.result]) {
|
||||
var field = document.querySelector('input[data-name="' + mapping.selector + '"]') ||
|
||||
document.querySelector('input[name*="' + mapping.selector + '"]') ||
|
||||
document.querySelector('input[id*="' + mapping.selector + '"]') ||
|
||||
document.querySelector('#cbid\\.wifidogx\\.default\\.' + mapping.selector);
|
||||
|
||||
if (field) {
|
||||
field.value = results[mapping.result];
|
||||
field.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
successCount++;
|
||||
messages.push(mapping.label + results[mapping.result]);
|
||||
}
|
||||
totalFields++;
|
||||
}
|
||||
});
|
||||
|
||||
return { successCount: successCount, totalFields: totalFields, messages: messages };
|
||||
}
|
||||
|
||||
function showAutoFillResult(stats) {
|
||||
ui.hideModal();
|
||||
|
||||
if (stats.successCount === stats.totalFields && stats.totalFields > 0) {
|
||||
ui.addNotification(null, E('p', _('Auto fill completed successfully! All fields have been filled.') + '<br>' + stats.messages.join('<br>')), 'info');
|
||||
} else if (stats.successCount > 0) {
|
||||
ui.addNotification(null, E('p', _('Auto fill partially completed.') + ' ' + stats.successCount + '/' + stats.totalFields + ' ' + _('fields filled successfully.') + '<br>' + stats.messages.join('<br>')), 'warning');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Auto fill failed. No fields could be filled. Please check your network connection and try again.')), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('wifidogx'), {}).then(function (res) {
|
||||
var isRunning = false;
|
||||
@@ -253,6 +40,25 @@ function getServiceStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
function generateDeviceId(macAddress) {
|
||||
var macPart = macAddress.replace(/-/g, '');
|
||||
var randomPart = '';
|
||||
for (var i = 0; i < 7; i++) {
|
||||
randomPart += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return 'AW' + randomPart + macPart;
|
||||
}
|
||||
|
||||
function generateLocationId() {
|
||||
var adminCode = '110101';
|
||||
var serviceType = '100';
|
||||
var sequenceNum = '';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
sequenceNum += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return adminCode + serviceType + sequenceNum;
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
var renderHTML = "";
|
||||
var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
@@ -284,7 +90,6 @@ return view.extend({
|
||||
s.tab('gateway', _('Gateway Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
s.tab('rule', _('Rule Settings'));
|
||||
s.tab('qos', _('QoS Settings'));
|
||||
s.tab('location', _('Authentication Location Settings'));
|
||||
|
||||
// basic settings
|
||||
@@ -368,7 +173,7 @@ return view.extend({
|
||||
o = s.taboption('basic', form.Flag, 'disable_portal_auth', _('Disable Portal Authentication'),
|
||||
_('When enabled, users can access the internet without portal authentication. Firewall redirect rules will not be created. Use this mode for pure traffic statistics without captive portal.'));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
o.default = '0';
|
||||
|
||||
o = s.taboption('basic', form.ListValue, 'log_level', _('Log Level'),
|
||||
_('The log level of the apfree-wifidog.'));
|
||||
@@ -380,7 +185,7 @@ return view.extend({
|
||||
o.value(2, _('Critical'));
|
||||
o.value(1, _('Alert'));
|
||||
o.value(0, _('Emergency'));
|
||||
o.defaulValue = 0;
|
||||
o.default = 0;
|
||||
o.optional = false;
|
||||
|
||||
// gateway settings
|
||||
@@ -392,7 +197,7 @@ return view.extend({
|
||||
o = ss.option(form.Flag, 'gateway_auth_enabled', _('Auth Enabled'),
|
||||
_('Enable the authentication of the gateway.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = true;
|
||||
o.default = true;
|
||||
|
||||
o = ss.option(widgets.DeviceSelect, 'gateway_name', _('Gateway Name'));
|
||||
o.rmempty = false;
|
||||
@@ -425,14 +230,14 @@ return view.extend({
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.defaulValue = 60;
|
||||
o.default = 60;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'client_timeout', _('Client Timeout'),
|
||||
_('The timeout of the client.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.defaulValue = 5;
|
||||
o.default = 5;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'wired_passed', _('Wired Passed'),
|
||||
_('Wired users do not need to authenticate to access the internet.'));
|
||||
@@ -441,23 +246,30 @@ return view.extend({
|
||||
o = s.taboption('advanced', form.Flag, 'apple_cna', _('Apple CNA'),
|
||||
_('Enable Apple Captive Network Assistant.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
o.default = false;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'js_filter', _('JS Filter'),
|
||||
_('Enable JS redirect.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = true;
|
||||
o.default = true;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'enable_anti_nat', _('Enable Anti NAT'),
|
||||
_('Enable Anti NAT devices.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
o.default = false;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'privileged_ops_secret', _('Privileged Ops Secret'),
|
||||
_('Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices.'));
|
||||
o.password = true;
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.default = 'chawrt@2026';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ttl_value', _('TTL Value'),
|
||||
_('The TTL value of the gateway support.'));
|
||||
o.datatype = 'string';
|
||||
o.rmempty = false;
|
||||
o.defaulValue = '64,128';
|
||||
o.default = '64,128';
|
||||
o.depends('enable_anti_nat', '1');
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'anti_nat_permit_macs', _('Anti NAT Permit MAC'),
|
||||
@@ -466,17 +278,87 @@ return view.extend({
|
||||
o.rmempty = true;
|
||||
o.depends('enable_anti_nat', '1');
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'enable_event_log', _('Enable Internet Access Log'),
|
||||
_('Enable logging of internet access events and user activities.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
|
||||
// Auth Server Settings
|
||||
o = s.taboption('auth', form.SectionValue, '_auth', form.GridSection, 'auth');
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
ss.handleAdd = function (ev, name) {
|
||||
var self = this;
|
||||
if (name) {
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.super('handleAdd', [ev, name]);
|
||||
};
|
||||
|
||||
ss.handleRename = function (section_id, ev) {
|
||||
var self = this;
|
||||
var oldName = section_id;
|
||||
var nameEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'auth-rename-input',
|
||||
'value': oldName,
|
||||
'style': 'width: 100%;'
|
||||
});
|
||||
ui.showModal(_('Rename Authentication Server'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this authentication server profile.')),
|
||||
E('div', { 'class': 'cbi-section-table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, _('Name')),
|
||||
E('div', { 'class': 'td' }, [nameEl])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () {
|
||||
var newName = nameEl.value.trim();
|
||||
if (!newName) {
|
||||
ui.addNotification(null, E('p', _('Please enter a name.')), 'error');
|
||||
return;
|
||||
}
|
||||
if (newName === oldName) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === newName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === newName && authSections[i]['.name'] !== oldName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
ui.hideModal();
|
||||
uci.rename('wifidogx', section_id, newName);
|
||||
self.map.save(null, true);
|
||||
}}, _('Rename'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
o = ss.option(form.Value, 'auth_server_hostname', _('Auth Server Hostname'),
|
||||
_('The domain or IP address of the authentication server.'));
|
||||
o.rmempty = false;
|
||||
@@ -499,29 +381,6 @@ return view.extend({
|
||||
o.optional = false;
|
||||
o.default = '/wifidog/';
|
||||
|
||||
|
||||
// QoS settings
|
||||
o = s.taboption('qos', form.Flag, 'enable_qos', _('Enable Global QoS'),
|
||||
_('Enable Global QoS.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
|
||||
o = s.taboption('qos', form.Value, 'qos_up', _('Global QoS Up'),
|
||||
_('The global QoS up value(Mbps).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.defaulValue = 0;
|
||||
o.depends('enable_qos', '1');
|
||||
|
||||
o = s.taboption('qos', form.Value, 'qos_down', _('Global QoS Down'),
|
||||
_('The global QoS down value(Mbps).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.defaulValue = 0;
|
||||
o.depends('enable_qos', '1');
|
||||
|
||||
// Authentication Location Settings
|
||||
// Add auto-fill button
|
||||
o = s.taboption('location', form.Button, '_auto_fill', _('Auto Fill All Fields'),
|
||||
@@ -544,11 +403,8 @@ return view.extend({
|
||||
getLocationPromise(results)
|
||||
];
|
||||
|
||||
// Wait for all promises to complete and fill form fields
|
||||
Promise.all(promises).then(function () {
|
||||
setTimeout(function () {
|
||||
fillFormFields(results);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -583,10 +439,11 @@ return view.extend({
|
||||
results.locationId = generateLocationId();
|
||||
document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + results.locationId;
|
||||
} else {
|
||||
document.getElementById('mac-status').innerHTML = _('RPC returned error, trying fallback...');
|
||||
return getMacAddressFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('mac-status').innerHTML = _('✗ RPC call failed, trying fallback...');
|
||||
document.getElementById('mac-status').innerHTML = _('RPC call failed, trying fallback...');
|
||||
return getMacAddressFallback(results);
|
||||
});
|
||||
}
|
||||
@@ -625,8 +482,8 @@ return view.extend({
|
||||
formatAndSetCoordinates(results, lat, lon);
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + results.latitude + ', ' + results.longitude;
|
||||
} else {
|
||||
var errorMsg = (response && response.message) ? response.message : 'Unknown error';
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg;
|
||||
document.getElementById('location-status').innerHTML = _('RPC returned error, trying fallback...');
|
||||
return getLocationFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...');
|
||||
@@ -741,8 +598,6 @@ return view.extend({
|
||||
o.rmempty = false;
|
||||
o.datatype = 'string';
|
||||
o.optional = false;
|
||||
o.depends('auth_server_mode', 'cloud');
|
||||
o.depends('auth_server_mode', 'bypass');
|
||||
o.placeholder = '110101100123456';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value === '')
|
||||
@@ -766,7 +621,8 @@ return view.extend({
|
||||
|
||||
// Validate service type code (7-9 digits)
|
||||
var serviceType = value.substring(6, 9);
|
||||
if (serviceType !== '100' && serviceType !== '2' + value.charAt(7) + value.charAt(8) && serviceType !== '3' + value.charAt(7) + value.charAt(8)) {
|
||||
var firstDigit = serviceType.charAt(0);
|
||||
if (serviceType !== '100' && firstDigit !== '2' && firstDigit !== '3') {
|
||||
return _('Service type code (7-9 digits) must be "100" for commercial internet service locations, "2XX" for non-commercial locations, or "3XX" for WiFi wireless collection terminals');
|
||||
}
|
||||
|
||||
@@ -936,6 +792,81 @@ return view.extend({
|
||||
ss.addremove = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
ss.handleAdd = function (ev, name) {
|
||||
var self = this;
|
||||
if (name) {
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.super('handleAdd', [ev, name]);
|
||||
};
|
||||
|
||||
ss.handleRename = function (section_id, ev) {
|
||||
var self = this;
|
||||
var oldName = section_id;
|
||||
var nameEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'longconn-rename-input',
|
||||
'value': oldName,
|
||||
'style': 'width: 100%;'
|
||||
});
|
||||
ui.showModal(_('Rename Long Connection Profile'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this long connection profile.')),
|
||||
E('div', { 'class': 'cbi-section-table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, _('Name')),
|
||||
E('div', { 'class': 'td' }, [nameEl])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () {
|
||||
var newName = nameEl.value.trim();
|
||||
if (!newName) {
|
||||
ui.addNotification(null, E('p', _('Please enter a name.')), 'error');
|
||||
return;
|
||||
}
|
||||
if (newName === oldName) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === newName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === newName && longconnSections[i]['.name'] !== oldName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
ui.hideModal();
|
||||
uci.rename('wifidogx', section_id, newName);
|
||||
self.map.save(null, true);
|
||||
}}, _('Rename'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
o = ss.option(form.ListValue, 'long_conn_mode', _('Connection Mode'),
|
||||
_('The type of persistent connection to the remote management server.'));
|
||||
o.value('ws', _('WebSocket (ws://)'));
|
||||
@@ -1011,7 +942,7 @@ return view.extend({
|
||||
|
||||
// rule settings
|
||||
o = s.taboption('rule', form.DynamicList, 'trusted_wildcard_domains', _('Trusted Wildcard Domains'),
|
||||
_('The trusted wildcard domains of the gateway'));
|
||||
_('Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf.'));
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.datatype = 'wildcard';
|
||||
@@ -1025,7 +956,7 @@ return view.extend({
|
||||
o.placeholder = 'www.example.com';
|
||||
|
||||
o = s.taboption('rule', form.DynamicList, 'trusted_macs', _('Trusted MACs'),
|
||||
_('The trusted wildcard domains of the gateway.'));
|
||||
_('The trusted MAC addresses of the gateway.'));
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.datatype = 'macaddr';
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require dom';
|
||||
'require uci';
|
||||
|
||||
// Global variables from original display.js
|
||||
var chartRegistry = {};
|
||||
var hostNames = {}; // mac => hostname
|
||||
var hostInfo = {}; // ip => mac
|
||||
var hostNameMacSectionId = "";
|
||||
var isPaused = false;
|
||||
var lastUpdated = null;
|
||||
|
||||
// Line chart variables (from l7.js)
|
||||
var downloadLineChart = {}, uploadLineChart = {};
|
||||
var lineCategories = { ipv4: [], ipv6: [], mac: [] };
|
||||
var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
|
||||
// Color palette for chart series
|
||||
var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
||||
|
||||
var resizeListenerAdded = false;
|
||||
|
||||
// Pre-fill with 60 empty points for a smooth start
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
for (var i = 0; i < 60; i++) {
|
||||
lineCategories[type].push('');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to convert hex to rgba (from l7.js)
|
||||
function hexToRgba(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ?
|
||||
'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' :
|
||||
null;
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
// --- Core Data Logic from display.js ---
|
||||
|
||||
loadHostNames: async function() {
|
||||
try {
|
||||
await uci.sections('hostnames', "hostname", function (params) {
|
||||
hostNameMacSectionId = params['.name'];
|
||||
for (var key in params) {
|
||||
if (key.startsWith('.')) continue;
|
||||
var macAddr = key.split('_').join(':');
|
||||
hostNames[macAddr] = params[key];
|
||||
}
|
||||
});
|
||||
|
||||
const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text');
|
||||
dhcpLeases.split('\n').forEach(function(line) {
|
||||
if (line === '') return;
|
||||
const [mac, ip, hostname] = line.split(' ');
|
||||
if (!hostNames.hasOwnProperty(mac)) {
|
||||
hostNames[mac] = hostname;
|
||||
}
|
||||
});
|
||||
|
||||
const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text');
|
||||
arp.split('\n').forEach(function(line, i) {
|
||||
if (i === 0 || line === '') return;
|
||||
const [ip, mac] = line.split(' ');
|
||||
hostInfo[ip] = mac;
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error getting host names:', e);
|
||||
}
|
||||
},
|
||||
|
||||
loadHostSpeedData: async function() {
|
||||
var self = this;
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json')
|
||||
]);
|
||||
|
||||
const defaultData = {status: "success", data: []};
|
||||
const ipv4Data = results[0] || defaultData;
|
||||
const ipv6Data = results[1] || defaultData;
|
||||
const macData = results[2] || defaultData;
|
||||
|
||||
ipv4Data.data.forEach(function(item) {
|
||||
const mac = hostInfo[item.ip];
|
||||
if (mac) {
|
||||
item.mac = mac;
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
macData.data.forEach(function(item) {
|
||||
const mac = item.mac;
|
||||
if (mac) {
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
|
||||
self.renderHostSpeed(ipv4Data, "ipv4");
|
||||
self.renderHostSpeed(ipv6Data, "ipv6");
|
||||
self.renderHostSpeed(macData, "mac");
|
||||
|
||||
lastUpdated = new Date();
|
||||
var timestampEl = document.getElementById('display-last-updated');
|
||||
if (timestampEl) {
|
||||
timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString());
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error polling data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
pollData: function() {
|
||||
poll.add(L.bind(async function() {
|
||||
if (isPaused) return;
|
||||
await this.loadHostNames();
|
||||
await this.loadHostSpeedData();
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
// --- UI Rendering and Interaction (New structure based on l7.js) ---
|
||||
|
||||
pie: function(id, data, valueFormatter) {
|
||||
var total = data.reduce(function(n, d) { return n + d.value; }, 0);
|
||||
data.sort(function(a, b) { return b.value - a.value; });
|
||||
|
||||
if (total === 0) {
|
||||
data = [{ value: 1, color: '#cccccc', name: _('no traffic') }];
|
||||
}
|
||||
|
||||
data.forEach(function(d, i) {
|
||||
if (!d.color) {
|
||||
var hue = (i * 137.508) % 360;
|
||||
d.color = 'hsl(' + hue + ', 75%, 55%)';
|
||||
}
|
||||
});
|
||||
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (valueFormatter) {
|
||||
// 将 ECharts params 对象转换为自定义格式
|
||||
return valueFormatter({
|
||||
name: params.name,
|
||||
value: params.value,
|
||||
percent: params.percent.toFixed(2)
|
||||
});
|
||||
}
|
||||
return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['25%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 10,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
labelLine: { show: false },
|
||||
data: data.map(function(d) {
|
||||
return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } };
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
var dom = typeof id === 'string' ? document.getElementById(id) : id;
|
||||
if (!chartRegistry[id]) {
|
||||
chartRegistry[id] = echarts.init(dom);
|
||||
}
|
||||
chartRegistry[id].setOption(option, true);
|
||||
return chartRegistry[id];
|
||||
},
|
||||
|
||||
updateStackedLineCharts: function(type, perHostDownload, perHostUpload) {
|
||||
var now = new Date().toLocaleTimeString();
|
||||
lineCategories[type].push(now);
|
||||
lineCategories[type].shift();
|
||||
|
||||
var processChartData = function(seriesData, perHostData) {
|
||||
var allHosts = Object.keys(seriesData);
|
||||
Object.keys(perHostData).forEach(function(host) {
|
||||
if (allHosts.indexOf(host) === -1) {
|
||||
allHosts.push(host);
|
||||
}
|
||||
});
|
||||
|
||||
allHosts.forEach(function(host) {
|
||||
if (!seriesData[host]) {
|
||||
seriesData[host] = Array(59).fill(0);
|
||||
}
|
||||
var rate = perHostData[host] || 0;
|
||||
seriesData[host].push(rate);
|
||||
seriesData[host].shift();
|
||||
});
|
||||
|
||||
return Object.keys(seriesData).map(function(host, index) {
|
||||
var color = colorPalette[index % colorPalette.length];
|
||||
return {
|
||||
name: host,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: { width: 1, color: color },
|
||||
showSymbol: false,
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: hexToRgba(color, 0.5) },
|
||||
{ offset: 1, color: hexToRgba(color, 0) }
|
||||
])
|
||||
},
|
||||
data: seriesData[host]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload);
|
||||
var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload);
|
||||
|
||||
var legendData = downloadChartSeries.map(function(s) { return s.name; });
|
||||
|
||||
if (downloadLineChart[type]) {
|
||||
downloadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: downloadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadLineChart[type]) {
|
||||
uploadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: uploadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderHostSpeed: function(data, type) {
|
||||
if (!data || data.status !== "success" || !Array.isArray(data.data)) return;
|
||||
|
||||
var rows = [];
|
||||
var txRateData = [], rxRateData = [];
|
||||
var txVolumeData = [], rxVolumeData = [];
|
||||
var tx_rate_total = 0, rx_rate_total = 0;
|
||||
var tx_bytes_total = 0, rx_bytes_total = 0;
|
||||
var perHostTxRate = {};
|
||||
var perHostRxRate = {};
|
||||
|
||||
data.data.forEach(item => {
|
||||
if (!item || !item.incoming || !item.outgoing) return;
|
||||
|
||||
var host = item.ip || item.mac || '';
|
||||
var hostname = item.hostname || hostNames[item.mac] || '';
|
||||
var displayName = hostname || host;
|
||||
|
||||
// 判断连接是否活跃
|
||||
var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0;
|
||||
var activityIcon = isActive ? '🟢' : '⚪';
|
||||
|
||||
rows.push([
|
||||
E('span', { 'class': 'host-cell' }, [
|
||||
E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon),
|
||||
E('span', {}, ' ' + host)
|
||||
]),
|
||||
E('span', { 'class': 'hostname-cell' }, [
|
||||
E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'),
|
||||
E('span', {}, ' ' + (hostname || _('Unknown')))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets))
|
||||
]),
|
||||
E('div', { 'class': 'button-container' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-edit',
|
||||
'style': 'margin-right: 5px;',
|
||||
'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '✏️'),
|
||||
E('span', {}, ' ' + _('Edit'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🗑️'),
|
||||
E('span', {}, ' ' + _('Delete'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
rx_rate_total += item.outgoing.rate;
|
||||
tx_rate_total += item.incoming.rate;
|
||||
rx_bytes_total += item.outgoing.total_bytes;
|
||||
tx_bytes_total += item.incoming.total_bytes;
|
||||
|
||||
rxRateData.push({ value: item.outgoing.rate, label: displayName });
|
||||
txRateData.push({ value: item.incoming.rate, label: displayName });
|
||||
rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName });
|
||||
txVolumeData.push({ value: item.incoming.total_bytes, label: displayName });
|
||||
|
||||
perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate;
|
||||
perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate;
|
||||
});
|
||||
|
||||
this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate);
|
||||
|
||||
var table = document.getElementById(type + '-speed-data');
|
||||
cbi_update_table(table, rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
|
||||
var hostEl = document.getElementById(type + '-host-val');
|
||||
if (hostEl) hostEl.textContent = data.data.length;
|
||||
|
||||
var txRateEl = document.getElementById(type + '-tx-rate-val');
|
||||
if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total);
|
||||
|
||||
var rxRateEl = document.getElementById(type + '-rx-rate-val');
|
||||
if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total);
|
||||
|
||||
var txVolEl = document.getElementById(type + '-tx-volume-val');
|
||||
if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total);
|
||||
|
||||
var rxVolEl = document.getElementById(type + '-rx-volume-val');
|
||||
if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total);
|
||||
},
|
||||
|
||||
// --- Interaction Handlers from display.js ---
|
||||
|
||||
handleDeleteHost: function(host, type) {
|
||||
ui.showModal(_('Delete Host'), [
|
||||
E('p', _('Are you sure you want to delete this host?')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => {
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text');
|
||||
this.loadHostSpeedData();
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
ui.hideModal();
|
||||
}
|
||||
})}, _('Delete'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleEditSpeed: function(host, mac, hostname, type) {
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => {
|
||||
let rate_limit_dl = 0, rate_limit_ul = 0;
|
||||
if (res && res.status === 'success' && Array.isArray(res.data)) {
|
||||
const item = res.data.find(d => (d.ip === host || d.mac === host));
|
||||
if (item) {
|
||||
rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024;
|
||||
rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul);
|
||||
}, this)).catch(e => {
|
||||
console.error('Error getting speed limit:', e);
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) {
|
||||
const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac });
|
||||
|
||||
ui.showModal(_('Edit Speed Limit'), [
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]),
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Download Limit')),
|
||||
E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Upload Limit')),
|
||||
E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'cbi-page-actions right' }, [
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => {
|
||||
const dl_val = document.getElementById('dl-rate').value;
|
||||
const ul_val = document.getElementById('ul-rate').value;
|
||||
const newName = document.getElementById('host-name').value;
|
||||
try {
|
||||
if (mac && newName !== hostname) {
|
||||
hostNames[mac] = newName;
|
||||
await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName);
|
||||
await uci.save('hostnames');
|
||||
await uci.apply('hostnames');
|
||||
}
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Speed limit updated')));
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
})}, _('Save'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
validateData: function(value, type) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i;
|
||||
const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i;
|
||||
return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value);
|
||||
},
|
||||
|
||||
createAddControls: function(type, placeholder) {
|
||||
const input = E('input', {
|
||||
type: 'text',
|
||||
class: 'cbi-input-text control-input',
|
||||
style: (type === 'ipv6') ? 'width:320px' : 'width:180px',
|
||||
placeholder: _(placeholder)
|
||||
});
|
||||
const addBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-add',
|
||||
disabled: true
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '➕'),
|
||||
E('span', {}, ' ' + _('Add'))
|
||||
]);
|
||||
const refreshBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
click: () => this.loadHostSpeedData()
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🔄'),
|
||||
E('span', {}, ' ' + _('Refresh'))
|
||||
]);
|
||||
|
||||
input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); });
|
||||
addBtn.addEventListener('click', ui.createHandlerFn(this, async () => {
|
||||
const value = input.value.trim();
|
||||
if (!this.validateData(value, type)) {
|
||||
return ui.addNotification(null, E('p', _('Data format error')));
|
||||
}
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Updated successfully!')));
|
||||
input.value = '';
|
||||
addBtn.disabled = true;
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
}));
|
||||
|
||||
return E('div', { 'class': 'display-controls' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🖥️'),
|
||||
E('label', { 'class': 'control-label' }, _('Add Host:')),
|
||||
input
|
||||
]),
|
||||
E('div', { 'class': 'control-buttons' }, [
|
||||
addBtn,
|
||||
refreshBtn,
|
||||
E('div', { 'class': 'control-group status-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🕐'),
|
||||
E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
initializeUI: function() {
|
||||
if (window.echarts) {
|
||||
var self = this;
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
var dlChartEl = document.getElementById(type + '-download-speed-line-chart');
|
||||
var ulChartEl = document.getElementById(type + '-upload-speed-line-chart');
|
||||
if (!dlChartEl || !ulChartEl) return;
|
||||
|
||||
var baseChartOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var tooltipContent = params[0].axisValueLabel + '<br/>';
|
||||
params.sort(function(a, b) { return b.value - a.value; });
|
||||
params.forEach(function(item) {
|
||||
if (item.value > 0) {
|
||||
tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '<br/>';
|
||||
}
|
||||
});
|
||||
return tooltipContent;
|
||||
}
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: lineCategories[type] },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); } } },
|
||||
series: []
|
||||
};
|
||||
|
||||
downloadLineChart[type] = echarts.init(dlChartEl);
|
||||
downloadLineChart[type].setOption(baseChartOption);
|
||||
|
||||
uploadLineChart[type] = echarts.init(ulChartEl);
|
||||
uploadLineChart[type].setOption(baseChartOption);
|
||||
});
|
||||
|
||||
// 添加窗口大小变化监听器,使图表能够响应式调整
|
||||
if (!resizeListenerAdded) {
|
||||
var resizeTimer = null;
|
||||
var resizeHandler = function() {
|
||||
// 使用防抖,避免频繁触发 resize
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function() {
|
||||
// 调整折线图大小
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
if (downloadLineChart[type]) {
|
||||
downloadLineChart[type].resize();
|
||||
}
|
||||
if (uploadLineChart[type]) {
|
||||
uploadLineChart[type].resize();
|
||||
}
|
||||
});
|
||||
// 调整饼图大小
|
||||
Object.keys(chartRegistry).forEach(function(chartId) {
|
||||
if (chartRegistry[chartId]) {
|
||||
chartRegistry[chartId].resize();
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeListenerAdded = true;
|
||||
}
|
||||
|
||||
this.pollData();
|
||||
} else {
|
||||
setTimeout(this.initializeUI.bind(this), 50);
|
||||
}
|
||||
},
|
||||
|
||||
// --- Main Render Function (New) ---
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
const createTab = (type, title, placeholder) => {
|
||||
return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'line-chart-row' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Download Speed')]),
|
||||
E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Upload Speed')]),
|
||||
E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'kpi-row' }, [
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ])
|
||||
]),
|
||||
E('div', { 'class': 'chart-grid' }, [
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ])
|
||||
])
|
||||
]),
|
||||
E('table', { 'class': 'table', 'id': type + '-speed-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]),
|
||||
E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ])
|
||||
]),
|
||||
self.createAddControls(type, placeholder)
|
||||
]);
|
||||
};
|
||||
|
||||
var tabContainer = E('div', {}, [
|
||||
createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'),
|
||||
createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'),
|
||||
createTab('mac', 'MAC', 'Please enter a valid MAC address')
|
||||
]);
|
||||
|
||||
var node = E([], [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }),
|
||||
E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }),
|
||||
E('div', { 'class': 'l7-view-container' }, [
|
||||
E('h2', [ _('Auth User Speed Monitor') ]),
|
||||
tabContainer
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(tabContainer.childNodes);
|
||||
|
||||
setTimeout(() => this.initializeUI(), 0);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -266,14 +266,14 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateStatus();
|
||||
|
||||
// Poll status every 5 seconds
|
||||
L.Poll.add(function() {
|
||||
return updateStatus();
|
||||
}, 5);
|
||||
|
||||
// Initial update
|
||||
updateStatus();
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require dom';
|
||||
|
||||
var chartRegistry = {};
|
||||
var downloadLineChart, uploadLineChart;
|
||||
|
||||
// Data structures for stacked line charts
|
||||
var lineCategories = [];
|
||||
var downloadSeriesData = {};
|
||||
var uploadSeriesData = {};
|
||||
|
||||
// Color palette for chart series
|
||||
var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
||||
|
||||
var currentSortInfo = {
|
||||
table: null,
|
||||
column: null,
|
||||
reverse: false
|
||||
};
|
||||
var sidLookupTable = {};
|
||||
var isPaused = false;
|
||||
var lastUpdated = null;
|
||||
var pollActive = false;
|
||||
var lastSIDData = null;
|
||||
var resizeListenerAdded = false;
|
||||
var resizeTimer = null;
|
||||
|
||||
// Pre-fill with 60 empty points for a smooth start
|
||||
for (var i = 0; i < 60; i++) {
|
||||
lineCategories.push('');
|
||||
}
|
||||
|
||||
// Helper to convert hex to rgba
|
||||
function hexToRgba(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ?
|
||||
'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' :
|
||||
null;
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
this.loadSIDData(),
|
||||
this.loadL7ProtoData()
|
||||
]);
|
||||
},
|
||||
|
||||
showError: function(message) {
|
||||
var errorEl = document.getElementById('l7-error-message');
|
||||
if (errorEl) {
|
||||
errorEl.textContent = message;
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
hideError: function() {
|
||||
var errorEl = document.getElementById('l7-error-message');
|
||||
if (errorEl) {
|
||||
errorEl.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
loadSIDData: function() {
|
||||
var self = this;
|
||||
return fs.exec_direct('/usr/bin/aw-bpfctl', ['sid', 'json'], 'json').then(function(result) {
|
||||
self.hideError();
|
||||
lastSIDData = result;
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
console.error('Error loading SID data:', error);
|
||||
self.showError(_('Error loading SID data: %s').format(error.message));
|
||||
return { status: 'error', data: [] };
|
||||
});
|
||||
},
|
||||
|
||||
loadL7ProtoData: function() {
|
||||
var self = this;
|
||||
return fs.exec_direct('/usr/bin/aw-bpfctl', ['l7', 'json'], 'json').then(function(result) {
|
||||
self.hideError();
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
console.error('Error loading L7 protocol data:', error);
|
||||
self.showError(_('Error loading L7 protocol data: %s').format(error.message));
|
||||
return { status: 'error', data: [] };
|
||||
});
|
||||
},
|
||||
|
||||
updateStackedLineCharts: function(perServiceDownload, perServiceUpload) {
|
||||
var now = new Date().toLocaleTimeString();
|
||||
lineCategories.push(now);
|
||||
lineCategories.shift();
|
||||
|
||||
var processChartData = function(seriesData, perServiceData) {
|
||||
var allServices = Object.keys(seriesData);
|
||||
Object.keys(perServiceData).forEach(function(service) {
|
||||
if (allServices.indexOf(service) === -1) {
|
||||
allServices.push(service);
|
||||
}
|
||||
});
|
||||
|
||||
allServices.forEach(function(service) {
|
||||
if (!seriesData[service]) {
|
||||
seriesData[service] = Array(59).fill(0);
|
||||
}
|
||||
var rate = perServiceData[service] || 0;
|
||||
seriesData[service].push(rate);
|
||||
seriesData[service].shift();
|
||||
});
|
||||
|
||||
return Object.keys(seriesData).map(function(service, index) {
|
||||
var color = colorPalette[index % colorPalette.length];
|
||||
return {
|
||||
name: service,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: { width: 1, color: color },
|
||||
showSymbol: false,
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: hexToRgba(color, 0.5) },
|
||||
{ offset: 1, color: hexToRgba(color, 0) }
|
||||
])
|
||||
},
|
||||
data: seriesData[service]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var downloadChartSeries = processChartData(downloadSeriesData, perServiceDownload);
|
||||
var uploadChartSeries = processChartData(uploadSeriesData, perServiceUpload);
|
||||
|
||||
var legendData = downloadChartSeries.map(function(s) { return s.name; });
|
||||
|
||||
if (downloadLineChart) {
|
||||
downloadLineChart.setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: downloadChartSeries,
|
||||
xAxis: { data: lineCategories }
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadLineChart) {
|
||||
uploadLineChart.setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: uploadChartSeries,
|
||||
xAxis: { data: lineCategories }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
pie: function(id, data, valueFormatter) {
|
||||
var total = data.reduce(function(n, d) { return n + d.value; }, 0);
|
||||
|
||||
data.sort(function(a, b) { return b.value - a.value; });
|
||||
|
||||
if (total === 0) {
|
||||
data = [{ value: 1, color: '#cccccc', name: _('no traffic') }];
|
||||
}
|
||||
|
||||
data.forEach(function(d, i) {
|
||||
if (!d.color) {
|
||||
var hue = (i * 137.508) % 360;
|
||||
d.color = 'hsl(' + hue + ', 75%, 55%)';
|
||||
}
|
||||
});
|
||||
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (valueFormatter) {
|
||||
// 将 ECharts params 对象转换为自定义格式
|
||||
return valueFormatter({
|
||||
name: params.name,
|
||||
value: params.value,
|
||||
percent: params.percent.toFixed(2)
|
||||
});
|
||||
}
|
||||
return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['25%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 10,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
labelLine: { show: false },
|
||||
data: data.map(function(d) {
|
||||
return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } };
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
var dom = typeof id === 'string' ? document.getElementById(id) : id;
|
||||
|
||||
if (!chartRegistry[id]) {
|
||||
chartRegistry[id] = echarts.init(dom);
|
||||
}
|
||||
|
||||
chartRegistry[id].setOption(option, true);
|
||||
|
||||
return chartRegistry[id];
|
||||
},
|
||||
|
||||
sortTable: function(table, column) {
|
||||
var tbody = table.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
var rows = Array.from(tbody.querySelectorAll('tr:not(.table-titles):not(.placeholder)'));
|
||||
var reverse = (currentSortInfo.table === table && currentSortInfo.column === column) ? !currentSortInfo.reverse : false;
|
||||
|
||||
table.querySelectorAll('th').forEach(function(th) {
|
||||
th.classList.remove('th-sort-asc', 'th-sort-desc');
|
||||
});
|
||||
|
||||
var th = table.querySelector('th:nth-child(' + (column + 1) + ')');
|
||||
th.classList.add(reverse ? 'th-sort-desc' : 'th-sort-asc');
|
||||
|
||||
rows.sort(function(row1, row2) {
|
||||
var a = row1.cells[column].getAttribute('data-value') || row1.cells[column].textContent;
|
||||
var b = row2.cells[column].getAttribute('data-value') || row2.cells[column].textContent;
|
||||
|
||||
if (!isNaN(a) && !isNaN(b)) { a = Number(a); b = Number(b); }
|
||||
|
||||
if (a < b) return reverse ? 1 : -1;
|
||||
if (a > b) return reverse ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentSortInfo.table = table;
|
||||
currentSortInfo.column = column;
|
||||
currentSortInfo.reverse = reverse;
|
||||
|
||||
rows.forEach(function(row) { tbody.removeChild(row); });
|
||||
rows.forEach(function(row) { tbody.appendChild(row); });
|
||||
},
|
||||
|
||||
formatMbps: function(bits) {
|
||||
if (typeof bits !== 'number') return '0.00 Mbps';
|
||||
return (bits / 1024 / 1024).toFixed(2) + ' Mbps';
|
||||
},
|
||||
|
||||
formatMB: function(bytes) {
|
||||
if (typeof bytes !== 'number') return '0.00 MB';
|
||||
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||||
},
|
||||
|
||||
renderSIDData: function(data) {
|
||||
var rows = [];
|
||||
var txRateData = [], rxRateData = [];
|
||||
var txVolumeData = [], rxVolumeData = [];
|
||||
var tx_rate_total = 0, rx_rate_total = 0;
|
||||
var tx_bytes_total = 0, rx_bytes_total = 0;
|
||||
var perServiceTxRate = {};
|
||||
var perServiceRxRate = {};
|
||||
var self = this;
|
||||
var allItems = [];
|
||||
|
||||
if (data && data.status === 'success' && Array.isArray(data.data)) {
|
||||
allItems = data.data;
|
||||
var listSizeEl = document.getElementById('sid-size-select');
|
||||
var listSize = listSizeEl ? parseInt(listSizeEl.value, 10) : 10;
|
||||
|
||||
var activeConnections = allItems.filter(function(item) { return item.incoming.rate > 0 || item.outgoing.rate > 0; });
|
||||
var inactiveConnections = allItems.filter(function(item) { return item.incoming.rate === 0 && item.outgoing.rate === 0; });
|
||||
|
||||
activeConnections.sort(function(a, b) { return (b.incoming.rate + b.outgoing.rate) - (a.incoming.rate + a.outgoing.rate); });
|
||||
inactiveConnections.sort(function(a, b) { return b.incoming.total_bytes - a.incoming.total_bytes; });
|
||||
|
||||
var displayData = activeConnections;
|
||||
if (displayData.length < listSize) {
|
||||
displayData = displayData.concat(inactiveConnections.slice(0, listSize - displayData.length));
|
||||
}
|
||||
|
||||
if (displayData.length > listSize) {
|
||||
displayData = displayData.slice(0, listSize);
|
||||
}
|
||||
|
||||
displayData.forEach(function(item) {
|
||||
var domainOrL7Proto = 'unknown';
|
||||
var lookupInfo = sidLookupTable[item.sid];
|
||||
|
||||
if (lookupInfo) {
|
||||
domainOrL7Proto = lookupInfo.name;
|
||||
} else if (item.sid_type === 'Domain' && item.domain && item.domain !== 'unknown') {
|
||||
domainOrL7Proto = item.domain;
|
||||
} else if (item.sid_type === 'L7' && item.l7_proto_desc && item.l7_proto_desc !== 'unknown') {
|
||||
domainOrL7Proto = item.l7_proto_desc;
|
||||
}
|
||||
|
||||
// 判断连接是否活跃
|
||||
var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0;
|
||||
var activityIcon = isActive ? '🟢' : '⚪';
|
||||
|
||||
rows.push([
|
||||
E('span', { 'class': 'sid-cell' }, [
|
||||
E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon),
|
||||
E('span', {}, ' ' + item.sid)
|
||||
]),
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon' }, '🌐'),
|
||||
E('span', {}, ' ' + domainOrL7Proto)
|
||||
]),
|
||||
[ item.incoming.rate, E('span', { 'class': 'speed-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.incoming.rate))
|
||||
])],
|
||||
[ item.incoming.total_bytes, E('span', { 'class': 'volume-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes))
|
||||
])],
|
||||
[ item.incoming.total_packets, E('span', { 'class': 'packet-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets))
|
||||
])],
|
||||
[ item.outgoing.rate, E('span', { 'class': 'speed-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.outgoing.rate))
|
||||
])],
|
||||
[ item.outgoing.total_bytes, E('span', { 'class': 'volume-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes))
|
||||
])],
|
||||
[ item.outgoing.total_packets, E('span', { 'class': 'packet-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets))
|
||||
])]
|
||||
]);
|
||||
|
||||
txRateData.push({ value: item.incoming.rate, label: domainOrL7Proto });
|
||||
rxRateData.push({ value: item.outgoing.rate, label: domainOrL7Proto });
|
||||
txVolumeData.push({ value: item.incoming.total_bytes, label: domainOrL7Proto });
|
||||
rxVolumeData.push({ value: item.outgoing.total_bytes, label: domainOrL7Proto });
|
||||
|
||||
perServiceTxRate[domainOrL7Proto] = (perServiceTxRate[domainOrL7Proto] || 0) + item.incoming.rate;
|
||||
perServiceRxRate[domainOrL7Proto] = (perServiceRxRate[domainOrL7Proto] || 0) + item.outgoing.rate;
|
||||
});
|
||||
|
||||
allItems.forEach(function(item) {
|
||||
tx_rate_total += item.incoming.rate;
|
||||
rx_rate_total += item.outgoing.rate;
|
||||
tx_bytes_total += item.incoming.total_bytes;
|
||||
rx_bytes_total += item.outgoing.total_bytes;
|
||||
});
|
||||
}
|
||||
|
||||
this.updateStackedLineCharts(perServiceTxRate, perServiceRxRate);
|
||||
|
||||
var table = document.getElementById('sid-data');
|
||||
cbi_update_table('#sid-data', rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
var headers = table.querySelectorAll('th');
|
||||
|
||||
if (!table.hasAttribute('data-sort-initialized')) {
|
||||
headers.forEach(function(header, index) {
|
||||
header.style.cursor = 'pointer';
|
||||
header.addEventListener('click', function() { self.sortTable(table, index); });
|
||||
});
|
||||
table.setAttribute('data-sort-initialized', 'true');
|
||||
}
|
||||
|
||||
table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) {
|
||||
if (!rows[rowIndex]) return;
|
||||
Array.from(row.cells).forEach(function(cell, cellIndex) {
|
||||
if (Array.isArray(rows[rowIndex][cellIndex])) {
|
||||
cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.pie('sid-tx-rate-pie', txRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-rx-rate-pie', rxRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-tx-volume-pie', txVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-rx-volume-pie', rxVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; });
|
||||
|
||||
var sidTotalEl = document.getElementById('sid-total-val');
|
||||
if(sidTotalEl) sidTotalEl.textContent = allItems.length;
|
||||
|
||||
var txRateEl = document.getElementById('sid-tx-rate-val');
|
||||
if(txRateEl) txRateEl.textContent = '%1024.2mbps'.format(tx_rate_total);
|
||||
|
||||
var rxRateEl = document.getElementById('sid-rx-rate-val');
|
||||
if(rxRateEl) rxRateEl.textContent = '%1024.2mbps'.format(rx_rate_total);
|
||||
|
||||
var txVolEl = document.getElementById('sid-tx-volume-val');
|
||||
if(txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total);
|
||||
|
||||
var rxVolEl = document.getElementById('sid-rx-volume-val');
|
||||
if(rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total);
|
||||
|
||||
lastUpdated = new Date();
|
||||
var timestampEl = document.getElementById('last-updated');
|
||||
if (timestampEl) {
|
||||
timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString());
|
||||
}
|
||||
},
|
||||
|
||||
renderL7ProtoData: function(data) {
|
||||
var rows = [];
|
||||
var self = this;
|
||||
|
||||
sidLookupTable = {};
|
||||
|
||||
if (data && data.status === 'success' && data.data) {
|
||||
if (Array.isArray(data.data.protocols)) {
|
||||
data.data.protocols.forEach(function(item) {
|
||||
sidLookupTable[item.sid] = { type: 'protocol', name: item.protocol };
|
||||
rows.push([
|
||||
E('span', { 'class': 'id-cell' }, item.id),
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon l7' }, '🔌'),
|
||||
E('span', {}, ' ' + item.protocol)
|
||||
]),
|
||||
E('span', { 'class': 'sid-cell' }, item.sid)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(data.data.domains)) {
|
||||
data.data.domains.forEach(function(item) {
|
||||
sidLookupTable[item.sid] = { type: 'domain', name: item.domain };
|
||||
rows.push([
|
||||
E('span', { 'class': 'id-cell' }, item.id),
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon domain' }, '🌍'),
|
||||
E('span', {}, ' ' + item.domain)
|
||||
]),
|
||||
E('span', { 'class': 'sid-cell' }, item.sid)
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var table = document.getElementById('l7proto-data');
|
||||
var headers = table.querySelectorAll('th');
|
||||
|
||||
if (!table.hasAttribute('data-sort-initialized')) {
|
||||
headers.forEach(function(header, index) {
|
||||
header.style.cursor = 'pointer';
|
||||
header.addEventListener('click', function() { self.sortTable(table, index); });
|
||||
});
|
||||
table.setAttribute('data-sort-initialized', 'true');
|
||||
}
|
||||
|
||||
cbi_update_table('#l7proto-data', rows, E('em', _('No data recorded yet.')));
|
||||
},
|
||||
|
||||
pollL7Data: function() {
|
||||
if (pollActive) return;
|
||||
|
||||
var self = this;
|
||||
pollActive = true;
|
||||
|
||||
self.loadL7ProtoData().then(function(l7data) {
|
||||
self.renderL7ProtoData(l7data);
|
||||
return self.loadSIDData();
|
||||
}).then(function(sidData){
|
||||
self.renderSIDData(sidData);
|
||||
});
|
||||
|
||||
poll.add(function() {
|
||||
if (isPaused) return Promise.resolve();
|
||||
|
||||
return self.loadL7ProtoData().then(function(data) {
|
||||
self.renderL7ProtoData(data);
|
||||
}).then(function() {
|
||||
return self.loadSIDData().then(function(data) {
|
||||
self.renderSIDData(data);
|
||||
});
|
||||
});
|
||||
}, 5);
|
||||
},
|
||||
|
||||
initializeUI: function() {
|
||||
if (window.echarts) {
|
||||
var dlChartEl = document.getElementById('download-speed-line-chart');
|
||||
var ulChartEl = document.getElementById('upload-speed-line-chart');
|
||||
if (!dlChartEl || !ulChartEl) return;
|
||||
|
||||
var baseChartOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var tooltipContent = params[0].axisValueLabel + '<br/>';
|
||||
params.sort(function(a, b) { return b.value - a.value; });
|
||||
params.forEach(function(item) {
|
||||
if (item.value > 0) {
|
||||
tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mbps'.format(item.value) + '<br/>';
|
||||
}
|
||||
});
|
||||
return tooltipContent;
|
||||
}
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: lineCategories },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mbps'.format(val); } } },
|
||||
series: []
|
||||
};
|
||||
|
||||
|
||||
downloadLineChart = echarts.init(dlChartEl);
|
||||
downloadLineChart.setOption(baseChartOption);
|
||||
|
||||
uploadLineChart = echarts.init(ulChartEl);
|
||||
uploadLineChart.setOption(baseChartOption);
|
||||
|
||||
// 添加窗口大小变化监听器,使图表能够响应式调整
|
||||
if (!resizeListenerAdded) {
|
||||
var resizeTimer = null;
|
||||
var resizeHandler = function() {
|
||||
// 使用防抖,避免频繁触发 resize
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function() {
|
||||
// 调整折线图大小
|
||||
if (downloadLineChart) {
|
||||
downloadLineChart.resize();
|
||||
}
|
||||
if (uploadLineChart) {
|
||||
uploadLineChart.resize();
|
||||
}
|
||||
// 调整饼图大小
|
||||
Object.keys(chartRegistry).forEach(function(chartId) {
|
||||
if (chartRegistry[chartId]) {
|
||||
chartRegistry[chartId].resize();
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeListenerAdded = true;
|
||||
}
|
||||
|
||||
this.pollL7Data();
|
||||
} else {
|
||||
setTimeout(this.initializeUI.bind(this), 50);
|
||||
}
|
||||
}, render: function() {
|
||||
var self = this;
|
||||
|
||||
var controls = E('div', { 'class': 'l7-controls' }, [
|
||||
E('div', { 'class': 'l7-controls-left' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '📊'),
|
||||
E('label', { 'for': 'sid-size-select', 'class': 'control-label' }, _('Show entries:')),
|
||||
E('select', {
|
||||
'id': 'sid-size-select',
|
||||
'class': 'cbi-input-select',
|
||||
'change': ui.createHandlerFn(this, function() {
|
||||
if (lastSIDData) {
|
||||
self.renderSIDData(lastSIDData);
|
||||
}
|
||||
})
|
||||
}, [
|
||||
E('option', { 'value': '10' }, '10'),
|
||||
E('option', { 'value': '15' }, '15'),
|
||||
E('option', { 'value': '20' }, '20'),
|
||||
E('option', { 'value': '25' }, '25'),
|
||||
E('option', { 'value': '50' }, '50')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'l7-controls-right' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🕐'),
|
||||
E('span', { 'id': 'last-updated', 'class': 'last-updated-text' }, _('Last updated: never'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'id': 'pause-resume-btn',
|
||||
'click': function(ev) {
|
||||
isPaused = !isPaused;
|
||||
var btn = ev.target;
|
||||
if (isPaused) {
|
||||
btn.innerHTML = '<span class="btn-icon">▶️</span> ' + _('Resume');
|
||||
btn.classList.remove('cbi-button-action');
|
||||
btn.classList.add('cbi-button-positive');
|
||||
} else {
|
||||
btn.innerHTML = '<span class="btn-icon">⏸️</span> ' + _('Pause');
|
||||
btn.classList.remove('cbi-button-positive');
|
||||
btn.classList.add('cbi-button-action');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '⏸️'),
|
||||
E('span', {}, ' ' + _('Pause'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
var tabContainer = E('div', {}, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'sid', 'data-tab-title': _('L7 SID Data') }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'line-chart-row' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Download Speed')]),
|
||||
E('div', { id: 'download-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Upload Speed')]),
|
||||
E('div', { id: 'upload-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'kpi-row' }, [
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-total-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('L7 Protocol Data')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ])
|
||||
]),
|
||||
E('div', { 'class': 'chart-grid' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Download Speed / SID')]),
|
||||
E('div', { id: 'sid-tx-rate-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Upload Speed / SID')]),
|
||||
E('div', { id: 'sid-rx-rate-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Download Total')]),
|
||||
E('div', { id: 'sid-tx-volume-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Upload Total')]),
|
||||
E('div', { id: 'sid-rx-volume-pie', style: 'width: 100%; height: 300px;' })
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('table', { 'class': 'table', 'id': 'sid-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🆔'), ' ', _('SID') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed (Bit/s)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download (Bytes)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download (Packets)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed (Bit/s)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload (Bytes)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload (Packets)') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '8' }, [
|
||||
E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
controls
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'l7proto', 'data-tab-title': _('L7 Protocol Data') }, [
|
||||
E('table', { 'class': 'table', 'id': 'l7proto-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '3' }, [
|
||||
E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
var node = E([], [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }),
|
||||
E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }),
|
||||
|
||||
E('div', { 'class': 'l7-view-container' }, [
|
||||
E('h2', [ _('L7 Data Monitor') ]),
|
||||
E('div', { 'id': 'l7-error-message' }),
|
||||
tabContainer
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(tabContainer.childNodes);
|
||||
|
||||
setTimeout(this.initializeUI.bind(this), 0);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -15,6 +15,9 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "apfree-wifidog"
|
||||
msgstr "门户认证"
|
||||
|
||||
@@ -165,6 +168,9 @@ msgstr "信任的通配符域名"
|
||||
msgid "The trusted wildcard domains of the gateway."
|
||||
msgstr "网关的信任通配符域名"
|
||||
|
||||
msgid "Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf."
|
||||
msgstr "依赖 aw-bpf 的 DNS XDP(dns_ringbuf_portal)。格式如 .example.com;解析到的 IP 写入门户防火墙 inet wifidogx,不会写入 inet awbpf。"
|
||||
|
||||
msgid "App White List"
|
||||
msgstr "应用白名单"
|
||||
|
||||
@@ -228,6 +234,12 @@ msgstr "启用防NAT"
|
||||
msgid "Enable Anti NAT devices."
|
||||
msgstr "启用防NAT设备"
|
||||
|
||||
msgid "Privileged Ops Secret"
|
||||
msgstr "特权管控秘钥"
|
||||
|
||||
msgid "Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices."
|
||||
msgstr "AWAS云端管理平台动态授权远程OTA升级、重启及系统配置变更所需的本地安全秘钥。生产环境请修改默认值。"
|
||||
|
||||
msgid "TTL Value"
|
||||
msgstr "TTL值"
|
||||
|
||||
@@ -1093,3 +1105,48 @@ msgstr "MQTT认证的用户名。"
|
||||
msgid "The password for MQTT authentication."
|
||||
msgstr "MQTT认证的密码。"
|
||||
|
||||
msgid "Add Authentication Server"
|
||||
msgstr "添加认证服务器"
|
||||
|
||||
msgid "Enter a unique name for this authentication server profile."
|
||||
msgstr "请输入认证服务器配置文件的唯一名称。"
|
||||
|
||||
msgid "Add Long Connection Profile"
|
||||
msgstr "添加长连接配置"
|
||||
|
||||
msgid "Enter a unique name for this long connection profile."
|
||||
msgstr "请输入长连接配置文件的唯一名称。"
|
||||
|
||||
msgid "Rename Authentication Server"
|
||||
msgstr "重命名认证服务器"
|
||||
|
||||
msgid "Enter a new unique name for this authentication server profile."
|
||||
msgstr "请输入认证服务器配置文件的新名称。"
|
||||
|
||||
msgid "Rename Long Connection Profile"
|
||||
msgstr "重命名长连接配置"
|
||||
|
||||
msgid "Enter a new unique name for this long connection profile."
|
||||
msgstr "请输入长连接配置文件的新名称。"
|
||||
|
||||
msgid "Please enter a name."
|
||||
msgstr "请输入名称。"
|
||||
|
||||
msgid "The name \"%s\" already exists in Auth Server profiles. Please choose a different name."
|
||||
msgstr "名称\"%s\"已存在于认证服务器配置中,请选择其他名称。"
|
||||
|
||||
msgid "The name \"%s\" already exists in Long Connection profiles. Please choose a different name."
|
||||
msgstr "名称\"%s\"已存在于长连接配置中,请选择其他名称。"
|
||||
|
||||
msgid "Created new authentication server profile \"%s\"."
|
||||
msgstr "已创建新的认证服务器配置\"%s\"。"
|
||||
|
||||
msgid "Created new long connection profile \"%s\"."
|
||||
msgstr "已创建新的长连接配置\"%s\"。"
|
||||
|
||||
msgid "Renamed authentication server profile from \"%s\" to \"%s\"."
|
||||
msgstr "已将认证服务器配置从\"%s\"重命名为\"%s\"。"
|
||||
|
||||
msgid "Renamed long connection profile from \"%s\" to \"%s\"."
|
||||
msgstr "已将长连接配置从\"%s\"重命名为\"%s\"。"
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
[ -f "/etc/config/hostnames" ] || {
|
||||
echo 'config hostname' > /etc/config/hostnames
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -70,7 +70,7 @@ case "$1" in
|
||||
json_init
|
||||
json_add_object "result"
|
||||
json_add_string "status" "fail"
|
||||
json_add_string "message" "WAN interface not found. Available interfaces: $(ls /sys/class/net/ | tr '\n' ' ')"
|
||||
json_add_string "message" "WAN interface not found"
|
||||
json_close_object
|
||||
json_dump
|
||||
fi
|
||||
|
||||
@@ -3,56 +3,27 @@
|
||||
"title": "apfree-wifidog",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "wifidogx"
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-apfree-wifidog" ]
|
||||
}
|
||||
},
|
||||
"admin/QoS": {
|
||||
"title": "QoS",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
}
|
||||
},
|
||||
"admin/QoS/wifidogx": {
|
||||
"title": "Auth User",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/QoS/wifidogx/display"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-apfree-wifidog"
|
||||
]
|
||||
}
|
||||
},
|
||||
"admin/QoS/wifidogx/display": {
|
||||
"title": "Display",
|
||||
"admin/services/wifidogx/config": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "wifidogx/display"
|
||||
"path": "wifidogx"
|
||||
}
|
||||
},
|
||||
"admin/QoS/wifidogx/status": {
|
||||
"admin/services/wifidogx/status": {
|
||||
"title": "Status",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "wifidogx/status"
|
||||
}
|
||||
},
|
||||
"admin/QoS/xdpi": {
|
||||
"title": "L7 application",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "xdpi/l7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,12 @@
|
||||
},
|
||||
"file": {
|
||||
"/etc/init.d/wifidogx": [ "exec" ],
|
||||
"/usr/bin/wdctlx": [ "exec" ],
|
||||
"/usr/bin/aw-bpfctl": [ "exec" ],
|
||||
"/usr/bin/awk": [ "exec" ]
|
||||
"/usr/bin/wdctlx": [ "exec" ]
|
||||
},
|
||||
"uci": ["wifidogx", "hostnames", "network"]
|
||||
"uci": ["wifidogx", "network"]
|
||||
},
|
||||
"write": {
|
||||
"uci": ["wifidogx", "hostnames"],
|
||||
"uci": ["wifidogx"],
|
||||
"file": {
|
||||
"/etc/wifidogx/*": [ "write" ]
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
|
||||
|
||||
PKG_MAINTAINER:=timsaya
|
||||
|
||||
PKG_VERSION:=0.12.10
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=0.12.11
|
||||
PKG_RELEASE:=10
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -470,6 +470,26 @@ var callSetDefaultRateLimit = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callGetTrafficQuotas = rpc.declare({
|
||||
object: 'luci.bandix',
|
||||
method: 'getTrafficQuotas',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSetTrafficQuota = rpc.declare({
|
||||
object: 'luci.bandix',
|
||||
method: 'setTrafficQuota',
|
||||
params: ['mac', 'minute_bytes', 'hourly_bytes', 'daily_bytes', 'weekly_bytes', 'monthly_bytes', 'total_bytes'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDeleteTrafficQuota = rpc.declare({
|
||||
object: 'luci.bandix',
|
||||
method: 'deleteTrafficQuota',
|
||||
params: ['mac'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
@@ -1903,6 +1923,74 @@ return view.extend({
|
||||
.schedule-rules-info {
|
||||
}
|
||||
|
||||
.traffic-quota-section {
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid ${scheme === 'dark' ? 'rgba(255,255,255,0.16)' : 'rgba(0,0,0,0.12)'};
|
||||
}
|
||||
.traffic-quota-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.traffic-quota-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.traffic-quota-input-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.traffic-quota-input-row .form-input { min-width: 0; flex: 1; }
|
||||
.traffic-quota-input-row .cbi-input-select { width: 82px; flex-shrink: 0; }
|
||||
.traffic-quota-usage {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: ${scheme === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'};
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.traffic-quota-usage-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.traffic-quota-status {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
color: #fff;
|
||||
background: #10b981;
|
||||
}
|
||||
.traffic-quota-status.blocked { background: #ef4444; }
|
||||
.traffic-quota-status.unconfigured { background: #6b7280; }
|
||||
.traffic-quota-cell {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px dashed ${scheme === 'dark' ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.14)'};
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.traffic-quota-blocked-badge {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: #ef4444;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.traffic-quota-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 统计区域样式 */
|
||||
.traffic-stats-container {
|
||||
display: flex;
|
||||
@@ -2904,7 +2992,7 @@ return view.extend({
|
||||
E('th', {}, _('Device Info')),
|
||||
E('th', {}, _('LAN Traffic')),
|
||||
E('th', {}, _('WAN Traffic')),
|
||||
E('th', {}, _('Rate Limit')),
|
||||
E('th', {}, _('Rules / Quota')),
|
||||
E('th', {}, _('Actions'))
|
||||
])
|
||||
]),
|
||||
@@ -3171,7 +3259,30 @@ return view.extend({
|
||||
});
|
||||
}
|
||||
|
||||
// 创建限速设置模态框
|
||||
function createQuotaInput(period, label) {
|
||||
return E('div', { 'class': 'form-group', 'style': 'margin-bottom: 0;' }, [
|
||||
E('label', { 'class': 'form-label' }, label),
|
||||
E('div', { 'class': 'traffic-quota-input-row' }, [
|
||||
E('input', {
|
||||
'type': 'number',
|
||||
'min': '0',
|
||||
'step': '0.01',
|
||||
'class': 'form-input',
|
||||
'id': 'traffic-quota-' + period + '-value',
|
||||
'placeholder': '0'
|
||||
}),
|
||||
E('select', { 'class': 'cbi-input-select', 'id': 'traffic-quota-' + period + '-unit' }, [
|
||||
E('option', { 'value': '1' }, 'B'),
|
||||
E('option', { 'value': '1024' }, 'KB'),
|
||||
E('option', { 'value': '1048576' }, 'MB'),
|
||||
E('option', { 'value': '1073741824' }, 'GB'),
|
||||
E('option', { 'value': '1099511627776' }, 'TB')
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// 创建设备设置模态框
|
||||
var bandixModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'rate-limit-bandix-modal' }, [
|
||||
E('div', { 'class': 'modal-content bandix-modal' }, [
|
||||
E('div', { 'class': 'bandix-modal-body', }, [
|
||||
@@ -3211,6 +3322,46 @@ return view.extend({
|
||||
|
||||
document.body.appendChild(bandixModal);
|
||||
|
||||
// 创建独立的流量配额模态框
|
||||
var trafficQuotaModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'traffic-quota-bandix-modal' }, [
|
||||
E('div', { 'class': 'modal-content bandix-modal' }, [
|
||||
E('div', { 'class': 'bandix-modal-header' }, [
|
||||
E('h3', { 'class': 'bandix-modal-title' }, _('Traffic Quota'))
|
||||
]),
|
||||
E('div', { 'class': 'bandix-modal-body' }, [
|
||||
E('div', { 'class': 'device-summary', 'id': 'traffic-quota-device-summary' }),
|
||||
E('div', { 'class': 'traffic-quota-section', 'style': 'margin-top: 0; padding-top: 0; border-top: none;' }, [
|
||||
E('div', { 'class': 'traffic-quota-header' }, [
|
||||
E('div', { 'style': 'font-weight: 600;' }, _('Quota Status')),
|
||||
E('span', { 'class': 'traffic-quota-status', 'id': 'traffic-quota-status' }, _('Not configured'))
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7;' },
|
||||
_('WAN upload and download are counted together. Enter 0 for unlimited.')),
|
||||
E('div', { 'class': 'traffic-quota-grid' }, [
|
||||
createQuotaInput('minute', _('Minute Quota')),
|
||||
createQuotaInput('hourly', _('Hourly Quota')),
|
||||
createQuotaInput('daily', _('Daily Quota')),
|
||||
createQuotaInput('weekly', _('Weekly Quota')),
|
||||
createQuotaInput('monthly', _('Monthly Quota')),
|
||||
createQuotaInput('total', _('Lifetime Quota'))
|
||||
]),
|
||||
E('div', { 'class': 'traffic-quota-usage', 'id': 'traffic-quota-usage' }, [
|
||||
E('div', { 'style': 'opacity: 0.7;' }, _('No quota usage data'))
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px;' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-negative', 'id': 'traffic-quota-delete-btn', 'style': 'display: none;' }, _('Delete Quota')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'traffic-quota-save-btn' }, _('Save Quota'))
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'bandix-modal-footer' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'traffic-quota-modal-close' }, _('Close'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
document.body.appendChild(trafficQuotaModal);
|
||||
|
||||
// 创建添加规则模态框
|
||||
var addRuleModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'add-rule-bandix-modal' }, [
|
||||
E('div', { 'class': 'modal-content bandix-modal' }, [
|
||||
@@ -3946,9 +4097,17 @@ return view.extend({
|
||||
|
||||
// 模态框事件处理
|
||||
var currentDevice = null;
|
||||
var currentQuotaDevice = null;
|
||||
var quotaFormDirty = false;
|
||||
var editingRule = null;
|
||||
var showRateLimitModal;
|
||||
|
||||
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
|
||||
input.addEventListener(input.tagName === 'SELECT' ? 'change' : 'input', function () {
|
||||
quotaFormDirty = true;
|
||||
});
|
||||
});
|
||||
|
||||
// 显示模态框
|
||||
showRateLimitModal = function (device) {
|
||||
currentDevice = device;
|
||||
@@ -3994,6 +4153,33 @@ return view.extend({
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function showTrafficQuotaModal(device) {
|
||||
currentQuotaDevice = device;
|
||||
quotaFormDirty = false;
|
||||
var modal = document.getElementById('traffic-quota-bandix-modal');
|
||||
var deviceSummary = document.getElementById('traffic-quota-device-summary');
|
||||
var modalContent = [
|
||||
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
|
||||
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
|
||||
];
|
||||
var modalBadges = buildDeviceUplinkChBadges(device);
|
||||
if (modalBadges.length) {
|
||||
modalContent.push(E('div', { 'class': 'device-summary-badges device-uplink-badges-wrap' }, modalBadges));
|
||||
}
|
||||
deviceSummary.innerHTML = E('div', {}, modalContent).innerHTML;
|
||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(device.mac), false);
|
||||
modal.classList.add('show');
|
||||
loadTrafficQuotaForm();
|
||||
}
|
||||
|
||||
function hideTrafficQuotaModal() {
|
||||
document.getElementById('traffic-quota-bandix-modal').classList.remove('show');
|
||||
setTimeout(function () {
|
||||
currentQuotaDevice = null;
|
||||
quotaFormDirty = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 加载定时限速规则列表
|
||||
function loadScheduleRules() {
|
||||
if (!currentDevice) return;
|
||||
@@ -4163,8 +4349,60 @@ return view.extend({
|
||||
// 绑定 hostname 保存按钮事件
|
||||
document.getElementById('hostname-save-btn').addEventListener('click', saveHostname);
|
||||
|
||||
document.getElementById('traffic-quota-save-btn').addEventListener('click', function () {
|
||||
if (!currentQuotaDevice) return;
|
||||
var mac = currentQuotaDevice.mac;
|
||||
var values = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'].map(readQuotaInput);
|
||||
if (values.some(function (value) { return value === null; })) {
|
||||
ui.addNotification(null, E('p', {}, _('Quota values must be non-negative and within the supported range.')), 'error');
|
||||
return;
|
||||
}
|
||||
var button = this;
|
||||
button.disabled = true;
|
||||
setTrafficQuotaFormDisabled(true);
|
||||
callSetTrafficQuota(mac, values[0], values[1], values[2], values[3], values[4], values[5]).then(function (result) {
|
||||
if (result && result.success === false) throw new Error(result.error || _('Failed to save traffic quota'));
|
||||
return fetchAllTrafficQuotas(true);
|
||||
}).then(function () {
|
||||
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
||||
quotaFormDirty = false;
|
||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), false);
|
||||
}
|
||||
updateDeviceData();
|
||||
}).catch(function (error) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to save traffic quota') + ': ' + (error.message || error)), 'error');
|
||||
}).finally(function () {
|
||||
button.disabled = false;
|
||||
setTrafficQuotaFormDisabled(false);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('traffic-quota-delete-btn').addEventListener('click', function () {
|
||||
if (!currentQuotaDevice) return;
|
||||
var mac = currentQuotaDevice.mac;
|
||||
showConfirmDialog(
|
||||
_('Delete Traffic Quota'),
|
||||
_('Delete this quota and all of its accumulated usage?'),
|
||||
function () {
|
||||
callDeleteTrafficQuota(mac).then(function (result) {
|
||||
if (result && result.success === false) throw new Error(result.error || _('Failed to delete traffic quota'));
|
||||
return fetchAllTrafficQuotas(true);
|
||||
}).then(function () {
|
||||
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
||||
quotaFormDirty = false;
|
||||
renderTrafficQuotaForm(null, false);
|
||||
}
|
||||
updateDeviceData();
|
||||
}).catch(function (error) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to delete traffic quota') + ': ' + (error.message || error)), 'error');
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 绑定关闭按钮事件
|
||||
document.getElementById('bandix-modal-close').addEventListener('click', hideRateLimitModal);
|
||||
document.getElementById('traffic-quota-modal-close').addEventListener('click', hideTrafficQuotaModal);
|
||||
|
||||
// 历史趋势:状态与工具
|
||||
var latestDevices = [];
|
||||
@@ -4175,6 +4413,154 @@ return view.extend({
|
||||
var allScheduleRules = []; // 存储所有设备的定时限速规则
|
||||
var isScheduleRulesLoading = false; // 防止轮询重入
|
||||
|
||||
var allTrafficQuotas = [];
|
||||
var trafficQuotasRequest = null;
|
||||
var trafficQuotasLoadError = null;
|
||||
|
||||
function fetchAllTrafficQuotas(forceRefresh) {
|
||||
if (trafficQuotasRequest) {
|
||||
if (!forceRefresh) return trafficQuotasRequest;
|
||||
return trafficQuotasRequest.then(function () {
|
||||
return fetchAllTrafficQuotas(true);
|
||||
});
|
||||
}
|
||||
trafficQuotasRequest = callGetTrafficQuotas().then(function (res) {
|
||||
if (!res || res.success === false || res.status === 'error') {
|
||||
throw new Error((res && (res.error || res.message)) || _('Failed to load traffic quotas'));
|
||||
}
|
||||
var quotas = [];
|
||||
if (res && res.data && Array.isArray(res.data.quotas)) {
|
||||
quotas = res.data.quotas;
|
||||
} else if (res && Array.isArray(res.quotas)) {
|
||||
quotas = res.quotas;
|
||||
}
|
||||
allTrafficQuotas = quotas;
|
||||
trafficQuotasLoadError = null;
|
||||
return quotas;
|
||||
}).catch(function (error) {
|
||||
console.error('Failed to fetch traffic quotas:', error);
|
||||
allTrafficQuotas = [];
|
||||
trafficQuotasLoadError = error && error.message ? error.message : _('Failed to load traffic quotas');
|
||||
return [];
|
||||
}).finally(function () {
|
||||
trafficQuotasRequest = null;
|
||||
});
|
||||
return trafficQuotasRequest;
|
||||
}
|
||||
|
||||
function getTrafficQuotaForDevice(mac) {
|
||||
for (var i = 0; i < allTrafficQuotas.length; i++) {
|
||||
if (allTrafficQuotas[i] && allTrafficQuotas[i].mac === mac) {
|
||||
return allTrafficQuotas[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function quotaPeriods(quota) {
|
||||
return [
|
||||
{ key: 'minute', label: _('Minute'), limit: quota.minute_bytes || 0, used: quota.minute_used_bytes || 0 },
|
||||
{ key: 'hourly', label: _('Hourly'), limit: quota.hourly_bytes || 0, used: quota.hourly_used_bytes || 0 },
|
||||
{ key: 'daily', label: _('Daily'), limit: quota.daily_bytes || 0, used: quota.daily_used_bytes || 0 },
|
||||
{ key: 'weekly', label: _('Weekly'), limit: quota.weekly_bytes || 0, used: quota.weekly_used_bytes || 0 },
|
||||
{ key: 'monthly', label: _('Monthly'), limit: quota.monthly_bytes || 0, used: quota.monthly_used_bytes || 0 },
|
||||
{ key: 'total', label: _('Lifetime'), limit: quota.total_bytes || 0, used: quota.total_used_bytes || 0 }
|
||||
];
|
||||
}
|
||||
|
||||
function setQuotaInput(period, bytes) {
|
||||
var valueInput = document.getElementById('traffic-quota-' + period + '-value');
|
||||
var unitSelect = document.getElementById('traffic-quota-' + period + '-unit');
|
||||
if (!valueInput || !unitSelect) return;
|
||||
var units = [1099511627776, 1073741824, 1048576, 1024, 1];
|
||||
var unit = period === 'minute' ? 1048576 : 1073741824;
|
||||
if (bytes > 0) {
|
||||
for (var i = 0; i < units.length; i++) {
|
||||
if (bytes >= units[i] && bytes % units[i] === 0) {
|
||||
unit = units[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
unitSelect.value = String(unit);
|
||||
valueInput.value = bytes > 0 ? String(bytes / unit) : '0';
|
||||
}
|
||||
|
||||
function readQuotaInput(period) {
|
||||
var value = Number(document.getElementById('traffic-quota-' + period + '-value').value || 0);
|
||||
var unit = Number(document.getElementById('traffic-quota-' + period + '-unit').value || 1);
|
||||
var bytes = Math.round(value * unit);
|
||||
if (!Number.isFinite(bytes) || bytes < 0 || !Number.isSafeInteger(bytes)) return null;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function setTrafficQuotaFormDisabled(disabled) {
|
||||
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
|
||||
input.disabled = disabled;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTrafficQuotaForm(quota, preserveInputs) {
|
||||
if (!preserveInputs) {
|
||||
var periods = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'];
|
||||
periods.forEach(function (period) {
|
||||
setQuotaInput(period, quota ? (quota[period + '_bytes'] || 0) : 0);
|
||||
});
|
||||
}
|
||||
|
||||
var status = document.getElementById('traffic-quota-status');
|
||||
var usage = document.getElementById('traffic-quota-usage');
|
||||
var deleteBtn = document.getElementById('traffic-quota-delete-btn');
|
||||
if (!status || !usage || !deleteBtn) return;
|
||||
|
||||
status.classList.toggle('blocked', !!(quota && quota.blocked));
|
||||
status.classList.toggle('unconfigured', !quota);
|
||||
status.textContent = !quota ? (trafficQuotasLoadError ? _('Unavailable') : _('Not configured')) : (quota.blocked ? _('Blocked') : _('Active'));
|
||||
deleteBtn.style.display = quota ? '' : 'none';
|
||||
usage.innerHTML = '';
|
||||
if (!quota) {
|
||||
usage.appendChild(E('div', { 'style': 'opacity: 0.7;' }, trafficQuotasLoadError || _('Usage starts when a quota is first saved.')));
|
||||
return;
|
||||
}
|
||||
quotaPeriods(quota).forEach(function (period) {
|
||||
usage.appendChild(E('div', { 'class': 'traffic-quota-usage-row' }, [
|
||||
E('span', {}, period.label),
|
||||
E('span', {}, formatSize(period.used) + ' / ' + (period.limit > 0 ? formatSize(period.limit) : _('Unlimited')))
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
function loadTrafficQuotaForm() {
|
||||
if (!currentQuotaDevice) return Promise.resolve();
|
||||
var mac = currentQuotaDevice.mac;
|
||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), quotaFormDirty);
|
||||
return fetchAllTrafficQuotas().then(function () {
|
||||
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), quotaFormDirty);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildTrafficQuotaCell(mac) {
|
||||
var quota = getTrafficQuotaForDevice(mac);
|
||||
if (!quota) return null;
|
||||
var periods = quotaPeriods(quota).filter(function (period) { return period.limit > 0; });
|
||||
var mostUsed = null;
|
||||
periods.forEach(function (period) {
|
||||
var ratio = period.used / period.limit;
|
||||
if (!mostUsed || ratio > mostUsed.ratio) mostUsed = { period: period, ratio: ratio };
|
||||
});
|
||||
var text = quota.blocked ? _('Quota exceeded') : _('Quota active');
|
||||
if (mostUsed) {
|
||||
text = mostUsed.period.label + ': ' + formatSize(mostUsed.period.used) + ' / ' + formatSize(mostUsed.period.limit);
|
||||
}
|
||||
return E('div', {
|
||||
'class': 'traffic-quota-cell',
|
||||
'style': quota.blocked ? 'color: #ef4444; font-weight: 600;' : '',
|
||||
'title': quota.blocked ? _('WAN traffic is blocked because a quota was reached.') : _('Traffic Quota')
|
||||
}, text);
|
||||
}
|
||||
|
||||
// 获取所有定时限速规则
|
||||
function fetchAllScheduleRules() {
|
||||
if (isScheduleRulesLoading) return Promise.resolve();
|
||||
@@ -5862,7 +6248,7 @@ return view.extend({
|
||||
createSortableHeader(_('Device Info'), 'online'),
|
||||
createSplitHeader(_('LAN Traffic'), 'lan_speed', 'lan_traffic'),
|
||||
createSplitHeader(_('WAN Traffic'), 'wan_speed', 'wan_traffic'),
|
||||
E('th', {}, _('Schedule Rules')),
|
||||
E('th', {}, _('Rules / Quota')),
|
||||
E('th', {}, _('Actions'))
|
||||
])
|
||||
]),
|
||||
@@ -5890,9 +6276,9 @@ return view.extend({
|
||||
// 填充数据
|
||||
filteredDevices.forEach(function (device) {
|
||||
var isOnline = isDeviceOnline(device);
|
||||
var deviceQuota = getTrafficQuotaForDevice(device.mac);
|
||||
|
||||
// 默认使用窄主题样式
|
||||
var buttonText = '⚙';
|
||||
var buttonText = 'R';
|
||||
|
||||
var actionButton = E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
@@ -5903,6 +6289,15 @@ return view.extend({
|
||||
showRateLimitModal(device);
|
||||
});
|
||||
|
||||
var quotaButton = E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'title': _('Traffic Quota')
|
||||
}, 'Q');
|
||||
|
||||
quotaButton.addEventListener('click', function () {
|
||||
showTrafficQuotaModal(device);
|
||||
});
|
||||
|
||||
var deleteButton = E('button', {
|
||||
'class': 'cbi-button cbi-button-reset',
|
||||
'title': _('Delete Device')
|
||||
@@ -5935,7 +6330,10 @@ return view.extend({
|
||||
E('span', {
|
||||
'class': 'device-status ' + (isOnline ? 'online' : 'offline')
|
||||
}),
|
||||
device.host || '-'
|
||||
device.host || '-',
|
||||
deviceQuota && deviceQuota.blocked
|
||||
? E('span', { 'class': 'traffic-quota-blocked-badge' }, _('Quota blocked'))
|
||||
: ''
|
||||
]),
|
||||
E('div', { 'class': 'device-ip' }, [
|
||||
device.conn ? E('span', {
|
||||
@@ -6149,12 +6547,14 @@ return view.extend({
|
||||
};
|
||||
}
|
||||
|
||||
return E('td', {}, rulesInfo);
|
||||
var quotaCell = buildTrafficQuotaCell(device.mac);
|
||||
return E('td', {}, quotaCell ? [rulesInfo, quotaCell] : rulesInfo);
|
||||
})(),
|
||||
|
||||
// 操作
|
||||
E('td', { 'class': 'device-actions' }, [
|
||||
actionButton,
|
||||
quotaButton,
|
||||
deleteButton
|
||||
])
|
||||
]);
|
||||
@@ -6193,6 +6593,16 @@ return view.extend({
|
||||
});
|
||||
return cardActionBtn;
|
||||
})(),
|
||||
(function () {
|
||||
var cardQuotaBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'title': _('Traffic Quota')
|
||||
}, 'Q');
|
||||
cardQuotaBtn.addEventListener('click', function () {
|
||||
showTrafficQuotaModal(device);
|
||||
});
|
||||
return cardQuotaBtn;
|
||||
})(),
|
||||
(function () {
|
||||
var cardDeleteBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-reset',
|
||||
@@ -6279,6 +6689,14 @@ return view.extend({
|
||||
rulesContent
|
||||
]);
|
||||
})(),
|
||||
(function () {
|
||||
var quotaCell = buildTrafficQuotaCell(device.mac);
|
||||
if (!quotaCell) return '';
|
||||
return E('div', { 'class': 'device-card-section' }, [
|
||||
E('div', { 'class': 'device-card-section-label' }, _('Traffic Quota')),
|
||||
quotaCell
|
||||
]);
|
||||
})(),
|
||||
// LAN流量(直接显示,不需要展开/收起)
|
||||
E('div', { 'class': 'device-card-section', 'style': 'margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0, 0, 0, 0.1);' }, [
|
||||
E('div', { 'class': 'device-card-section-label' }, _('LAN Traffic')),
|
||||
@@ -7499,12 +7917,25 @@ return view.extend({
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// 轮询获取流量配额状态(每1秒)
|
||||
poll.add(function () {
|
||||
return fetchAllTrafficQuotas().then(function () {
|
||||
if (window.__bandixRenderTable) window.__bandixRenderTable();
|
||||
if (currentQuotaDevice) {
|
||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(currentQuotaDevice.mac), quotaFormDirty);
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
|
||||
// Traffic Statistics 不再自动刷新,改为手动查询
|
||||
|
||||
// 立即执行一次,不等待轮询
|
||||
updateDeviceData();
|
||||
refreshWhitelistStatus();
|
||||
fetchAllScheduleRules();
|
||||
fetchAllTrafficQuotas().then(function () {
|
||||
if (window.__bandixRenderTable) window.__bandixRenderTable();
|
||||
});
|
||||
|
||||
// 初始化时间范围查询功能
|
||||
setTimeout(function () {
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "ID de programa ancla de egreso TCX"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "Se usa cuando tc_order es before/after. Debe ser un ID de programa de egreso valido en la misma interfaz."
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "Cuota de tráfico"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "Estado de la cuota"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "Cuota por minuto"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "Cuota por hora"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Cuota diaria"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "Cuota semanal"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "Cuota mensual"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "Cuota total acumulada"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "La subida y la descarga WAN se contabilizan juntas. Introduzca 0 para no establecer límite."
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "Sin configurar"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "Bloqueado"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Activa"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "No hay datos de uso de la cuota"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "Eliminar cuota"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "Guardar cuota"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "El uso comienza a contabilizarse al guardar una cuota por primera vez."
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "Minuto"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "Total acumulado"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "Cuota excedida"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "Cuota activa"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "El tráfico WAN está bloqueado porque se alcanzó una cuota."
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "Bloqueado por cuota"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "Los valores de cuota deben ser no negativos y estar dentro del rango admitido."
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "No se pudo guardar la cuota de tráfico"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "Eliminar cuota de tráfico"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "¿Eliminar esta cuota y todo su uso acumulado?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "No se pudo eliminar la cuota de tráfico"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "Reglas / Cuota"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "No se pudieron cargar las cuotas de tráfico"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "No disponible"
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "ID du programme d ancrage egress TCX"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "Utilise lorsque tc_order est before/after. Doit etre un ID de programme egress valide sur la meme interface."
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "Quota de trafic"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "État du quota"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "Quota par minute"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "Quota par heure"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Quota journalier"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "Quota hebdomadaire"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "Quota mensuel"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "Quota total cumulé"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "Le trafic montant et descendant WAN est comptabilisé ensemble. Saisissez 0 pour un trafic illimité."
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "Non configuré"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "Bloqué"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "Aucune donnée d'utilisation du quota"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "Supprimer le quota"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "Enregistrer le quota"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "Le suivi de l'utilisation commence lors du premier enregistrement d'un quota."
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "Minute"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "Total cumulé"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "Quota dépassé"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "Quota actif"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "Le trafic WAN est bloqué car un quota a été atteint."
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "Blocage par quota"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "Les valeurs de quota doivent être positives ou nulles et respecter la plage prise en charge."
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "Impossible d'enregistrer le quota de trafic"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "Supprimer le quota de trafic"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "Supprimer ce quota et toutes ses données d'utilisation cumulées ?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "Impossible de supprimer le quota de trafic"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "Règles / Quota"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "Impossible de charger les quotas de trafic"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "Indisponible"
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "TCX egressアンカープログラムID"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "tc_orderがbefore/afterのときに使用します。同じインターフェース上の有効なegressプログラムIDである必要があります。"
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "トラフィッククォータ"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "クォータの状態"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "1分あたりのクォータ"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "1時間あたりのクォータ"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "1日あたりのクォータ"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "1週間あたりのクォータ"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "1か月あたりのクォータ"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "累計クォータ"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "WANのアップロードとダウンロードは合算されます。無制限にするには0を入力してください。"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未設定"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "ブロック中"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "有効"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "クォータ使用量データはありません"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "クォータを削除"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "クォータを保存"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "クォータを初めて保存した時点から使用量の計測を開始します。"
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "1分"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "累計"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "クォータ超過"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "クォータ有効"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "クォータに達したためWANトラフィックがブロックされています。"
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "クォータによりブロック"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "クォータ値は0以上で、サポートされている範囲内である必要があります。"
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "トラフィッククォータを保存できませんでした"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "トラフィッククォータを削除"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "このクォータと累積された使用量をすべて削除しますか?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "トラフィッククォータを削除できませんでした"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "ルール / クォータ"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "トラフィッククォータを読み込めませんでした"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "利用不可"
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "ID programu kotwicy TCX egress"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "Uzywane, gdy tc_order to before/after. Musi to byc prawidlowe ID programu egress na tym samym interfejsie."
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "Limit transferu"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "Stan limitu"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "Limit minutowy"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "Limit godzinowy"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Limit dzienny"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "Limit tygodniowy"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "Limit miesięczny"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "Łączny limit"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "Ruch wysyłany i pobierany przez WAN jest sumowany. Wpisz 0, aby wyłączyć limit."
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "Nie skonfigurowano"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "Zablokowano"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Aktywny"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "Brak danych o wykorzystaniu limitu"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "Usuń limit"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "Zapisz limit"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "Naliczanie użycia rozpoczyna się po pierwszym zapisaniu limitu."
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "Minuta"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "Łącznie"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "Limit przekroczony"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "Limit aktywny"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "Ruch WAN jest zablokowany, ponieważ osiągnięto limit."
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "Blokada przez limit"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "Wartości limitu muszą być nieujemne i mieścić się w obsługiwanym zakresie."
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "Nie udało się zapisać limitu transferu"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "Usuń limit transferu"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "Usunąć ten limit i wszystkie dane o naliczonym użyciu?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "Nie udało się usunąć limitu transferu"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "Reguły / Limit"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "Nie udało się wczytać limitów transferu"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "Niedostępne"
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "ID якорной программы TCX egress"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "Используется, когда tc_order имеет значение before/after. Должен быть корректный ID egress-программы на том же интерфейсе."
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "Квота трафика"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "Состояние квоты"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "Минутная квота"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "Часовая квота"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Суточная квота"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "Недельная квота"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "Месячная квота"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "Общая квота"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "Входящий и исходящий трафик WAN учитываются вместе. Введите 0, чтобы отключить ограничение."
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "Не настроено"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "Заблокировано"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Активна"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "Нет данных об использовании квоты"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "Удалить квоту"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "Сохранить квоту"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "Учет трафика начинается после первого сохранения квоты."
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "Минута"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "Всего"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "Квота превышена"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "Квота активна"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "Трафик WAN заблокирован, так как достигнута квота."
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "Блокировка по квоте"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "Значения квоты должны быть неотрицательными и находиться в поддерживаемом диапазоне."
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "Не удалось сохранить квоту трафика"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "Удалить квоту трафика"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "Удалить эту квоту и все накопленные данные об использовании?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "Не удалось удалить квоту трафика"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "Правила / Квота"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "Не удалось загрузить квоты трафика"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "Недоступно"
|
||||
|
||||
@@ -1285,3 +1285,93 @@ msgstr "TCX 出站锚点程序 ID"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "当 tc_order 为 before/after 时使用。必须是同一接口上的有效出站程序 ID。"
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "流量配额"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "配额状态"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "每小时配额"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "每天配额"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "每周配额"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "每月配额"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "累计总量配额"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "WAN 上传和下载合并计算;输入 0 表示不限量。"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未配置"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "已阻断"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "生效中"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "暂无配额用量数据"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "删除配额"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "保存配额"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "首次保存配额后开始累计用量。"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "累计"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "配额已用尽"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "配额生效中"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "因达到流量配额,WAN 流量已被阻断。"
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "配额阻断"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "配额必须是支持范围内的非负数。"
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "保存流量配额失败"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "删除流量配额"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "确定删除此配额及其全部累计用量吗?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "删除流量配额失败"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "规则 / 配额"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "加载流量配额失败"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "不可用"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "每分钟配额"
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "每分钟"
|
||||
|
||||
@@ -1258,3 +1258,93 @@ msgstr "TCX 出站錨點程式 ID"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "當 tc_order 為 before/after 時使用。必須是同一介面上的有效出站程式 ID。"
|
||||
|
||||
msgid "Traffic Quota"
|
||||
msgstr "流量配額"
|
||||
|
||||
msgid "Quota Status"
|
||||
msgstr "配額狀態"
|
||||
|
||||
msgid "Minute Quota"
|
||||
msgstr "每分鐘配額"
|
||||
|
||||
msgid "Hourly Quota"
|
||||
msgstr "每小時配額"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "每天配額"
|
||||
|
||||
msgid "Weekly Quota"
|
||||
msgstr "每週配額"
|
||||
|
||||
msgid "Monthly Quota"
|
||||
msgstr "每月配額"
|
||||
|
||||
msgid "Lifetime Quota"
|
||||
msgstr "累計總量配額"
|
||||
|
||||
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
|
||||
msgstr "WAN 上傳和下載合併計算;輸入 0 表示不限量。"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未設定"
|
||||
|
||||
msgid "Blocked"
|
||||
msgstr "已阻斷"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "生效中"
|
||||
|
||||
msgid "No quota usage data"
|
||||
msgstr "暫無配額用量資料"
|
||||
|
||||
msgid "Delete Quota"
|
||||
msgstr "刪除配額"
|
||||
|
||||
msgid "Save Quota"
|
||||
msgstr "儲存配額"
|
||||
|
||||
msgid "Usage starts when a quota is first saved."
|
||||
msgstr "首次儲存配額後開始累計用量。"
|
||||
|
||||
msgid "Minute"
|
||||
msgstr "每分鐘"
|
||||
|
||||
msgid "Lifetime"
|
||||
msgstr "累計"
|
||||
|
||||
msgid "Quota exceeded"
|
||||
msgstr "配額已用盡"
|
||||
|
||||
msgid "Quota active"
|
||||
msgstr "配額生效中"
|
||||
|
||||
msgid "WAN traffic is blocked because a quota was reached."
|
||||
msgstr "因達到流量配額,WAN 流量已被阻斷。"
|
||||
|
||||
msgid "Quota blocked"
|
||||
msgstr "配額阻斷"
|
||||
|
||||
msgid "Quota values must be non-negative and within the supported range."
|
||||
msgstr "配額必須是支援範圍內的非負數。"
|
||||
|
||||
msgid "Failed to save traffic quota"
|
||||
msgstr "儲存流量配額失敗"
|
||||
|
||||
msgid "Delete Traffic Quota"
|
||||
msgstr "刪除流量配額"
|
||||
|
||||
msgid "Delete this quota and all of its accumulated usage?"
|
||||
msgstr "確定刪除此配額及其全部累計用量嗎?"
|
||||
|
||||
msgid "Failed to delete traffic quota"
|
||||
msgstr "刪除流量配額失敗"
|
||||
|
||||
msgid "Rules / Quota"
|
||||
msgstr "規則 / 配額"
|
||||
|
||||
msgid "Failed to load traffic quotas"
|
||||
msgstr "載入流量配額失敗"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "無法使用"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user