Compare commits

...
85 Commits
Author SHA1 Message Date
github-actions[bot] 4b5f70450b 🎄 Sync 2026-09-15 18:29:05
Merge-upstream / merge (push) Waiting to run
2026-09-15 18:29:05 +08:00
kiddin9 17d91bad15 Update homeproxy.patch 2026-09-15 18:26:42 +08:00
github-actions[bot] 4f589e6a41 🍕 Sync 2026-09-15 18:23:13 2026-09-15 18:23:13 +08:00
kiddin9 d03b99505a Create homeproxy.patch 2026-09-15 18:20:59 +08:00
github-actions[bot] c224e11cc7 Sync 2026-09-15 17:04:17 2026-09-15 17:04:17 +08:00
kiddin9 578dfde655 Delete .github/diy/patches/homeproxy.patch 2026-09-15 17:01:58 +08:00
kiddin9 17c94aacbb Delete .github/diy/packages/sing-box/patches/002-support-clash-proxy-providers.patch 2026-09-15 16:59:35 +08:00
kiddin9 700d0d7e02 Update upstream.yml 2026-09-15 16:59:08 +08:00
github-actions[bot] fa60b9df94 🏅 Sync 2026-09-15 13:42:35 2026-09-15 13:42:35 +08:00
kiddin9 cf471bd833 Create homeproxy.patch 2026-09-15 13:40:28 +08:00
kiddin9 2acd07fa2f Update 002-support-clash-proxy-providers.patch 2026-09-15 13:37:18 +08:00
kiddin9 90bee3aa55 Delete .github/diy/packages/sing-box/patches/001-support-remote-rule-set-path.patch 2026-09-15 13:37:03 +08:00
github-actions[bot] d34867614a 🎉 Sync 2026-09-15 12:29:06
Merge-upstream / merge (push) Waiting to run
2026-09-15 12:29:06 +08:00
kiddin9 25adce2488 Create 002-support-clash-proxy-providers.patch 2026-09-15 12:27:03 +08:00
kiddin9 aaa3fd83ad Create 001-support-remote-rule-set-path.patch 2026-09-15 12:26:31 +08:00
github-actions[bot] ac76f4cbc9 🎨 Sync 2026-09-15 11:19:33 2026-09-15 11:19:33 +08:00
kiddin9 a9e0557b71 Update upstream.yml 2026-09-15 11:17:18 +08:00
kiddin9 1aa5f26196 Update upstream.yml 2026-09-15 11:08:58 +08:00
github-actions[bot] 7280abda02 🎁 Sync 2026-09-15 11:06:14 2026-09-15 11:06:14 +08:00
github-actions[bot] 7b7c6e31cd 🛸 Sync 2026-09-15 02:19:07
Merge-upstream / merge (push) Waiting to run
2026-09-15 02:19:07 +08:00
kiddin9 f6683ec9ac Update upstream.yml 2026-09-15 02:16:45 +08:00
github-actions[bot] 921c6ff346 🍓 Sync 2026-09-15 02:13:06 2026-09-15 02:13:06 +08:00
kiddin9 7872f8755a Update luci-mod-status.patch 2026-09-15 02:10:35 +08:00
kiddin9 cfa884d090 Update upstream.yml 2026-09-15 02:10:26 +08:00
kiddin9 0bc6317028 Update upstream.yml 2026-09-15 02:05:16 +08:00
kiddin9 127e9ab526 Update upstream.yml 2026-09-15 02:03:03 +08:00
kiddin9 55f4afaae1 Update upstream.yml 2026-09-15 02:02:39 +08:00
kiddin9 40019bd445 Update upstream.yml 2026-09-15 01:54:56 +08:00
github-actions[bot] 0dbf9b779b 🍉 Sync 2026-09-15 01:05:01 2026-09-15 01:05:01 +08:00
github-actions[bot] 5a1af7b30a 🛸 Sync 2026-09-15 00:39:41 2026-09-15 00:39:41 +08:00
kiddin9 e1c9ca3f42 Update luci-mod-status.patch 2026-09-15 00:37:24 +08:00
github-actions[bot] 5fc27ca614 💐 Sync 2026-09-14 23:49:49 2026-09-14 23:49:49 +08:00
kiddin9 593e4e0ff8 Update upstream.yml 2026-09-14 23:47:43 +08:00
github-actions[bot] 1ffa1631e1 🎈 Sync 2026-09-14 23:41:35 2026-09-14 23:41:35 +08:00
kiddin9 ed6695f0b6 Update luci-mod-status.patch 2026-09-14 23:39:10 +08:00
github-actions[bot] 8a76697528 🍕 Sync 2026-09-14 23:30:37 2026-09-14 23:30:37 +08:00
kiddin9 d6d7df32a5 Update luci-mod-status.patch 2026-09-14 23:28:05 +08:00
github-actions[bot] 26d174538f 🦄 Sync 2026-09-13 19:17:01
Merge-upstream / merge (push) Canceled after 0s
2026-09-13 19:17:01 +08:00
kiddin9 d9f7c5c117 Update upstream.yml 2026-09-13 19:15:00 +08:00
github-actions[bot] 7614ee5c63 💐 Sync 2026-09-13 10:49:47 2026-09-13 10:49:47 +08:00
github-actions[bot] 70d35bab31 🗽 Sync 2026-09-13 02:49:28
Merge-upstream / merge (push) Canceled after 0s
2026-09-13 02:49:28 +08:00
kiddin9 bdd9cea5d7 Update upstream.yml 2026-09-13 02:47:28 +08:00
github-actions[bot] 795028f9ea 💐 Sync 2026-09-13 00:41:32 2026-09-13 00:41:32 +08:00
github-actions[bot] cdcf288975 🎨 Sync 2026-09-12 22:40:36 2026-09-12 22:40:36 +08:00
kiddin9 83bcd4708f Update ssr-plus.patch 2026-09-12 22:38:23 +08:00
kiddin9 07891fa751 Update upstream.yml 2026-09-12 20:57:13 +08:00
github-actions[bot] 5f6047bcba 🗽 Sync 2026-09-12 10:51:28 2026-09-12 10:51:28 +08:00
github-actions[bot] f56e31d6d8 Sync 2026-09-12 01:28:05 2026-09-12 01:28:05 +08:00
github-actions[bot] 84e13cad49 Sync 2026-09-11 23:47:18 2026-09-11 23:47:18 +08:00
github-actions[bot] b5ce6b03b2 🎈 Sync 2026-09-11 10:41:59 2026-09-11 10:41:59 +08:00
github-actions[bot] 0ee799dbde 🍓 Sync 2026-09-11 01:35:11 2026-09-11 01:35:11 +08:00
github-actions[bot] 91e0df6c86 🐤 Sync 2026-09-10 23:45:40 2026-09-10 23:45:40 +08:00
github-actions[bot] d9c423b124 🌴 Sync 2026-09-10 18:27:49
Merge-upstream / merge (push) Canceled after 0s
2026-09-10 18:27:49 +08:00
kiddin9 bea159edee Update upstream.yml 2026-09-10 18:25:42 +08:00
github-actions[bot] 9dcc7b440b 💐 Sync 2026-09-10 18:21:54 2026-09-10 18:21:54 +08:00
kiddin9 e4a587bfc3 Update upstream.yml 2026-09-10 18:19:42 +08:00
github-actions[bot] 705f63c409 🗽 Sync 2026-09-10 18:16:50 2026-09-10 18:16:50 +08:00
kiddin9 e895da1d6b Update upstream.yml 2026-09-10 18:14:28 +08:00
github-actions[bot] 87517f518f 🐶 Sync 2026-09-10 17:05:24 2026-09-10 17:05:24 +08:00
github-actions[bot] ca30fdbbbb 🛸 Sync 2026-09-10 16:56:29 2026-09-10 16:56:29 +08:00
kiddin9 81d5757031 Update upstream.yml 2026-09-10 16:54:18 +08:00
github-actions[bot] 18bc066e07 🗽 Sync 2026-09-10 14:57:19 2026-09-10 14:57:19 +08:00
github-actions[bot] 7b0b16dcd4 💐 Sync 2026-09-10 14:49:21 2026-09-10 14:49:21 +08:00
kiddin9 0e9e9c2496 Update luci-app-passwall.patch 2026-09-10 14:47:22 +08:00
kiddin9 3a12ffbcad Delete .github/diy/patches/frp.patch 2026-09-10 14:34:37 +08:00
github-actions[bot] e45467bc23 🌴 Sync 2026-09-09 14:20:42 2026-09-09 14:20:42 +08:00
kiddin9 99e36fe9b0 Update luci-app-passwall2.patch 2026-09-09 14:18:32 +08:00
github-actions[bot] f8f5792e57 Sync 2026-09-09 01:26:15 2026-09-09 01:26:15 +08:00
kiddin9 a179224c4d Update upstream.yml 2026-09-09 01:23:49 +08:00
github-actions[bot] 302b45db1b 🎈 Sync 2026-09-09 01:06:03 2026-09-09 01:06:03 +08:00
kiddin9 b6b067e0ad Update upstream.yml 2026-09-09 01:03:32 +08:00
github-actions[bot] b0cf524450 🎨 Sync 2026-09-09 00:58:08 2026-09-09 00:58:08 +08:00
kiddin9 f4103e8c2a Create frp.patch 2026-09-09 00:55:42 +08:00
github-actions[bot] 2d6112f2cd 🐤 Sync 2026-09-08 23:54:39 2026-09-08 23:54:39 +08:00
github-actions[bot] 3d2cd8c73a 🎁 Sync 2026-09-08 10:47:09 2026-09-08 10:47:09 +08:00
github-actions[bot] 629b00127c 💐 Sync 2026-09-08 03:58:36 2026-09-08 03:58:36 +08:00
github-actions[bot] 51ae3e2992 🌴 Sync 2026-09-08 01:08:11
Merge-upstream / merge (push) Canceled after 0s
2026-09-08 01:08:11 +08:00
github-actions[bot] 8ff254bfa0 🏅 Sync 2026-09-07 22:51:35 2026-09-07 22:51:35 +08:00
github-actions[bot] 6e956beb69 🔥 Sync 2026-09-07 20:49:02 2026-09-07 20:49:02 +08:00
kiddin9 c05bdbe2bc Update upstream.yml 2026-09-07 20:46:37 +08:00
github-actions[bot] 7cef4bfa84 🎄 Sync 2026-09-07 10:34:05 2026-09-07 10:34:05 +08:00
github-actions[bot] 6348b68a14 💋 Sync 2026-09-07 03:05:06
Merge-upstream / merge (push) Canceled after 0s
2026-09-07 03:05:06 +08:00
kiddin9 95ded25b90 Update upstream.yml 2026-09-07 03:02:57 +08:00
github-actions[bot] 4b545cbffa 🐤 Sync 2026-09-07 02:18:00 2026-09-07 02:18:00 +08:00
github-actions[bot] 65e584daaa 🤞 Sync 2026-09-06 22:50:31 2026-09-06 22:50:31 +08:00
1433 changed files with 280308 additions and 56117 deletions
+29
View File
@@ -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'
});
+1 -1
View File
@@ -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})
+1 -1
View File
@@ -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})
+13 -41
View File
@@ -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);
+ }
+1 -1
View File
@@ -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 \
+13 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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 2AgentFlow 接入共享 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 3LuCI 可见性
目标:
- 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 5AgentFlow 后端项目环境集成
目标:
- 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 子进程都能退出。
+8 -27
View File
@@ -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
View File
@@ -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
+70
View File
@@ -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))
+125
View File
@@ -0,0 +1,125 @@
![ApFreeWiFiDog](https://github.com/liudf0716/apfree_wifidog/blob/master/logo.png)
[![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)
+39
View File
@@ -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'
+304
View File
@@ -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
View File
@@ -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
View File
@@ -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)?
+89
View File
@@ -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))
+17
View File
@@ -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'
+64
View File
@@ -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
}
+17
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cloudflared --version | grep "$PKG_VERSION"
+78
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+83 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
+2 -2
View File
@@ -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
+96
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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 -7
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
+63
View File
@@ -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))
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
frpc --version | grep "$PKG_VERSION"
+45
View File
@@ -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
View File
@@ -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)))
+48 -322
View File
@@ -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
+29 -205
View File
@@ -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() {
+282
View File
@@ -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
View File
@@ -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)))
-86
View File
@@ -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
-27
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -11,7 +11,8 @@
},
"standalone": {
"entry": "index.html",
"basePath": "/apps/kaiplus/"
"basePath": "/apps/kaiplus/",
"url": "/apps/kaiplus/"
},
"config": {
"providerOrder": ["uci"],
+2
View File
@@ -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'
+11
View File
@@ -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
+70
View File
@@ -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)))
+9
View File
@@ -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'
+32
View File
@@ -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"
}
+18
View File
@@ -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
+849
View File
@@ -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;
}
+67
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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)
+3 -3
View File
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.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)
+1 -1
View File
@@ -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
+5 -28
View File
@@ -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"))
-9
View File
@@ -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 "状态"
+15
View File
@@ -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 ""
+169
View File
@@ -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 XDPdns_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" ]
}
+2 -2
View File
@@ -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 () {
+90
View File
@@ -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"
+90
View File
@@ -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"
+90
View File
@@ -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 "利用不可"
+90
View File
@@ -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"
+90
View File
@@ -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 "Недоступно"
+90
View File
@@ -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 "每分钟"
+90
View File
@@ -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