Compare commits

...
107 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
github-actions[bot] cb845a1f2d 🌴 Sync 2026-09-06 14:11:16
Merge-upstream / merge (push) Canceled after 0s
2026-09-06 14:11:16 +08:00
github-actions[bot] 3f15c041aa 🍉 Sync 2026-09-06 13:36:06 2026-09-06 13:36:06 +08:00
kiddin9 b2ff7cbe2f Delete .github/diy/patches/mosdns.patch 2026-09-06 13:33:57 +08:00
github-actions[bot] c27f496810 🏅 Sync 2026-09-06 10:36:07 2026-09-06 10:36:07 +08:00
github-actions[bot] dee375ce66 🔥 Sync 2026-09-06 07:02:02
Merge-upstream / merge (push) Canceled after 0s
2026-09-06 07:02:02 +08:00
kiddin9 7c496d5738 Update upstream.yml 2026-09-06 06:59:56 +08:00
github-actions[bot] d3321f4c3c 🎄 Sync 2026-09-06 05:58:26 2026-09-06 05:58:26 +08:00
kiddin9 f5aa63986c Delete .github/diy/patches/filebrowser.patch 2026-09-06 05:56:21 +08:00
github-actions[bot] a2faaa2a01 🛸 Sync 2026-09-06 05:52:01 2026-09-06 05:52:01 +08:00
kiddin9 570b248f8b Update upstream.yml 2026-09-06 05:50:03 +08:00
github-actions[bot] 87d47fbcb2 🔥 Sync 2026-09-06 05:41:59 2026-09-06 05:41:59 +08:00
github-actions[bot] 380ee71a08 🌴 Sync 2026-09-06 03:52:04 2026-09-06 03:52:04 +08:00
github-actions[bot] 8ca86b3a5e 🦄 Sync 2026-09-06 03:35:37 2026-09-06 03:35:37 +08:00
github-actions[bot] dbdd70b00d 🎁 Sync 2026-09-06 02:50:41 2026-09-06 02:50:41 +08:00
kiddin9 e70e8fddee Create filebrowser.patch 2026-09-06 02:48:51 +08:00
github-actions[bot] 02c6c789ba Sync 2026-09-06 02:17:46 2026-09-06 02:17:46 +08:00
github-actions[bot] f638f12257 🗽 Sync 2026-09-06 00:43:47
Merge-upstream / merge (push) Canceled after 0s
2026-09-06 00:43:48 +08:00
kiddin9 259b690118 Update upstream.yml 2026-09-06 00:41:54 +08:00
github-actions[bot] fc820ab4f5 🌴 Sync 2026-09-05 22:29:54 2026-09-05 22:29:54 +08:00
kiddin9 8eec681054 Update upstream.yml 2026-09-05 22:27:44 +08:00
github-actions[bot] 01aa5b5f9e 🔥 Sync 2026-09-05 21:18:46 2026-09-05 21:18:46 +08:00
kiddin9 28a85181b8 Update upstream.yml 2026-09-05 21:16:25 +08:00
1494 changed files with 286379 additions and 56685 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);
+ }
-6
View File
@@ -1,6 +0,0 @@
--- a/luci-app-mosdns/root/etc/hotplug.d/iface/99-mosdns
+++ b/luci-app-mosdns/root/etc/hotplug.d/iface/99-mosdns
@@ -1,2 +1,2 @@
#!/bin/sh
-[ "$ACTION" = ifup ] && /etc/init.d/mosdns restart
+[[ "$ACTION" = ifup && "$(uci -q get mosdns.mosdns.enabled)" == 1 ]] && /etc/init.d/mosdns restart
+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 \
+19 -11
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
@@ -355,6 +361,7 @@ jobs:
(
git_sparse_clone frp https://github.com/laipeng668/luci applications/luci-app-frpc
git_sparse_clone frp https://github.com/laipeng668/luci applications/luci-app-frps
git_sparse_clone frp https://github.com/laipeng668/packages net/frp
) &
(
git_sparse_clone master "https://github.com/coolsnowwolf/lede" package/wwan package/lean package/network/services/shellsync package/qca/shortcut-fe && cp -rf wwan/*/* ./ ; rm -Rf wwan
@@ -387,13 +394,13 @@ 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 net/cloudreve \
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
net/go-nats net/go-wol net/bitsrunlogin-go net/transfer net/udp2raw net/msd_lite \
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/frp net/qBittorrent-Enhanced-Edition \
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/qBittorrent-Enhanced-Edition \
net/tinyportmapper net/nexttrace net/rustdesk-server net/tuic-server net/transmission-web-control \
net/ipset-lists net/ShadowVPN net/nps net/dnsforwarder \
net/ps3netsrv net/brook net/q \
@@ -404,7 +411,7 @@ jobs:
utils/phicomm-k3screenctrl utils/joker utils/7z utils/dhrystone utils/supervisor utils/tinymembench utils/pcat-mgr utils/fan2go \
utils/coremark utils/watchcat multimedia/you-get multimedia/lux multimedia/gmediarender multimedia/ykdl multimedia/gallery-dl \
sound/spotifyd devel/go-rice devel/rust-bindgen admin/gotop \
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn lang/node/node-pnpm
git_clone https://github.com/koshev-msk/modemfeed && mv -n modemfeed/*/!(telephony)/!(ookla-speedtest) ./; rm -rf modemfeed
git_sparse_clone openwrt-25.12 "https://github.com/immortalwrt/luci" applications collections/luci-nginx protocols/luci-proto-minieap protocols/luci-proto-quectel
mv -f applications luciapp && rm -rf luciapp/luci-app-firewall
@@ -424,7 +431,7 @@ jobs:
run: |
shopt -s extglob
set +e
mv -n luciapp/!(luci-app-noddos|luci-app-filebrowser-go|luci-app-openvpn-server|luci-app-chrony|luci-app-kodexplorer|luci-app-cshark|luci-app-dnscrypt-proxy|luci-app-https-dns-proxy|luci-app-ssr-mudb-server|luci-app-ledtrig-*|luci-app-antiblock) ./ ; rm -Rf luciapp
mv -n luciapp/!(luci-app-noddos|luci-app-openvpn-server|luci-app-chrony|luci-app-kodexplorer|luci-app-cshark|luci-app-dnscrypt-proxy|luci-app-https-dns-proxy|luci-app-ssr-mudb-server|luci-app-ledtrig-*|luci-app-antiblock) ./ ; rm -Rf luciapp
mv -n lean/!(r8101|r8125|r8126|r8168) ./ ; rm -Rf lean
mv -n liep/!(luci-app-filebrowser) ./ ; rm -Rf liep
mv -n istore-packages/!(qBittorrent*|luci-app-qbittorrent-ee|baidupcs-web|luci-app-LingTiGameAcc|inter_i40e) ./;rm -rf istore-packages
@@ -464,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 '/')"
@@ -532,7 +539,8 @@ jobs:
-e "s/body\[theme=light\]/:root[data-darkmode=false]/g" luci-app-quickstart/htdocs/luci-static/quickstart/style.css
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/+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)
+18 -5
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:=6
PKG_VERSION:=0.3.3
PKG_RELEASE:=11
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
@@ -52,6 +52,16 @@ define Package/$(PKG_NAME)/conffiles
/etc/config/agentflow
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ "$${IPKG_INSTROOT}" = "" ]; then
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json 2>/dev/null)" = "/usr/share/agentflow/agentflow-plugin.json" ]; then
rm -f /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
fi
fi
exit 0
endef
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)
$(CP) $(DL_DIR)/$(AGENTFLOW_DOWNLOAD) $(PKG_BUILD_DIR)/agentflow
@@ -64,11 +74,14 @@ define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config $(1)/usr/share/agentflow/www $(1)/usr/share/linkeasefull/desktop-apps.d
$(INSTALL_BIN) $(PKG_BUILD_DIR)/agentflow $(1)/usr/sbin/agentflow
$(INSTALL_BIN) ./files/agentflow.init $(1)/etc/init.d/agentflow
$(INSTALL_BIN) ./files/agentflow.uci-default $(1)/etc/uci-defaults/09-agentflow
$(INSTALL_CONF) ./files/agentflow.config $(1)/etc/config/agentflow
$(INSTALL_DATA) ./files/agentflow-plugin.json $(1)/usr/share/agentflow/agentflow-plugin.json
$(INSTALL_DATA) ./files/www/logo.svg $(1)/usr/share/agentflow/www/logo.svg
ln -sf /usr/share/agentflow/agentflow-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
endef
$(eval $(call Download,agentflow))
@@ -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 子进程都能退出。
+41
View File
@@ -0,0 +1,41 @@
{
"schemaVersion": 1,
"id": "agentflow",
"name": "AgentFlow",
"icon": "logo.svg",
"staticRoot": "/usr/share/agentflow/www",
"desktop": {
"mode": "module",
"entry": "desktop-entry.js",
"isolation": "shadow-dom"
},
"standalone": {
"entry": "index.html",
"basePath": "/apps/agentflow/",
"url": "/cgi-bin/luci/admin/services/agentflow/open",
"externalOpen": {
"enabled": true,
"label": "Open AgentFlow"
}
},
"auth": {
"mode": "passthrough"
},
"backend": {
"type": "http",
"scheme": "http",
"host": "127.0.0.1",
"portFromUci": "agentflow.@agentflow[0].port",
"defaultPort": 9000,
"upstreamBasePath": "/apps/agentflow/",
"pathMode": "preserve",
"proxyMode": "app-base"
},
"window": {
"width": 1180,
"height": 760,
"singleton": true
},
"capabilities": ["agent", "coding", "workflow"],
"categories": ["development", "ai"]
}
+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
+21
View File
@@ -0,0 +1,21 @@
<svg
width="150"
height="150"
viewBox="0 0 150 150"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="150" height="150" rx="30" fill="#f5f1e8" />
<text
x="50%"
y="54%"
text-anchor="middle"
dominant-baseline="middle"
font-size="68"
font-weight="700"
font-family="Georgia, 'Times New Roman', serif"
letter-spacing="-4"
fill="#111111"
>
AF
</text>
</svg>

After

Width:  |  Height:  |  Size: 408 B

@@ -0,0 +1,48 @@
import json
import pathlib
import unittest
APP_DIR = pathlib.Path(__file__).resolve().parents[1]
class DesktopManifestContractTest(unittest.TestCase):
def setUp(self):
self.manifest = json.loads(
(APP_DIR / "files" / "agentflow-plugin.json").read_text()
)
def test_manifest_uses_agentflow_app_base_proxy(self):
self.assertEqual(self.manifest["id"], "agentflow")
self.assertEqual(self.manifest["staticRoot"], "/usr/share/agentflow/www")
self.assertEqual(self.manifest["standalone"]["entry"], "index.html")
self.assertEqual(self.manifest["standalone"]["basePath"], "/apps/agentflow/")
self.assertEqual(
self.manifest["standalone"]["url"],
"/cgi-bin/luci/admin/services/agentflow/open",
)
self.assertEqual(
self.manifest["standalone"]["externalOpen"],
{"enabled": True, "label": "Open AgentFlow"},
)
self.assertEqual(self.manifest["auth"]["mode"], "passthrough")
backend = self.manifest["backend"]
self.assertEqual(backend["upstreamBasePath"], "/apps/agentflow/")
self.assertEqual(backend["pathMode"], "preserve")
self.assertEqual(backend["proxyMode"], "app-base")
def test_desktop_entry_matches_manifest_contract(self):
desktop = self.manifest["desktop"]
self.assertEqual(desktop["mode"], "module")
self.assertEqual(desktop["entry"], "desktop-entry.js")
self.assertEqual(desktop["isolation"], "shadow-dom")
self.assertFalse(
(APP_DIR / "files" / "www" / "desktop-entry.js").exists(),
"desktop-entry.js must be served by AgentFlow backend, not the static iframe wrapper",
)
if __name__ == "__main__":
unittest.main()
@@ -1,65 +0,0 @@
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[3]
APPLICATIONS = ROOT / "applications"
def read(relative):
return (APPLICATIONS / relative).read_text()
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:=40
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
-1
View File
@@ -30,7 +30,6 @@ start_service() {
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
procd_set_param command "$PROG" run
procd_append_param command --config "$config_file"
procd_append_param command --disable-timestamp
procd_append_param command --logfile "$LOG_DIR/dae.log"
procd_append_param command --logfile-maxbackups "$log_maxbackups"
procd_append_param command --logfile-maxsize "$log_maxsize"
+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
+78
View File
@@ -0,0 +1,78 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser-q
PKG_VERSION:=1.5.6-stable
PKG_RELEASE:=6
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=kiddin9
PKG_BUILD_DEPENDS:=golang/host node/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/filebrowser-q
SECTION:=utils
CATEGORY:=Utilities
SUBMENU:=Filesystem
TITLE:=FileBrowser Quantum - Modern Web File Manager
URL:=https://github.com/gtsteffaniak/filebrowser
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Package/filebrowser-q/description
FileBrowser Quantum provides a modern, responsive web-based file management
interface with multi-source, real-time search, and enhanced preview features.
endef
define Package/filebrowser-q/conffiles
/etc/filebrowser-q/
/etc/config/filebrowser-q
endef
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
( \
pushd $(PKG_BUILD_DIR)/frontend && \
npm install && \
npm run build ; \
)
( \
cd $(PKG_BUILD_DIR)/backend && \
$(GO_PKG_VARS) \
go build \
-trimpath \
-ldflags="-w -s" \
-o $(PKG_BUILD_DIR)/filebrowser . ; \
)
endef
define Package/filebrowser-q/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
endef
$(eval $(call BuildPackage,filebrowser-q))
+5
View File
@@ -0,0 +1,5 @@
config filebrowser 'config'
option enabled '0'
option listen_port '8787'
option root_path '/'
+67
View File
@@ -0,0 +1,67 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
CONF="filebrowser-q"
PROG="/usr/bin/filebrowser-q"
CONF_PATH="/etc/filebrowser-q/config.yaml"
DB_PATH="/etc/filebrowser-q/database.db"
start_service() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_port root_path root_name
config_get listen_port "config" "listen_port" "8787"
config_get root_path "config" "root_path" "/"
root_name=""
[ "$root_path" = "/" ] && root_name="root"
if [ ! -f "$CONF_PATH" ]; then
mkdir -p "$(dirname "$CONF_PATH")"
cat <<EOF > "$CONF_PATH"
server:
port: $listen_port
database: "$DB_PATH"
sources:
- path: "$root_path"
name: "$root_name"
config:
defaultEnabled: true
auth:
adminUsername: "admin"
adminPassword: "admin"
userDefaults:
ui:
locale: "zhCN"
EOF
else
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
sed -e "s/ port:.*/ port: $listen_port/" \
-e "s, - path:.*, - path: \"$root_path\"," \
-e "s/ name: \".*\"/ name: \"$root_name\"/" \
-i "$CONF_PATH"
fi
procd_open_instance
procd_set_param command "$PROG"
procd_append_param command -c "$CONF_PATH"
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "$CONF"
}
+49 -40
View File
@@ -1,47 +1,46 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2017-2024
#
# This is free software, licensed under the GNU General Public License v2.
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser
PKG_VERSION:=1.5.6-stable
PKG_RELEASE=1
PKG_VERSION:=2.63.23
PKG_RELEASE:=23
ifeq ($(ARCH),aarch64)
PKG_ARCH:=arm64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
PKG_ARCH:=armv7
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=amd64
PKG_HASH:=skip
endif
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_BUILD_DEPENDS:=golang/host node/host node-pnpm/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/filebrowser/filebrowser/v2
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/version.CommitSHA=$(PKG_VERSION) \
$(GO_PKG)/version.Version=v$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/filebrowser
SECTION:=net
CATEGORY:=Network
DEPENDS:=@(arm||aarch64||x86_64)
TITLE:=FileBrowser Quantum
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Web File Browser
URL:=https://github.com/filebrowser/filebrowser
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Package/filebrowser/description
The best free self-hosted web-based file manager.
endef
PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser
PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION)
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
filebrowser provides a file managing interface within a specified directory
and it can be used to upload, delete, preview, rename and edit your files.
It allows the creation of multiple users and each user can have its own directory.
It can be used as a standalone app or as a middleware.
endef
define Package/filebrowser/conffiles
@@ -49,15 +48,25 @@ define Package/filebrowser/conffiles
/etc/config/filebrowser
endef
define Package/filebrowser/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
$(INSTALL_DIR) $(1)/etc/filebrowser
$(INSTALL_CONF) $(CURDIR)/files/config.yaml $(1)/etc/filebrowser/config.yaml
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser
define Build/Compile
( \
export PNPM_HOME="$(PKG_BUILD_DIR)/frontend" ; \
pushd "$(PKG_BUILD_DIR)/frontend" ; \
pnpm install --frozen-lockfile ; \
pnpm run build ; \
popd ; \
$(call GoPackage/Build/Compile) ; \
)
endef
define Package/filebrowser/install
$(call GoPackage/Package/Install/Bin,$(1))
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
endef
$(eval $(call GoBinPackage,filebrowser))
$(eval $(call BuildPackage,filebrowser))
-13
View File
@@ -1,13 +0,0 @@
server:
port: 8989
database: "/etc/filebrowser/database.db"
sources:
- path: "/" # Do not use a root "/" directory or include the "/var" folder
config:
defaultEnabled: true
auth:
adminUsername: admin
adminPassword: "admin"
userDefaults:
ui:
locale: "cn"
+2
View File
@@ -2,4 +2,6 @@ config filebrowser 'config'
option enabled '0'
option listen_port '8989'
option root_path '/'
option base_url ''
option disable_exec '1'
+33 -16
View File
@@ -1,33 +1,50 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=10
CONF="filebrowser"
PROG="/usr/bin/filebrowser"
CONF_PATH="/etc/filebrowser/config.yaml"
PID_FILE="/var/run/filebrowser.pid"
DB_PATH="/etc/filebrowser/database.db"
start() {
start_service() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_port root_path base_url
mkdir -p "${DB_PATH%/*}"
local listen_port root_path base_url disable_exec
config_get listen_port "config" "listen_port" "8989"
config_get root_path "config" "root_path" "/"
echo "Starting filebrowser..."
sed -e "s/ port:.*/ port: $listen_port/" \
-e "s, - path:.*, - path: \"$root_path\"," \
-i "$CONF_PATH"
start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH"
config_get root_path "config" "root_path" "/mnt/"
config_get base_url "config" "base_url"
config_get_bool disable_exec "config" "disable_exec" "1"
procd_open_instance
procd_set_param command "$PROG"
procd_append_param command --database "$DB_PATH"
procd_append_param command --address "[::]"
procd_append_param command --port "$listen_port"
procd_append_param command --root "$root_path"
procd_append_param command --disable-exec="$disable_exec"
[ -z "$base_url" ] || procd_append_param command --baseurl "$base_url"
if [ ! -e "$DB_PATH" ]; then
procd_append_param command --username "admin"
procd_append_param command --password "$("$PROG" hash "admin")"
fi
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
stop() {
kill -9 `pidof filebrowser | sed "s/$$//g"` 2>/dev/null
rm -f "$PID_FILE"
echo "filebrowser stopped"
service_triggers() {
procd_add_reload_trigger "$CONF"
}
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=frp
PKG_VERSION:=0.70.1
PKG_RELEASE:=5
PKG_VERSION:=0.71.0
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'
+17 -67
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"
@@ -147,25 +99,23 @@ start_instance() {
procd_set_param stderr 1
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
while [ $count -lt 10 ]; do
if ip link show "$dev" >/dev/null 2>&1; then
ifconfig "$dev" "$localip" pointopoint "$remoteip" up && \
_log "Configured $dev with IP $localip peer $remoteip"
break
fi
sleep 1
count=$((count + 1))
done
) &
(
count=0
while [ $count -lt 10 ]; do
if [ -d "/sys/class/net/$dev" ]; then
ifconfig "$dev" "$localip" pointopoint "$remoteip" up && \
_log "Configured $dev with IP $localip peer $remoteip"
break
fi
sleep 1
count=$((count + 1))
done
) &
fi
}
+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:=24
PKG_RELEASE:=32
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
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:=27
PKG_RELEASE:=35
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
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:=26
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:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
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:=3
PKG_RELEASE:=5
LUCI_DEPENDS:=+agentflow +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -2,28 +2,128 @@ local http = require "luci.http"
module("luci.controller.agentflow", package.seeall)
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
local DEFAULT_BASE_PATH = "/apps/agentflow/"
local DEFAULT_PORT = 9000
function index()
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
local open = entry({"admin", "services", "agentflow", "open"}, call("agentflow_open"))
open.leaf = true
open.dependent = false
open.sysauth = false
if not nixio.fs.access("/etc/config/agentflow") then
return
end
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
page.dependent = true
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
end
local function uhttpd_has_apps_proxy_prefix()
local uci = require "luci.model.uci".cursor()
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
for _, mapping in ipairs(mappings) do
if mapping == APPS_PROXY_PREFIX then
return true
end
end
return false
end
local function uhttpd_supports_proxy_prefix()
local sys = require "luci.sys"
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
end
local function uhttpd_apps_proxy_available()
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
end
local function linkeasefull_running()
local sys = require "luci.sys"
return sys.call("[ -x /etc/init.d/linkeasefull ] && /etc/init.d/linkeasefull running >/dev/null 2>&1") == 0
end
local function normalized_base_path(path)
path = path or DEFAULT_BASE_PATH
if path:sub(1, 1) ~= "/" then
path = "/" .. path
end
if path:sub(-1) ~= "/" then
path = path .. "/"
end
return path
end
local function authority_host(authority)
if not authority or authority == "" then
return ""
end
if authority:sub(1, 1) == "[" then
return authority:match("^%[([^%]]+)%]") or ""
end
return authority:match("^([^:]+)") or authority
end
local function url_authority(host, port)
if not host or host == "" then
host = "127.0.0.1"
end
if host:find(":") and host:sub(1, 1) ~= "[" then
host = "[" .. host .. "]"
end
return host .. ":" .. tostring(port)
end
local function request_or_lan_host()
local uci = require "luci.model.uci".cursor()
local host = authority_host(http.getenv("HTTP_HOST") or "")
if host ~= "" then
return host
end
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
end
local function agentflow_config()
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or DEFAULT_PORT
if port < 1 or port > 65535 then
port = DEFAULT_PORT
end
local base_path = normalized_base_path(uci:get_first("agentflow", "agentflow", "base_path"))
return port, base_path
end
local function agentflow_entry_url()
local port, base_path = agentflow_config()
if linkeasefull_running() and uhttpd_apps_proxy_available() then
return base_path
end
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
end
function agentflow_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or 9000
if port < 1 or port > 65535 then
port = 9000
end
local port, base_path = agentflow_config()
local entry_url = agentflow_entry_url()
local status = {
running = (sys.call("pidof agentflow >/dev/null") == 0),
port = port
port = port,
base_path = base_path,
entry_url = entry_url,
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
}
http.prepare_content("application/json")
http.write_json(status)
end
function agentflow_open()
local entry_url = agentflow_entry_url()
http.redirect(entry_url)
end
+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"))
@@ -1,12 +1,20 @@
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
if (st && el) {
if (!st.running) {
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
var accessMode = '';
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
} else {
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
+ accessMode
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
}
}
});
+6 -9
View File
@@ -10,6 +10,12 @@ msgstr "AgentFlow 提供用于编排编码智能体和工作流的 Web 管理界
msgid "Click to open AgentFlow"
msgstr "点击打开 AgentFlow"
msgid "Access mode: LinkEase Desktop /apps proxy."
msgstr "访问模式:LinkEase Desktop /apps 代理。"
msgid "Access mode: external port."
msgstr "访问模式:外部端口。"
msgid "Collecting data..."
msgstr "正在获取数据……"
@@ -37,15 +43,6 @@ msgstr "请选择硬盘作为数据目录"
msgid "Required. AgentFlow stores its configuration, database and workspace data under this directory."
msgstr "必需。AgentFlow 在此目录中保存配置、数据库和工作区数据。"
msgid "Shared runtime home"
msgstr "共享运行时 HOME"
msgid "AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory."
msgstr "AgentFlow 和其他运行时感知应用共用此 HOME。除非 mise 显式设置了运行时目录,否则它会从所选 Configs 目录推导。"
msgid "Not configured"
msgstr "未配置"
msgid "Status"
msgstr "状态"
@@ -0,0 +1,32 @@
from pathlib import Path
import unittest
APP_DIR = Path(__file__).resolve().parents[1]
class AgentFlowLuciOpenContractTest(unittest.TestCase):
def read(self, relative):
return (APP_DIR / relative).read_text(encoding="utf-8")
def test_luci_open_matches_linkease_proxy_pattern(self):
makefile = self.read("Makefile")
controller = self.read("luasrc/controller/agentflow.lua")
status = self.read("luasrc/view/agentflow/status.htm")
self.assertNotIn("+luci-lib-linkeaseauth", makefile)
self.assertIn('entry({"admin", "services", "agentflow", "open"}', controller)
self.assertIn("open.sysauth = false", controller)
self.assertIn("function agentflow_open()", controller)
self.assertIn("uhttpd_apps_proxy_available()", controller)
self.assertIn("linkeasefull_running()", controller)
self.assertIn("return base_path", controller)
self.assertIn("url_authority(request_or_lan_host(), port)", controller)
self.assertIn("http.redirect(entry_url)", controller)
self.assertNotIn('linkease_auth_url("auth")', controller)
self.assertIn('url("admin/services/agentflow/open")', status)
self.assertNotIn('window.location.hostname + ":" + st.port', status)
if __name__ == "__main__":
unittest.main()
+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);
fillFormFields(results);
});
};
@@ -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;
},

Some files were not shown because too many files have changed in this diff Show More