✨ Sync 2026-06-07 00:43:45

This commit is contained in:
github-actions[bot] committed 2026-06-07 00:43:45 +08:00
1 parent 75dec81517
commit 85f7b195f8
18 files changed
+4294 -2

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.5
PKG_RELEASE:=1
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)/config
choice
prompt "Backend dependency"
default PACKAGE_$(PKG_NAME)_daed
help
Select which backend package luci-app-daede should pull in by default.
config PACKAGE_$(PKG_NAME)_daed
bool "daed backend"
config PACKAGE_$(PKG_NAME)_dae
bool "dae backend"
endchoice
endef
define Package/$(PKG_NAME)
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI app for daede dashboard
DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed
PKGARCH:=all
MAINTAINER:=kenzok8
endef
define Package/$(PKG_NAME)/description
LuCI web interface for managing dae or daed backends.
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
po2lmo ./po/zh-cn/daede.po $(PKG_BUILD_DIR)/daede.zh-cn.lmo
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/daede
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DATA) ./root/etc/config/daede $(1)/etc/config/daede
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./root/etc/uci-defaults/90-luci-app-daede-init $(1)/etc/uci-defaults/90-luci-app-daede-init
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-daede.json $(1)/usr/share/luci/menu.d/luci-app-daede.json
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-daede.json $(1)/usr/share/rpcd/acl.d/luci-app-daede.json
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/pkg-info.sh $(1)/usr/share/luci-app-daede/pkg-info.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-geo.sh $(1)/usr/share/luci-app-daede/update-geo.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-pkg.sh $(1)/usr/share/luci-app-daede/update-pkg.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/gen-dae-config.sh $(1)/usr/share/luci-app-daede/gen-dae-config.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/*.js $(1)/www/luci-static/resources/view/daede/
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
$(INSTALL_DATA) $(PKG_BUILD_DIR)/daede.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/daede.zh-cn.lmo
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
if [ -x /etc/uci-defaults/90-luci-app-daede-init ]; then
( . /etc/uci-defaults/90-luci-app-daede-init )
rm -f /etc/uci-defaults/90-luci-app-daede-init
fi
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
/etc/init.d/uhttpd restart >/dev/null 2>&1 || true
fi
exit 0
endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
fi
exit 0
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
'require fs';
'require rpc';
'require uci';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
const BACKENDS = {
daed: {
name: 'daed',
label: 'daed',
uci: 'daed',
initd: '/etc/init.d/daed',
log: '/var/log/daed/daed.log',
pkg: 'daed',
hasWebUI: true,
useNetns: true,
defaultListen: '0.0.0.0:2023'
},
dae: {
name: 'dae',
label: 'dae',
uci: 'dae',
initd: '/etc/init.d/dae',
log: '/var/log/dae/dae.log',
pkg: 'dae',
config: '/etc/dae/config.dae',
example: '/etc/dae/example.dae',
hasWebUI: false,
useNetns: false
}
};
function execOk(cmd, args) {
return L.resolveDefault(fs.exec(cmd, args || []), { code: 1 }).then(function(res) {
return res && res.code === 0;
});
}
function exists(path) {
return L.resolveDefault(fs.stat(path), null).then(function(st) {
return !!st;
});
}
function serviceStatus(name) {
return L.resolveDefault(callServiceList(name), {}).then(function(svc) {
let pid = 0, running = false;
try {
const inst = svc[name].instances[name];
running = !!inst.running;
pid = inst.pid || 0;
} catch (e) {}
return { running: running, pid: pid };
});
}
function detectInstalledBackends() {
return Promise.all([
exists(BACKENDS.dae.initd),
exists(BACKENDS.daed.initd)
]).then(function(r) {
return { dae: r[0], daed: r[1] };
});
}
function detectRunning() {
return Promise.all([
execOk('/usr/bin/pgrep', ['-x', 'dae']),
execOk('/usr/bin/pgrep', ['-x', 'daed'])
]).then(function(r) {
return { dae: r[0], daed: r[1] };
});
}
function loadActiveBackend() {
return L.resolveDefault(uci.load('daede'), null).then(function() {
const active = uci.get('daede', 'config', 'active_backend');
return BACKENDS[active] ? active : '';
});
}
function setActiveBackend(name) {
if (!BACKENDS[name])
return Promise.reject(new Error('invalid backend: ' + name));
return exists('/etc/config/daede').then(function(ok) {
if (!ok)
return fs.write('/etc/config/daede', '');
}).then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config=daede']); })
.then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config.active_backend=' + name]); })
.then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); });
}
function detectBackend() {
return Promise.all([
detectInstalledBackends(),
detectRunning(),
loadActiveBackend()
]).then(function(r) {
const installed = r[0];
const running = r[1];
const preferred = r[2];
let name = preferred;
if (!name || !installed[name])
name = installed.daed ? 'daed' : (installed.dae ? 'dae' : 'daed');
return {
name: name,
backend: BACKENDS[name],
installed: installed,
running: running,
preferred: preferred,
bothInstalled: !!(installed.dae && installed.daed)
};
});
}
return baseclass.extend({
BACKENDS: BACKENDS,
detectInstalledBackends: detectInstalledBackends,
detectRunning: detectRunning,
detectBackend: detectBackend,
setActiveBackend: setActiveBackend,
serviceStatus: serviceStatus
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,305 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require fs';
'require poll';
'require ui';
'require view';
'require view.daede.backend as backend';
const MAX_LINES = 5000;
const CSS = [
'.dd-log-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
'.dd-log-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:10px 14px;margin-bottom:10px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
'.dd-log-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
'.dd-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:0 0 8px;margin-bottom:8px;border-bottom:1px dashed rgba(128,128,128,.2)}',
'.dd-log-toolbar label{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;cursor:pointer;margin:0;opacity:.85}',
'.dd-log-toolbar input[type="checkbox"]{margin:0}',
'.dd-log-toolbar input[type="text"]{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;min-width:160px}',
'.dd-log-toolbar .dd-log-btn{font-size:11.5px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;cursor:pointer}',
'.dd-log-toolbar .dd-log-btn:hover{background:rgba(128,128,128,.1)}',
'.dd-log-toolbar .dd-log-meta{margin-left:auto;font-size:10.5px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
'.dd-log-pane{height:60vh;min-height:360px;overflow:auto;padding:8px 10px;border:1px solid rgba(0,0,0,.08);border-radius:7px;background:#1a1d21;color:#d8dde6;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all}',
'.dd-log-pane .dd-line{padding:1px 4px;border-radius:3px;display:block}',
'.dd-log-pane .dd-line.dd-info{color:#cfd6df}',
'.dd-log-pane .dd-line.dd-warn{color:#e8b95a}',
'.dd-log-pane .dd-line.dd-error{color:#ea7878;background:rgba(234,120,120,.06)}',
'.dd-log-pane .dd-line.dd-debug{color:#7a8290;opacity:.7}',
'.dd-log-pane .dd-line.dd-hidden{display:none}',
'.dd-log-pane .dd-empty{opacity:.5;font-style:italic}',
/* 简化后的字段视觉 */
'.dd-log-pane .dd-ts{color:#6b7480;margin-right:8px}',
'.dd-log-pane .dd-lvl{display:inline-block;min-width:38px;padding:0 5px;margin-right:8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:.4px;text-align:center;vertical-align:1px}',
'.dd-log-pane .dd-lvl-info{color:#7fc7a8;background:rgba(127,199,168,.08)}',
'.dd-log-pane .dd-lvl-warn{color:#e8b95a;background:rgba(232,185,90,.10)}',
'.dd-log-pane .dd-lvl-error{color:#ea7878;background:rgba(234,120,120,.10)}',
'.dd-log-pane .dd-lvl-debug{color:#7a8290;background:rgba(122,130,144,.10)}',
'.dd-log-pane .dd-msg{color:inherit}',
'.dd-log-pane .dd-kv{margin-left:6px;color:#6b7480;font-size:11px;opacity:.85}',
'body.dark .dd-log-card,html[data-theme="dark"] .dd-log-card,html[data-bs-theme="dark"] .dd-log-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
'body.dark .dd-log-pane,html[data-theme="dark"] .dd-log-pane,html[data-bs-theme="dark"] .dd-log-pane{border-color:rgba(255,255,255,.1)}'
].join('');
/* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */
const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/;
function detectLevel(line) {
// daed/dae logs use lvl=info / [INFO] / level=warning style
const m = line.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i);
if (!m) return '';
const lvl = m[1].toUpperCase();
if (lvl === 'DEBUG') return 'dd-debug';
if (lvl === 'INFO') return 'dd-info';
if (lvl.startsWith('WARN')) return 'dd-warn';
return 'dd-error';
}
/* 简化时间戳并按本地时区显示:
* - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成本地时间
* - 旧 logrus 短格式 (e.g. "May 25 07:04:59") 无时区信息,原样提取
*/
function shortTs(raw) {
if (!raw) return raw;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(raw)) {
const d = new Date(raw);
if (!isNaN(d.getTime())) {
const pad = n => String(n).padStart(2, '0');
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
}
const m = raw.match(/(\d{2}:\d{2}:\d{2})/);
return m ? m[1] : raw;
}
function lvlShort(level) {
const u = (level || '').toUpperCase();
if (u === 'WARNING') return 'WARN';
return u || '-';
}
function lvlClass(level) {
const u = (level || '').toUpperCase();
if (u === 'DEBUG') return 'dd-lvl-debug';
if (u === 'INFO') return 'dd-lvl-info';
if (u === 'WARN' || u === 'WARNING') return 'dd-lvl-warn';
return 'dd-lvl-error';
}
function buildLine(ln) {
const cls = detectLevel(ln);
const m = ln.match(RE_LINE);
if (!m) {
return E('div', { 'class': 'dd-line ' + cls }, ln);
}
const ts = shortTs(m[1]);
const lvl = m[2];
const msg = m[3] !== undefined ? m[3] : (m[4] || '');
const kv = (m[5] || '').trim();
const parts = [
E('span', { 'class': 'dd-ts' }, ts),
E('span', { 'class': 'dd-lvl ' + lvlClass(lvl) }, lvlShort(lvl)),
E('span', { 'class': 'dd-msg' }, msg)
];
if (kv) parts.push(E('span', { 'class': 'dd-kv' }, kv));
return E('div', { 'class': 'dd-line ' + cls }, parts);
}
return view.extend({
load: function() {
return backend.detectBackend();
},
render(ctx) {
const self = this;
const LOG_PATH = ctx.backend.log;
const state = {
lastContent: '',
lastSize: -1,
paused: false,
autoScroll: true,
filter: '',
userScrolled: false
};
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [
E('div', { 'class': 'dd-empty' }, _('Loading…'))
]);
// detect manual scroll → auto-pause auto-scroll
pane.addEventListener('scroll', function() {
const atBottom = pane.scrollHeight - pane.scrollTop - pane.clientHeight < 4;
state.userScrolled = !atBottom;
});
const meta = E('span', { 'class': 'dd-log-meta' }, '');
const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' });
cbAuto.addEventListener('change', function() {
state.autoScroll = cbAuto.checked;
if (state.autoScroll) {
pane.scrollTop = pane.scrollHeight;
state.userScrolled = false;
}
});
const cbPause = E('input', { 'type': 'checkbox' });
cbPause.addEventListener('change', function() {
state.paused = cbPause.checked;
});
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
E('option', { 'value': '' }, _('All')),
E('option', { 'value': 'info' }, 'INFO'),
E('option', { 'value': 'warn' }, 'WARN'),
E('option', { 'value': 'error' }, 'ERROR'),
E('option', { 'value': 'alive' }, _('Node Status')),
E('option', { 'value': 'my_group' }, _('Proxy Traffic'))
]);
selFilter.addEventListener('change', function() {
state.filter = selFilter.value;
applyFilter();
});
const btnClear = E('button', { 'class': 'dd-log-btn' }, _('Clear View'));
btnClear.addEventListener('click', function() {
while (pane.firstChild) pane.removeChild(pane.firstChild);
state.lastContent = '';
});
const btnDownload = E('button', { 'class': 'dd-log-btn' }, _('Download'));
btnDownload.addEventListener('click', function() {
const blob = new Blob([state.lastContent || ''], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = ctx.name + '-' + (new Date()).toISOString().replace(/[:.]/g, '-') + '.log';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
const btnTruncate = E('button', { 'class': 'dd-log-btn' }, _('Clear File'));
btnTruncate.addEventListener('click', function() {
if (!confirm(_('Truncate %s log on the router? This cannot be undone.').format(ctx.name)))
return;
fs.write(LOG_PATH, '').then(function() {
state.lastContent = '';
state.lastSize = 0;
}).catch(function() {});
});
function applyFilter() {
const f = state.filter;
pane.querySelectorAll('.dd-line').forEach(function(el) {
if (!f || el.textContent.toLowerCase().indexOf(f) !== -1)
el.classList.remove('dd-hidden');
else
el.classList.add('dd-hidden');
});
}
function appendLines(text) {
const lines = text.split('\n');
const frag = document.createDocumentFragment();
for (let i = 0; i < lines.length; i++) {
const ln = lines[i];
if (!ln) continue;
const el = buildLine(ln);
if (state.filter && ln.toLowerCase().indexOf(state.filter) === -1)
el.classList.add('dd-hidden');
frag.appendChild(el);
}
// drop empty-state placeholder if present
const empty = pane.querySelector('.dd-empty');
if (empty) pane.removeChild(empty);
pane.appendChild(frag);
// cap rendered rows
const overflow = pane.children.length - MAX_LINES;
if (overflow > 0) {
for (let i = 0; i < overflow; i++) pane.removeChild(pane.firstChild);
}
}
function renderEmpty(msg) {
while (pane.firstChild) pane.removeChild(pane.firstChild);
pane.appendChild(E('div', { 'class': 'dd-empty' }, msg));
}
function tick() {
if (state.paused) return Promise.resolve();
return fs.stat(LOG_PATH).then(function(st) {
const size = st.size || 0;
if (size === state.lastSize) {
meta.textContent = '%d bytes · live'.format(size);
return;
}
// File rotated/truncated → full reload
const rotated = size < state.lastSize;
return fs.read_direct(LOG_PATH, 'text').then(function(content) {
content = content || '';
let delta;
if (rotated || state.lastContent === '') {
// full replace
while (pane.firstChild) pane.removeChild(pane.firstChild);
delta = content;
} else if (content.indexOf(state.lastContent) === 0) {
// pure append
delta = content.slice(state.lastContent.length);
} else {
// content changed mid-stream → full replace
while (pane.firstChild) pane.removeChild(pane.firstChild);
delta = content;
}
if (delta) appendLines(delta);
state.lastContent = content;
state.lastSize = size;
meta.textContent = '%d bytes · live'.format(size);
if (state.autoScroll && !state.userScrolled)
pane.scrollTop = pane.scrollHeight;
});
}).catch(function(e) {
const msg = String(e);
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
renderEmpty(_('Log file does not exist yet.'));
else
renderEmpty(_('Error reading log: %s').format(msg));
state.lastSize = -1;
state.lastContent = '';
meta.textContent = '';
});
}
poll.add(tick);
tick();
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, [
E('label', {}, [ cbAuto, _('Auto-scroll') ]),
E('label', {}, [ cbPause, _('Pause') ]),
selFilter,
btnClear,
btnDownload,
btnTruncate,
meta
]);
return E('div', { 'class': 'dd-log-wrap' }, [
E('style', {}, CSS),
E('div', { 'class': 'dd-log-card' }, [
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
toolbar,
pane
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,309 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require fs';
'require poll';
'require uci';
'require ui';
'require view';
'require view.daede.backend as backend';
const DATA_PATHS = {
geoip: '/usr/share/v2ray/geoip.dat',
geosite: '/usr/share/v2ray/geosite.dat'
};
const HEALTH_PATHS = {
btf: '/sys/kernel/btf/vmlinux',
netns: '/run/netns/daens'
};
const CSS = [
'.dd-up-wrap{padding:6px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
'.dd-card-title{font-size:12px;font-weight:600;opacity:.55;margin:0 0 10px;letter-spacing:.3px;text-transform:uppercase}',
'.dd-up-row{display:grid;grid-template-columns:24px 130px 1fr auto auto;gap:10px;align-items:center;font-size:12px;padding:7px 0;border-top:1px dashed rgba(128,128,128,.18)}',
'.dd-up-row:first-of-type{border-top:0}',
'.dd-up-icon{font-size:14px;text-align:center;line-height:1}',
'.dd-up-ok{color:#3da66a}',
'.dd-up-warn{color:#d39e00}',
'.dd-up-err{color:#d96d6d}',
'.dd-up-name{font-weight:600;opacity:.85}',
'.dd-up-meta{opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.dd-up-version{font-size:11px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap}',
'.dd-up-btn{font-size:11px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;white-space:nowrap}',
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
'.dd-up-log.show{display:block}',
'body.dark .dd-up-log,html[data-theme="dark"] .dd-up-log,html[data-bs-theme="dark"] .dd-up-log{color:#a3d9ad}',
/* 手风琴折叠组 —— 与 config.js dd-adv 同构 */
'.dd-adv{margin-bottom:14px}',
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;cursor:pointer;user-select:none;font-size:11.5px;font-weight:600;background:rgba(128,128,128,.04);border:1px solid rgba(0,0,0,.06);border-radius:10px;color:inherit;letter-spacing:.3px;text-transform:uppercase;opacity:.7;box-shadow:0 2px 8px rgba(0,0,0,.03)}',
'.dd-adv-bar:hover{background:rgba(56,134,161,.06);opacity:.95}',
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s;text-transform:none}',
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
'.dd-adv-body{margin-top:8px;padding:10px 14px;border:1px solid rgba(0,0,0,.06);border-radius:10px;background:rgba(255,255,255,.02);box-shadow:0 2px 8px rgba(0,0,0,.03)}',
'.dd-adv.dd-closed{margin-bottom:14px}',
'.dd-adv.dd-closed .dd-adv-body{display:none}',
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,body.dark .dd-adv-bar,body.dark .dd-adv-body,html[data-theme="dark"] .dd-adv-bar,html[data-theme="dark"] .dd-adv-body,html[data-bs-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-body{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}'
].join('');
function fmtBytes(n) {
if (!n && n !== 0) return '-';
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
return (n / 1024 / 1024).toFixed(1) + ' MB';
}
function fmtMtime(epoch) {
if (!epoch) return '';
const d = new Date(epoch * 1000);
return d.toISOString().slice(0, 10);
}
function probeFile(path) {
return L.resolveDefault(fs.stat(path), null).then(function(st) {
return { exists: !!st, size: st ? st.size : 0, mtime: st ? st.mtime : 0 };
});
}
function probePkg(pkg) {
return fs.exec('/usr/share/luci-app-daede/pkg-info.sh', [pkg]).then(function(res) {
const out = (res.stdout || '').trim().split('\t');
return { installed: out[0] || '', latest: out[1] || '' };
}).catch(function() {
return { installed: '', latest: '' };
});
}
function tailLog(path, into) {
return L.resolveDefault(fs.read_direct(path, 'text'), '').then(function(content) {
if (content) {
into.textContent = content;
into.classList.add('show');
}
});
}
return view.extend({
load: function() {
return Promise.all([
backend.detectBackend(),
uci.load('daed').catch(function() {})
]).then(function(r) {
return r[0];
});
},
render: function(ctx) {
const logPane = E('pre', { 'class': 'dd-up-log', 'id': 'dd-up-log' }, '');
const corePkgs = [];
const addCorePkg = function(name) {
if (corePkgs.indexOf(name) === -1)
corePkgs.push(name);
};
addCorePkg(ctx.name);
addCorePkg('daed');
addCorePkg('dae');
const dataBody = E('div', { 'id': 'dd-up-data' }, E('em', {}, _('Probing…')));
const pkgBody = E('div', { 'id': 'dd-up-pkg' }, E('em', {}, _('Probing…')));
const healthBody = E('div', { 'id': 'dd-up-health' }, E('em', {}, _('Probing…')));
const mkRow = function(icon, iconCls, name, meta, btn) {
return E('div', { 'class': 'dd-up-row' }, [
E('span', { 'class': 'dd-up-icon ' + iconCls }, icon),
E('span', { 'class': 'dd-up-name' }, name),
E('span', { 'class': 'dd-up-meta', 'title': meta }, meta),
E('span', {}, ''),
btn || E('span', {}, '')
]);
};
// === Data updates (GeoIP / GeoSite) ===
const updateGeo = function(kind, btn) {
const label = kind === 'geoip' ? 'GeoIP' : 'GeoSite';
btn.disabled = true;
btn.textContent = '...';
return fs.exec('/usr/share/luci-app-daede/update-geo.sh', [kind]).then(function(res) {
if (res.code === 0) {
setTimeout(function() { tailLog('/tmp/luci-app-daede.' + kind + '.log', logPane); }, 2000);
}
}).catch(function() {}).finally(function() {
btn.disabled = false;
btn.textContent = _('Update');
});
};
// === Package updates (dae|daed / luci-app-daede) ===
const upgradePkg = function(pkg, btn) {
if (!confirm(_('Run "apk upgrade %s" now? This may restart the active backend.').format(pkg)))
return;
btn.disabled = true;
btn.textContent = '...';
return fs.exec('/usr/share/luci-app-daede/update-pkg.sh', [pkg]).then(function(res) {
if (res.code === 0) {
setTimeout(function() { tailLog('/tmp/luci-app-daede.pkg-' + pkg + '.log', logPane); }, 3000);
}
}).catch(function() {}).finally(function() {
btn.disabled = false;
btn.textContent = _('Upgrade');
});
};
const refresh = function() {
const probes = [
probeFile(DATA_PATHS.geoip),
probeFile(DATA_PATHS.geosite),
probeFile(HEALTH_PATHS.btf),
backend.detectRunning(),
backend.serviceStatus(ctx.name)
];
corePkgs.forEach(function(pkg) {
probes.push(probePkg(pkg));
});
probes.push(probePkg('luci-app-daede'));
if (ctx.backend.useNetns)
probes.push(probeFile(HEALTH_PATHS.netns));
return Promise.all(probes).then(function(r) {
const geoip = r[0], geosite = r[1], btf = r[2], running = r[3], service = r[4];
const pkgOffset = 5;
const coreInfo = {};
corePkgs.forEach(function(pkg, i) {
coreInfo[pkg] = r[pkgOffset + i];
});
const luci = r[pkgOffset + corePkgs.length];
const ns = r[pkgOffset + corePkgs.length + 1];
// data rows
while (dataBody.firstChild) dataBody.removeChild(dataBody.firstChild);
[
{ k: 'geoip', name: 'GeoIP', r: geoip },
{ k: 'geosite', name: 'GeoSite', r: geosite }
].forEach(function(entry) {
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Update'));
btn.addEventListener('click', function() { updateGeo(entry.k, btn); });
const meta = entry.r.exists
? fmtBytes(entry.r.size) + (entry.r.mtime ? ' · ' + fmtMtime(entry.r.mtime) : '')
: _('missing — click Update to fetch');
dataBody.appendChild(mkRow(
entry.r.exists ? '✓' : '✗',
entry.r.exists ? 'dd-up-ok' : 'dd-up-err',
entry.name,
meta,
btn
));
});
// pkg rows
while (pkgBody.firstChild) pkgBody.removeChild(pkgBody.firstChild);
corePkgs.map(function(pkg) {
const label = _('%s binary').format(pkg) + (ctx.name === pkg ? ' · ' + _('active') : '');
return { k: pkg, name: label, r: coreInfo[pkg] };
}).concat([
{ k: 'luci-app-daede', name: 'luci-app-daede', r: luci }
]).forEach(function(entry) {
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Upgrade'));
btn.addEventListener('click', function() { upgradePkg(entry.k, btn); });
const sameVersion = entry.r.installed && entry.r.latest && entry.r.installed === entry.r.latest;
const updatable = entry.r.installed && entry.r.latest && entry.r.installed !== entry.r.latest;
let meta;
if (!entry.r.installed) {
meta = _('not installed via package manager');
btn.disabled = true;
btn.textContent = _('Unavailable');
} else if (sameVersion) {
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('up to date');
} else if (updatable) {
meta = _('installed') + ': ' + entry.r.installed + ' → ' + _('latest') + ': ' + entry.r.latest;
} else {
meta = _('installed') + ': ' + entry.r.installed + (entry.r.latest ? ' · ' + _('latest') + ': ' + entry.r.latest : '');
}
pkgBody.appendChild(mkRow(
updatable ? '⚠' : (entry.r.installed ? '✓' : '✗'),
updatable ? 'dd-up-warn' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
entry.name,
meta,
btn
));
});
// health rows
while (healthBody.firstChild) healthBody.removeChild(healthBody.firstChild);
healthBody.appendChild(mkRow(
running[ctx.name] ? '✓' : '✗',
running[ctx.name] ? 'dd-up-ok' : 'dd-up-err',
_('%s process').format(ctx.name),
running[ctx.name] ? _('pgrep reports the backend is running') : _('backend process is not running')
));
if (btf.exists)
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btf + ' · ' + fmtBytes(btf.size)));
else
healthBody.appendChild(mkRow('✗', 'dd-up-err', _('Kernel BTF'), _('not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf')));
if (ctx.backend.hasWebUI) {
const listenAddr = uci.get('daed', 'config', 'listen_addr') || ctx.backend.defaultListen;
const port = listenAddr.split(':').slice(-1)[0];
healthBody.appendChild(mkRow(
service.running ? '✓' : '✗',
service.running ? 'dd-up-ok' : 'dd-up-err',
_('WebUI/API'),
service.running ? _('service is running, expected on port %s').format(port) : _('service is stopped')
));
} else {
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Hot reload'), _('/etc/init.d/dae hot_reload is available')));
}
if (ctx.backend.useNetns && ns && ns.exists) {
const btn = E('button', { 'class': 'dd-up-btn' }, _('Clean'));
btn.addEventListener('click', function() {
btn.disabled = true;
fs.exec('/sbin/ip', ['netns', 'del', 'daens']).finally(function() { btn.disabled = false; });
});
healthBody.appendChild(mkRow('⚠', 'dd-up-warn', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('exists, may block daed start'), btn));
} else if (ctx.backend.useNetns) {
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('clean')));
}
});
};
poll.add(refresh);
refresh();
return E('div', { 'class': 'dd-up-wrap' }, [
E('style', {}, CSS),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
dataBody
]),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
pkgBody
]),
(function() {
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, _('System Health')),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
E('div', { 'class': 'dd-adv-body' }, [ healthBody ])
]);
adv.firstChild.addEventListener('click', function() {
adv.classList.toggle('dd-closed');
});
return adv;
})(),
logPane
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
+362
View File
@@ -0,0 +1,362 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Service Status"
msgstr ""
msgid "Collecting data…"
msgstr ""
msgid "RUNNING"
msgstr ""
msgid "STOPPED"
msgstr ""
msgid "Backend"
msgstr ""
msgid "Listen"
msgstr ""
msgid "Toggle service"
msgstr ""
msgid "Open WebUI"
msgstr ""
msgid "Restart"
msgstr ""
msgid "Hot Reload"
msgstr ""
msgid "Active backend"
msgstr ""
msgid "Active"
msgstr ""
msgid "Not installed"
msgstr ""
msgid "Switch to %s"
msgstr ""
msgid "%s is not installed"
msgstr ""
msgid "%s is not installed. Install it first, then switch backend."
msgstr ""
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
msgstr ""
msgid "Stopping current backend…"
msgstr ""
msgid "Switching to %s…"
msgstr ""
msgid "Switched to %s. Click start when you want it to run."
msgstr ""
msgid "Backend switched to %s. Click start to run it."
msgstr ""
msgid "Switch failed."
msgstr ""
msgid "Failed to switch backend: %s"
msgstr ""
msgid "Action \"%s\" error: %s"
msgstr ""
msgid "Action \"%s\" failed: %s"
msgstr ""
msgid "Action \"%s\" succeeded."
msgstr ""
msgid "Toggle failed: %s"
msgstr ""
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
msgstr ""
msgid "daede Settings"
msgstr ""
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
msgstr ""
msgid "Listen Address"
msgstr ""
msgid "Host:port that the daed WebUI and GraphQL API listen on."
msgstr ""
msgid "Max Log Size (MB)"
msgstr ""
msgid "Rotate the log file once it grows past this many megabytes."
msgstr ""
msgid "Max Log Backups"
msgstr ""
msgid "Number of rotated log files to keep."
msgstr ""
msgid "Log Advanced Settings"
msgstr ""
msgid "dae Settings"
msgstr ""
msgid "eBPF-based Linux high-performance transparent proxy solution."
msgstr ""
msgid "Configuration File"
msgstr ""
msgid "dae Configuration"
msgstr ""
msgid "Text-only mode."
msgstr ""
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
msgstr ""
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
msgstr ""
msgid "Save and Hot Reload"
msgstr ""
msgid "Initialize from example"
msgstr ""
msgid "+ Insert Block…"
msgstr ""
msgid "Insert config block at cursor"
msgstr ""
msgid "lines"
msgstr ""
msgid "⚠ %d placeholder"
msgstr ""
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
msgstr ""
msgid " / "
msgstr ""
msgid "这些 "
msgstr ""
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
msgstr ""
msgid "Placeholder lines (click line number to jump):"
msgstr ""
msgid "Jump to line"
msgstr ""
msgid "Validating…"
msgstr ""
msgid "Validate failed: %s"
msgstr ""
msgid "Saving…"
msgstr ""
msgid "Reload failed: %s"
msgstr ""
msgid "Validated · saved · hot-reloaded"
msgstr ""
msgid "Save failed: %s"
msgstr ""
msgid "Loading example…"
msgstr ""
msgid "Example loaded"
msgstr ""
msgid "Init failed: %s"
msgstr ""
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
msgstr ""
msgid "Loading…"
msgstr ""
msgid "Log file does not exist yet."
msgstr ""
msgid "Error reading log: %s"
msgstr ""
msgid "Log file cleared."
msgstr ""
msgid "Failed to clear log: %s"
msgstr ""
msgid "Truncate %s log on the router? This cannot be undone."
msgstr ""
msgid "Settings"
msgstr ""
msgid "Updates"
msgstr ""
msgid "Log"
msgstr ""
msgid "Data Updates"
msgstr ""
msgid "Package Updates"
msgstr ""
msgid "System Health"
msgstr ""
msgid "Update"
msgstr ""
msgid "Upgrade"
msgstr ""
msgid "Clean"
msgstr ""
msgid "missing — click Update to fetch"
msgstr ""
msgid "%s download started — will refresh in 1–3 min."
msgstr ""
msgid "%s update already running."
msgstr ""
msgid "%s update failed: %s"
msgstr ""
msgid "%s update error: %s"
msgstr ""
msgid "%s binary"
msgstr ""
msgid "active"
msgstr ""
msgid "installed"
msgstr ""
msgid "latest"
msgstr ""
msgid "up to date"
msgstr ""
msgid "not installed via package manager"
msgstr ""
msgid "Unavailable"
msgstr ""
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
msgstr ""
msgid "%s upgrade started — see log below."
msgstr ""
msgid "%s upgrade already running."
msgstr ""
msgid "%s upgrade failed: %s"
msgstr ""
msgid "%s upgrade error: %s"
msgstr ""
msgid "Probing…"
msgstr ""
msgid "%s process"
msgstr ""
msgid "Kernel BTF"
msgstr ""
msgid "WebUI/API"
msgstr ""
msgid "Hot reload"
msgstr ""
msgid "netns daens"
msgstr ""
msgid "pgrep reports the backend is running"
msgstr ""
msgid "backend process is not running"
msgstr ""
msgid "service is running, expected on port %s"
msgstr ""
msgid "service is stopped"
msgstr ""
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
msgstr ""
msgid "/etc/init.d/dae hot_reload is available"
msgstr ""
msgid "netns daens removed."
msgstr ""
msgid "exists, may block daed start"
msgstr ""
msgid "clean"
msgstr ""
msgid "%s Realtime Log"
msgstr ""
msgid "Auto-scroll"
msgstr ""
msgid "Pause"
msgstr ""
msgid "Filter (substring)"
msgstr ""
msgid "Clear View"
msgstr ""
msgid "Clear File"
msgstr ""
msgid "Download"
msgstr ""
+653
View File
@@ -0,0 +1,653 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: PACKAGE VERSION\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh-Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
# ---- config.js: Service Status ----
msgid "Service Status"
msgstr "服务状态"
msgid "Collecting data…"
msgstr "正在收集数据…"
msgid "RUNNING"
msgstr "运行中"
msgid "STOPPED"
msgstr "已停止"
msgid "Backend"
msgstr "后端"
msgid "Listen"
msgstr "监听"
msgid "Toggle service"
msgstr "开关服务"
msgid "Open WebUI"
msgstr "打开 WebUI"
msgid "Restart"
msgstr "重启"
msgid "Hot Reload"
msgstr "热重载"
# ---- config.js: Backend Switcher ----
msgid "Active backend"
msgstr "当前后端"
msgid "Active"
msgstr "当前"
msgid "Not installed"
msgstr "未安装"
msgid "Switch to %s"
msgstr "切换到 %s"
msgid "%s is not installed"
msgstr "%s 未安装"
msgid "%s is not installed. Install it first, then switch backend."
msgstr "%s 未安装。请先安装再切换后端。"
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。"
msgid "Stopping current backend…"
msgstr "停止当前后端中…"
msgid "Switching to %s…"
msgstr "切换至 %s 中…"
msgid "Switched to %s. Click start when you want it to run."
msgstr "已切换至 %s。点击开关启动。"
msgid "Backend switched to %s. Click start to run it."
msgstr "已切换至 %s。点击开关启动。"
msgid "Switch failed."
msgstr "切换失败。"
msgid "Failed to switch backend: %s"
msgstr "切换后端失败:%s"
# ---- config.js: Init Actions ----
msgid "Action \"%s\" error: %s"
msgstr "操作 \"%s\" 出错:%s"
msgid "Action \"%s\" failed: %s"
msgstr "操作 \"%s\" 失败:%s"
msgid "Action \"%s\" succeeded."
msgstr "操作 \"%s\" 成功。"
msgid "Toggle failed: %s"
msgstr "开关切换失败:%s"
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。"
# ---- config.js: daede Settings ----
msgid "daede Settings"
msgstr "daede 设置"
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。"
msgid "Listen Address"
msgstr "监听地址"
msgid "Host:port that the daed WebUI and GraphQL API listen on."
msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。"
msgid "Max Log Size (MB)"
msgstr "单文件大小上限(MB)"
msgid "Rotate the log file once it grows past this many megabytes."
msgstr "单个日志文件超过此大小后轮转。"
msgid "Max Log Backups"
msgstr "保留旧日志数"
msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings"
msgstr "日志高级设置"
# ---- config.js: dae Settings ----
msgid "dae Parameters"
msgstr "dae 参数"
msgid "Configuration file path and log rotation settings for dae."
msgstr "dae 配置文件路径与日志轮转参数。"
msgid "Configuration File"
msgstr "配置文件"
# ---- config.js: dae Editor ----
msgid "dae Configuration"
msgstr "dae 配置"
msgid "Text-only mode."
msgstr "纯文本模式。"
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅、节点、路由、DNS)。「从示例初始化」会载入官方模板——保存前请把占位的订阅链接换成你自己的。想图形化点选管理,请把后端切回 daed。"
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。"
# ---- config.js: dae Parameters ----
msgid "dae Parameters"
msgstr "dae 参数"
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
msgid "Save and Hot Reload"
msgstr "保存并热重载"
msgid "Initialize from example"
msgstr "从示例初始化"
msgid "+ Insert Block…"
msgstr "+ 插入块…"
msgid "Insert config block at cursor"
msgstr "在光标处插入配置块"
msgid "lines"
msgstr "行"
msgid "⚠ %d placeholder"
msgstr "⚠ %d 处占位"
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
msgstr "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
msgid " / "
msgstr " / "
msgid "这些 "
msgstr "这些 "
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
msgstr " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
msgid "Placeholder lines (click line number to jump):"
msgstr "占位行(点行号跳转):"
msgid "Jump to line"
msgstr "跳转到该行"
msgid "Validating…"
msgstr "校验中…"
msgid "Validate failed: %s"
msgstr "配置校验失败:%s"
msgid "Saving…"
msgstr "保存中…"
msgid "Reload failed: %s"
msgstr "热重载失败:%s"
msgid "Validated · saved · hot-reloaded"
msgstr "已校验 · 已保存 · 已热重载"
msgid "Save failed: %s"
msgstr "保存失败:%s"
msgid "Loading example…"
msgstr "加载示例中…"
msgid "Example loaded"
msgstr "示例已加载"
msgid "Init failed: %s"
msgstr "初始化失败:%s"
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
msgstr "当前后端未安装,请从软件源安装 dae 或 daed。"
# ---- log.js ----
msgid "Loading…"
msgstr "加载中…"
msgid "Log file does not exist yet."
msgstr "日志文件尚不存在。"
msgid "Error reading log: %s"
msgstr "读取日志出错:%s"
msgid "Log file cleared."
msgstr "日志文件已清空。"
msgid "Failed to clear log: %s"
msgstr "清空日志失败:%s"
msgid "Truncate %s log on the router? This cannot be undone."
msgstr "确定要清空路由器上的 %s 日志吗?此操作不可恢复。"
# ---- updates.js: Data Updates ----
msgid "Settings"
msgstr "设置"
msgid "Updates"
msgstr "更新"
msgid "Log"
msgstr "日志"
msgid "Data Updates"
msgstr "数据更新"
msgid "Package Updates"
msgstr "软件包更新"
msgid "System Health"
msgstr "系统健康"
msgid "Update"
msgstr "更新"
msgid "Upgrade"
msgstr "升级"
msgid "Clean"
msgstr "清理"
msgid "missing — click Update to fetch"
msgstr "缺失——点 Update 下载"
msgid "%s download started — will refresh in 1–3 min."
msgstr "%s 下载已开始——1–3 分钟后刷新。"
msgid "%s update already running."
msgstr "%s 更新已在运行。"
msgid "%s update failed: %s"
msgstr "%s 更新失败:%s"
msgid "%s update error: %s"
msgstr "%s 更新出错:%s"
# ---- updates.js: Package Updates ----
msgid "%s binary"
msgstr "%s 内核"
msgid "active"
msgstr "当前"
msgid "installed"
msgstr "已安装"
msgid "latest"
msgstr "最新"
msgid "up to date"
msgstr "已是最新"
msgid "not installed via package manager"
msgstr "非包管理器安装"
msgid "Unavailable"
msgstr "不可用"
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
msgstr "立即运行 \"apk upgrade %s\"?这可能会重启当前后端。"
msgid "%s upgrade started — see log below."
msgstr "%s 升级已开始——见下方日志。"
msgid "%s upgrade already running."
msgstr "%s 升级已在运行。"
msgid "%s upgrade failed: %s"
msgstr "%s 升级失败:%s"
msgid "%s upgrade error: %s"
msgstr "%s 升级出错:%s"
# ---- updates.js: System Health ----
msgid "Probing…"
msgstr "检测中…"
msgid "%s process"
msgstr "%s 进程"
msgid "Kernel BTF"
msgstr "内核 BTF"
msgid "WebUI/API"
msgstr "WebUI/API"
msgid "Hot reload"
msgstr "热重载"
msgid "netns daens"
msgstr "netns daens"
msgid "pgrep reports the backend is running"
msgstr "pgrep 检测到后端正在运行"
msgid "backend process is not running"
msgstr "后端进程未运行"
msgid "service is running, expected on port %s"
msgstr "服务运行中,预期监听端口 %s"
msgid "service is stopped"
msgstr "服务已停止"
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
msgstr "不可用——eBPF 需要 CONFIG_DEBUG_INFO_BTF 或 vmlinux-btf"
msgid "/etc/init.d/dae hot_reload is available"
msgstr "dae 热重载可用"
msgid "netns daens removed."
msgstr "netns daens 已清理。"
msgid "exists, may block daed start"
msgstr "已存在,可能阻碍 daed 启动"
msgid "clean"
msgstr "干净"
# ---- log.js toolbar ----
msgid "%s Realtime Log"
msgstr "%s 实时日志"
msgid "Auto-scroll"
msgstr "自动滚动"
msgid "Pause"
msgstr "暂停"
msgid "Filter (substring)"
msgstr "过滤(子串)"
msgid "Clear View"
msgstr "清空显示"
msgid "Clear File"
msgstr "清空文件"
msgid "Download"
msgstr "下载"
# ---- config.js: Subscription Card ----
msgid "Subscription Management"
msgstr "订阅管理"
msgid "Paste subscription URL here"
msgstr "在此粘贴订阅链接"
msgid "Apply and Save"
msgstr "应用并保存"
msgid "Applying…"
msgstr "应用…"
msgid "Saved, start dae to apply"
msgstr "已保存,启动 dae 后生效"
msgid "Saved and applied"
msgstr "已保存并应用"
msgid "Saved, reload failed"
msgstr "已保存,重载失败"
msgid "Not found, load example first"
msgstr "未找到订阅行,请先载入示例"
msgid "Failed"
msgstr "失败"
msgid "This will replace your current config. Continue?"
msgstr "这将覆盖当前配置,继续?"
msgid "Example loaded — save to apply"
msgstr "示例已载入,保存后生效"
msgid "Saved OK (start dae to apply)"
msgstr "已保存(启动 dae 后生效)"
msgid "Configuration Editor"
msgstr "配置编辑"
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
# ---- config.js: Proxy Health ----
msgid "Test YouTube"
msgstr "测试 YouTube"
msgid "Testing…"
msgstr "测试中…"
msgid "YouTube reachable · %d ms"
msgstr "YouTube 可访问 · %d ms"
msgid "YouTube unreachable (timeout)"
msgstr "YouTube 无法访问(超时)"
msgid "YouTube unreachable (HTTP %s)"
msgstr "YouTube 无法访问(HTTP %s)"
# ---- config.js: dae friendly form ----
msgid "Subscriptions"
msgstr "订阅"
msgid "Airport / subscription links. dae resolves them into the node pool."
msgstr "机场 / 订阅链接,dae 会解析成节点池里的节点。"
msgid "Tag"
msgstr "标签"
msgid "Subscription URL"
msgstr "订阅链接"
msgid "URL is required"
msgstr "订阅链接不能为空"
msgid "Must start with http(s):// or file://"
msgstr "需以 http(s):// 或 file:// 开头"
msgid "Enabled"
msgstr "启用"
msgid "Nodes"
msgstr "节点"
msgid "Single share links (ss / vmess / vless / trojan / tuic / hysteria2 / socks5 …)."
msgstr "每行添加一个节点分享链接,支持 ss、vmess、vless、trojan、tuic、hysteria2、socks5。"
msgid "Share Link"
msgstr "分享链接"
msgid "Groups"
msgstr "分组"
msgid "Outbound groups. Leave the source empty to use all nodes. Set the route fallback to one of these."
msgstr "出站分组。来源留空表示用全部节点。路由兜底要指向其中一个分组。"
msgid "Name"
msgstr "名称"
msgid "Policy"
msgstr "策略"
msgid "Min moving average latency"
msgstr "最小移动平均延迟"
msgid "Min last latency"
msgstr "最小最近延迟"
msgid "Min average of last 10"
msgstr "最近 10 次最小均值"
msgid "Random"
msgstr "随机"
msgid "Fixed (first node)"
msgstr "固定(第一个节点)"
msgid "Source"
msgstr "来源"
msgid "Subscriptions and nodes that feed this group. Pick from the list or type a name. Leave empty to use all nodes."
msgstr "进入这个分组的订阅和节点。从列表里选,或手输名称。留空=用全部节点。"
msgid "subscription"
msgstr "订阅"
msgid "node"
msgstr "节点"
msgid "Name filter"
msgstr "名称筛选"
msgid "Optional. Narrows subscriptions in the source to nodes whose name matches — e.g. 香港 or 新加坡|日本. Nodes you picked by name are always kept."
msgstr "可选。只对来源里的订阅做收窄,留下名称匹配的节点,如 香港 或 新加坡|日本;你手动挑的单个节点始终保留。"
msgid "Routing"
msgstr "路由"
msgid "Private IPs direct"
msgstr "私网 IP 直连"
msgid "LAN / private addresses bypass the proxy."
msgstr "局域网 / 私有地址不走代理。"
msgid "China direct"
msgstr "国内直连"
msgid "China mainland IPs and domains bypass the proxy."
msgstr "中国大陆 IP 和域名不走代理。"
msgid "Block ads"
msgstr "拦截广告"
msgid "Drop traffic matching the ad domain list."
msgstr "丢弃命中广告域名表的流量。"
msgid "Fallback group"
msgstr "兜底分组"
msgid "Default outbound for everything else."
msgstr "其余流量的默认出站。"
msgid "Custom rules"
msgstr "自定义规则"
msgid "Raw dae routing lines, evaluated before fallback. Target must be a group name above (or direct/block)."
msgstr "原始 dae 路由行,在兜底前生效。箭头右边要填上面的分组名(或 direct / block)。"
msgid "China upstream"
msgstr "国内 DNS 上游"
msgid "Resolves China-mainland domains."
msgstr "解析中国大陆域名。"
msgid "Fallback upstream"
msgstr "兜底 DNS 上游"
msgid "Resolves everything else."
msgstr "解析其余域名。"
msgid "Add a subscription or node, then save — it takes effect automatically."
msgstr "填好订阅或节点,点保存就自动生效。"
msgid "Apply failed: %s"
msgstr "应用失败:%s"
msgid "Saved · validated · hot-reloaded"
msgstr "已保存 · 已校验 · 已热重载"
msgid "Please fix the highlighted fields."
msgstr "请先修正高亮的字段。"
msgid "Import from existing config"
msgstr "从现有配置导入"
msgid "Existing config detected"
msgstr "检测到已有配置"
msgid "You have a hand-written config.dae. Import its subscriptions and nodes into the form, or keep editing manually below. Saving the form overwrites config.dae."
msgstr "你已有手写的 config.dae。可把里面的订阅和节点导入表单,或在下方继续手动编辑。表单保存会覆盖 config.dae。"
msgid "Importing…"
msgstr "导入中…"
msgid "Imported, reloading…"
msgstr "已导入,正在刷新…"
msgid "Import failed"
msgstr "导入失败"
msgid "Advanced / Manual Mode"
msgstr "高级 / 手动模式"
msgid "Edit config.dae directly. Saving the form above regenerates the file and overwrites changes made here."
msgstr "直接编辑 config.dae。上方表单保存会重新生成文件并覆盖这里的改动。"
msgid "Update Subscriptions"
msgstr "更新订阅"
msgid "Re-fetch all airport subscriptions (reloads dae)"
msgstr "重新拉取所有机场订阅(重载 dae)"
msgid "Updating subscriptions…"
msgstr "更新订阅中…"
msgid "dae is stopped; it fetches subscriptions on start."
msgstr "dae 未运行,启动时会自动拉取订阅。"
msgid "Update failed: %s"
msgstr "更新失败:%s"
msgid "Subscriptions updated (dae reloaded)"
msgstr "订阅已更新(dae 已重载)"
msgid "Section"
msgstr "区块"
msgid "DNS"
msgstr "DNS"
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
msgstr "dae 配置文件为空。点击「从示例初始化」开始,或直接在此粘贴你的配置。"
# ---- log.js: filter dropdown ----
msgid "All"
msgstr "全部"
msgid "Node Status"
msgstr "节点状态"
msgid "Proxy Traffic"
msgstr "代理流量"
msgid "Logging"
msgstr "日志"
msgid "Save manual config"
msgstr "保存手动配置"
msgid "Save and Apply"
msgstr "保存并应用"
msgid "Fixed link — nothing to fetch."
msgstr "固定链接,无需更新。"
+446
View File
@@ -0,0 +1,446 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: PACKAGE VERSION\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh-Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
# ---- config.js: Service Status ----
msgid "Service Status"
msgstr "服务状态"
msgid "Collecting data…"
msgstr "正在收集数据…"
msgid "RUNNING"
msgstr "运行中"
msgid "STOPPED"
msgstr "已停止"
msgid "Backend"
msgstr "后端"
msgid "Listen"
msgstr "监听"
msgid "Toggle service"
msgstr "开关服务"
msgid "Open WebUI"
msgstr "打开 WebUI"
msgid "Restart"
msgstr "重启"
msgid "Hot Reload"
msgstr "热重载"
# ---- config.js: Backend Switcher ----
msgid "Active backend"
msgstr "当前后端"
msgid "Active"
msgstr "当前"
msgid "Not installed"
msgstr "未安装"
msgid "Switch to %s"
msgstr "切换到 %s"
msgid "%s is not installed"
msgstr "%s 未安装"
msgid "%s is not installed. Install it first, then switch backend."
msgstr "%s 未安装。请先安装再切换后端。"
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。"
msgid "Stopping current backend…"
msgstr "停止当前后端中…"
msgid "Switching to %s…"
msgstr "切换至 %s 中…"
msgid "Switched to %s. Click start when you want it to run."
msgstr "已切换至 %s。点击开关启动。"
msgid "Backend switched to %s. Click start to run it."
msgstr "已切换至 %s。点击开关启动。"
msgid "Switch failed."
msgstr "切换失败。"
msgid "Failed to switch backend: %s"
msgstr "切换后端失败:%s"
# ---- config.js: Init Actions ----
msgid "Action \"%s\" error: %s"
msgstr "操作 \"%s\" 出错:%s"
msgid "Action \"%s\" failed: %s"
msgstr "操作 \"%s\" 失败:%s"
msgid "Action \"%s\" succeeded."
msgstr "操作 \"%s\" 成功。"
msgid "Toggle failed: %s"
msgstr "开关切换失败:%s"
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。"
# ---- config.js: daede Settings ----
msgid "daede Settings"
msgstr "daede 设置"
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。"
msgid "Listen Address"
msgstr "监听地址"
msgid "Host:port that the daed WebUI and GraphQL API listen on."
msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。"
msgid "Max Log Size (MB)"
msgstr "单文件大小上限(MB)"
msgid "Rotate the log file once it grows past this many megabytes."
msgstr "单个日志文件超过此大小后轮转。"
msgid "Max Log Backups"
msgstr "保留旧日志数"
msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings"
msgstr "日志高级设置"
# ---- config.js: dae Settings ----
msgid "dae Parameters"
msgstr "dae 参数"
msgid "Configuration file path and log rotation settings for dae."
msgstr "dae 配置文件路径与日志轮转参数。"
msgid "Configuration File"
msgstr "配置文件"
# ---- config.js: dae Editor ----
msgid "dae Configuration"
msgstr "dae 配置"
msgid "Text-only mode."
msgstr "纯文本模式。"
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅、节点、路由、DNS)。「从示例初始化」会载入官方模板——保存前请把占位的订阅链接换成你自己的。想图形化点选管理,请把后端切回 daed。"
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。"
# ---- config.js: dae Parameters ----
msgid "dae Parameters"
msgstr "dae 参数"
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
msgid "Save and Hot Reload"
msgstr "保存并热重载"
msgid "Initialize from example"
msgstr "从示例初始化"
msgid "+ Insert Block…"
msgstr "+ 插入块…"
msgid "Insert config block at cursor"
msgstr "在光标处插入配置块"
msgid "lines"
msgstr "行"
msgid "⚠ %d placeholder"
msgstr "⚠ %d 处占位"
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
msgstr "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
msgid " / "
msgstr " / "
msgid "这些 "
msgstr "这些 "
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
msgstr " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
msgid "Placeholder lines (click line number to jump):"
msgstr "占位行(点行号跳转):"
msgid "Jump to line"
msgstr "跳转到该行"
msgid "Validating…"
msgstr "校验中…"
msgid "Validate failed: %s"
msgstr "配置校验失败:%s"
msgid "Saving…"
msgstr "保存中…"
msgid "Reload failed: %s"
msgstr "热重载失败:%s"
msgid "Validated · saved · hot-reloaded"
msgstr "已校验 · 已保存 · 已热重载"
msgid "Save failed: %s"
msgstr "保存失败:%s"
msgid "Loading example…"
msgstr "加载示例中…"
msgid "Example loaded"
msgstr "示例已加载"
msgid "Init failed: %s"
msgstr "初始化失败:%s"
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
msgstr "当前后端未安装,请从软件源安装 dae 或 daed。"
# ---- log.js ----
msgid "Loading…"
msgstr "加载中…"
msgid "Log file does not exist yet."
msgstr "日志文件尚不存在。"
msgid "Error reading log: %s"
msgstr "读取日志出错:%s"
msgid "Log file cleared."
msgstr "日志文件已清空。"
msgid "Failed to clear log: %s"
msgstr "清空日志失败:%s"
msgid "Truncate %s log on the router? This cannot be undone."
msgstr "确定要清空路由器上的 %s 日志吗?此操作不可恢复。"
# ---- updates.js: Data Updates ----
msgid "Settings"
msgstr "设置"
msgid "Updates"
msgstr "更新"
msgid "Log"
msgstr "日志"
msgid "Data Updates"
msgstr "数据更新"
msgid "Package Updates"
msgstr "软件包更新"
msgid "System Health"
msgstr "系统健康"
msgid "Update"
msgstr "更新"
msgid "Upgrade"
msgstr "升级"
msgid "Clean"
msgstr "清理"
msgid "missing — click Update to fetch"
msgstr "缺失——点 Update 下载"
msgid "%s download started — will refresh in 1–3 min."
msgstr "%s 下载已开始——1–3 分钟后刷新。"
msgid "%s update already running."
msgstr "%s 更新已在运行。"
msgid "%s update failed: %s"
msgstr "%s 更新失败:%s"
msgid "%s update error: %s"
msgstr "%s 更新出错:%s"
# ---- updates.js: Package Updates ----
msgid "%s binary"
msgstr "%s 内核"
msgid "active"
msgstr "当前"
msgid "installed"
msgstr "已安装"
msgid "latest"
msgstr "最新"
msgid "up to date"
msgstr "已是最新"
msgid "not installed via package manager"
msgstr "非包管理器安装"
msgid "Unavailable"
msgstr "不可用"
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
msgstr "立即运行 \"apk upgrade %s\"?这可能会重启当前后端。"
msgid "%s upgrade started — see log below."
msgstr "%s 升级已开始——见下方日志。"
msgid "%s upgrade already running."
msgstr "%s 升级已在运行。"
msgid "%s upgrade failed: %s"
msgstr "%s 升级失败:%s"
msgid "%s upgrade error: %s"
msgstr "%s 升级出错:%s"
# ---- updates.js: System Health ----
msgid "Probing…"
msgstr "检测中…"
msgid "%s process"
msgstr "%s 进程"
msgid "Kernel BTF"
msgstr "内核 BTF"
msgid "WebUI/API"
msgstr "WebUI/API"
msgid "Hot reload"
msgstr "热重载"
msgid "netns daens"
msgstr "netns daens"
msgid "pgrep reports the backend is running"
msgstr "pgrep 检测到后端正在运行"
msgid "backend process is not running"
msgstr "后端进程未运行"
msgid "service is running, expected on port %s"
msgstr "服务运行中,预期监听端口 %s"
msgid "service is stopped"
msgstr "服务已停止"
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
msgstr "不可用——eBPF 需要 CONFIG_DEBUG_INFO_BTF 或 vmlinux-btf"
msgid "/etc/init.d/dae hot_reload is available"
msgstr "dae 热重载可用"
msgid "netns daens removed."
msgstr "netns daens 已清理。"
msgid "exists, may block daed start"
msgstr "已存在,可能阻碍 daed 启动"
msgid "clean"
msgstr "干净"
# ---- log.js toolbar ----
msgid "%s Realtime Log"
msgstr "%s 实时日志"
msgid "Auto-scroll"
msgstr "自动滚动"
msgid "Pause"
msgstr "暂停"
msgid "All"
msgstr "全部"
msgid "Node Status"
msgstr "节点状态"
msgid "Proxy Traffic"
msgstr "代理流量"
msgid "Clear View"
msgstr "清空显示"
msgid "Clear File"
msgstr "清空文件"
msgid "Download"
msgstr "下载"
# ---- config.js: Subscription Card ----
msgid "Subscription Management"
msgstr "订阅管理"
msgid "Paste subscription URL here"
msgstr "在此粘贴订阅链接"
msgid "Apply and Save"
msgstr "应用并保存"
msgid "Applying…"
msgstr "应用…"
msgid "Saved, start dae to apply"
msgstr "已保存,启动 dae 后生效"
msgid "Saved and applied"
msgstr "已保存并应用"
msgid "Saved, reload failed"
msgstr "已保存,重载失败"
msgid "Not found, load example first"
msgstr "未找到订阅行,请先载入示例"
msgid "Failed"
msgstr "失败"
msgid "This will replace your current config. Continue?"
msgstr "这将覆盖当前配置,继续?"
msgid "Example loaded — save to apply"
msgstr "示例已载入,保存后生效"
msgid "Saved OK (start dae to apply)"
msgstr "已保存(启动 dae 后生效)"
msgid "Configuration Editor"
msgstr "配置编辑"
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
# ---- config.js: Proxy Health ----
msgid "Check Proxy Status"
msgstr "检测代理状态"
msgid "OK, %d flows through proxy"
msgstr "正常,%d 条代理流量"
msgid "No proxy traffic"
msgstr "暂无代理流量"
+2
View File
@@ -0,0 +1,2 @@
config daede 'config'
option active_backend 'daed'
@@ -0,0 +1,153 @@
#!/bin/sh
# Patch the daed package's /etc/init.d/daed to clean up /run/netns/daens on
# stop_service(). Without this, restarting daed fails with:
# fatal: failed to create netns: open /run/netns/daens: file exists
#
# This script lives under /etc/uci-defaults/ which OpenWrt runs once on
# every package install/upgrade and then removes - so each time luci-app-daede
# is (re-)installed we re-patch the daed init.d even if it was overwritten
# by a daed package upgrade in between.
#
# The patch is idempotent: a marker comment guards against double-injection.
set -e
# luci-app-daede owns its own LuCI menu and view path. Remove stale files from
# the official luci-app-daed package and earlier daede builds that reused
# /view/daed.
rm -f /usr/share/luci/menu.d/luci-app-daed.json
rm -f /usr/share/rpcd/acl.d/luci-app-daed.json
rm -rf /usr/share/luci-app-daed
rm -rf /www/luci-static/resources/view/daed
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
rm -rf /tmp/luci-modulecache/* 2>/dev/null || true
if [ -f /etc/config/daed_luci ]; then
old_backend="$(uci -q get daed_luci.config.active_backend || true)"
if ! uci -q get daede.config >/dev/null 2>&1; then
touch /etc/config/daede
uci -q set daede.config=daede
case "$old_backend" in
dae|daed) uci -q set daede.config.active_backend="$old_backend" ;;
esac
uci -q commit daede
elif ! uci -q get daede.config.active_backend >/dev/null 2>&1; then
case "$old_backend" in
dae|daed)
uci -q set daede.config.active_backend="$old_backend"
uci -q commit daede
;;
esac
fi
rm -f /etc/config/daed_luci
fi
if ! uci -q get daede.config >/dev/null 2>&1; then
touch /etc/config/daede
uci -q set daede.config=daede
if [ -f /etc/init.d/daed ]; then
uci -q set daede.config.active_backend='daed'
else
uci -q set daede.config.active_backend='dae'
fi
uci -q commit daede
fi
# Remove --disable-timestamp from dae init so log lines carry timestamps
DAE_INIT="/etc/init.d/dae"
if [ -f "$DAE_INIT" ]; then
sed -i 's/--disable-timestamp//' "$DAE_INIT"
sed -i '/procd_append_param command *$/d' "$DAE_INIT"
fi
# dae expects geoip.dat / geosite.dat under /usr/share/dae/;
# v2ray-geoip installs them to /usr/share/v2ray/. Create symlinks.
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/dae/geoip.dat ]; then
mkdir -p /usr/share/dae
ln -sf /usr/share/v2ray/geoip.dat /usr/share/dae/geoip.dat
fi
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/dae/geosite.dat ]; then
mkdir -p /usr/share/dae
ln -sf /usr/share/v2ray/geosite.dat /usr/share/dae/geosite.dat
fi
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/daed/geoip.dat ]; then
mkdir -p /usr/share/daed
ln -sf /usr/share/v2ray/geoip.dat /usr/share/daed/geoip.dat
fi
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/daed/geosite.dat ]; then
mkdir -p /usr/share/daed
ln -sf /usr/share/v2ray/geosite.dat /usr/share/daed/geosite.dat
fi
# Seed default dae form sections (group/routing/dns) for the LuCI editor.
# Idempotent: only add what's missing. Lives in the `dae` UCI package,
# consumed by gen-dae-config.sh.
if [ -f /etc/config/dae ]; then
if ! uci -q show dae | grep -q "=group$"; then
g="$(uci add dae group)"
uci -q set "dae.$g.name=proxy"
uci -q set "dae.$g.policy=min_moving_avg"
fi
if ! uci -q get dae.routing >/dev/null 2>&1; then
uci -q set dae.routing=routing
uci -q set dae.routing.private_direct=1
uci -q set dae.routing.cn_direct=1
uci -q set dae.routing.block_ads=0
uci -q set dae.routing.fallback=proxy
fi
if ! uci -q get dae.dns >/dev/null 2>&1; then
uci -q set dae.dns=dns
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
fi
# Migrate legacy group filters to the unified source / name_filter fields,
# then drop the old keys so the form shows real, editable values (the old
# fields used to re-populate the new ones and looked un-deletable).
# `uci get` joins a list into one space-separated line; the unquoted $() word-
# splits it back into items. dae tags/keywords are whitespace-free, so this is
# the intended split (do NOT set IFS=newline here — it would merge the items).
i=0
while uci -q get "dae.@group[$i]" >/dev/null 2>&1; do
if [ -z "$(uci -q get dae.@group[$i].source)" ]; then
for v in $(uci -q get dae.@group[$i].filter_sub 2>/dev/null); do
uci -q add_list dae.@group[$i].source="$v"
done
fi
if [ -z "$(uci -q get dae.@group[$i].name_filter)" ]; then
for v in $(uci -q get dae.@group[$i].filter_node 2>/dev/null); do
uci -q add_list dae.@group[$i].name_filter="$v"
done
fi
uci -q delete dae.@group[$i].filter_sub 2>/dev/null
uci -q delete dae.@group[$i].filter_node 2>/dev/null
i=$((i+1))
done
uci -q commit dae
fi
INIT="/etc/init.d/daed"
MARKER="# luci-app-daede: netns-cleanup"
[ -f "$INIT" ] || exit 0
grep -q "$MARKER" "$INIT" && exit 0
# Inject `ip netns del daens 2>/dev/null || true` right after `rm -f "$LOG"`
# inside stop_service(). awk is more reliable than sed for multiline contexts.
awk -v marker="$MARKER" '
BEGIN { in_stop = 0; injected = 0 }
/^stop_service\(\)/ { in_stop = 1 }
in_stop && /^}/ { in_stop = 0 }
{ print }
in_stop && !injected && /rm -f "\$LOG"/ {
print "\tip netns del daens 2>/dev/null || true " marker
injected = 1
}
' "$INIT" > "${INIT}.new" && mv "${INIT}.new" "$INIT"
chmod +x "$INIT"
exit 0
reload_service() {
restart
}
@@ -0,0 +1,368 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# gen-dae-config.sh - build /etc/dae/config.dae from the dae UCI form sections,
# validate with `dae validate`, then overwrite + hot reload only on success.
#
# generate read UCI -> render config.dae -> validate -> overwrite + hot reload
# import parse existing config.dae subscription{}/node{} -> fill UCI form
#
# UCI model (package dae):
# config subscription tag/url, enabled
# config node tag/link, enabled
# config group name/policy, list source, list name_filter
# (old list filter_sub/filter_node still read as fallback)
# config routing 'routing' private_direct/cn_direct/block_ads/fallback, list custom
# config dns 'dns' cn_upstream/fallback_upstream
. /lib/functions.sh
CONFIG_DAE="/etc/dae/config.dae"
TMP_GEN="/tmp/dae-gen.dae"
DAE_BIN="/usr/bin/dae"
DAE_INITD="/etc/init.d/dae"
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
sanitize() {
printf '%s' "$1" | tr -d "'" | tr -d '\n\r'
}
# ===================== generate =====================
SUB_BUF=""
NODE_BUF=""
GROUP_BUF=""
FIRST_GROUP=""
emit_sub() {
local s="$1" tag url enabled
config_get tag "$s" tag ""
config_get url "$s" url ""
config_get_bool enabled "$s" enabled 1
[ "$enabled" = "1" ] || return 0
url="$(sanitize "$url")"
[ -n "$url" ] || return 0
tag="$(sanitize "$tag")"
if [ -n "$tag" ]; then
SUB_BUF="${SUB_BUF} ${tag}: '${url}'
"
else
SUB_BUF="${SUB_BUF} '${url}'
"
fi
}
emit_node() {
local s="$1" tag link enabled
config_get tag "$s" tag ""
config_get link "$s" link ""
config_get_bool enabled "$s" enabled 1
[ "$enabled" = "1" ] || return 0
link="$(sanitize "$link")"
[ -n "$link" ] || return 0
tag="$(sanitize "$tag")"
if [ -n "$tag" ]; then
NODE_BUF="${NODE_BUF} ${tag}: '${link}'
"
else
NODE_BUF="${NODE_BUF} '${link}'
"
fi
}
emit_group() {
local s="$1" name policy
config_get name "$s" name ""
config_get policy "$s" policy "min_moving_avg"
name="$(sanitize "$name")"
[ -n "$name" ] || return 0
[ -n "$FIRST_GROUP" ] || FIRST_GROUP="$name"
GROUP_BUF="${GROUP_BUF} ${name} {
"
# Build the source: 'source' entries that match a subscription tag become
# subtag(), the rest (node tags / keywords) become name(). Old filter_sub /
# filter_node configs still work as a fallback when 'source' is unset.
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
# source: new field, else old filter_sub (decoupled from the name filter so
# editing one never silently drops the other)
config_list_foreach "$s" source _collect_source
[ -z "$SUBS_ACC" ] && [ -z "$NODES_ACC" ] && config_list_foreach "$s" filter_sub _collect_source
# optional name filter narrows the subscription pool only (AND); hand-picked
# nodes are explicit choices and stay in unconditionally
config_list_foreach "$s" name_filter _collect_kw
[ -z "$KW_ACC" ] && config_list_foreach "$s" filter_node _collect_kw
local andclause=""
[ -n "$KW_ACC" ] && andclause=" && name(${KW_ACC})"
local emitted=0
# multiple filter lines = OR, so subtag and name lines union the sources
if [ -n "$SUBS_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: subtag(${SUBS_ACC})${andclause}
"
emitted=1
fi
if [ -n "$NODES_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: name(${NODES_ACC})
"
emitted=1
fi
# name filter with no source = filter the whole pool by name
if [ "$emitted" = 0 ] && [ -n "$KW_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: name(${KW_ACC})
"
fi
GROUP_BUF="${GROUP_BUF} policy: ${policy}
}
"
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
}
SUBS_ACC=""
NODES_ACC=""
KW_ACC=""
# Subscription tags, space-padded for membership tests (" tag " match).
SUB_TAGS=""
collect_subtag() {
local t; config_get t "$1" tag ""
t="$(sanitize "$t")"
[ -n "$t" ] && SUB_TAGS="${SUB_TAGS} ${t} "
}
is_subtag() {
case "$SUB_TAGS" in *" $1 "*) return 0 ;; *) return 1 ;; esac
}
# dae filter args are bare only for plain identifiers; anything with regex
# chars / CJK / spaces must be wrapped as regex: '...' or the lexer chokes.
filter_arg() {
case "$1" in
*[!A-Za-z0-9_]*) printf "regex: '%s'" "$1" ;;
*) printf '%s' "$1" ;;
esac
}
# name-filter args do substring matching: regex metachars -> regex:, else keyword:.
# Intentionally stricter than filter_arg: a plain CJK keyword here stays a
# substring `keyword:` match, whereas filter_arg wraps any non-word char as regex.
kw_arg() {
if printf '%s' "$1" | grep -q '[][|().*+?^$\\]'; then
printf "regex: '%s'" "$1"
else
printf "keyword: '%s'" "$1"
fi
}
# classify a source entry: subscription tag -> subtag bucket, else name bucket
_collect_source() {
local v="$(sanitize "$1")"
[ -n "$v" ] || return 0
if is_subtag "$v"; then
SUBS_ACC="${SUBS_ACC:+$SUBS_ACC, }$(filter_arg "$v")"
else
NODES_ACC="${NODES_ACC:+$NODES_ACC, }$(filter_arg "$v")"
fi
}
_collect_kw() {
local v="$(sanitize "$1")"
[ -n "$v" ] || return 0
KW_ACC="${KW_ACC:+$KW_ACC, }$(kw_arg "$v")"
}
generate() {
config_load dae
# global overridable knobs (fall back to defaults)
local dial_mode log_level wan_interface lan_interface
config_get dial_mode config dial_mode "domain"
config_get log_level config log_level "info"
config_get wan_interface config wan_interface "auto"
config_get lan_interface config lan_interface ""
SUB_BUF=""; NODE_BUF=""; GROUP_BUF=""; FIRST_GROUP=""
SUB_TAGS=""
config_foreach collect_subtag subscription
config_foreach emit_sub subscription
config_foreach emit_node node
config_foreach emit_group group
# fall back to a default group when none defined
if [ -z "$GROUP_BUF" ]; then
GROUP_BUF=" proxy {
policy: min_moving_avg
}
"
FIRST_GROUP="proxy"
fi
# routing / dns singletons
local private_direct cn_direct block_ads fallback cn_up fb_up
config_get_bool private_direct routing private_direct 1
config_get_bool cn_direct routing cn_direct 1
config_get_bool block_ads routing block_ads 0
config_get fallback routing fallback "$FIRST_GROUP"
[ -n "$fallback" ] || fallback="$FIRST_GROUP"
fallback="$(sanitize "$fallback")"
config_get cn_up dns cn_upstream "udp://dns.alidns.com:53"
config_get fb_up dns fallback_upstream "tcp+udp://dns.google:53"
cn_up="$(sanitize "$cn_up")"
fb_up="$(sanitize "$fb_up")"
{
echo "# Auto-generated by luci-app-daede form. Manual edits are overwritten on next save."
echo "global {"
echo " tproxy_port: 12345"
echo " tproxy_port_protect: true"
echo " log_level: ${log_level}"
[ -n "$lan_interface" ] && echo " lan_interface: ${lan_interface}"
echo " wan_interface: ${wan_interface}"
echo " auto_config_kernel_parameter: true"
echo " dial_mode: ${dial_mode}"
echo " tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'"
echo " udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'"
echo " check_interval: 30s"
echo " check_tolerance: 50ms"
echo "}"
echo ""
if [ -n "$SUB_BUF" ]; then
echo "subscription {"
printf '%s' "$SUB_BUF"
echo "}"
echo ""
fi
if [ -n "$NODE_BUF" ]; then
echo "node {"
printf '%s' "$NODE_BUF"
echo "}"
echo ""
fi
echo "dns {"
echo " ipversion_prefer: 4"
echo " upstream {"
echo " cndns: '${cn_up}'"
echo " fallbackdns: '${fb_up}'"
echo " }"
echo " routing {"
echo " request {"
echo " qname(geosite:cn) -> cndns"
echo " fallback: fallbackdns"
echo " }"
echo " }"
echo "}"
echo ""
echo "group {"
printf '%s' "$GROUP_BUF"
echo "}"
echo ""
echo "routing {"
echo " pname(NetworkManager) -> direct"
# multicast / broadcast direct (geoip:private doesn't cover these)
echo " dip(224.0.0.0/3) -> direct"
echo " dip(255.255.255.255/32) -> direct"
echo " dip('ff00::/8') -> direct"
[ "$private_direct" = "1" ] && echo " dip(geoip:private) -> direct"
# NTP time sync direct
echo " l4proto(udp) && dport(123) -> direct"
# OS connectivity checks direct (avoid captive-portal false positives)
echo " domain(connectivitycheck.gstatic.com) -> direct"
echo " domain(msftconnecttest.com) -> direct"
if [ "$cn_direct" = "1" ]; then
echo " dip(geoip:cn) -> direct"
echo " domain(geosite:cn) -> direct"
fi
[ "$block_ads" = "1" ] && echo " domain(geosite:category-ads-all) -> block"
config_list_foreach routing custom _emit_custom
echo " fallback: ${fallback}"
echo "}"
} > "$TMP_GEN"
chmod 600 "$TMP_GEN" # dae rejects world/group-accessible config files
# overwrite live config only if validation passes
local out
out="$("$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
if [ $? -ne 0 ]; then
echo "validate failed: $(echo "$out" | head -1)" >&2
rm -f "$TMP_GEN"
return 1
fi
mkdir -p /etc/dae
cat "$TMP_GEN" > "$CONFIG_DAE"
chmod 600 "$CONFIG_DAE"
rm -f "$TMP_GEN"
if /usr/bin/pgrep -x dae >/dev/null 2>&1; then
"$DAE_INITD" hot_reload >/dev/null 2>&1
fi
echo "ok"
return 0
}
_emit_custom() {
local line="$(printf '%s' "$1" | tr -d '\n\r')"
[ -n "$line" ] || return 0
echo " ${line}"
}
# ===================== import =====================
# Fill the UCI form from an existing config.dae subscription{}/node{} block.
# Idempotent: wipe same-type sections first, then rebuild from file.
import_block() {
# $1=block name (subscription|node) $2=uci section type $3=link/url field
local block="$1" stype="$2" field="$3"
awk -v blk="$block" '
$0 ~ "^[[:space:]]*"blk"[[:space:]]*\\{" { inb=1; next }
inb && /^[[:space:]]*\}/ { inb=0 }
inb { print }
' "$CONFIG_DAE" 2>/dev/null | while IFS= read -r raw; do
# drop comment, trim
line="$(printf '%s' "$raw" | sed 's/#.*$//' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
[ -n "$line" ] || continue
tag=""; val=""
case "$line" in
\'*|\"*)
# starts with a quote -> untagged link
val="$line"
;;
[A-Za-z0-9_]*:*)
# identifier ':' link -> tagged (split on first colon)
tag="$(printf '%s' "$line" | sed "s/[[:space:]]*:.*//")"
val="$(printf '%s' "$line" | sed "s/^[^:]*:[[:space:]]*//")"
;;
*)
val="$line"
;;
esac
# strip surrounding quotes
val="$(printf '%s' "$val" | sed "s/^['\"]//; s/['\"]$//")"
[ -n "$val" ] || continue
# skip dae example placeholders (ss://LINK, server-ip:port, localhost:1080 ...)
case "$val" in
*://LINK|*LINK*|*server-ip*|*://localhost:*|*example.com*) continue ;;
esac
sid="$(uci add dae "$stype")"
[ -n "$tag" ] && uci set "dae.$sid.tag=$tag"
uci set "dae.$sid.$field=$val"
uci set "dae.$sid.enabled=1"
done
}
do_import() {
[ -f "$CONFIG_DAE" ] || { echo "no config.dae" >&2; return 1; }
while uci -q delete dae.@subscription[0] >/dev/null 2>&1; do :; done
while uci -q delete dae.@node[0] >/dev/null 2>&1; do :; done
import_block subscription subscription url
import_block node node link
uci commit dae
echo "ok"
}
# ===================== entry =====================
case "$1" in
generate) generate ;;
import) do_import ;;
*) echo "usage: $0 {generate|import}" >&2; exit 2 ;;
esac
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
# pkg-info.sh <package>
# Prints "<installed>\t<latest>" for the named package, where either field is
# empty when unknown. Used by the Updates view to decide whether to enable
# the [Upgrade] button.
PKG="$1"
case "$PKG" in
dae|daed|luci-app-daede) ;;
*) echo "" ; exit 64 ;;
esac
installed=""
latest=""
if command -v apk >/dev/null 2>&1; then
if apk info -e "$PKG" >/dev/null 2>&1; then
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-" {
sub("^" p "-", "", $1);
print $1;
exit
}
')
fi
latest=$(apk search "^${PKG}\$" 2>/dev/null | sort -V | tail -1 | sed "s/^${PKG}-//")
elif command -v opkg >/dev/null 2>&1; then
installed=$(opkg status "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
latest=$(opkg info "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
fi
printf '%s\t%s\n' "$installed" "$latest"
@@ -0,0 +1,27 @@
#!/bin/sh
# proxy-check.sh - test whether dae's transparent proxy can actually reach a
# blocked site (default YouTube). dae proxies the router's own WAN-bound traffic
# (wan_interface), so a plain request from here is routed through dae just like a
# LAN client would be. Prints one line: ok=<0|1> code=<http> ms=<n>
URL="${1:-https://www.youtube.com/generate_204}"
code=000
ms=0
if command -v curl >/dev/null 2>&1; then
out="$(curl -4 -sL --max-time 8 --connect-timeout 4 \
-o /dev/null -w '%{http_code} %{time_total}' "$URL" 2>/dev/null)"
code="$(printf '%s' "$out" | awk '{print $1}')"
t="$(printf '%s' "$out" | awk '{print $2}')"
[ -n "$code" ] || code=000
ms="$(awk -v x="${t:-0}" 'BEGIN{printf "%d", x*1000}')"
else
# busybox wget has no status/timing output — success is exit 0
if wget -q -T 8 -O /dev/null "$URL" 2>/dev/null; then code=204; else code=000; fi
fi
ok=0
case "$code" in 200|204|301|302) ok=1 ;; esac
echo "ok=$ok code=$code ms=$ms"
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# update-geo.sh <geoip|geosite>
# Fork-and-detach so the LuCI RPC call returns immediately. The diagnostics
# card polls the .dat file's mtime/size to detect completion. A lock file
# in /tmp prevents concurrent runs.
TYPE="$1"
case "$TYPE" in
geoip)
URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
DEST="/usr/share/v2ray/geoip.dat"
;;
geosite)
URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
DEST="/usr/share/v2ray/geosite.dat"
;;
*)
echo "usage: $0 <geoip|geosite>" >&2
exit 64
;;
esac
LOCK="/tmp/luci-app-daede.${TYPE}.lock"
LOG="/tmp/luci-app-daede.${TYPE}.log"
if [ -f "$LOCK" ]; then
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${TYPE} update already in progress" >&2
exit 75
fi
# Spawn detached worker — parent returns immediately to LuCI.
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"' EXIT INT TERM
TMP="${DEST}.new"
mkdir -p "$(dirname "$DEST")"
echo "$(date '+%F %T') begin: $URL"
if ! curl -fsSL --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
echo "$(date '+%F %T') download failed"
rm -f "$TMP"
exit 1
fi
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
if [ "$size" -lt 102400 ]; then
echo "$(date '+%F %T') file too small ($size bytes)"
rm -f "$TMP"
exit 2
fi
mv "$TMP" "$DEST"
echo "$(date '+%F %T') updated $DEST ($size bytes)"
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
+68
View File
@@ -0,0 +1,68 @@
#!/bin/sh
# update-pkg.sh <dae|daed|luci-app-daede>
# Refresh package indexes and upgrade the named package via apk (25.12+) or
# opkg (24.10). Forks the work to background so the LuCI RPC call returns
# immediately; the result is streamed to /tmp/luci-app-daede.pkg.<name>.log.
PKG="$1"
case "$PKG" in
dae|daed|luci-app-daede) ;;
*)
echo "usage: $0 <dae|daed|luci-app-daede>" >&2
exit 64
;;
esac
LOCK="/tmp/luci-app-daede.pkg-${PKG}.lock"
LOG="/tmp/luci-app-daede.pkg-${PKG}.log"
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${PKG} update already in progress" >&2
exit 75
fi
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"' EXIT INT TERM
echo "$(date '+%F %T') begin upgrade: $PKG"
if command -v apk >/dev/null 2>&1; then
echo "--- apk update ---"
apk update 2>&1
echo "--- apk upgrade $PKG ---"
# --no-self-upgrade so apk itself does not jump versions mid-op
apk upgrade --no-self-upgrade "$PKG" 2>&1
rc=$?
elif command -v opkg >/dev/null 2>&1; then
echo "--- opkg update ---"
opkg update 2>&1
echo "--- opkg upgrade $PKG ---"
opkg upgrade "$PKG" 2>&1
rc=$?
else
echo "no package manager found"
exit 3
fi
echo "$(date '+%F %T') done (rc=$rc)"
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
echo "reloading rpcd to pick up new ACL"
/etc/init.d/rpcd reload 2>&1
fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -0,0 +1,36 @@
{
"admin/services/daede": {
"title": "daede",
"order": 20,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-daede" ]
}
},
"admin/services/daede/config": {
"title": "Settings",
"order": 10,
"action": {
"type": "view",
"path": "daede/config"
}
},
"admin/services/daede/updates": {
"title": "Updates",
"order": 20,
"action": {
"type": "view",
"path": "daede/updates"
}
},
"admin/services/daede/log": {
"title": "Log",
"order": 30,
"action": {
"type": "view",
"path": "daede/log"
}
}
}
@@ -0,0 +1,74 @@
{
"luci-app-daede": {
"description": "Grant access to dae/daed configuration",
"read": {
"file": {
"/etc/init.d/dae": [ "read" ],
"/etc/init.d/daed": [ "read" ],
"/etc/config/daede": [ "read" ],
"/etc/dae/config.dae": [ "read" ],
"/etc/dae/example.dae": [ "read" ],
"/var/log/dae/dae.log": [ "read" ],
"/var/log/daed/daed.log": [ "read" ],
"/usr/share/v2ray/geoip.dat": [ "read" ],
"/usr/share/v2ray/geosite.dat": [ "read" ],
"/sys/kernel/btf/vmlinux": [ "read" ],
"/run/netns/daens": [ "read" ],
"/tmp/luci-app-daede.geoip.log": [ "read" ],
"/tmp/luci-app-daede.geosite.log": [ "read" ],
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
"/tmp/luci-app-daede.pkg-luci-app-daede.log": [ "read" ],
"/usr/bin/pgrep -x dae": [ "exec" ],
"/usr/bin/pgrep -x daed": [ "exec" ],
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
"/usr/bin/dae --version": [ "exec" ],
"/bin/rm -f /tmp/dae-validate.dae": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
},
"ubus": {
"service": [ "list" ]
},
"uci": [ "dae", "daed", "daede" ]
},
"write": {
"file": {
"/etc/dae/config.dae": [ "write" ],
"/tmp/dae-validate.dae": [ "write" ],
"/etc/config/daede": [ "write" ],
"/var/log/dae/dae.log": [ "write" ],
"/var/log/daed/daed.log": [ "write" ],
"/etc/init.d/dae start": [ "exec" ],
"/etc/init.d/dae stop": [ "exec" ],
"/etc/init.d/dae restart": [ "exec" ],
"/etc/init.d/dae enable": [ "exec" ],
"/etc/init.d/dae disable": [ "exec" ],
"/etc/init.d/dae hot_reload": [ "exec" ],
"/etc/init.d/daed start": [ "exec" ],
"/etc/init.d/daed stop": [ "exec" ],
"/etc/init.d/daed restart": [ "exec" ],
"/etc/init.d/daed enable": [ "exec" ],
"/etc/init.d/daed disable": [ "exec" ],
"/sbin/uci set dae.config.enabled=*": [ "exec" ],
"/sbin/uci set daed.config.enabled=*": [ "exec" ],
"/sbin/uci set daede.config=daede": [ "exec" ],
"/sbin/uci set daede.config.active_backend=*": [ "exec" ],
"/sbin/uci commit dae": [ "exec" ],
"/sbin/uci commit daed": [ "exec" ],
"/sbin/uci commit daede": [ "exec" ],
"/sbin/ip netns del daens": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geosite": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh dae": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh generate": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ]
},
"uci": [ "dae", "daed", "daede" ]
}
}
}
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
PKG_RELEASE:=11
PKG_RELEASE:=12
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=881417cc21602eb10504e1ba99068b28a114b858
PKG_SOURCE_VERSION:=4b0712931b2c14556b3ac4b6aaf921aeda07846c
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0