🏅 Sync 2026-10-10 12:03:15

This commit is contained in:
github-actions[bot] committed 2026-10-10 12:03:15 +08:00
1 parent ad68a3cd65
commit f080de4b89
109 files changed
+72311 -621

No files matched your search

+2 -2
View File
@@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=apfree-wifidog PKG_NAME:=apfree-wifidog
PKG_VERSION:=9.09.2955 PKG_VERSION:=9.10.2961
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
+17 -1
View File
@@ -2,7 +2,7 @@ config wifidogx 'common'
option external_interface 'wan' option external_interface 'wan'
option check_interval '60' option check_interval '60'
option client_timeout '60' option client_timeout '60'
option wired_passed '1' option wired_passed '0'
option enabled '1' option enabled '1'
option log_level '0' option log_level '0'
option js_filter '1' option js_filter '1'
@@ -18,6 +18,22 @@ config wifidogx 'common'
option privileged_ops_secret 'chawrt@2026' option privileged_ops_secret 'chawrt@2026'
option device_id 'AW58534605CC7D742A339' option device_id 'AW58534605CC7D742A339'
option local_portal 'https://qq.com' option local_portal 'https://qq.com'
list trusted_wildcard_domains '*.google.com'
list trusted_wildcard_domains '*.googleapis.com'
list trusted_wildcard_domains '*.googleusercontent.com'
list trusted_wildcard_domains '*.openai.com'
list trusted_wildcard_domains '*.chatgpt.com'
list trusted_wildcard_domains '*.oaistatic.com'
list trusted_wildcard_domains '*.oaiusercontent.com'
list trusted_wildcard_domains '*.x.ai'
list trusted_wildcard_domains '*.grok.com'
list trusted_wildcard_domains '*.x.com'
list trusted_wildcard_domains '*.twitter.com'
list trusted_wildcard_domains '*.twimg.com'
list trusted_wildcard_domains '*.anthropic.com'
list trusted_wildcard_domains '*.claude.ai'
list trusted_wildcard_domains '*.github.com'
list trusted_wildcard_domains '*.github.io'
config gateway 'apfree' config gateway 'apfree'
option gateway_name 'br-lan' option gateway_name 'br-lan'
+2 -2
View File
@@ -5,13 +5,13 @@ PKG_SHORT_SHA:=e4dd968
PKG_COMMIT_DATE:=2026.10.08 PKG_COMMIT_DATE:=2026.10.08
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=df7619e949a76f98a3b23516e919e40931090220 PKG_SOURCE_VERSION:=aea9b179d46fdff1743a79f4e803e768acbc0711
PKG_HASH:=skip PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dsmboot PKG_NAME:=dsmboot
PKG_VERSION:=5.2 PKG_VERSION:=5.2
PKG_RELEASE:=1 PKG_RELEASE:=2
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
Binary file not shown.
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser-q PKG_NAME:=filebrowser-q
PKG_VERSION:=1.5.8-stable PKG_VERSION:=2.0.0-stable
PKG_RELEASE:=8 PKG_RELEASE:=9
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
+5 -5
View File
@@ -3,17 +3,17 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=honk PKG_NAME:=honk
PKG_VERSION:=2026.10.9_pre2 PKG_VERSION:=2026.10.9_pre3
PKG_RELEASE:=14 PKG_RELEASE:=15
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49) # Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
# because that is what the GitHub release asset name uses. # because that is what the GitHub release asset name uses.
HONK_RELEASE_TAG:=debug.2026.10.9.native-api.2 HONK_RELEASE_TAG:=debug.2026.10.9.native-api.3
HONK_ASSET_TAG:=debug HONK_ASSET_TAG:=debug
# SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh. # SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh.
HONK_HASH_X86_64:=d46eecc460ad9f9fdfad30b80828e4b64ec9f94bbcabff942b8fe68d3793f9b2 HONK_HASH_X86_64:=179dac725195f04899ae871b9fbcf47deec4b99f6b30c539296d31fdcd9a8c2e
HONK_HASH_AARCH64:=b706100f6a9ba9879bf2c1e018f64072ebe66f128f8dcb8b8a11d803eefe8174 HONK_HASH_AARCH64:=6bcf738b8c618d9c87a412fa3dbb976e041349f6614ebd1ea2c7044ede3a4d48
PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILE:=LICENSE PKG_LICENSE_FILE:=LICENSE
+1
View File
@@ -11,6 +11,7 @@ global {
lan_interface: br-lan lan_interface: br-lan
wan_interface: auto wan_interface: auto
auto_config_kernel_parameter: true auto_config_kernel_parameter: true
disable_waiting_network: false
data_dir: '/etc/honk' data_dir: '/etc/honk'
log_file: '/var/log/honk/honk.log' log_file: '/var/log/honk/honk.log'
store_subscribe: true store_subscribe: true
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector-mod-telegram PKG_NAME:=internet-detector-mod-telegram
PKG_VERSION:=1.7.4 PKG_VERSION:=1.7.4
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector> PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
@@ -57,7 +57,7 @@ function httpRequest(url, proxyType, proxyHost, proxyPort, proxyUser, proxyPassw
if(length(r) > 0) { if(length(r) > 0) {
retCode = r[0]; retCode = r[0];
} }
data = replace(data, retCodeRegexp, ''); data = trim(replace(data, retCodeRegexp, ''));
} }
} }
return { retCode, data }; return { retCode, data };
+3 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector PKG_NAME:=internet-detector
PKG_VERSION:=1.7.4 PKG_VERSION:=1.7.4
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector> PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
@@ -62,6 +62,8 @@ define Package/$(PKG_NAME)/install
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua $(1)/usr/lib/lua/internet-detector/modules/mod_public_ip.lua $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua $(1)/usr/lib/lua/internet-detector/modules/mod_public_ip.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua $(1)/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua $(1)/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_regular_script.lua $(1)/usr/lib/lua/internet-detector/modules/mod_regular_script.lua $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_regular_script.lua $(1)/usr/lib/lua/internet-detector/modules/mod_regular_script.lua
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
$(INSTALL_DATA) ./files/lib/upgrade/keep.d/internet-detector $(1)/lib/upgrade/keep.d/internet-detector
endef endef
define Package/$(PKG_NAME)/postinst define Package/$(PKG_NAME)/postinst
@@ -0,0 +1 @@
/etc/internet-detector/*
+1 -1
View File
@@ -1 +1 @@
both snapshot
@@ -1 +1 @@
26.280.51432~86d1549 26.281.30313~37e6a6b
+3 -3
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-clashoo PKG_NAME:=luci-app-clashoo
PKG_VERSION:=1.31.0 PKG_VERSION:=1.31.0
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_MAINTAINER:=kenzok8 PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -13,7 +13,7 @@ define Package/luci-app-clashoo
CATEGORY:=LuCI CATEGORY:=LuCI
SUBMENU:=3. Applications SUBMENU:=3. Applications
TITLE:=LuCI app for Clashoo TITLE:=LuCI app for Clashoo
DEPENDS:=+luci-base +clashoo DEPENDS:=+luci-base +clashoo +luci-i18n-clashoo-zh-cn
PKGARCH:=all PKGARCH:=all
MAINTAINER:=kenzok8 MAINTAINER:=kenzok8
endef endef
@@ -75,7 +75,7 @@ define Package/luci-i18n-clashoo-zh-cn
SECTION:=luci SECTION:=luci
CATEGORY:=LuCI CATEGORY:=LuCI
TITLE:=luci-app-clashoo - Simplified Chinese (zh-cn) TITLE:=luci-app-clashoo - Simplified Chinese (zh-cn)
DEPENDS:=luci-app-clashoo DEPENDS:=+luci-base
DEFAULT:=LUCI_LANG_zh_Hans||(ALL&&m) DEFAULT:=LUCI_LANG_zh_Hans||(ALL&&m)
HIDDEN:=1 HIDDEN:=1
PKGARCH:=all PKGARCH:=all
@@ -143,6 +143,7 @@ const callOverview = rpc.declare({ object: 'luci.clashoo', method: 'ove
const callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} }); const callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} });
const callListSingboxProfiles = rpc.declare({ object: 'luci.clashoo', method: 'list_singbox_profiles', expect: {} }); const callListSingboxProfiles = rpc.declare({ object: 'luci.clashoo', method: 'list_singbox_profiles', expect: {} });
const callGetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile', params: ['name'], expect: {} }); const callGetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile', params: ['name'], expect: {} });
const callGetSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile_chunk', params: ['name', 'index'], expect: {} });
const callSaveSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'save_singbox_profile_chunk', params: ['name', 'content', 'index', 'total'], expect: {} }); const callSaveSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'save_singbox_profile_chunk', params: ['name', 'content', 'index', 'total'], expect: {} });
const callSetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'set_singbox_profile', params: ['name'], expect: {} }); const callSetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'set_singbox_profile', params: ['name'], expect: {} });
const callDeleteSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'delete_singbox_profile', params: ['name'], expect: {} }); const callDeleteSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'delete_singbox_profile', params: ['name'], expect: {} });
@@ -234,14 +235,66 @@ return baseclass.extend({
smartFlushCache: function () { return L.resolveDefault(callSmartFlushCache(), { success: false }); }, smartFlushCache: function () { return L.resolveDefault(callSmartFlushCache(), { success: false }); },
listSingboxProfiles: function () { return L.resolveDefault(callListSingboxProfiles(), { profiles: [], active: '' }); }, listSingboxProfiles: function () { return L.resolveDefault(callListSingboxProfiles(), { profiles: [], active: '' }); },
getSingboxProfile: function (name) { return L.resolveDefault(callGetSingboxProfile(name), {}); }, getSingboxProfile: function (name) {
return L.resolveDefault(callGetSingboxProfile(name), {}).then(function (r) {
if (!r || r.error !== 'too large') return r || {};
var decoder = new TextDecoder('utf-8', { fatal: true });
var parts = [], total = 0, size = 0, mtime = '', received = 0;
function readNext(index) {
return L.resolveDefault(callGetSingboxProfileChunk(name, String(index)), {}).then(function (chunk) {
if (!chunk || chunk.error)
return { error: (chunk && chunk.error) || 'read_failed' };
if (!Number.isInteger(chunk.index) || !Number.isInteger(chunk.total) ||
!Number.isInteger(chunk.size) || chunk.index !== index ||
chunk.total < 1 || chunk.total > 683 || chunk.size < 1 ||
chunk.size > 16 * 1024 * 1024 || typeof chunk.content_b64 !== 'string')
return { error: 'invalid_chunk' };
if (index === 0) {
total = chunk.total;
size = chunk.size;
mtime = chunk.mtime;
} else if (chunk.total !== total || chunk.size !== size || chunk.mtime !== mtime) {
return { error: 'file_changed' };
}
try {
var raw = atob(chunk.content_b64);
var bytes = new Uint8Array(raw.length);
for (var i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
received += bytes.length;
parts.push(decoder.decode(bytes, { stream: true }));
} catch (e) {
return { error: 'invalid_chunk' };
}
if (index + 1 < total) return readNext(index + 1);
if (received !== size) return { error: 'file_changed' };
try { parts.push(decoder.decode()); }
catch (e) { return { error: 'invalid_chunk' }; }
return { content: parts.join(''), name: name };
});
}
return readNext(0);
});
},
saveSingboxProfile: function (name, content) { saveSingboxProfile: function (name, content) {
var chunkSize = 24576; var chunkSize = 24576;
var total = Math.max(1, Math.ceil((content || '').length / chunkSize)); var value = content || '';
var chunks = [];
for (var offset = 0; offset < value.length;) {
var end = Math.min(offset + chunkSize, value.length);
if (end < value.length && /[\uD800-\uDBFF]/.test(value.charAt(end - 1))) end--;
chunks.push(value.slice(offset, end));
offset = end;
}
if (!chunks.length) chunks.push('');
var total = chunks.length;
var index = 0; var index = 0;
function sendNext() { function sendNext() {
var chunk = (content || '').slice(index * chunkSize, (index + 1) * chunkSize); var chunk = chunks[index];
return L.resolveDefault(callSaveSingboxProfileChunk(name, chunk, String(index), String(total)), {}).then(function (r) { return L.resolveDefault(callSaveSingboxProfileChunk(name, chunk, String(index), String(total)), {}).then(function (r) {
if (!r || !r.success) if (!r || !r.success)
return { success: false, error: (r && r.error) || 'upload_failed', message: (r && (r.message || r.error)) || _('Upload failed') }; return { success: false, error: (r && r.error) || 'upload_failed', message: (r && (r.message || r.error)) || _('Upload failed') };
@@ -578,7 +578,7 @@ function createConfigEditor(mode) {
}); });
var host = E('div', { 'class': 'cl-cm-host', style: 'display:none' }); var host = E('div', { 'class': 'cl-cm-host', style: 'display:none' });
var wrap = E('div', { 'class': 'cl-cm-wrap' }, [host, ta]); var wrap = E('div', { 'class': 'cl-cm-wrap' }, [host, ta]);
var cm = null, usingCM = false; var cm = null, usingCM = false, valueSeq = 0;
function showTextarea(content) { function showTextarea(content) {
usingCM = false; usingCM = false;
@@ -598,11 +598,14 @@ function createConfigEditor(mode) {
el: wrap, el: wrap,
textarea: ta, textarea: ta,
setValue: function (content, readOnly) { setValue: function (content, readOnly) {
var seq = ++valueSeq;
content = (content == null) ? '' : String(content); content = (content == null) ? '' : String(content);
ta.readOnly = !!readOnly; ta.readOnly = !!readOnly;
if (content.length > LARGE) { showTextarea(content); return; } showTextarea(content);
if (content.length > LARGE) return;
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); return; } if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); return; }
loadCodeMirror().then(function () { loadCodeMirror().then(function () {
if (seq !== valueSeq) return;
if (!cm && window.CodeMirror) { if (!cm && window.CodeMirror) {
cm = window.CodeMirror(host, { cm = window.CodeMirror(host, {
value: '', mode: mode, value: '', mode: mode,
@@ -615,7 +618,7 @@ function createConfigEditor(mode) {
host.classList.add('cl-cm-dark'); host.classList.add('cl-cm-dark');
} }
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); } else showTextarea(content); if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); } else showTextarea(content);
}).catch(function () { showTextarea(content); }); }).catch(function () { if (seq === valueSeq) showTextarea(content); });
}, },
getValue: function () { getValue: function () {
return (usingCM && cm) ? cm.getValue() : ta.value; return (usingCM && cm) ? cm.getValue() : ta.value;
@@ -1707,8 +1710,9 @@ return view.extend({
var self = this; var self = this;
var safeText = function (v) { return (v == null || v === 'null') ? '' : String(v); }; var safeText = function (v) { return (v == null || v === 'null') ? '' : String(v); };
var formatJsonForEditor = function (content) { var formatJsonForEditor = function (content) {
if (!content) return '';
try { try {
return JSON.stringify(JSON.parse(content || '{}'), null, 2) + '\n'; return JSON.stringify(JSON.parse(content), null, 2) + '\n';
} catch (e) { } catch (e) {
return content || ''; return content || '';
} }
@@ -1741,7 +1745,7 @@ return view.extend({
var msg = r.changes && r.changes.length ? _("Fixed deprecated fields: ") + r.changes.join(', ') : _("Configuration is up to date; no fixes needed"); var msg = r.changes && r.changes.length ? _("Fixed deprecated fields: ") + r.changes.join(', ') : _("Configuration is up to date; no fixes needed");
ui.addNotification(null, E('p', msg)); ui.addNotification(null, E('p', msg));
/* 重新加载编辑器内容 */ /* 重新加载编辑器内容 */
clashoo.getSingboxProfile(name).then(function (gr) { ed.setValue(formatJsonForEditor(gr.content || '')); }); loadEditor(name);
} else { } else {
ui.addNotification(null, E('p', _("Fix failed: ") + ((r && r.message) || ''))); ui.addNotification(null, E('p', _("Fix failed: ") + ((r && r.message) || '')));
} }
@@ -1759,14 +1763,26 @@ return view.extend({
]) ])
]); ]);
var loadSeq = 0;
function loadEditor(name) { function loadEditor(name) {
var seq = ++loadSeq;
editorTitle.textContent = _("Editing: ") + name; editorTitle.textContent = _("Editing: ") + name;
saveBtn.removeAttribute('disabled'); saveBtn.disabled = true;
migrateBtn.removeAttribute('disabled'); migrateBtn.disabled = true;
ed.textarea.dataset.name = name; ed.textarea.dataset.name = '';
ed.setValue(_("Loading…")); ed.setValue(_("Loading…"));
clashoo.getSingboxProfile(name).then(function (r) { clashoo.getSingboxProfile(name).then(function (r) {
ed.setValue(formatJsonForEditor(r.content || '')); if (seq !== loadSeq) return;
if (!r || r.error || typeof r.content !== 'string' || !r.content)
throw new Error((r && (r.message || r.error)) || 'read_failed');
ed.setValue(formatJsonForEditor(r.content));
ed.textarea.dataset.name = name;
saveBtn.disabled = false;
migrateBtn.disabled = false;
}).catch(function (e) {
if (seq !== loadSeq) return;
ed.setValue('');
ui.addNotification(null, E('p', _("Load failed") + ': ' + (e.message || e)), 'error');
}); });
} }
@@ -247,7 +247,7 @@ function clKernelUnlock() {
return view.extend({ return view.extend({
_busy: false, _busy: false,
_op: null, /* 'start' | 'stop' | 'restart' | null */ _op: null, /* 'start' | 'stop' | 'restart' | 'reload' | null */
_opTimers: null, /* array of setTimeout IDs for cleanup */ _opTimers: null, /* array of setTimeout IDs for cleanup */
_accessRefreshing: false, _accessRefreshing: false,
_coreSwitchBusy: false, _coreSwitchBusy: false,
@@ -313,6 +313,7 @@ return view.extend({
tcp_mode: uci.get('clashoo', 'config', 'tcp_mode') || 'tun', tcp_mode: uci.get('clashoo', 'config', 'tcp_mode') || 'tun',
udp_mode: uci.get('clashoo', 'config', 'udp_mode') || (uci.get('clashoo', 'config', 'tcp_mode') || 'tun'), udp_mode: uci.get('clashoo', 'config', 'udp_mode') || (uci.get('clashoo', 'config', 'tcp_mode') || 'tun'),
stack: uci.get('clashoo', 'config', 'stack') || 'system', stack: uci.get('clashoo', 'config', 'stack') || 'system',
enhanced_mode: uci.get('clashoo', 'config', 'enhanced_mode') || 'fake-ip',
config: configName, config: configName,
local_ip: location.hostname, local_ip: location.hostname,
health_status: 'loading' health_status: 'loading'
@@ -788,7 +789,29 @@ return view.extend({
if (disabled) return; if (disabled) return;
return running ? self._stop() : self._start(); return running ? self._stop() : self._start();
} }
}, [E('span', { 'class': 'cl-service-knob' })]) }, [E('span', { 'class': 'cl-service-knob' })]),
E('button', {
type: 'button',
'class': 'btn cbi-button-action cl-btn-sm',
title: _("Restart Service"),
disabled: (disabled || !running) ? '' : null,
click: function (ev) {
ev.preventDefault();
if (disabled || !running) return;
return self._restart();
}
}, _("Restart")),
E('button', {
type: 'button',
'class': 'btn cbi-button-action cl-btn-sm',
title: _("Reload Service"),
disabled: (disabled || !running) ? '' : null,
click: function (ev) {
ev.preventDefault();
if (disabled || !running) return;
return self._reload();
}
}, _("Reload"))
]); ]);
}, },
@@ -1561,6 +1584,7 @@ return view.extend({
var tcpMode = String(st.tcp_mode || '').toLowerCase(); var tcpMode = String(st.tcp_mode || '').toLowerCase();
var udpMode = String(st.udp_mode || tcpMode).toLowerCase(); var udpMode = String(st.udp_mode || tcpMode).toLowerCase();
var stackMode = String(st.stack || '').toLowerCase(); var stackMode = String(st.stack || '').toLowerCase();
var enhancedMode = String(st.enhanced_mode || 'fake-ip').toLowerCase();
var tpMode = 'custom'; var tpMode = 'custom';
if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed') if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed')
@@ -1570,7 +1594,7 @@ return view.extend({
else if (tcpMode === 'tun' && udpMode === 'tun') else if (tcpMode === 'tun' && udpMode === 'tun')
tpMode = 'tun-system'; tpMode = 'tun-system';
else if (tcpMode === 'redirect' && udpMode === 'tproxy') else if (tcpMode === 'redirect' && udpMode === 'tproxy')
tpMode = 'fake-ip'; tpMode = enhancedMode === 'redir-host' ? 'redir-host' : 'fake-ip';
var panelType = st.panel_type || 'zashboard'; var panelType = st.panel_type || 'zashboard';
var panelUrl = this._dashboardUrl(st); var panelUrl = this._dashboardUrl(st);
var panels = ['metacubexd', 'yacd', 'zashboard', 'razord']; var panels = ['metacubexd', 'yacd', 'zashboard', 'razord'];
@@ -1581,7 +1605,7 @@ return view.extend({
return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]); return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]);
})); }));
}; };
var tpModeOptions = [['fake-ip','Fake-IP'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']]; var tpModeOptions = [['fake-ip','Fake-IP'],['redir-host','Redir-Host'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']];
if (tpMode === 'custom') if (tpMode === 'custom')
tpModeOptions.unshift(['custom',_("Custom")]); tpModeOptions.unshift(['custom',_("Custom")]);
@@ -1638,6 +1662,7 @@ return view.extend({
sel.disabled = true; sel.disabled = true;
self._op = 'mode'; self._op = 'mode';
self._lastSt = self._lastSt || {}; self._lastSt = self._lastSt || {};
self._lastSt.enhanced_mode = mode === 'redir-host' ? 'redir-host' : 'fake-ip';
if (mode === 'tun-mixed') { if (mode === 'tun-mixed') {
self._lastSt.tcp_mode = 'tun'; self._lastSt.tcp_mode = 'tun';
self._lastSt.udp_mode = 'tun'; self._lastSt.udp_mode = 'tun';
@@ -1787,10 +1812,12 @@ return view.extend({
尤其是切换 tab 回到 overview 时缓存数据与首次 poll 数据通常一致 */ 尤其是切换 tab 回到 overview 时缓存数据与首次 poll 数据通常一致 */
var sig = JSON.stringify({ var sig = JSON.stringify({
running: st.running, running: st.running,
pid: st.pid,
health: st.health_status, health: st.health_status,
proxy_mode: st.proxy_mode, proxy_mode: st.proxy_mode,
tcp_mode: st.tcp_mode, tcp_mode: st.tcp_mode,
udp_mode: st.udp_mode, udp_mode: st.udp_mode,
enhanced_mode: st.enhanced_mode,
stack: st.stack_type || st.stack, stack: st.stack_type || st.stack,
config: st.config, config: st.config,
core_type: st.core_type, core_type: st.core_type,
@@ -1933,14 +1960,13 @@ return view.extend({
return _("Health check in progress…"); return _("Health check in progress…");
}, },
/* fn: fire-and-forget RPC + 秒速轮询直到状态到位 */ /* toggle switch instantly (optimistic), then poll until the operation takes effect */
/* toggle switch instantly (optimistic), do not wait for RPC */
_svc: function (fn, opKey) { _svc: function (fn, opKey) {
if (this._busy) return Promise.resolve(); if (this._busy) return Promise.resolve();
// preflight: refuse start when no profile selected. // preflight: refuse start when no profile selected.
// backend select_config also defends but toast is slow, UI would flash. // backend select_config also defends but toast is slow, UI would flash.
if (opKey === 'start' || opKey === 'restart') { if (opKey === 'start' || opKey === 'restart' || opKey === 'reload') {
var st0 = this._lastSt || {}; var st0 = this._lastSt || {};
var hasConfig = !!(st0.config || st0.conf_path); var hasConfig = !!(st0.config || st0.conf_path);
if (!hasConfig) { if (!hasConfig) {
@@ -1957,21 +1983,25 @@ return view.extend({
var btn = document.querySelector('.cl-service-switch'); var btn = document.querySelector('.cl-service-switch');
if (btn) { if (btn) {
var running = btn.getAttribute('aria-pressed') === 'true'; var running = btn.getAttribute('aria-pressed') === 'true';
var newRunning = opKey === 'start'; var newRunning = opKey !== 'stop';
if (newRunning !== running) { if (newRunning !== running) {
btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false'); btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false');
btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off'); btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off');
} }
self._showOpMsg(opKey === 'stop' ? _("Stopping…") : _("Starting…")); self._showOpMsg(opKey === 'stop' ? _("Stopping…") : opKey === 'reload' ? _("Reloading…") : _("Starting…"));
} }
var maxWait = opKey === 'stop' ? 15000 : 35000; var maxWait = opKey === 'stop' ? 15000 : 35000;
fn().catch(function () {}); /* fire-and-forget */
var started = Date.now(); var started = Date.now();
var pollTimer = null; var pollTimer = null;
var initialPid = (this._lastSt && this._lastSt.pid) || 0;
var sawStopped = false;
var rpcDone = false;
var finished = false;
function finish(finalMsg) { function finish(finalMsg) {
if (finished) return;
finished = true;
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
if (finalMsg) self._showOpMsg(finalMsg); if (finalMsg) self._showOpMsg(finalMsg);
self._busy = false; self._busy = false;
@@ -1979,19 +2009,34 @@ return view.extend({
self._pollOverview(true); self._pollOverview(true);
} }
Promise.resolve().then(fn).then(function (r) {
if (!r || r.success !== true)
throw new Error((r && (r.message || r.error)) || 'RPC failed');
rpcDone = true;
}).catch(function (e) {
ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e)), 'error');
finish(null);
});
function pollOnce() { function pollOnce() {
if (finished) return;
L.resolveDefault(clashoo.status(), {}).then(function (st) { L.resolveDefault(clashoo.status(), {}).then(function (st) {
if (finished) return;
st = st || {}; st = st || {};
var elapsed = Date.now() - started; var elapsed = Date.now() - started;
if (opKey === 'stop') { if (opKey === 'stop') {
if (st.running === false) return finish(_("Stopped") + ' ⚪'); if (rpcDone && st.running === false) return finish(_("Stopped") + ' ⚪');
} else { } else {
if (st.running === true) return finish(_("Running 🟢")); if (st.running === false) sawStopped = true;
if (st.health_status === 'fail') return finish(null); if (rpcDone && st.health_status === 'fail') return finish(null);
if (rpcDone && st.running === true &&
(opKey !== 'restart' || (initialPid && st.pid && st.pid !== initialPid) || sawStopped))
return finish(_("Running 🟢"));
} }
if (elapsed >= maxWait) return finish(null); if (elapsed >= maxWait) return finish(null);
pollTimer = setTimeout(pollOnce, 500); pollTimer = setTimeout(pollOnce, 500);
}).catch(function () { }).catch(function () {
if (finished) return;
if (Date.now() - started < maxWait) pollTimer = setTimeout(pollOnce, 500); if (Date.now() - started < maxWait) pollTimer = setTimeout(pollOnce, 500);
else finish(null); else finish(null);
}); });
@@ -2002,6 +2047,7 @@ return view.extend({
_start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); }, _start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
_stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); }, _stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
_restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); }, _restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); },
_reload: function () { return this._svc(function () { return clashoo.reload(); }, 'reload'); },
_updSubs: function (ev) { _updSubs: function (ev) {
var btn = ev && ev.target; var btn = ev && ev.target;
+15
View File
@@ -1059,12 +1059,27 @@ msgstr "推荐留空"
msgid "Redirect Port" msgid "Redirect Port"
msgstr "Redirect 端口" msgstr "Redirect 端口"
msgid "Reload"
msgstr "重新加载"
msgid "Reload Service"
msgstr "重新加载服务"
msgid "Reloading…"
msgstr "重新加载中…"
msgid "Refresh" msgid "Refresh"
msgstr "刷新" msgstr "刷新"
msgid "Refresh Access Check" msgid "Refresh Access Check"
msgstr "刷新访问检查" msgstr "刷新访问检查"
msgid "Restart"
msgstr "重启"
msgid "Restart Service"
msgstr "重启服务"
msgid "Regular domains use overseas DNS only and follow proxy rules; DoT/DoQ is blocked. Takes effect after restart.<br>" msgid "Regular domains use overseas DNS only and follow proxy rules; DoT/DoQ is blocked. Takes effect after restart.<br>"
msgstr "普通域名只走境外 DNS 并遵守代理规则,阻断 DoT/DoQ。重启生效。<br>" msgstr "普通域名只走境外 DNS 并遵守代理规则,阻断 DoT/DoQ。重启生效。<br>"
@@ -390,20 +390,25 @@ if (core == 'singbox' && dcore == '5') {
return { error: 'invalid_core_target', message: '无效的内核类型与版本组合' }; return { error: 'invalid_core_target', message: '无效的内核类型与版本组合' };
} }
function prepare_singbox_runtime() { let migrate_singbox;
let active = uci_get('clashoo', 'config', 'singbox_active') || '';
function prepare_singbox_runtime(candidate, staged) {
let active = candidate || uci_get('clashoo', 'config', 'singbox_active') || '';
let src = active ? (SINGBOX_DIR + '/' + active) : SINGBOX_RUNTIME_CONFIG; let src = active ? (SINGBOX_DIR + '/' + active) : SINGBOX_RUNTIME_CONFIG;
if (!access(src, 'r')) src = SINGBOX_RUNTIME_CONFIG; if (!candidate && !access(src, 'r')) src = SINGBOX_RUNTIME_CONFIG;
if (!access(src, 'r')) if (!access(src, 'r'))
return { success: false, message: '请先生成或选择 sing-box 配置文件' }; return { success: false, message: '请先生成或选择 sing-box 配置文件' };
system('mkdir -p ' + shell_quote(SINGBOX_RUNTIME_DIR) + ' ' + shell_quote(SINGBOX_WORKDIR) + ' >/dev/null 2>&1'); system('mkdir -p ' + shell_quote(SINGBOX_RUNTIME_DIR) + ' ' + shell_quote(SINGBOX_WORKDIR) + ' >/dev/null 2>&1');
let tmp = shell_read('mktemp ' + shell_quote(SINGBOX_RUNTIME_CONFIG + '.tmp.XXXXXX') + ' 2>/dev/null'); let tmp_pipe = popen('mktemp ' + shell_quote(SINGBOX_RUNTIME_CONFIG + '.tmp.XXXXXX') + ' 2>/dev/null');
let tmp = tmp_pipe ? trim(tmp_pipe.read('all') || '') : '';
if (tmp_pipe) tmp_pipe.close();
if (!tmp) if (!tmp)
return { success: false, message: '无法创建 sing-box 临时运行配置' }; return { success: false, message: '无法创建 sing-box 临时运行配置' };
let raw = readfile(src) || ''; let raw = readfile(src) || '';
let cfg = raw ? json(raw) : null; let cfg = null;
try { cfg = raw ? json(raw) : null; } catch (e) {}
if (!cfg || type(cfg) != 'object') { if (!cfg || type(cfg) != 'object') {
unlink(tmp); unlink(tmp);
return { success: false, message: 'sing-box 配置 JSON 无法解析' }; return { success: false, message: 'sing-box 配置 JSON 无法解析' };
@@ -443,6 +448,9 @@ if (uci_get('clashoo', 'config', 'core_only') != '1') {
} }
} }
if (staged)
return { success: true, path: tmp };
if (system('mv -f ' + shell_quote(tmp) + ' ' + shell_quote(SINGBOX_RUNTIME_CONFIG) + ' >/dev/null 2>&1') != 0) { if (system('mv -f ' + shell_quote(tmp) + ' ' + shell_quote(SINGBOX_RUNTIME_CONFIG) + ' >/dev/null 2>&1') != 0) {
unlink(tmp); unlink(tmp);
return { success: false, message: '无法提交 sing-box 运行配置' }; return { success: false, message: '无法提交 sing-box 运行配置' };
@@ -1555,7 +1563,7 @@ return null;
} }
// Migrate sing-box config from pre-1.12 deprecated formats to 1.12+ format // Migrate sing-box config from pre-1.12 deprecated formats to 1.12+ format
function migrate_singbox(cfg) { migrate_singbox = function(cfg) {
// --- Fix DNS servers: address-based → type-based, remove detour --- // --- Fix DNS servers: address-based → type-based, remove detour ---
if (cfg.dns && cfg.dns.servers) { if (cfg.dns && cfg.dns.servers) {
cfg.dns.servers = map(cfg.dns.servers || [], function(srv) { cfg.dns.servers = map(cfg.dns.servers || [], function(srv) {
@@ -1910,7 +1918,7 @@ function migrate_singbox(cfg) {
} }
return cfg; return cfg;
} };
function restore_backup_json_locked(raw) { function restore_backup_json_locked(raw) {
let rollback_ready = false; let rollback_ready = false;
@@ -2063,12 +2071,14 @@ let c = cursor();
c.load('clashoo'); c.load('clashoo');
let configured_core = get_core_type(); let configured_core = get_core_type();
let running = is_running(); let running = is_running();
let pid = running ? procd_instance_pid('clashoo', configured_core == 'singbox' ? 'singbox' : null) : 0;
let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule'; let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule';
let conf_path = c.get('clashoo', 'config', 'use_config') || ''; let conf_path = c.get('clashoo', 'config', 'use_config') || '';
let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard'; let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard';
let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun'; let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun';
let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode; let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode;
let stack = c.get('clashoo', 'config', 'stack') || 'system'; let stack = c.get('clashoo', 'config', 'stack') || 'system';
let enhanced_mode = c.get('clashoo', 'config', 'enhanced_mode') || 'fake-ip';
let api = get_clash_api_auth(); let api = get_clash_api_auth();
let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090'); let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090');
let dash_pass = api.pass || ''; let dash_pass = api.pass || '';
@@ -2091,8 +2101,8 @@ let conf_name = slash >= 0 ? substr(conf_path, slash + 1) : conf_path;
if (configured_core == 'singbox' && config) if (configured_core == 'singbox' && config)
conf_name = config; conf_name = config;
let dcore = c.get('clashoo', 'config', 'dcore') || '2'; let dcore = c.get('clashoo', 'config', 'dcore') || '2';
return { running, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config, return { running, pid, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config,
panel_type, tcp_mode, udp_mode, stack, panel_type, tcp_mode, udp_mode, stack, enhanced_mode,
effective_tcp_mode, effective_udp_mode, effective_tcp_mode, effective_udp_mode,
runtime_degraded, runtime_degrade_reason, runtime_degraded, runtime_degrade_reason,
health_status, health_detail, has_tun_device, health_status, health_detail, has_tun_device,
@@ -2145,8 +2155,18 @@ call: function() {
let c = cursor(); let c = cursor();
c.load('clashoo'); c.load('clashoo');
sync_legacy_core_fields(c); sync_legacy_core_fields(c);
let enabled = c.get('clashoo', 'config', 'enable') == '1';
let core_type = c.get('clashoo', 'config', 'core_type') || 'mihomo';
let conf_path = core_type == 'singbox' ? c.get('clashoo', 'config', 'singbox_active') || '' :
c.get('clashoo', 'config', 'use_config') || '';
c.commit('clashoo'); c.commit('clashoo');
let rc = system('uci commit clashoo >/dev/null 2>&1'); let rc = system('uci commit clashoo >/dev/null 2>&1');
// Issue #67: auto-start on first configuration. If the service is enabled
// but not running (fresh install where the user configured settings but the
// service never started), start it now so no reboot or manual Startup-page
// restart is needed. Fire-and-forget like the other service operations.
if (rc == 0 && enabled && conf_path != '' && !is_running())
system('sh /usr/share/clashoo/rpc/rpc_async.sh start >/dev/null 2>&1');
return { success: rc === 0 }; return { success: rc === 0 };
} }
}, },
@@ -2177,7 +2197,7 @@ set_mode: {
args: { mode: 'mode' }, args: { mode: 'mode' },
call: function(req) { call: function(req) {
let mode = req.args?.mode || 'fake-ip'; let mode = req.args?.mode || 'fake-ip';
if (mode != 'fake-ip' && mode != 'tun-system' && mode != 'tun-gvisor' && mode != 'tun-mixed') if (mode != 'fake-ip' && mode != 'redir-host' && mode != 'tun-system' && mode != 'tun-gvisor' && mode != 'tun-mixed')
return { error: 'invalid mode' }; return { error: 'invalid mode' };
let c = cursor(); c.load('clashoo'); let c = cursor(); c.load('clashoo');
if (mode == 'tun-system' || mode == 'tun-gvisor' || mode == 'tun-mixed') { if (mode == 'tun-system' || mode == 'tun-gvisor' || mode == 'tun-mixed') {
@@ -2191,7 +2211,7 @@ call: function(req) {
c.set('clashoo', 'config', 'tun_mode', '0'); c.set('clashoo', 'config', 'tun_mode', '0');
c.set('clashoo', 'config', 'tcp_mode', 'redirect'); c.set('clashoo', 'config', 'tcp_mode', 'redirect');
c.set('clashoo', 'config', 'udp_mode', 'tproxy'); c.set('clashoo', 'config', 'udp_mode', 'tproxy');
c.set('clashoo', 'config', 'enhanced_mode', 'fake-ip'); c.set('clashoo', 'config', 'enhanced_mode', mode == 'redir-host' ? 'redir-host' : 'fake-ip');
} }
c.commit('clashoo'); c.commit('clashoo');
system('uci commit clashoo >/dev/null 2>&1'); system('uci commit clashoo >/dev/null 2>&1');
@@ -2716,12 +2736,14 @@ try {
c.load('clashoo'); c.load('clashoo');
let configured_core = get_core_type(); let configured_core = get_core_type();
let running = is_running(); let running = is_running();
let pid = running ? procd_instance_pid('clashoo', configured_core == 'singbox' ? 'singbox' : null) : 0;
let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule'; let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule';
let conf_path = c.get('clashoo', 'config', 'use_config') || ''; let conf_path = c.get('clashoo', 'config', 'use_config') || '';
let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard'; let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard';
let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun'; let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun';
let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode; let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode;
let stack = c.get('clashoo', 'config', 'stack') || 'system'; let stack = c.get('clashoo', 'config', 'stack') || 'system';
let enhanced_mode = c.get('clashoo', 'config', 'enhanced_mode') || 'fake-ip';
let api = get_clash_api_auth(); let api = get_clash_api_auth();
let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090'); let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090');
let dash_pass = api.pass || ''; let dash_pass = api.pass || '';
@@ -2744,8 +2766,8 @@ let api = get_clash_api_auth();
if (configured_core == 'singbox' && config) conf_name = config; if (configured_core == 'singbox' && config) conf_name = config;
let dcore = c.get('clashoo', 'config', 'dcore') || '2'; let dcore = c.get('clashoo', 'config', 'dcore') || '2';
let status_payload = { running, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config, let status_payload = { running, pid, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config,
panel_type, tcp_mode, udp_mode, stack, panel_type, tcp_mode, udp_mode, stack, enhanced_mode,
effective_tcp_mode, effective_udp_mode, effective_tcp_mode, effective_udp_mode,
runtime_degraded, runtime_degrade_reason, runtime_degraded, runtime_degrade_reason,
health_status, health_detail, has_tun_device, health_status, health_detail, has_tun_device,
@@ -3909,6 +3931,36 @@ return { content: readfile(path) || '', name };
} }
}, },
get_singbox_profile_chunk: {
args: { name: 'name', index: 'index' },
call: function(req) {
let name = req.args?.name || '';
let index_raw = req.args?.index || '0';
if (!name || index(name, '/') >= 0 || index(name, '..') >= 0 ||
!match(index_raw, /^[0-9]+$/))
return { error: 'invalid request' };
let path = SINGBOX_DIR + '/' + name;
let s = stat(path);
let chunk_size = 24576;
if (!s || !access(path, 'r')) return { error: 'not found' };
if (s.size < 1 || s.size > 16 * 1024 * 1024) return { error: 'too large' };
let total = int((s.size + chunk_size - 1) / chunk_size);
let idx = int(index_raw);
if (idx < 0 || idx >= total) return { error: 'invalid chunk' };
let fp = open(path, 'r');
if (!fp) return { error: 'read_failed' };
if (!fp.seek(idx * chunk_size, 0)) {
fp.close();
return { error: 'read_failed' };
}
let content = fp.read(chunk_size);
fp.close();
if (!content) return { error: 'read_failed' };
return { content_b64: b64enc(content), name, index: idx, total,
size: s.size, mtime: '' + s.mtime };
}
},
save_singbox_profile: { save_singbox_profile: {
args: { name: 'name', content: 'content' }, args: { name: 'name', content: 'content' },
call: function(req) { call: function(req) {
@@ -3988,16 +4040,40 @@ if (!name || index(name, '/') >= 0 || index(name, '..') >= 0)
return { success: false, error: 'invalid name' }; return { success: false, error: 'invalid name' };
let src = '/usr/share/clashoo/config/singbox/' + name; let src = '/usr/share/clashoo/config/singbox/' + name;
if (!access(src, 'r')) return { success: false, error: 'not found' }; if (!access(src, 'r')) return { success: false, error: 'not found' };
let running = is_running();
let c = cursor(); c.load('clashoo'); let c = cursor(); c.load('clashoo');
let previous = {};
for (let key in ['core_type', 'singbox_active', 'enable', 'core', 'dcore'])
previous[key] = c.get('clashoo', 'config', key);
let dcore = previous.dcore;
if (dcore != '4' && dcore != '5' ||
!access(dcore == '5' ? SINGBOX_BINARY_PATHS[1] : SINGBOX_BINARY_PATHS[0], 'x'))
dcore = access(SINGBOX_BINARY_PATHS[0], 'x') ? '4' :
access(SINGBOX_BINARY_PATHS[1], 'x') ? '5' : '';
if (!dcore)
return { success: false, error: 'core_missing', message: '请先安装 sing-box 内核' };
let prepared = prepare_singbox_runtime(name, true);
if (!prepared.success)
return { success: false, error: 'prepare_failed', message: prepared.message || '无法准备 sing-box 运行环境' };
c.set('clashoo', 'config', 'core_type', 'singbox'); c.set('clashoo', 'config', 'core_type', 'singbox');
c.set('clashoo', 'config', 'singbox_active', name); c.set('clashoo', 'config', 'singbox_active', name);
c.set('clashoo', 'config', 'enable', '1'); c.set('clashoo', 'config', 'enable', '1');
c.set('clashoo', 'config', 'dcore', dcore);
sync_legacy_core_fields(c); sync_legacy_core_fields(c);
c.commit('clashoo'); if (!c.commit('clashoo')) {
let prepared = prepare_singbox_runtime(); unlink(prepared.path);
if (!prepared.success) return { success: false, error: 'commit_failed', message: '无法保存 sing-box 配置选择' };
return { success: false, error: 'prepare_failed', message: prepared.message || '无法准备 sing-box 运行环境' }; }
if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); if (system('mv -f ' + shell_quote(prepared.path) + ' ' + shell_quote(SINGBOX_RUNTIME_CONFIG) + ' >/dev/null 2>&1') != 0) {
unlink(prepared.path);
for (let key in previous) {
if (previous[key] == null) c.delete('clashoo', 'config', key);
else c.set('clashoo', 'config', key, previous[key]);
}
c.commit('clashoo');
return { success: false, error: 'runtime_failed', message: '无法提交 sing-box 运行配置' };
}
system('sh /usr/share/clashoo/rpc/rpc_async.sh ' + (running ? 'restart' : 'start') + ' >/dev/null 2>&1');
return { success: true }; return { success: true };
} }
}, },
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-cpu-perf PKG_NAME:=luci-app-cpu-perf
PKG_VERSION:=0.6.2 PKG_VERSION:=0.6.2
PKG_RELEASE:=3 PKG_RELEASE:=4
LUCI_TITLE:=CPU performance information and management for LuCI LUCI_TITLE:=CPU performance information and management for LuCI
LUCI_DEPENDS:=+ucode +ucode-mod-fs LUCI_DEPENDS:=+ucode +ucode-mod-fs
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
+12 -12
View File
@@ -9,31 +9,31 @@ OpenWrt >= 22.03.
**OpenWrt >= 25.12:** **OpenWrt >= 25.12:**
wget --no-check-certificate -O /tmp/luci-app-cpu-perf-0.6.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-cpu-perf-0.6.2-r1.apk wget --no-check-certificate -O /tmp/luci-app-cpu-perf-0.6.2-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-cpu-perf-0.6.2-r2.apk
apk --allow-untrusted add /tmp/luci-app-cpu-perf-0.6.2-r1.apk apk --allow-untrusted add /tmp/luci-app-cpu-perf-0.6.2-r2.apk
rm /tmp/luci-app-cpu-perf-0.6.2-r1.apk rm /tmp/luci-app-cpu-perf-0.6.2-r2.apk
service rpcd restart service rpcd restart
service cpu-perf start service cpu-perf start
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-cpu-perf-ru-0.6.2-r1.apk wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru-0.6.2-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-cpu-perf-ru-0.6.2-r2.apk
apk --allow-untrusted add /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk apk --allow-untrusted add /tmp/luci-i18n-cpu-perf-ru-0.6.2-r2.apk
rm /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk rm /tmp/luci-i18n-cpu-perf-ru-0.6.2-r2.apk
**OpenWrt <= 24.10:** **OpenWrt <= 24.10:**
wget --no-check-certificate -O /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-cpu-perf_0.6.2-r1_all.ipk wget --no-check-certificate -O /tmp/luci-app-cpu-perf_0.6.2-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-cpu-perf_0.6.2-r2_all.ipk
opkg install /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk opkg install /tmp/luci-app-cpu-perf_0.6.2-r2_all.ipk
rm /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk rm /tmp/luci-app-cpu-perf_0.6.2-r2_all.ipk
service rpcd restart service rpcd restart
service cpu-perf start service cpu-perf start
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru_0.6.2-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-cpu-perf-ru_0.6.2-r2_all.ipk
opkg install /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk opkg install /tmp/luci-i18n-cpu-perf-ru_0.6.2-r2_all.ipk
rm /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk rm /tmp/luci-i18n-cpu-perf-ru_0.6.2-r2_all.ipk
## Screenshots: ## Screenshots:
@@ -42,7 +42,9 @@ return view.extend({
} }
}).catch(e => { }).catch(e => {
ui.addNotification(null, ui.addNotification(null,
E('p', _('Failed to get %s init status: %s').format(this.appName, e))); E('p', _('Failed to get %s init status: %s').format(this.appName, e)),
'warning'
);
}); });
}, },
@@ -54,7 +56,9 @@ return view.extend({
return true; return true;
}).catch(e => { }).catch(e => {
ui.addNotification(null, ui.addNotification(null,
E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e))); E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e)),
'warning'
);
}); });
}, },
@@ -390,7 +394,10 @@ return view.extend({
this.callCpuPerf(), this.callCpuPerf(),
uci.load(this.appName), uci.load(this.appName),
]).catch(e => { ]).catch(e => {
ui.addNotification(null, E('p', _('An error has occurred') + ': %s'.format(e.message))); ui.addNotification(null,
E('p', _('An error has occurred') + ': %s'.format(e.message)),
'warning'
);
}); });
}, },
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-disks-info PKG_NAME:=luci-app-disks-info
PKG_VERSION:=0.6.1 PKG_VERSION:=0.6.1
PKG_RELEASE:=2 PKG_RELEASE:=3
LUCI_TITLE:=Information about connected disk devices (partitions, filesystems, SMART). LUCI_TITLE:=Information about connected disk devices (partitions, filesystems, SMART).
LUCI_DEPENDS:=+ucode +ucode-mod-fs +fdisk +smartmontools +smartmontools-drivedb LUCI_DEPENDS:=+ucode +ucode-mod-fs +fdisk +smartmontools +smartmontools-drivedb
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
+12 -12
View File
@@ -10,30 +10,30 @@ OpenWrt >= 22.03.
**OpenWrt >= 25.12:** **OpenWrt >= 25.12:**
apk update apk update
wget --no-check-certificate -O /tmp/luci-app-disks-info-0.6.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-disks-info-0.6.1-r1.apk wget --no-check-certificate -O /tmp/luci-app-disks-info-0.6.1-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-disks-info-0.6.1-r2.apk
apk --allow-untrusted add /tmp/luci-app-disks-info-0.6.1-r1.apk apk --allow-untrusted add /tmp/luci-app-disks-info-0.6.1-r2.apk
rm /tmp/luci-app-disks-info-0.6.1-r1.apk rm /tmp/luci-app-disks-info-0.6.1-r2.apk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-disks-info-ru-0.6.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-disks-info-ru-0.6.1-r1.apk wget --no-check-certificate -O /tmp/luci-i18n-disks-info-ru-0.6.1-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-disks-info-ru-0.6.1-r2.apk
apk --allow-untrusted add /tmp/luci-i18n-disks-info-ru-0.6.1-r1.apk apk --allow-untrusted add /tmp/luci-i18n-disks-info-ru-0.6.1-r2.apk
rm /tmp/luci-i18n-disks-info-ru-0.6.1-r1.apk rm /tmp/luci-i18n-disks-info-ru-0.6.1-r2.apk
**OpenWrt <= 24.10:** **OpenWrt <= 24.10:**
opkg update opkg update
wget --no-check-certificate -O /tmp/luci-app-disks-info_0.6.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-disks-info_0.6.1-r1_all.ipk wget --no-check-certificate -O /tmp/luci-app-disks-info_0.6.1-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-disks-info_0.6.1-r2_all.ipk
opkg install /tmp/luci-app-disks-info_0.6.1-r1_all.ipk opkg install /tmp/luci-app-disks-info_0.6.1-r2_all.ipk
rm /tmp/luci-app-disks-info_0.6.1-r1_all.ipk rm /tmp/luci-app-disks-info_0.6.1-r2_all.ipk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-disks-info-ru_0.6.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-disks-info-ru_0.6.1-r1_all.ipk wget --no-check-certificate -O /tmp/luci-i18n-disks-info-ru_0.6.1-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-disks-info-ru_0.6.1-r2_all.ipk
opkg install /tmp/luci-i18n-disks-info-ru_0.6.1-r1_all.ipk opkg install /tmp/luci-i18n-disks-info-ru_0.6.1-r2_all.ipk
rm /tmp/luci-i18n-disks-info-ru_0.6.1-r1_all.ipk rm /tmp/luci-i18n-disks-info-ru_0.6.1-r2_all.ipk
## Screenshots: ## Screenshots:
@@ -190,7 +190,7 @@ return view.extend({
[ '-l', 'scttempint,' + (pSave ? num + ',p' : num), device ] [ '-l', 'scttempint,' + (pSave ? num + ',p' : num), device ]
).then(res => { ).then(res => {
window.location.reload(); window.location.reload();
}).catch(e => ui.addNotification(null, E('p', {}, e.message))); }).catch(e => ui.addNotification(null, E('p', {}, e.message), 'warning'));
}, },
createDiskTable(fdiskData, dfData) { createDiskTable(fdiskData, dfData) {
@@ -983,7 +983,7 @@ return view.extend({
ui.tabs.initTabGroup(tabsContainer.children); ui.tabs.initTabGroup(tabsContainer.children);
devicesNode.replaceWith(devicesTabs); devicesNode.replaceWith(devicesTabs);
}).catch(e => ui.addNotification(null, E('p', {}, e.message))); }).catch(e => ui.addNotification(null, E('p', {}, e.message), 'warning'));
}; };
return E([ return E([
+2 -2
View File
@@ -281,5 +281,5 @@ msgstr "нет"
msgid "passed" msgid "passed"
msgstr "пройден" msgstr "пройден"
msgid "да" msgid "yes"
msgstr "" msgstr "да"
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-interfaces-statistics PKG_NAME:=luci-app-interfaces-statistics
PKG_VERSION:=0.4.2 PKG_VERSION:=0.4.2
PKG_RELEASE:=1 PKG_RELEASE:=2
LUCI_TITLE:=Network interfaces statistics LUCI_TITLE:=Network interfaces statistics
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_LICENSE:=MIT PKG_LICENSE:=MIT
+12 -12
View File
@@ -7,29 +7,29 @@ OpenWrt >= 21.02.
**OpenWrt >= 25.12:** **OpenWrt >= 25.12:**
wget --no-check-certificate -O /tmp/luci-app-interfaces-statistics-0.4.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-interfaces-statistics-0.4.2-r1.apk wget --no-check-certificate -O /tmp/luci-app-interfaces-statistics-0.4.2-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-interfaces-statistics-0.4.2-r2.apk
apk --allow-untrusted add /tmp/luci-app-interfaces-statistics-0.4.2-r1.apk apk --allow-untrusted add /tmp/luci-app-interfaces-statistics-0.4.2-r2.apk
rm /tmp/luci-app-interfaces-statistics-0.4.2-r1.apk rm /tmp/luci-app-interfaces-statistics-0.4.2-r2.apk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-interfaces-statistics-ru-0.4.2-r1.apk wget --no-check-certificate -O /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r2.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-interfaces-statistics-ru-0.4.2-r2.apk
apk --allow-untrusted add /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r1.apk apk --allow-untrusted add /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r2.apk
rm /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r1.apk rm /tmp/luci-i18n-interfaces-statistics-ru-0.4.2-r2.apk
**OpenWrt <= 24.10:** **OpenWrt <= 24.10:**
wget --no-check-certificate -O /tmp/luci-app-interfaces-statistics_0.4.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-interfaces-statistics_0.4.2-r1_all.ipk wget --no-check-certificate -O /tmp/luci-app-interfaces-statistics_0.4.2-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-interfaces-statistics_0.4.2-r2_all.ipk
opkg install /tmp/luci-app-interfaces-statistics_0.4.2-r1_all.ipk opkg install /tmp/luci-app-interfaces-statistics_0.4.2-r2_all.ipk
rm /tmp/luci-app-interfaces-statistics_0.4.2-r1_all.ipk rm /tmp/luci-app-interfaces-statistics_0.4.2-r2_all.ipk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-interfaces-statistics-ru_0.4.2-r1_all.ipk wget --no-check-certificate -O /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r2_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-interfaces-statistics-ru_0.4.2-r2_all.ipk
opkg install /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r1_all.ipk opkg install /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r2_all.ipk
rm /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r1_all.ipk rm /tmp/luci-i18n-interfaces-statistics-ru_0.4.2-r2_all.ipk
## Screenshots: ## Screenshots:
@@ -59,12 +59,12 @@ return view.extend({
update() { update() {
return this.callNetDevice().then(ifacesData => { return this.callNetDevice().then(ifacesData => {
this.setIfacesData(ifacesData); this.setIfacesData(ifacesData);
}).catch(e => ui.addNotification(null, E('p', {}, e.message))); }).catch(e => ui.addNotification(null, E('p', {}, e.message), 'warning'));
}, },
load() { load() {
return this.callNetDevice().catch( return this.callNetDevice().catch(
e => ui.addNotification(null, E('p', {}, e.message))); e => ui.addNotification(null, E('p', {}, e.message), 'warning'));
}, },
render(ifacesData) { render(ifacesData) {
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-internet-detector PKG_NAME:=luci-app-internet-detector
PKG_VERSION:=1.7.4 PKG_VERSION:=1.7.4
PKG_RELEASE:=4 PKG_RELEASE:=5
LUCI_TITLE:=LuCI support for internet-detector LUCI_TITLE:=LuCI support for internet-detector
LUCI_DEPENDS:=+internet-detector LUCI_DEPENDS:=+internet-detector
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
@@ -227,7 +227,9 @@ return view.extend({
} }
}).catch(e => { }).catch(e => {
ui.addNotification(null, ui.addNotification(null,
E('p', _('Failed to get %s init status: %s').format(this.appName, e))); E('p', _('Failed to get %s init status: %s').format(this.appName, e)),
'warning'
);
}); });
}, },
@@ -239,7 +241,9 @@ return view.extend({
return true; return true;
}).catch(e => { }).catch(e => {
ui.addNotification(null, ui.addNotification(null,
E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e))); E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e)),
'warning'
);
}); });
}, },
@@ -659,8 +663,10 @@ return view.extend({
return this.callback(rc); return this.callback(rc);
}; };
}).catch(e => { }).catch(e => {
ui.addNotification(null, E('p', _('Unable to save the contents') ui.addNotification(null,
+ ': %s'.format(e.message))); E('p', _('Unable to save the contents') + ': %s'.format(e.message)),
'warning'
);
}); });
}, },
@@ -690,8 +696,10 @@ return view.extend({
this.getInit(), this.getInit(),
uci.load(this.appName), uci.load(this.appName),
]).catch(e => { ]).catch(e => {
ui.addNotification( ui.addNotification(null,
null, E('p', _('An error has occurred') + ': %s'.format(e.message))); E('p', _('An error has occurred') + ': %s'.format(e.message)),
'warning'
);
}); });
}, },
@@ -23,7 +23,7 @@ return view.extend({
s.tab('template', _('Edit Template'), _('Edit the template that is used for generating the ksmbd configuration.')); s.tab('template', _('Edit Template'), _('Edit the template that is used for generating the ksmbd configuration.'));
o = s.taboption('general', form.Flag, 'enabled', _('Enable')); o = s.taboption('general', form.Flag, 'enabled', _('Enable'));
o.default = '1'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o = s.taboption('general', form.Value, 'name', _('Hostname')); o = s.taboption('general', form.Value, 'name', _('Hostname'));
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-log-viewer PKG_NAME:=luci-app-log-viewer
PKG_VERSION:=1.5.0 PKG_VERSION:=1.5.0
PKG_RELEASE:=4 PKG_RELEASE:=5
LUCI_TITLE:=Advanced syslog and kernel log (tail, search, etc) LUCI_TITLE:=Advanced syslog and kernel log (tail, search, etc)
LUCI_DEPENDS:=+ucode +ucode-mod-fs LUCI_DEPENDS:=+ucode +ucode-mod-fs
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
+12 -12
View File
@@ -11,29 +11,29 @@ Supported LuCI themes: luci-theme-bootstrap, luci-theme-material, luci-theme-ope
**OpenWrt >= 25.12:** **OpenWrt >= 25.12:**
wget --no-check-certificate -O /tmp/luci-app-log-viewer-1.5.0-r4.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-log-viewer-1.5.0-r4.apk wget --no-check-certificate -O /tmp/luci-app-log-viewer-1.5.0-r5.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-log-viewer-1.5.0-r5.apk
apk --allow-untrusted add /tmp/luci-app-log-viewer-1.5.0-r4.apk apk --allow-untrusted add /tmp/luci-app-log-viewer-1.5.0-r5.apk
rm /tmp/luci-app-log-viewer-1.5.0-r4.apk rm /tmp/luci-app-log-viewer-1.5.0-r5.apk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-log-viewer-ru-1.5.0-r4.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-log-viewer-ru-1.5.0-r4.apk wget --no-check-certificate -O /tmp/luci-i18n-log-viewer-ru-1.5.0-r5.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-log-viewer-ru-1.5.0-r5.apk
apk --allow-untrusted add /tmp/luci-i18n-log-viewer-ru-1.5.0-r4.apk apk --allow-untrusted add /tmp/luci-i18n-log-viewer-ru-1.5.0-r5.apk
rm /tmp/luci-i18n-log-viewer-ru-1.5.0-r4.apk rm /tmp/luci-i18n-log-viewer-ru-1.5.0-r5.apk
**OpenWrt <= 24.10:** **OpenWrt <= 24.10:**
wget --no-check-certificate -O /tmp/luci-app-log-viewer_1.5.0-r4_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-log-viewer_1.5.0-r4_all.ipk wget --no-check-certificate -O /tmp/luci-app-log-viewer_1.5.0-r5_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-log-viewer_1.5.0-r5_all.ipk
opkg install /tmp/luci-app-log-viewer_1.5.0-r4_all.ipk opkg install /tmp/luci-app-log-viewer_1.5.0-r5_all.ipk
rm /tmp/luci-app-log-viewer_1.5.0-r4_all.ipk rm /tmp/luci-app-log-viewer_1.5.0-r5_all.ipk
service rpcd restart service rpcd restart
i18n-ru: i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-log-viewer-ru_1.5.0-r4_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-log-viewer-ru_1.5.0-r4_all.ipk wget --no-check-certificate -O /tmp/luci-i18n-log-viewer-ru_1.5.0-r5_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-log-viewer-ru_1.5.0-r5_all.ipk
opkg install /tmp/luci-i18n-log-viewer-ru_1.5.0-r4_all.ipk opkg install /tmp/luci-i18n-log-viewer-ru_1.5.0-r5_all.ipk
rm /tmp/luci-i18n-log-viewer-ru_1.5.0-r4_all.ipk rm /tmp/luci-i18n-log-viewer-ru_1.5.0-r5_all.ipk
## Screenshots: ## Screenshots:
@@ -772,7 +772,9 @@ return baseclass.extend({
URL.revokeObjectURL(link.href); URL.revokeObjectURL(link.href);
}).catch(err => { }).catch(err => {
ui.addNotification(null, ui.addNotification(null,
E('p', {}, _('Download error') + ': ' + err.message)); E('p', {}, _('Download error') + ': ' + err.message),
'warning'
);
}).finally(() => { }).finally(() => {
this.enableFormElems(); this.enableFormElems();
}); });
@@ -247,7 +247,7 @@ return view.extend({
E('th', {}, [ _('WAN') ]), E('th', {}, [ _('WAN') ]),
E('th', {}, [ _('Local') ]), E('th', {}, [ _('Local') ]),
E('th', {}, [ _('Remote') ]), E('th', {}, [ _('Remote') ]),
E('th', {}, [ _('Backup') ]), E('th', {}, [ _('Backup', 'multipath mode') ]),
E('th', {}, [ _('cwnd') ]), E('th', {}, [ _('cwnd') ]),
E('th', {}, [ _('RTT / var (ms)') ]), E('th', {}, [ _('RTT / var (ms)') ]),
E('th', {}, [ _('Retrans / total') ]), E('th', {}, [ _('Retrans / total') ]),
@@ -116,7 +116,7 @@ return L.view.extend({
// Same option as luci-app-mptcp's main MPTCP page -- one uci value, // Same option as luci-app-mptcp's main MPTCP page -- one uci value,
// editable from either page. The Download column below only has an // editable from either page. The Download column below only has an
// effect while this is enabled (it's what gets pushed to the // effect while this is enabled (it's what gets pushed to the
// gateway as dscp_remote_id); showing it while sync is off would be // gateway as dscp_remote_ids); showing it while sync is off would be
// a dead control, so its visibility is decided from this option's // a dead control, so its visibility is decided from this option's
// *persisted* value at page load, not live -- toggling the box // *persisted* value at page load, not live -- toggling the box
// below needs Save & Apply, then a reload of this page, before the // below needs Save & Apply, then a reload of this page, before the
@@ -149,7 +149,7 @@ return L.view.extend({
if (vpsSyncEnabled) { if (vpsSyncEnabled) {
o = s.option(form.ListValue, 'download_interface', _('Download interface'), o = s.option(form.ListValue, 'download_interface', _('Download interface'),
_('Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler (pins the VPS’s own send-side choice via dscp_remote_id) and can be a different WAN than Upload.') + ' ' + _('Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler (pins the VPS’s own send-side choice to the subflows of every address of that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN than Upload.') + ' ' +
_('Leave empty to just mirror this row’s Upload interface to the gateway instead -- the same "download always follows upload" behavior mqvpn already has built in natively.') + ' ' + _('Leave empty to just mirror this row’s Upload interface to the gateway instead -- the same "download always follows upload" behavior mqvpn already has built in natively.') + ' ' +
_('mqvpn has no equivalent: its downlink always mirrors whichever WAN carries the class on upload, so this field has no effect on mqvpn.')); _('mqvpn has no equivalent: its downlink always mirrors whichever WAN carries the class on upload, so this field has no effect on mqvpn.'));
o.load = function(section_id) { o.load = function(section_id) {
+4 -5
View File
@@ -107,6 +107,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "BPF-Scheduler (nicht auf allen Plattformen verfügbar):" msgstr "BPF-Scheduler (nicht auf allen Plattformen verfügbar):"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "Backup" msgstr "Backup"
@@ -392,12 +393,10 @@ msgstr ""
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"Gateway (VPS) → Router. Wirkt nur für den MPTCP-Scheduler bpf_dscp (legt die"
" sendeseitige Wahl des VPS über dscp_remote_id fest) und kann ein anderes "
"WAN sein als beim Upload."
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
+8 -9
View File
@@ -108,8 +108,9 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "Ordonnanceurs BPF (non disponibles sur toutes les plateformes) :" msgstr "Ordonnanceurs BPF (non disponibles sur toutes les plateformes) :"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "Secours" msgstr "Backup"
#: luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json:47 #: luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json:47
msgid "Bandwidth" msgid "Bandwidth"
@@ -388,12 +389,10 @@ msgstr "Sous-flux Fullmesh pour chaque paire d'adresses IP"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"Passerelle (VPS) → routeur. Ne s'applique qu'à l'ordonnanceur bpf_dscp de "
"MPTCP (fixe le choix d'émission du VPS via dscp_remote_id) et peut être un "
"WAN différent de celui de l'envoi."
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
@@ -918,7 +917,7 @@ msgstr "JOIN de sous-flux accepté par le pair (SYN/ACK)"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:33 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:33
msgid "Subflow JOIN accepted by peer (SYN/ACK, backup)" msgid "Subflow JOIN accepted by peer (SYN/ACK, backup)"
msgstr "JOIN de sous-flux accepté par le pair (SYN/ACK, secours)" msgstr "JOIN de sous-flux accepté par le pair (SYN/ACK, backup)"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:30 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:30
msgid "Subflow JOIN attempts received (SYN)" msgid "Subflow JOIN attempts received (SYN)"
@@ -926,7 +925,7 @@ msgstr "Tentatives de JOIN de sous-flux reçues (SYN)"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:31 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:31
msgid "Subflow JOIN attempts received (SYN, backup)" msgid "Subflow JOIN attempts received (SYN, backup)"
msgstr "Tentatives de JOIN de sous-flux reçues (SYN, secours)" msgstr "Tentatives de JOIN de sous-flux reçues (SYN, backup)"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:26 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:26
msgid "Subflow JOIN attempts sent (SYN)" msgid "Subflow JOIN attempts sent (SYN)"
@@ -983,7 +982,7 @@ msgstr ""
"Nombre d'intervalles de retransmission au niveau MPTCP sans trafic et avec " "Nombre d'intervalles de retransmission au niveau MPTCP sans trafic et avec "
"des données en attente sur un sous-flux donné, à partir duquel celui-ci est " "des données en attente sur un sous-flux donné, à partir duquel celui-ci est "
"déclaré périmé. Une valeur faible de stale_loss_cnt permet une bascule " "déclaré périmé. Une valeur faible de stale_loss_cnt permet une bascule "
"rapide entre actif et secours ; une valeur élevée maximise l'utilisation des" "rapide entre actif et backup ; une valeur élevée maximise l'utilisation des"
" liens dans les cas limites, par exemple une liaison avec pertes et un fort " " liens dans les cas limites, par exemple une liaison avec pertes et un fort "
"taux d'erreur binaire, ou un pair qui suspend le traitement des données." "taux d'erreur binaire, ou un pair qui suspend le traitement des données."
+4 -5
View File
@@ -108,6 +108,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "Scheduler BPF (non disponibili su tutte le piattaforme):" msgstr "Scheduler BPF (non disponibili su tutte le piattaforme):"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "Backup" msgstr "Backup"
@@ -389,12 +390,10 @@ msgstr "Flussi secondari fullmesh per ogni coppia di indirizzi IP"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"Gateway (VPS) → router. Ha effetto solo per lo scheduler bpf_dscp di MPTCP "
"(fissa la scelta di invio del VPS tramite dscp_remote_id) e può essere una "
"WAN diversa da quella di upload."
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
+4 -5
View File
@@ -104,6 +104,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "Ordonançadors BPF (pas disponibles sus totas las plataformas) :" msgstr "Ordonançadors BPF (pas disponibles sus totas las plataformas) :"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "Secors" msgstr "Secors"
@@ -385,12 +386,10 @@ msgstr "Jos flux Fullmesh per cada parelh d’adreças IP"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"Palanca (VPS) → router. S'aplica solament a l'ordonançador bpf_dscp de MPTCP"
" (fixa la causida de mandadís del VPS via dscp_remote_id) e pòt èsser un WAN"
" diferent de lo del mandadís."
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
+4 -5
View File
@@ -105,6 +105,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "Планировщики BPF (доступны не на всех платформах):" msgstr "Планировщики BPF (доступны не на всех платформах):"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "Резерв" msgstr "Резерв"
@@ -384,12 +385,10 @@ msgstr "Подпотоки Fullmesh для каждой пары IP-адресо
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"Шлюз (VPS) → маршрутизатор. Действует только для планировщика bpf_dscp в "
"MPTCP (закрепляет выбор передающей стороны VPS через dscp_remote_id) и может"
" отличаться от WAN, выбранного для передачи."
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
+4 -2
View File
@@ -101,6 +101,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "" msgstr ""
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "" msgstr ""
@@ -372,8 +373,9 @@ msgstr ""
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
+4 -4
View File
@@ -101,6 +101,7 @@ msgid "BPF schedulers (not available on all platforms):"
msgstr "BPF 调度器(并非所有平台都可用):" msgstr "BPF 调度器(并非所有平台都可用):"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_diagnostics.js:250
msgctxt "multipath mode"
msgid "Backup" msgid "Backup"
msgstr "备用" msgstr "备用"
@@ -374,11 +375,10 @@ msgstr "每对IP地址的全网格子流"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:152
msgid "" msgid ""
"Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler " "Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler "
"(pins the VPS’s own send-side choice via dscp_remote_id) and can be a " "(pins the VPS’s own send-side choice to the subflows of every address of "
"different WAN than Upload." "that WAN, IPv4 and IPv6, via dscp_remote_ids) and can be a different WAN "
"than Upload."
msgstr "" msgstr ""
"网关 (VPS) → 路由器。仅对 MPTCP 的 bpf_dscp 调度器生效(通过 dscp_remote_id 固定 VPS "
"自身的发送侧选择),可以与上传所用的 WAN 不同。"
#: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124 #: luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_dscp_routing.js:124
msgid "Gateway sync" msgid "Gateway sync"
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_MAINTAINER:=nosignal <https://github.com/nosignals/neko> PKG_MAINTAINER:=nosignal <https://github.com/nosignals/neko>
PKG_NAME:=luci-app-neko PKG_NAME:=luci-app-neko
PKG_VERSION:=1.2.4-beta PKG_VERSION:=1.2.4-beta
PKG_RELEASE:=3 PKG_RELEASE:=5
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
+455
View File
@@ -0,0 +1,455 @@
<?php
/**
* MIT License
*
* Copyright (c) 2024 Nosignal <https://github.com/nosignals>
*
* Contributors:
* - bobbyunknown <https://github.com/bobbyunknown>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
include './cfg.php';
include './devinfo.php';
$str_cfg=substr($selected_config, strlen("$neko_dir/config")+1);
$core_mode = exec("uci -q get neko.cfg.core_mode");
if(isset($_POST['neko'])){
$dt = $_POST['neko'];
if ($core_mode == 'mihomo') {
if ($dt == 'start') shell_exec("$neko_dir/core/neko -s");
if ($dt == 'disable') shell_exec("$neko_dir/core/neko -k");
if ($dt == 'restart') shell_exec("$neko_dir/core/neko -r");
if ($dt == 'clear') {
shell_exec("echo 'Log cleared...' > $neko_dir/tmp/log.txt");
shell_exec("echo 'Log cleared...' > $neko_dir/tmp/neko_log.txt");
}
} elseif ($core_mode == 'singbox') {
if ($dt == 'start') shell_exec("$neko_dir/core/singbox -s");
if ($dt == 'disable') shell_exec("$neko_dir/core/singbox -k");
if ($dt == 'restart') shell_exec("$neko_dir/core/singbox -r");
if ($dt == 'clear') {
shell_exec("echo 'Log cleared...' > $neko_dir/tmp/log.txt");
shell_exec("echo 'Log cleared...' > $neko_dir/tmp/singbox_log.txt");
}
}
}
$neko_status=exec("uci -q get neko.cfg.enabled");
if ($core_mode == 'mihomo') {
$binary_log = "$neko_dir/tmp/mihomo_log.txt";
} elseif ($core_mode == 'singbox') {
$binary_log = "$neko_dir/tmp/singbox_log.txt";
}
if ($core_mode == 'mihomo') {
} elseif ($core_mode == 'singbox') {
}
?>
<?php
include './header.php';
include './navbar.php';
?>
<div class="container p-3">
<div class="card">
<div class="card-header d-flex align-items-center">
<i data-feather="home" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Neko Home</h5>
</div>
<div class="card-body">
<!-- core mode -->
<div class="card mb-4">
<div class="card-body p-3">
<div class="d-flex align-items-center justify-content-center">
<?php if($core_mode == 'mihomo'): ?>
<i data-feather="box" class="feather-lg text-primary me-3" style="width: 32px; height: 32px;"></i>
<?php else: ?>
<i data-feather="cpu" class="feather-lg text-success me-3" style="width: 32px; height: 32px;"></i>
<?php endif; ?>
<div class="text-center">
<div class="small text-muted mb-1">Active Core</div>
<h5 class="mb-0 fw-bold <?php echo ($core_mode == 'mihomo') ? 'text-primary' : 'text-success'; ?>">
<?php echo strtoupper($core_mode); ?>
</h5>
</div>
</div>
</div>
</div>
<!-- System Information Card -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center">
<i data-feather="server" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">System Information</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php
$show_ip = exec("uci -q get neko.cfg.show_ip");
$show_isp = exec("uci -q get neko.cfg.show_isp");
if($show_ip == '1') {
?>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="globe" class="feather-sm me-2"></i>
<span>IP Address</span>
</div>
<span id="ip-address">Loading...</span>
</div>
<?php
}
if($show_isp == '1') {
?>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="wifi" class="feather-sm me-2"></i>
<span>ISP</span>
</div>
<span id="isp-info">Loading...</span>
</div>
<?php
}
?>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="smartphone" class="feather-sm me-2"></i>
<span>Devices</span>
</div>
<span><?php echo $devices ?></span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="cpu" class="feather-sm me-2"></i>
<span>RAM</span>
</div>
<span><?php echo "$ramUsage/$ramTotal MB" ?></span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="hard-drive" class="feather-sm me-2"></i>
<span>OS Version</span>
</div>
<span><?php echo $OSVer ?></span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="code" class="feather-sm me-2"></i>
<span>Kernel Version</span>
</div>
<span><?php echo $kernelv ?></span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center border-bottom">
<div class="d-flex align-items-center">
<i data-feather="clock" class="feather-sm me-2"></i>
<span>Uptime</span>
</div>
<span><?php echo "$hours h $minutes m $seconds s"?></span>
</div>
</div>
</div>
</div>
<!-- Neko Status Card -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center">
<i data-feather="box" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Neko</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<i data-feather="activity" class="feather-sm me-2"></i>
<span>Status</span>
</div>
<div class="d-grid">
<div class="btn-group col" role="group">
<?php
if($neko_status==1) echo "<button type=\"button\" class=\"btn btn-success\">RUNNING</button>\n";
else echo "<button type=\"button\" class=\"btn btn-outline-primary\">DISABLED</button>\n";
echo "<button type=\"button\" class=\"btn btn-warning\">$str_cfg</button>\n";
?>
</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<i data-feather="toggle-right" class="feather-sm me-2"></i>
<span>Control</span>
</div>
<form action="index.php" method="post">
<div class="d-grid">
<div class="btn-group col" role="group">
<?php
if ($core_mode == 'mihomo') {
echo '<button type="submit" name="neko" value="start" class="btn btn' . ($neko_status == 1 ? '-outline' : '') . '-success ' . ($neko_status == 1 ? 'disabled' : '') . '">
<i data-feather="play" class="feather-sm"></i> Enable Mihomo
</button>';
echo '<button type="submit" name="neko" value="disable" class="btn btn' . ($neko_status == 0 ? '-outline' : '') . '-primary ' . ($neko_status == 0 ? 'disabled' : '') . '">
<i data-feather="stop-circle" class="feather-sm"></i> Disable Mihomo
</button>';
echo '<button type="submit" name="neko" value="restart" class="btn btn' . ($neko_status == 0 ? '-outline' : '') . '-warning ' . ($neko_status == 0 ? 'disabled' : '') . '">
<i data-feather="refresh-cw" class="feather-sm"></i> Restart Mihomo
</button>';
} elseif ($core_mode == 'singbox') {
echo '<button type="submit" name="neko" value="start" class="btn btn' . ($neko_status == 1 ? '-outline' : '') . '-success ' . ($neko_status == 1 ? 'disabled' : '') . '">
<i data-feather="play" class="feather-sm"></i> Enable Singbox
</button>';
echo '<button type="submit" name="neko" value="disable" class="btn btn' . ($neko_status == 0 ? '-outline' : '') . '-primary ' . ($neko_status == 0 ? 'disabled' : '') . '">
<i data-feather="stop-circle" class="feather-sm"></i> Disable Singbox
</button>';
echo '<button type="submit" name="neko" value="restart" class="btn btn' . ($neko_status == 0 ? '-outline' : '') . '-warning ' . ($neko_status == 0 ? 'disabled' : '') . '">
<i data-feather="refresh-cw" class="feather-sm"></i> Restart Singbox
</button>';
}
?>
</div>
</div>
</form>
</div>
<div>
<div class="d-flex align-items-center mb-2">
<i data-feather="settings" class="feather-sm me-2"></i>
<span>Running Mode</span>
</div>
<input class="form-control text-center" name="mode" type="text" placeholder="<?php
if ($core_mode == 'mihomo') {
echo $neko_cfg['echanced']." | ".$neko_cfg['mode'];
} else if ($core_mode == 'singbox') {
echo "SING-BOX | TUN";
}
?>" disabled>
</div>
</div>
</div>
<!-- Statistics Card -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center">
<i data-feather="bar-chart-2" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Traffic Statistics</h5>
</div>
<div class="card-body">
<!-- Statistik di tengah -->
<div class="row justify-content-center text-center mb-4">
<div class="col-md-6 col-xl-4">
<h2 class="mb-2"><span id="downtotal">-</span></h2>
<div class="text-muted">Download</div>
</div>
<div class="col-md-6 col-xl-4">
<h2 class="mb-2"><span id="uptotal">-</span></h2>
<div class="text-muted">Upload</div>
</div>
</div>
<!-- Chart dengan ukuran yang lebih kecil -->
<div class="chart" style="height: 150px;">
<canvas id="trafficChart"></canvas>
</div>
</div>
</div>
<!-- Logs Card -->
<div class="accordion mb-4">
<div class="accordion-item">
<div class="accordion-header" id="logsHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#logsCollapse">
<h5 class="mb-0">Neko Log</h5>
</button>
</div>
<div id="logsCollapse" class="accordion-collapse collapse" data-bs-parent="#logsHeader">
<div class="accordion-body">
<textarea class="form-control mb-3" id="logs" rows="10" readonly></textarea>
<h5 class="mb-3">Binary Log</h5>
<textarea class="form-control mb-3" id="bin_logs" rows="10" readonly></textarea>
<form action="index.php" method="post">
<button type="submit" name="neko" value="clear" class="btn btn-primary w-100">
Clear Log
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function formatBytes(bytes) {
if (bytes < 1024000) return (bytes/1024).toFixed(1) + " KB";
if (bytes < 1024000000) return (bytes/1024000).toFixed(1) + " MB";
return (bytes/1024000000).toFixed(2) + " GB";
}
let labels = [];
let downloadData = [];
let uploadData = [];
let trafficChart;
function initChart() {
const ctx = document.getElementById('trafficChart');
if (!ctx) return;
trafficChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Download',
data: downloadData,
borderColor: '#3B7DDD',
borderWidth: 2,
fill: false,
tension: 0.3,
pointRadius: 0
}, {
label: 'Upload',
data: uploadData,
borderColor: '#28A745',
borderWidth: 2,
fill: false,
tension: 0.3,
pointRadius: 0
}]
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
beginAtZero: true,
grid: {
borderDash: [2, 2]
}
}
}
}
});
}
function updateLogs() {
const logsCollapse = document.getElementById('logsCollapse');
if (!logsCollapse.classList.contains('show')) return;
fetch('./lib/log.php?data=neko')
.then(response => response.text())
.then(data => {
const logs = document.getElementById('logs');
if (logs) {
logs.value = data;
logs.scrollTop = logs.scrollHeight;
}
});
fetch('./lib/log.php?data=bin')
.then(response => response.text())
.then(data => {
const binLogs = document.getElementById('bin_logs');
if (binLogs) {
binLogs.value = data;
binLogs.scrollTop = binLogs.scrollHeight;
}
});
}
function updateStats() {
fetch('./lib/up.php')
.then(response => response.text())
.then(upResult => {
const uptotal = document.getElementById('uptotal');
if (uptotal) uptotal.textContent = upResult;
fetch('./lib/down.php')
.then(response => response.text())
.then(downResult => {
const downtotal = document.getElementById('downtotal');
if (downtotal) downtotal.textContent = downResult;
if (trafficChart) {
const now = new Date();
const timeStr = now.getHours() + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
labels.push(timeStr);
downloadData.push(parseFloat(downResult));
uploadData.push(parseFloat(upResult));
if (labels.length > 10) {
labels.shift();
downloadData.shift();
uploadData.shift();
}
trafficChart.update();
}
});
});
}
document.addEventListener('DOMContentLoaded', function() {
initChart();
setInterval(updateLogs, 1000);
setInterval(updateStats, 1000);
document.getElementById('logsCollapse').addEventListener('shown.bs.collapse', updateLogs);
});
document.addEventListener('DOMContentLoaded', function() {
<?php if($show_ip == '1' || $show_isp == '1') { ?>
fetch('http://ip-api.com/json/')
.then(response => response.json())
.then(data => {
<?php if($show_ip == '1') { ?>
document.getElementById('ip-address').textContent = data.query;
<?php } ?>
<?php if($show_isp == '1') { ?>
document.getElementById('isp-info').textContent = data.isp;
<?php } ?>
})
.catch(error => {
<?php if($show_ip == '1') { ?>
document.getElementById('ip-address').textContent = 'Failed to load';
<?php } ?>
<?php if($show_isp == '1') { ?>
document.getElementById('isp-info').textContent = 'Failed to load';
<?php } ?>
});
<?php } ?>
});
</script>
<?php include './footer.php'; ?>
+280
View File
@@ -0,0 +1,280 @@
<?php
/**
* MIT License
*
* Copyright (c) 2024 Nosignal <https://github.com/nosignals>
*
* Contributors:
* - bobbyunknown <https://github.com/bobbyunknown>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
include './cfg.php';
$themeDir = "$neko_www/assets/theme";
$arrFiles = array();
$arrFiles = glob("$themeDir/*.css");
$neko_version = exec("opkg list-installed | grep luci-app-neko | cut -d ' - ' -f3");
$neko_latest = exec("curl -m 5 -f -s https://raw.githubusercontent.com/nosignals/openwrt-neko/main/luci-app-neko/Makefile | grep PKG_VERSION: | cut -d= -f2");
$stat = 0;
if ($neko_version == $neko_latest){
$stat = 0;
}
else {
$stat = 1;
}
for($x=0;$x<count($arrFiles);$x++) $arrFiles[$x] = substr($arrFiles[$x], strlen($themeDir)+1);
if(isset($_POST['themechange'])){
$dt = $_POST['themechange'];
shell_exec("uci set neko.cfg.theme='$dt' && uci commit neko");
$neko_theme = $dt;
}
if(isset($_POST['fw'])){
$dt = $_POST['fw'];
if ($dt == 'enable') shell_exec("uci set neko.cfg.new_interface='1' && uci commit neko");
if ($dt == 'disable') shell_exec("uci set neko.cfg.new_interface='0' && uci commit neko");
}
if(isset($_POST['neko'])){
$dt = $_POST['neko'];
if ($dt == 'update'){
updateNeko();
}
}
if(isset($_POST['core_mode'])){
$dt = $_POST['core_mode'];
shell_exec("uci set neko.cfg.core_mode='$dt' && uci commit neko");
$core_mode = $dt;
}
if(isset($_POST['show_ip'])){
$show_ip = $_POST['show_ip'];
shell_exec("uci set neko.cfg.show_ip='$show_ip' && uci commit neko");
}
if(isset($_POST['show_isp'])){
$show_isp = $_POST['show_isp'];
shell_exec("uci set neko.cfg.show_isp='$show_isp' && uci commit neko");
}
if(isset($_POST['show_luci'])){
$show_luci = $_POST['show_luci'];
shell_exec("uci set neko.cfg.show_luci='$show_luci' && uci commit neko");
}
function updateNeko(){
$neko_latest = exec("curl -m 5 -f -s https://raw.githubusercontent.com/nosignals/openwrt-neko/main/luci-app-neko/Makefile | grep PKG_VERSION: | cut -d= -f2");
if(!empty($neko_latest)){
$url_update = "https://github.com/nosignals/openwrt-neko/releases/download/luci-app-neko_".$neko_latest."/luci-app-neko_".$neko_latest."_all.ipk";
$str_update = <<<EOF
#/bin/bash
wget -O /tmp/neko.ipk $url_update
cd /tmp
opkg remove luci-app-neko
opkg install neko.ipk
rm -r /tmp/neko.ipk
EOF;
echo "<h1>UPDATING NEKO TO VERSION ".$neko_latest."</br>";
echo "DONT CLOSE THIS TAB</br></h1>";
echo "if in 30s not showing notification, you can try again update";
file_put_contents('/tmp/neko_update', $str_update);
exec("chmod +x /tmp/neko_update");
shell_exec("/tmp/neko_update");
echo "<h1>Done Updating, Please reload this tab</h1>";
shell_exec("rm /tmp/neko_update");
}
else{
echo "<h1>Check your Internet Connection!!!.</h1>";
}
}
$fwstatus=shell_exec("uci get neko.cfg.new_interface");
?>
<?php include './header.php'; ?>
<?php include './navbar.php'; ?>
<div class="container p-3">
<div class="card">
<div class="card-header d-flex align-items-center">
<i data-feather="settings" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Core Settings</h5>
</div>
<div class="card-body">
<?php
$neko_status = exec("uci -q get neko.cfg.enabled");
$current_core = exec("uci -q get neko.cfg.core_mode");
?>
<?php if($neko_status == '1'): ?>
<div class="alert alert-info">
<i data-feather="info" class="feather-sm me-2"></i>
Core <?php echo strtoupper($current_core); ?> is currently running. Please stop the service first to change the core.
</div>
<?php endif; ?>
<form action="settings.php" method="post">
<div class="mb-3">
<label for="core_mode" class="form-label">Select Core Mode:</label>
<select name="core_mode" id="core_mode" class="form-select" <?php if($neko_status == '1') echo 'disabled'; ?>>
<option value="mihomo" <?php if($current_core == 'mihomo') echo 'selected'; ?>>Core Mihomo</option>
<option value="singbox" <?php if($current_core == 'singbox') echo 'selected'; ?>>Core Singbox</option>
</select>
</div>
<button type="submit" class="btn btn-primary" <?php if($neko_status == '1') echo 'disabled'; ?>>Save</button>
</form>
</div>
</div>
</div>
<div class="container p-3">
<div class="card">
<div class="card-header d-flex align-items-center">
<i data-feather="settings" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Neko Settings</h5>
</div>
<div class="card-body">
<?php
$show_ip = exec("uci -q get neko.cfg.show_ip");
$show_isp = exec("uci -q get neko.cfg.show_isp");
$show_luci = exec("uci -q get neko.cfg.show_luci");
?>
<form action="settings.php" method="post">
<div class="mb-3">
<label class="form-label">Show IP Address:</label>
<select name="show_ip" class="form-select">
<option value="1" <?php if($show_ip == '1') echo 'selected'; ?>>Enable</option>
<option value="0" <?php if($show_ip == '0') echo 'selected'; ?>>Disable</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Show ISP:</label>
<select name="show_isp" class="form-select">
<option value="1" <?php if($show_isp == '1') echo 'selected'; ?>>Enable</option>
<option value="0" <?php if($show_isp == '0') echo 'selected'; ?>>Disable</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Show Neko on LuCI:</label>
<select name="show_luci" class="form-select">
<option value="1" <?php if($show_luci == '1') echo 'selected'; ?>>Enable</option>
<option value="0" <?php if($show_luci == '0') echo 'selected'; ?>>Disable</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
</div>
<div class="container p-3">
<div class="card">
<div class="card-header d-flex align-items-center">
<i data-feather="info" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">Software Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless mb-3">
<tbody>
<tr>
<td class="col-2">Auto Reload Firewall</td>
<form action="settings.php" method="post">
<td class="d-grid">
<div class="btn-group col" role="group" aria-label="ctrl">
<button type="submit" name="fw" value="enable" class="btn btn<?php if($fwstatus==1) echo "-outline" ?>-success <?php if($fwstatus==1) echo "disabled" ?> d-grid">Enable</button>
<button type="submit" name="fw" value="disable" class="btn btn<?php if($fwstatus==0) echo "-outline" ?>-warning <?php if($fwstatus==0) echo "disabled" ?> d-grid">Disable</button>
</div>
</td>
</form>
</tr>
<tr>
<td class="col-1">Client Version</td>
<td class="col-4">
<div class="form-control text-center" id="cliver">-</div>
</td>
<td class="col-1">
<form action="settings.php" method="post">
<button type="submit" name="neko" value="update" class="btn btn-primary <?php if($stat==0) echo "disabled " ?>col-10">Update</button>
</form>
</td>
</tr>
<tr>
<td class="col-1">Core Version</td>
<td class="col-4">
<div class="form-control text-center" id="corever">-</div>
</td>
<td class="col-1">
<a class="btn btn-primary col-10" target="_blank" href="https://github.com/nosignals/openwrt-neko/releases">Update</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="container p-3">
<div class="card">
<div class="card-header d-flex align-items-center">
<i data-feather="info" class="feather-sm me-2"></i>
<h5 class="card-title mb-0">About</h5>
</div>
<div class="card-body">
<div class="text-center">
<h5 class="mb-3">NekoClash</h5>
<p>NekoClash is a family friendly Clash Proxy tool, this tool makes it easy for users to use Clash Proxy, and User can modify your own Theme based Bootstrap, inspired by OpenClash Tools. NekoClash has writen by PHP, and BASH.</p>
<p>This tool aims to make it easier to use Clash Proxy</p>
<p>If you have questions or feedback about NekoClash you can contact me on the <span class="badge bg-indigo"><b>DBAI Discord Server</b></span> link below</p>
<h5 class="mb-3">External Links</h5>
<div class="container mb-4">
<div class="row g-3 justify-content-center">
<div class="col-md-6">
<div class="d-grid">
<a class="btn btn-outline-info" target="_blank" href="https://discord.gg/vtV5QSq6D6">
<i data-feather="message-circle" class="feather-sm me-2"></i>
DBAI Discord
</a>
</div>
</div>
<div class="col-md-6">
<div class="d-grid">
<a class="btn btn-outline-info" target="_blank" href="https://github.com/nosignals">
<i data-feather="github" class="feather-sm me-2"></i>
Github
</a>
</div>
</div>
<div class="col-md-6">
<div class="d-grid">
<a class="btn btn-outline-info" target="_blank" href="https://www.facebook.com/groups/indowrt">
<i data-feather="facebook" class="feather-sm me-2"></i>
indoWRT Group
</a>
</div>
</div>
<div class="col-md-6">
<div class="d-grid">
<a class="btn btn-outline-info" target="_blank" href="https://github.com/MetaCubeX/mihomo">
<i data-feather="box" class="feather-sm me-2"></i>
Mihomo
</a>
</div>
</div>
</div>
</div>
<p class="mb-0">Please don't <b>CHANGE</b> or <b>REMOVE</b> this Credit!.</p>
</div>
</div>
</div>
</div>
<?php include './footer.php'; ?>
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_MAINTAINER:=Thaolga <https://github.com/Thaolga/luci-app-nekobox> PKG_MAINTAINER:=Thaolga <https://github.com/Thaolga/luci-app-nekobox>
PKG_NAME:=luci-app-nekobox PKG_NAME:=luci-app-nekobox
PKG_VERSION:=2.0.9 PKG_VERSION:=2.0.9
PKG_RELEASE:=6 PKG_RELEASE:=8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.303 PKG_VERSION:=0.1.306
PKG_RELEASE:=19 PKG_RELEASE:=20
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+2 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.4 PKG_VERSION:=26.10.4
PKG_RELEASE:=323 PKG_RELEASE:=325
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -66,6 +66,7 @@ config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy
select PACKAGE_iptables select PACKAGE_iptables
select PACKAGE_iptables-zz-legacy select PACKAGE_iptables-zz-legacy
select PACKAGE_iptables-mod-conntrack-extra select PACKAGE_iptables-mod-conntrack-extra
select PACKAGE_iptables-mod-extra
select PACKAGE_iptables-mod-iprange select PACKAGE_iptables-mod-iprange
select PACKAGE_iptables-mod-socket select PACKAGE_iptables-mod-socket
select PACKAGE_iptables-mod-tproxy select PACKAGE_iptables-mod-tproxy
+83 -6
View File
@@ -4,6 +4,7 @@ c_config = "passwall"
s_config = "passwall_server" s_config = "passwall_server"
local com = require "luci.passwall.com" local com = require "luci.passwall.com"
nixio = require "nixio" nixio = require "nixio"
require "nixio.util"
fs = require "nixio.fs" fs = require "nixio.fs"
sys = require "luci.sys" sys = require "luci.sys"
uci = require "luci.model.uci".cursor() uci = require "luci.model.uci".cursor()
@@ -14,6 +15,7 @@ i18n = require "luci.i18n"
curl_args = { "-skfL", "--connect-timeout 3", "--retry 3", "-H 'Accept: */*'" } curl_args = { "-skfL", "--connect-timeout 3", "--retry 3", "-H 'Accept: */*'" }
command_timeout = 300 command_timeout = 300
DIRECT_GID = 9911
OPENWRT_ARCH = nil OPENWRT_ARCH = nil
DISTRIB_ARCH = nil DISTRIB_ARCH = nil
OPENWRT_BOARD = nil OPENWRT_BOARD = nil
@@ -195,6 +197,30 @@ function exec_call(cmd)
return tonumber(rc), trim(out) return tonumber(rc), trim(out)
end end
function exec_call_as_gid(cmd, gid)
local reader, writer = nixio.pipe()
if not reader then return 255, "Unable to create command pipe" end
local pid = nixio.fork()
if not pid then
reader:close()
writer:close()
return 255, "Unable to fork command"
elseif pid == 0 then
reader:close()
nixio.dup(writer, nixio.stdout)
nixio.dup(writer, nixio.stderr)
writer:close()
if not nixio.setgid(gid) then os.exit(1) end
nixio.exec("/bin/sh", "-c", cmd)
os.exit(127)
end
writer:close()
local out = reader:readall() or ""
reader:close()
local _, status, code = nixio.waitpid(pid)
return status == "exited" and code or 255, trim(out)
end
function base64Decode(text) function base64Decode(text)
if type(text) ~= "string" then return "" end if type(text) ~= "string" then return "" end
local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+"):gsub("=+$", "") local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+"):gsub("=+$", "")
@@ -261,12 +287,38 @@ function get_domain_port_from_url(url)
return domain, port return domain, port
end end
function curl_base(url, file, args) function resolve_domain(domain, dns)
dns = dns or get_cache_var("LOCAL_DNS") or "223.5.5.5"
local address, port = dns:match("^(.-)#(%d+)$")
if not address then address, port = dns:match("^%[(.-)%]:(%d+)$") end
if not address then address, port = dns:match("^([^:]+):(%d+)$") end
address = (address or dns):gsub("^%[(.-)%]$", "%1")
port = port or "53"
local server = datatypes.ip6addr(address) and ("[" .. address .. "]") or address
local rc, result
for _, dns_server in ipairs({server .. ":" .. port, address .. "#" .. port, "223.5.5.5"}) do
rc, result = exec_call_as_gid("LC_ALL=C nslookup " .. util.shellquote(domain) .. " " .. util.shellquote(dns_server), DIRECT_GID)
local ips, seen, answer = {}, {}, false
for line in result:gmatch("[^\r\n]+") do
if line:match("^%s*Name:") then answer = true end
local ip = answer and line:match("^%s*Address%s*%d*:%s*(%S+)")
if ip and (datatypes.ip4addr(ip) or datatypes.ip6addr(ip)) and not seen[ip] then
ips[#ips + 1] = ip
seen[ip] = true
end
end
if #ips > 0 then return ips end
end
return nil, result, rc
end
function curl_base(url, file, args, gid)
if not args then args = {} end if not args then args = {} end
if file then if file then
args[#args + 1] = "-o " .. file args[#args + 1] = "-o " .. file
end end
local cmd = string.format('curl %s "%s"', table_join(args), url) local cmd = string.format('curl %s "%s"', table_join(args), url)
if gid then return exec_call_as_gid(cmd, gid) end
return exec_call(cmd) return exec_call(cmd)
end end
@@ -285,11 +337,36 @@ end
function curl_logic(url, file, args) function curl_logic(url, file, args)
local return_code, result = curl_proxy(url, file, args) local return_code, result = curl_proxy(url, file, args)
if not return_code or return_code ~= 0 then if not return_code or return_code ~= 0 then
return_code, result = curl_base(url, file, args) return_code, result = curl_direct(url, file, args)
end end
return return_code, result return return_code, result
end end
function curl_direct(url, file, args)
if get_cache_var("ENABLED_DEFAULT_ACL") ~= "1" and get_cache_var("ENABLED_ACLS") ~= "1" then
return curl_base(url, file, args)
end
local tmp_args = clone(args or {})
tmp_args[#tmp_args + 1] = "--noproxy '*'"
local domain, port = get_domain_port_from_url(url)
if domain then
local ips, err = resolve_domain(domain)
if not ips then return 6, err end
for i, ip in ipairs(ips) do
if datatypes.ip6addr(ip) then ips[i] = "[" .. ip .. "]" end
end
tmp_args[#tmp_args + 1] = "--resolve " .. util.shellquote(domain .. ":" .. port .. ":" .. table.concat(ips, ","))
end
return curl_base(url, file, tmp_args, DIRECT_GID)
end
function curl_auto(url, file, args)
if (uci_get_c("@global[0]", "localhost_proxy") or "1") == "1" then
return curl_base(url, file, args)
end
return curl_logic(url, file, args)
end
function url(...) function url(...)
local url = string.format("admin/services/%s", appname) local url = string.format("admin/services/%s", appname)
local args = { ... } local args = { ... }
@@ -1103,9 +1180,9 @@ local function get_api_json(url)
local return_code, content local return_code, content
if gh_proxy ~= "" then if gh_proxy ~= "" then
url = gh_proxy .. url url = gh_proxy .. url
return_code, content = curl_base(url, nil, curl_args) return_code, content = curl_direct(url, nil, curl_args)
else else
return_code, content = curl_logic(url, nil, curl_args) return_code, content = curl_auto(url, nil, curl_args)
end end
if return_code ~= 0 or content == "" then return {} end if return_code ~= 0 or content == "" then return {} end
return jsonc.parse(content) or {} return jsonc.parse(content) or {}
@@ -1233,9 +1310,9 @@ function to_download(app_name, url, size, task_id, keep_files)
local return_code, result local return_code, result
if gh_proxy ~= "" then if gh_proxy ~= "" then
url = gh_proxy .. url url = gh_proxy .. url
return_code, result = curl_base(url, tmp_file, _curl_args) return_code, result = curl_direct(url, tmp_file, _curl_args)
else else
return_code, result = curl_logic(url, tmp_file, _curl_args) return_code, result = curl_auto(url, tmp_file, _curl_args)
end end
result = return_code == 0 result = return_code == 0
+1 -1
View File
@@ -3,7 +3,7 @@
START=99 START=99
STOP=15 STOP=15
if [ -z "$IPKG_INSTROOT" ]; then if [ -z "$IPKG_INSTROOT" ] && [ -f /usr/share/passwall/utils.sh ]; then
. /usr/share/passwall/utils.sh . /usr/share/passwall/utils.sh
APP_FILE=${APP_PATH}/app.sh APP_FILE=${APP_PATH}/app.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock
@@ -51,7 +51,7 @@ check_run_environment() {
[ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; } [ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; }
if [ "$USE_TABLES" = "iptables" ]; then if [ "$USE_TABLES" = "iptables" ]; then
dep_list="iptables-mod-tproxy iptables-mod-socket iptables-mod-iprange iptables-mod-conntrack-extra kmod-ipt-nat" dep_list="iptables-mod-tproxy iptables-mod-socket iptables-mod-iprange iptables-mod-conntrack-extra iptables-mod-extra kmod-ipt-nat"
else else
dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat kmod-nf-reject kmod-nf-reject6" dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat kmod-nf-reject kmod-nf-reject6"
nftflag=1 nftflag=1
@@ -150,9 +150,8 @@ run_singbox() {
direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}') direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}')
json_add_string "direct_dns_tcp_server" "$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')" json_add_string "direct_dns_tcp_server" "$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')"
else else
local local_dns=$(echo -n $(echo "${LOCAL_DNS}" | sed "s/,/\n/g" | head -n1) | tr " " ",") json_add_string "direct_dns_udp_server" "$(echo ${LOCAL_DNS} | awk -F '#' '{print $1}')"
json_add_string "direct_dns_udp_server" "$(echo ${local_dns} | awk -F '#' '{print $1}')" direct_dns_port=$(echo ${LOCAL_DNS} | awk -F '#' '{print $2}')
direct_dns_port=$(echo ${local_dns} | awk -F '#' '{print $2}')
fi fi
json_add_string "direct_dns_port" "${direct_dns_port:-53}" json_add_string "direct_dns_port" "${direct_dns_port:-53}"
direct_dns_query_strategy=${direct_dns_query_strategy:-UseIP} direct_dns_query_strategy=${direct_dns_query_strategy:-UseIP}
@@ -259,9 +258,8 @@ run_xray() {
direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}') direct_dns_port=$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $2}')
json_add_string "direct_dns_tcp_server" "$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')" json_add_string "direct_dns_tcp_server" "$(echo ${direct_dns_tcp_server} | awk -F '#' '{print $1}')"
else else
local local_dns=$(echo -n $(echo "${LOCAL_DNS}" | sed "s/,/\n/g" | head -n1) | tr " " ",") json_add_string "direct_dns_udp_server" "$(echo ${LOCAL_DNS} | awk -F '#' '{print $1}')"
json_add_string "direct_dns_udp_server" "$(echo ${local_dns} | awk -F '#' '{print $1}')" direct_dns_port=$(echo ${LOCAL_DNS} | awk -F '#' '{print $2}')
direct_dns_port=$(echo ${local_dns} | awk -F '#' '{print $2}')
fi fi
json_add_string "direct_dns_port" "${direct_dns_port:-53}" json_add_string "direct_dns_port" "${direct_dns_port:-53}"
@@ -1095,7 +1093,7 @@ start_adblock() {
start_dns() { start_dns() {
echolog "DNS域名解析:" echolog "DNS域名解析:"
local china_ng_local_dns=$(IFS=','; set -- $LOCAL_DNS; [ "${1%%[#:]*}" = "127.0.0.1" ] && echo "$1" || ([ -n "$2" ] && echo "$*" || echo "$1")) local china_ng_local_dns="${LOCAL_DNS}"
local v2ray_local_dns local v2ray_local_dns
local direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto") local direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto")
@@ -1112,6 +1110,7 @@ start_dns() {
case "$direct_dns_mode" in case "$direct_dns_mode" in
udp) udp)
LOCAL_DNS=$(normalize_dns "$(config_n_get @global[0] direct_dns 223.5.5.5:53)") LOCAL_DNS=$(normalize_dns "$(config_n_get @global[0] direct_dns 223.5.5.5:53)")
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
china_ng_local_dns=${LOCAL_DNS} china_ng_local_dns=${LOCAL_DNS}
v2ray_local_dns="direct_dns_udp_server=${LOCAL_DNS}" v2ray_local_dns="direct_dns_udp_server=${LOCAL_DNS}"
;; ;;
@@ -1123,6 +1122,7 @@ start_dns() {
#当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。 #当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。
[ "$DNS_SHUNT" != "chinadns-ng" ] || [ "$ACL_RULE_DNSMASQ" = "1" ] && { [ "$DNS_SHUNT" != "chinadns-ng" ] || [ "$ACL_RULE_DNSMASQ" = "1" ] && {
LOCAL_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}" LOCAL_DNS="127.0.0.1#${NEXT_DNS_LISTEN_PORT}"
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${NEXT_DNS_LISTEN_PORT} -c ${china_ng_local_dns} -d chn ln_run "$(first_type chinadns-ng)" chinadns-ng "/dev/null" -b :: -l ${NEXT_DNS_LISTEN_PORT} -c ${china_ng_local_dns} -d chn
echolog " - ChinaDNS-NG(${LOCAL_DNS}) -> ${china_ng_local_dns}" echolog " - ChinaDNS-NG(${LOCAL_DNS}) -> ${china_ng_local_dns}"
echolog " * 请确保上游直连 DNS 支持 TCP 查询。" echolog " * 请确保上游直连 DNS 支持 TCP 查询。"
@@ -1140,6 +1140,7 @@ start_dns() {
[ -z "$1" ] && echo "" || echo "$1" | awk -F',' '{for(i=1;i<=NF;i++){if($i !~ /#/) $i=$i"#53";} print $0;}' OFS=',' [ -z "$1" ] && echo "" || echo "$1" | awk -F',' '{for(i=1;i<=NF;i++){if($i !~ /#/) $i=$i"#53";} print $0;}' OFS=','
} }
LOCAL_DNS=$(add_default_port "$LOCAL_DNS") LOCAL_DNS=$(add_default_port "$LOCAL_DNS")
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
IPT_APPEND_DNS=$(add_default_port "${IPT_APPEND_DNS:-$LOCAL_DNS}") IPT_APPEND_DNS=$(add_default_port "${IPT_APPEND_DNS:-$LOCAL_DNS}")
echo "$IPT_APPEND_DNS" | grep -q -E "(^|,)$LOCAL_DNS(,|$)" || IPT_APPEND_DNS="${IPT_APPEND_DNS:+$IPT_APPEND_DNS,}$LOCAL_DNS" echo "$IPT_APPEND_DNS" | grep -q -E "(^|,)$LOCAL_DNS(,|$)" || IPT_APPEND_DNS="${IPT_APPEND_DNS:+$IPT_APPEND_DNS,}$LOCAL_DNS"
[ -n "$DIRECT_DNS" ] && { [ -n "$DIRECT_DNS" ] && {
@@ -1573,7 +1574,7 @@ acl_app() {
chinadns_port=$(expr $chinadns_port + 1) chinadns_port=$(expr $chinadns_port + 1)
_china_ng_listen="127.0.0.1#${chinadns_port},::1#${chinadns_port}" _china_ng_listen="127.0.0.1#${chinadns_port},::1#${chinadns_port}"
_chinadns_local_dns=$(IFS=','; set -- $LOCAL_DNS; [ "${1%%[#:]*}" = "127.0.0.1" ] && echo "$1" || ([ -n "$2" ] && echo "$1,$2" || echo "$1")) _chinadns_local_dns="${LOCAL_DNS}"
_direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto") _direct_dns_mode=$(config_n_get @global[0] direct_dns_mode "auto")
case "${_direct_dns_mode}" in case "${_direct_dns_mode}" in
udp) udp)
@@ -1703,6 +1704,7 @@ start() {
sleep 2 sleep 2
} }
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2 mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
get_config get_config
export V2RAY_LOCATION_ASSET=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/") export V2RAY_LOCATION_ASSET=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
@@ -1892,6 +1894,7 @@ get_config() {
SMARTDNS_LISTEN_PORT=${NEXT_DNS_LISTEN_PORT} SMARTDNS_LISTEN_PORT=${NEXT_DNS_LISTEN_PORT}
NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1) NEXT_DNS_LISTEN_PORT=$(expr $NEXT_DNS_LISTEN_PORT + 1)
LOCAL_DNS="127.0.0.1#${SMARTDNS_LOCAL_PORT}" LOCAL_DNS="127.0.0.1#${SMARTDNS_LOCAL_PORT}"
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
set_cache_var "SMARTDNS_LOCAL_PORT" "${SMARTDNS_LOCAL_PORT}" set_cache_var "SMARTDNS_LOCAL_PORT" "${SMARTDNS_LOCAL_PORT}"
} }
} }
@@ -1905,7 +1908,8 @@ get_local_dns() {
DEFAULT_DNS=$(uci show dhcp.@dnsmasq[0] | grep "\.server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' '\n' | grep -v "\/" | sed ':label;N;s/\n/,/;b label') DEFAULT_DNS=$(uci show dhcp.@dnsmasq[0] | grep "\.server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' '\n' | grep -v "\/" | sed ':label;N;s/\n/,/;b label')
[ -z "${DEFAULT_DNS}" ] && [ "$(echo $ISP_DNS | tr ' ' '\n' | wc -l)" -ge 1 ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | tr '\n' ',' | sed 's/,$//') [ -z "${DEFAULT_DNS}" ] && [ "$(echo $ISP_DNS | tr ' ' '\n' | wc -l)" -ge 1 ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | tr '\n' ',' | sed 's/,$//')
LOCAL_DNS="${DEFAULT_DNS:-119.29.29.29,223.5.5.5}" LOCAL_DNS="${DEFAULT_DNS:-223.5.5.5}"
LOCAL_DNS="${LOCAL_DNS%%,*}"
} }
get_local_dns get_local_dns
@@ -201,12 +201,6 @@ if not is_file_nonzero(file_vpslist) then
process_address(dns) process_address(dns)
end end
end) end)
api.uci_foreach_c("subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
end)
f_out:close() f_out:close()
end end
if is_file_nonzero(file_vpslist) then if is_file_nonzero(file_vpslist) then
@@ -334,7 +334,7 @@ function add_rule(var)
end end
local function get_dns_config_key() local function get_dns_config_key()
local address_md5 = api.md5_string(sys.exec([[uci show passwall | grep -E '\.(address|download_address|domain_resolver_dns|domain_resolver_dns_https)=|^passwall\.sub_[^.]+\.url=' | cut -d "'" -f 2 | sort -u]])) local address_md5 = api.md5_string(sys.exec([[uci show passwall | grep -E '\.(address|download_address|domain_resolver_dns|domain_resolver_dns_https)=' | cut -d "'" -f 2 | sort -u]]))
local new_rules = sys.exec([[ local new_rules = sys.exec([[
for f in \ for f in \
/usr/share/passwall/rules/chnlist \ /usr/share/passwall/rules/chnlist \
@@ -467,12 +467,6 @@ function add_rule(var)
process_address(dns) process_address(dns)
end end
end) end)
api.uci_foreach_c("subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
end)
log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认")) log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认"))
end end
end end
@@ -360,12 +360,6 @@ if not is_file_nonzero(file_vpslist) then
process_address(dns) process_address(dns)
end end
end) end)
api.uci_foreach_c("subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
end)
f_out:close() f_out:close()
end end
if is_file_nonzero(file_vpslist) then if is_file_nonzero(file_vpslist) then
@@ -778,12 +778,6 @@ filter_vpsip() {
echo "$ipv6_addrs" | sed "s/^/add $IPSET_VPS6 /" | awk '1; END{print "COMMIT"}' | ipset -! -R echo "$ipv6_addrs" | sed "s/^/add $IPSET_VPS6 /" | awk '1; END{print "COMMIT"}' | ipset -! -R
echolog " - [$?]加入所有IPv6节点服务器IP到ipset[$IPSET_VPS6]直连完成" echolog " - [$?]加入所有IPv6节点服务器IP到ipset[$IPSET_VPS6]直连完成"
} }
#订阅方式为直连时
local subscribe_host=$(get_subscribe_host | grep -Ev "$EXCLUDE_VPSIP")
[ -n "$subscribe_host" ] && {
echo "$subscribe_host" | grep -Eo "$IPv4_REGEX" | sed "s/^/add $IPSET_VPS /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
echo "$subscribe_host" | grep -Eo "$IPv6_REGEX" | sed "s/^/add $IPSET_VPS6 /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
}
} }
filter_server_port() { filter_server_port() {
@@ -1434,6 +1428,11 @@ add_firewall_rule() {
filter_direct_node_list > /dev/null 2>&1 & filter_direct_node_list > /dev/null 2>&1 &
local direct_ipt
for direct_ipt in "$ipt_n" "$ipt_m" "$ip6t_n" "$ip6t_m"; do
$direct_ipt -I OUTPUT $(comment "PSW_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN
done
echolog "防火墙规则加载完成!" echolog "防火墙规则加载完成!"
} }
@@ -1509,7 +1508,7 @@ gen_include() {
[ -z "${_ipt}" ] && return [ -z "${_ipt}" ] && return
echo "*$2" echo "*$2"
${_ipt}-save -t $2 | grep "PSW" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "\-m socket .*\-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\|FORWARD\)/-I \1 1/" ${_ipt}-save -t $2 | grep "PSW" | grep -v "PSW_DIRECT" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "\-m socket .*\-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\|FORWARD\)/-I \1 1/"
echo 'COMMIT' echo 'COMMIT'
} }
local __ipt="" local __ipt=""
@@ -1529,6 +1528,8 @@ gen_include() {
echo "\${mangle_output_psw}" | while read line; do echo "\${mangle_output_psw}" | while read line; do
\$(${MY_PATH} insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "\${line}") \$(${MY_PATH} insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "\${line}")
done done
$ipt_n -I OUTPUT $(comment "PSW_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN
$ipt_m -I OUTPUT $(comment "PSW_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN
[ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW") [ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW")
[ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW") [ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW")
@@ -1552,6 +1553,8 @@ gen_include() {
echo "\${mangle_output_psw}" | while read line; do echo "\${mangle_output_psw}" | while read line; do
\$(${MY_PATH} insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "\${line}") \$(${MY_PATH} insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "\${line}")
done done
$ip6t_n -I OUTPUT $(comment "PSW_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN
$ip6t_m -I OUTPUT $(comment "PSW_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN
[ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW [ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW
@@ -838,12 +838,6 @@ filter_vpsip() {
echo "$ipv6_addrs" | insert_nftset $NFTSET_VPS6 echo "$ipv6_addrs" | insert_nftset $NFTSET_VPS6
echolog " - [$?]加入所有IPv6节点服务器IP到nftset[$NFTSET_VPS6]直连完成" echolog " - [$?]加入所有IPv6节点服务器IP到nftset[$NFTSET_VPS6]直连完成"
} }
#订阅方式为直连时
local subscribe_host=$(get_subscribe_host | grep -Ev "$EXCLUDE_VPSIP")
[ -n "$subscribe_host" ] && {
echo "$subscribe_host" | grep -Eo "$IPv4_REGEX" | grep -Ev "$EXCLUDE_VPSIP" | insert_nftset $NFTSET_VPS
echo "$subscribe_host" | grep -Eo "$IPv6_REGEX" | insert_nftset $NFTSET_VPS6
}
} }
filter_server_port() { filter_server_port() {
@@ -1509,6 +1503,9 @@ add_firewall_rule() {
filter_direct_node_list > /dev/null 2>&1 & filter_direct_node_list > /dev/null 2>&1 &
nft "insert rule $NFTABLE_NAME mangle_output meta skgid $DIRECT_GID counter return comment \"PSW_DIRECT\""
nft "insert rule $NFTABLE_NAME nat_output meta skgid $DIRECT_GID counter return comment \"PSW_DIRECT\""
echolog "防火墙规则加载完成!" echolog "防火墙规则加载完成!"
} }
@@ -139,7 +139,7 @@ local function curl(url, file)
if file then if file then
args[#args + 1] = "-o " .. file args[#args + 1] = "-o " .. file
end end
local return_code, result = api.curl_logic(url, nil, args) local return_code, result = api.curl_auto(url, nil, args)
if result and result ~= "" then if result and result ~= "" then
local body, code = result:match("^(.-)%s*([0-9]+)$") local body, code = result:match("^(.-)%s*([0-9]+)$")
if code then if code then
@@ -1814,11 +1814,11 @@ local function curl(url, file, ua, mode)
local return_code, result local return_code, result
if mode == "direct" then if mode == "direct" then
return_code, result = api.curl_base(url, file, curl_args) return_code, result = api.curl_direct(url, file, curl_args)
elseif mode == "proxy" then elseif mode == "proxy" then
return_code, result = api.curl_proxy(url, file, curl_args) return_code, result = api.curl_proxy(url, file, curl_args)
else else
return_code, result = api.curl_logic(url, file, curl_args) return_code, result = api.curl_auto(url, file, curl_args)
end end
if result and result ~= "" then if result and result ~= "" then
@@ -3,6 +3,7 @@
# Copyright (C) 2026 Openwrt-Passwall Organization # Copyright (C) 2026 Openwrt-Passwall Organization
CONFIG=passwall CONFIG=passwall
DIRECT_GID=9911
APP_PATH=/usr/share/${CONFIG} APP_PATH=/usr/share/${CONFIG}
TMP_PATH=/tmp/etc/${CONFIG} TMP_PATH=/tmp/etc/${CONFIG}
TMP_PATH2=${TMP_PATH}_tmp TMP_PATH2=${TMP_PATH}_tmp
@@ -503,17 +504,6 @@ kill_all() {
kill -9 $(pidof "$@") >/dev/null 2>&1 kill -9 $(pidof "$@") >/dev/null 2>&1
} }
get_subscribe_host(){
local line
uci show "${CONFIG}" | grep "=subscribe_list" | while read -r line; do
local section="$(echo "$line" | cut -d '.' -sf 2 | cut -d '=' -sf 1)"
local url="$(config_n_get $section url)"
[ -n "$url" ] || continue
url="$(host_from_url "$url")"
echo "$url"
done
}
gen_lanlist() { gen_lanlist() {
cat $USER_RULES_PATH/lanlist_ipv4 | tr -s '\n' | grep -v "^#" cat $USER_RULES_PATH/lanlist_ipv4 | tr -s '\n' | grep -v "^#"
} }
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.1 PKG_VERSION:=26.10.1
PKG_RELEASE:=138 PKG_RELEASE:=140
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
+3 -3
View File
@@ -4,9 +4,9 @@ START=99
STOP=15 STOP=15
if [ -z "$IPKG_INSTROOT" ]; then if [ -z "$IPKG_INSTROOT" ]; then
. /usr/share/passwall2/utils.sh . /usr/share/passwall2/utils.sh
APP_FILE=${APP_PATH}/app.sh APP_FILE=${APP_PATH}/app.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock
fi fi
set_lock() { set_lock() {
@@ -7,17 +7,17 @@ msgstr "(實驗性) 減少記憶體使用"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469
msgid "1. Select \"Accept Routes\" (to access remote devices)." msgid "1. Select \"Accept Routes\" (to access remote devices)."
msgstr "1. 勾選 接受路由 (以存取遠端設備)。" msgstr "1. 勾選「接受路由」(以存取遠端設備)。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470
msgid "" msgid ""
"2. In \"Advertise Routes\", select your local subnet (to allow remote " "2. In \"Advertise Routes\", select your local subnet (to allow remote "
"devices to access this LAN)." "devices to access this LAN)."
msgstr "2. 在 宣告路由 選擇本裝置的區域網段 (讓遠端能存取這個區網)。" msgstr "2. 在「宣告路由」選擇本裝置的區域網段 (讓遠端能存取這個區網)。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:471 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:471
msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)." msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)."
msgstr "3. 點選 自動配置防火牆 (允許流量轉送)。" msgstr "3. 點選「自動配置防火牆」(允許流量轉送)。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
msgid "Accept Routes" msgid "Accept Routes"
@@ -135,7 +135,7 @@ msgstr "停用 SNAT"
msgid "" msgid ""
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " "Disable Source NAT (SNAT) for traffic to advertised routes. Most users "
"should leave this unchecked." "should leave this unchecked."
msgstr "為宣告路由的流量停用來源地址轉換 (SNAT)。大多數使用者不應勾選此項。" msgstr "為宣告路由的流量停用來源位址轉換 (SNAT)。大多數使用者不應勾選此項。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)." msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
@@ -148,7 +148,7 @@ msgstr "已停用"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
msgid "Disabled: system DNS only." msgid "Disabled: system DNS only."
msgstr "已停用: 只有系統 DNS。" msgstr "已停用: 僅使用系統 DNS。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
msgid "Disconnect from Tailscale and expire current node key." msgid "Disconnect from Tailscale and expire current node key."
@@ -180,7 +180,7 @@ msgstr "啟用或停用 Tailscale 服務。停用後,該服務將停止,處
msgid "" msgid ""
"Enable this device as a Peer Relay server. Requires a public IP and an UDP " "Enable this device as a Peer Relay server. Requires a public IP and an UDP "
"port open on the router." "port open on the router."
msgstr "將此設備啟用為對等中繼伺服器。需要公開 IP 位址,且路由器上需要開啟一個 UDP 連接埠。" msgstr "將此設備做為對等中繼伺服器。需要公開 IP 位址,且路由器上需要開啟一個 UDP 連接埠。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
msgid "Enabled" msgid "Enabled"
@@ -198,21 +198,21 @@ msgstr "錯誤"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
msgid "Error caching DERP region map: %s" msgid "Error caching DERP region map: %s"
msgstr "快取 DERP 區域地圖時出錯: %s" msgstr "快取 DERP 區域對照表時出錯: %s"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
msgid "Error fetching DERP region map: %s" msgid "Error fetching DERP region map: %s"
msgstr "獲取 DERP 區域地圖時出錯: %s" msgstr "獲取 DERP 區域對照表時出錯: %s"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
msgid "Error reading cached DERP region map: %s" msgid "Error reading cached DERP region map: %s"
msgstr "讀取快取的 DERP 區域地圖時出錯: %s" msgstr "讀取快取的 DERP 區域對照表時出錯: %s"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446
msgid "" msgid ""
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node " "Essential configuration for Subnet Routing (Site-to-Site) and Exit Node "
"features." "features."
msgstr "子網路路由(站對站)和出口節點功能的基本設定" msgstr "子網路由(站對站)和出口節點功能的基本設定"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
@@ -332,7 +332,7 @@ msgstr "日誌"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
msgid "MagicDNS: Tailscale overrides resolv.conf." msgid "MagicDNS: Tailscale overrides resolv.conf."
msgstr "MagicDNS:Tailscale 會覆寫 resolv.conf。" msgstr "MagicDNS:Tailscale 將接管 resolv.conf。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90
msgid "N/A" msgid "N/A"
@@ -373,11 +373,11 @@ msgstr "上線"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
msgid "OpenWrt Forward" msgid "OpenWrt Forward"
msgstr "OpenWrt 轉發" msgstr "OpenWrt 轉送"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)" msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
msgstr "OpenWrt 轉送:透過 dnsmasq 轉送 MagicDNS。(僅支援 ts.net)" msgstr "OpenWrt 轉送:僅經由 dnsmasq 轉送至 MagicDNS。(僅支援tailxxxxxx.ts.net)"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488
msgid "" msgid ""
@@ -547,7 +547,7 @@ msgstr "啟用後,將阻止來自 Tailscale 網路的所有入站連線。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
msgid "When using the exit node, access to the local LAN is allowed." msgid "When using the exit node, access to the local LAN is allowed."
msgstr "使用出口節點時,允許訪問本地區域網。" msgstr "使用出口節點時,允許存取區域網路。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472 #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472
msgid "" msgid ""
@@ -563,7 +563,7 @@ msgstr "前"
msgid "" msgid ""
"and enables Masquerading and MSS Clamping (MTU fix) to ensure stable " "and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
"connections." "connections."
msgstr "並啟用地址偽裝和 MSS鉗制確保連線穩定。" msgstr "並啟用IP偽裝和 MSS 箝制(MTU 修復)以確保連線穩定。"
#~ msgid "" #~ msgid ""
#~ "Completely disable all Tailscale firewall auto-configuration, including " #~ "Completely disable all Tailscale firewall auto-configuration, including "
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wechatpush PKG_NAME:=luci-app-wechatpush
PKG_VERSION:=3.6.12 PKG_VERSION:=3.6.13
PKG_RELEASE:=8 PKG_RELEASE:=9
PKG_MAINTAINER:=tty228 <tty228@yeah.net> PKG_MAINTAINER:=tty228 <tty228@yeah.net>
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
+2
View File
@@ -16,6 +16,8 @@
- [x] 路由 Web、SSH 登录提示,自动拉黑、端口敲门 - [x] 路由 Web、SSH 登录提示,自动拉黑、端口敲门
- [x] 无人值守任务 - [x] 无人值守任务
自动封禁启用后,黑名单默认拦截访问路由器的流量;可另外开启“阻止黑名单 IP 的转发流量”,拦截经过路由器转发的所有流量(包括访问端口映射的下级设备、局域网客户端访问互联网)。开启转发封禁且已安装 conntrack 时,封禁还会尝试清理该 IP 的连接跟踪记录,以断开已有的流量卸载连接;未安装时跳过该步骤,新连接仍按黑名单规则拦截。关闭该选项或关闭自动封禁后,转发拦截规则会在下一次检测循环中被移除;与访问路由器的拦截规则一样,停止插件服务不会立即移除已生效的规则,它们会在防火墙重载或重启后消失,并在插件运行时于下一次检测循环中恢复。fw4 定时封禁按最近记录的剩余时长恢复。黑名单时间设置为 `0` 时永久封禁,手动清除黑名单后解除。
通知服务支持列表: 通知服务支持列表:
| 推送应用 | 方式 | 接口说明 | | 推送应用 | 方式 | 接口说明 |
| :-------- | :----- | :----- | | :-------- | :----- | :----- |
+2
View File
@@ -17,6 +17,8 @@ This OpenWrt plugin delivers real-time router notifications to your mobile devic
- [x] Security alerts (failed web/SSH logins with auto blacklist) - [x] Security alerts (failed web/SSH logins with auto blacklist)
- [x] Port knocking & automated tasks - [x] Port knocking & automated tasks
Auto-banning blocks blacklisted IPs from accessing the router. Optionally block all forwarded traffic from blacklisted IPs, including access to port-forwarded downstream devices and Internet access by LAN clients. With forwarded-traffic blocking enabled and conntrack installed, banning also attempts to clear that IP's existing connection-tracking entries to interrupt offloaded flows; without conntrack, this step is skipped while new connections remain blocked by the blacklist rules. Turning the option off, or turning auto-ban off, removes the forwarded-traffic drop rules on the next detection cycle; like the router-access drop rules, they are not removed by stopping the service, disappear after a firewall reload or reboot, and are restored on the plugin's next detection cycle while it runs. fw4 timed bans are restored with their last recorded remaining duration. Set the blacklist duration to `0` for permanent bans; remove the IP from the blacklist to lift the ban.
Supported services: Supported services:
| Push application | Method | description | | Push application | Method | description |
| :-------- | :----- | :----- | | :-------- | :----- | :----- |
@@ -574,7 +574,12 @@ return view.extend({
o.rmempty = false; o.rmempty = false;
o.datatype = 'and(uinteger,min(0))'; o.datatype = 'and(uinteger,min(0))';
o.depends('login_web_black', '1'); o.depends('login_web_black', '1');
o.description = _('\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab.'); o.description = _('"0" means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the "Do Not Disturb" tab.');
o = s.taboption('ipset', form.Flag, 'block_blacklisted_forwarding', _('Block forwarded traffic from blacklisted IPs'));
o.default = '0';
o.depends('login_web_black', '1');
o.description = _('Includes port-forwarded access to downstream devices and LAN clients accessing the Internet.');
o = s.taboption('ipset', form.Flag, 'port_knocking_enable', _('Port knocking')); o = s.taboption('ipset', form.Flag, 'port_knocking_enable', _('Port knocking'));
o.default = '0'; o.default = '0';
+8 -2
View File
@@ -582,8 +582,14 @@ msgid "Blacklisting time (s)"
msgstr "拉黑时间(秒)" msgstr "拉黑时间(秒)"
#: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:489 #: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:489
msgid "\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab." msgid "\"0\" means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab."
msgstr "ipset 中, \"0\" 为永久拉黑,慎用。如不幸误操作,请更改设备 IP 进入 LUCI 界面清空规则。<br/>注:封禁白名单位于 “免打扰” 选项卡。" msgstr "\"0\" 为永久拉黑,慎用。如不幸误操作,请更改设备 IP 进入 LUCI 界面清空规则。<br/>注:封禁白名单位于“免打扰”选项卡。"
msgid "Block forwarded traffic from blacklisted IPs"
msgstr "阻止黑名单 IP 的转发流量"
msgid "Includes port-forwarded access to downstream devices and LAN clients accessing the Internet."
msgstr "包括经端口映射访问下级设备,以及局域网客户端访问互联网的流量。"
#: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:491 #: applications/luci-app-wechatpush/htdocs/luci-static/resources/view/wechatpush/config.js:491
msgid "Port knocking" msgid "Port knocking"
@@ -48,7 +48,7 @@ read_config() {
"get_ipv4_mode" "ipv4_interface" "get_ipv6_mode" "ipv6_interface" \ "get_ipv4_mode" "ipv4_interface" "get_ipv6_mode" "ipv6_interface" \
"device_notification" "cpu_notification" "cpu_load_threshold" "temperature_threshold" \ "device_notification" "cpu_notification" "cpu_load_threshold" "temperature_threshold" \
"client_usage" "client_usage_max" "client_usage_disturb" "client_usage_whitelist" \ "client_usage" "client_usage_max" "client_usage_disturb" "client_usage_whitelist" \
"login_notification" "login_max_num" "login_web_black" "login_ip_black_timeout" "login_ip_white_list" "port_knocking_enable" "login_ip_white_timeout" "login_port_white" "login_port_forward_list" \ "login_notification" "login_max_num" "login_web_black" "login_ip_black_timeout" "block_blacklisted_forwarding" "login_ip_white_list" "port_knocking_enable" "login_ip_white_timeout" "login_port_white" "login_port_forward_list" \
"crontab_regular_time" "crontab_interval_time" \ "crontab_regular_time" "crontab_interval_time" \
"do_not_disturb_mode" "do_not_disturb_starttime" "do_not_disturb_endtime" "up_down_push_whitelist" "up_down_push_blacklist" "up_down_push_interface" "mac_online_list" "mac_offline_list" "cpu_threshold_duration" "cpu_notification_delay" "login_disturb" "login_notification_delay" "login_log_enable" \ "do_not_disturb_mode" "do_not_disturb_starttime" "do_not_disturb_endtime" "up_down_push_whitelist" "up_down_push_blacklist" "up_down_push_interface" "mac_online_list" "mac_offline_list" "cpu_threshold_duration" "cpu_notification_delay" "login_disturb" "login_notification_delay" "login_log_enable" \
"up_timeout" "down_timeout" "timeout_retry_count" "always_check_ip_list" "only_timeout_push" "passive_mode" "thread_num" "defaultSortColumn" "soc_code" "server_host" "server_port" \ "up_timeout" "down_timeout" "timeout_retry_count" "always_check_ip_list" "only_timeout_push" "passive_mode" "thread_num" "defaultSortColumn" "soc_code" "server_host" "server_port" \
@@ -72,12 +72,14 @@ read_config() {
for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do
eval $(echo ${str_version:0:2}"_version")=$(apk list | grep -w ^${str_version} | grep 'installed' | awk '{print $1}' | awk -F ${str_version}"-" '{print $2}') 2>/dev/null eval $(echo ${str_version:0:2}"_version")=$(apk list | grep -w ^${str_version} | grep 'installed' | awk '{print $1}' | awk -F ${str_version}"-" '{print $2}') 2>/dev/null
done done
(apk list | grep -w -q "^firewall4" | grep 'installed') && nftables_version="true"
else else
for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do
eval $(echo ${str_version:0:2}"_version")=$(opkg list-installed | grep -w ^${str_version} | awk '{print $3}') 2>/dev/null eval $(echo ${str_version:0:2}"_version")=$(opkg list-installed | grep -w ^${str_version} | awk '{print $3}') 2>/dev/null
done done
(opkg list-installed | grep -w -q "^firewall4") && nftables_version="true" fi
# firewall4 使用 nftables,避免回退到 iptables/ipset。
if command -v fw4 >/dev/null 2>&1; then
nftables_version="true"
fi fi
devices_json="${dir}/devices.json" devices_json="${dir}/devices.json"
tmp_devices_json="${dir}/devices.json.tmp" tmp_devices_json="${dir}/devices.json.tmp"
@@ -226,8 +228,8 @@ init() {
LockFile unlock LockFile unlock
# 防火墙初始化 # 防火墙初始化
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv4" init_ip_black "ipv4"
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv6" init_ip_black "ipv6"
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv4" [ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv4"
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv6" [ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv6"
tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u) tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
@@ -407,7 +409,75 @@ main() {
#[ ! -f "${dir}/send_enable.lock" ] && login_send #[ ! -f "${dir}/send_enable.lock" ] && login_send
# 因修改为实时推送,白名单设备并不会再更新时间,暂时性修复 # 因修改为实时推送,白名单设备并不会再更新时间,暂时性修复
# 修改为实时推送后,防火墙列表读取和设置需单独列出,移除黑名单无法实时,待改善 # 修改为实时推送后,防火墙列表读取和设置需单独列出,移除黑名单无法实时,待改善
# 防火墙重载会清除运行时规则,需在下一次循环恢复。
# nft 模式下先建立白名单 accept 规则,黑名单 drop 规则才能定位到其后;
# iptables 模式使用 -I 插入链首,因此必须先建立黑名单,再建立白名单。
if [ "$port_knocking_enable" = "1" ] && [ -n "$nftables_version" ]; then
init_ip_white "ipv4"
init_ip_white "ipv6"
fi
if [ "$login_web_black" = "1" ]; then
init_ip_black "ipv4"
init_ip_black "ipv6"
fi
if [ "$port_knocking_enable" = "1" ] && [ -z "$nftables_version" ]; then
init_ip_white "ipv4"
init_ip_white "ipv6"
fi
set_ip_black set_ip_black
if [ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && command -v conntrack >/dev/null 2>&1; then
for pending_file in "${dir}"/pending_black_flow_*; do
[ -f "$pending_file" ] || continue
IFS= read -r pending_ip <"$pending_file"
pending_set="wechatpush_blacklist"
pending_family="ipv4"
if [[ "$pending_ip" == *:* ]]; then
pending_set="wechatpush_blacklistv6"
pending_family="ipv6"
fi
if [ "$pending_family" = "ipv4" ]; then
[[ "$pending_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || pending_ip=""
else
valid_ipv6_blacklist_ip "$pending_ip" || pending_ip=""
fi
if [ -z "$pending_ip" ]; then
rm -f "$pending_file"
continue
fi
if [ -n "$nftables_version" ]; then
# 集合列举失败需保留重试文件,只有成功列举后才能判定元素确实不存在;
# nft 会规范化 IPv4/IPv6 写法,按规范化结果比较。
pending_elements=$(nft -j list set inet fw4 "$pending_set" 2>/dev/null) || continue
pending_elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$pending_elements") || continue
pending_found=""
if [[ "$pending_ip" == *:* ]]; then
pending_wanted=$(normalize_ipv6_blacklist_ip "$pending_ip")
while IFS= read -r pending_element; do
[ -n "$pending_element" ] || continue
[ "$(normalize_ipv6_blacklist_ip "$pending_element")" = "$pending_wanted" ] && pending_found="1" && break
done <<<"$pending_elements"
else
pending_wanted=$(normalize_ipv4_blacklist_ip "$pending_ip") || pending_wanted=""
while IFS= read -r pending_element; do
[ -n "$pending_element" ] || continue
pending_normalized=$(normalize_ipv4_blacklist_ip "$pending_element") || continue
[ "$pending_normalized" = "$pending_wanted" ] && pending_found="1" && break
done <<<"$pending_elements"
fi
[ -n "$pending_found" ] || {
rm -f "$pending_file"
continue
}
else
ipset list "$pending_set" >/dev/null 2>&1 || continue
ipset test "$pending_set" "$pending_ip" >/dev/null 2>&1 || {
rm -f "$pending_file"
continue
}
fi
clear_ip_black_flows "$pending_ip"
done
fi
tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u) tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
while IFS= read -r tmp_ip; do while IFS= read -r tmp_ip; do
@@ -2244,6 +2314,7 @@ add_ip_white() {
# 初始化白名单 # 初始化白名单
init_ip_white() { init_ip_white() {
[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] || return [ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] || return
local nat_table_cmd=""
# 设置 IP 版本变量 # 设置 IP 版本变量
if [ $1 == "ipv4" ]; then if [ $1 == "ipv4" ]; then
ipset_name="wechatpush_whitelist" ipset_name="wechatpush_whitelist"
@@ -2256,8 +2327,8 @@ init_ip_white() {
if [ -n "$nftables_version" ]; then if [ -n "$nftables_version" ]; then
! nft list set inet fw4 $ipset_name >/dev/null 2>&1 && nft add set inet fw4 $ipset_name { type ${1}_addr\; flags timeout\; timeout ${login_ip_white_timeout}s\; } ! nft list set inet fw4 $ipset_name >/dev/null 2>&1 && nft add set inet fw4 $ipset_name { type ${1}_addr\; flags timeout\; timeout ${login_ip_white_timeout}s\; }
nft -- add chain inet fw4 wechatpush_dstnat { type nat hook prerouting priority -100 \; } nft list chain inet fw4 wechatpush_dstnat >/dev/null 2>&1 || nft -- add chain inet fw4 wechatpush_dstnat { type nat hook prerouting priority -100 \; }
nft add chain inet fw4 wechatpush_srcnat { type nat hook postrouting priority 100 \; } nft list chain inet fw4 wechatpush_srcnat >/dev/null 2>&1 || nft add chain inet fw4 wechatpush_srcnat { type nat hook postrouting priority 100 \; }
else else
! ipset list $ipset_name >/dev/null 2>&1 && ipset create $ipset_name hash:ip timeout $login_ip_white_timeout $nat_table_cmd >/dev/null 2>&1 ! ipset list $ipset_name >/dev/null 2>&1 && ipset create $ipset_name hash:ip timeout $login_ip_white_timeout $nat_table_cmd >/dev/null 2>&1
fi fi
@@ -2301,58 +2372,313 @@ init_ip_white() {
unset port_forward unset port_forward
} }
# 写入连接跟踪清理重试文件,供后续检测循环重试。
# 登录监视子进程与主循环并发运行,必须原子落盘,避免主循环读到空文件后误删重试记录。
write_pending_black_flow() {
local key="${1//[:.]/_}" pending_file="${dir}/pending_black_flow_${1//[:.]/_}" pending_tmp="${dir}/.pending_black_flow_${key}.${BASHPID:-$$}"
printf '%s\n' "$1" >"$pending_tmp" && mv -f "$pending_tmp" "$pending_file"
}
# 新封禁无法匹配已走流表的连接,需要删除该来源的连接跟踪记录。
clear_ip_black_flows() {
command -v conntrack >/dev/null 2>&1 || return 0
local pending_file="${dir}/pending_black_flow_${1//[:.]/_}" original reply original_status reply_status
local -a family=()
[[ "$1" == *:* ]] && family=(-f ipv6)
conntrack ${family[@]} -D -s "$1" >/dev/null 2>&1
conntrack ${family[@]} -D --reply-src "$1" >/dev/null 2>&1
original=$(conntrack -L "${family[@]}" -s "$1" 2>/dev/null)
original_status=$?
reply=$(conntrack -L "${family[@]}" --reply-src "$1" 2>/dev/null)
reply_status=$?
if printf '%s\n%s\n' "$original" "$reply" | grep -Eq '\[(HW_)?OFFLOAD\]'; then
write_pending_black_flow "$1"
log_change "[WARN] Failed to clear offloaded connections for $1"
elif [ "$original_status" -eq 0 ] && [ "$reply_status" -eq 0 ]; then
rm -f "$pending_file"
else
write_pending_black_flow "$1"
fi
return 0
}
clear_ip_black_set_flows() {
command -v conntrack >/dev/null 2>&1 || return 0
local elements ip
if [ -n "$nftables_version" ]; then
elements=$(nft -j list set inet fw4 "$ipset_name") || return 1
elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$elements") || return 1
else
elements=$(ipset save "$ipset_name") || return 1
elements=$(awk -v set_name="$ipset_name" '$1 == "add" && $2 == set_name {print $3}' <<<"$elements")
fi
while IFS= read -r ip; do
[ -n "$ip" ] || continue
clear_ip_black_flows "$ip"
done <<<"$elements"
return 0
}
delete_nft_ip_black_forward_rule() {
local drop_rule_handle rules
while :; do
# 列举失败与“没有规则”需区分:返回非零由下一次检测循环重试清理。
rules=$(nft --handle list chain inet fw4 forward 2>/dev/null) || return 1
drop_rule_handle=$(awk -v set_name="@$ipset_name" 'index($0, set_name " ") && /comment "!wechatpush Forward Drop rule"/ {print $NF; exit}' <<<"$rules")
[ -n "$drop_rule_handle" ] || break
nft delete rule inet fw4 forward handle "$drop_rule_handle" >/dev/null 2>&1 || return 1
done
}
# 初始化黑名单规则 # 初始化黑名单规则
init_ip_black() { init_ip_black() {
[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] || return local nat_table_cmd=""
# 设置 IP 版本变量 # 设置 IP 版本变量
if [ $1 == "ipv4" ]; then if [ "$1" = "ipv4" ]; then
ipset_name="wechatpush_blacklist" ipset_name="wechatpush_blacklist"
ip_version="ip" ip_version="ip"
elif [ $1 == "ipv6" ]; then elif [ "$1" = "ipv6" ]; then
ipset_name="wechatpush_blacklistv6" ipset_name="wechatpush_blacklistv6"
ip_version="ip6" ip_version="ip6"
nat_table_cmd="family inet6" nat_table_cmd="family inet6"
fi fi
local pending_clear_marker="${dir}/pending_black_set_clear_${ipset_name}"
if [ "$login_web_black" != "1" ]; then
rm -f "$pending_clear_marker" "${dir}"/pending_black_flow_*
if [ -n "$nftables_version" ]; then
delete_nft_ip_black_forward_rule
else
${ip_version}tables -D FORWARD -m set --match-set "$ipset_name" src -j DROP >/dev/null 2>&1
fi
return
fi
[ -n "$nftables_version" ] && { if [ -n "$nftables_version" ]; then
! nft list set inet fw4 ${ipset_name} >/dev/null 2>&1 && nft add set inet fw4 ${ipset_name} { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; } local set_exists="" set_has_timeout="" want_timeout="" set_options="type ${1}_addr;" white_rule_handle input_rule set_meta
! nft list ruleset | grep "$ip_version saddr @${ipset_name} counter .* comment \"\!wechatpush Drop rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 input $ip_version saddr @${ipset_name} counter drop comment \"\!wechatpush Drop rule\" >/dev/null 2>&1 set_meta=$(nft -j list set inet fw4 "$ipset_name" 2>/dev/null)
} || { if [ -n "$set_meta" ]; then
ipset list $ipset_name >/dev/null 2>&1 || ipset create ${ipset_name} hash:ip timeout ${login_ip_black_timeout} ${nat_table_cmd} >/dev/null 2>&1 set_exists="1"
${ip_version}tables -C INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1 # 纯文本输出只显示默认 timeout,flags 需要通过 JSON 读取。
} [ -n "$(jq -r '[.nftables[] | select(.set) | .set.flags[]?] | index("timeout") // empty' <<<"$set_meta")" ] && set_has_timeout="1"
fi
[ "$login_ip_black_timeout" -gt 0 ] && want_timeout="1" && set_options="${set_options} flags timeout; timeout ${login_ip_black_timeout}s;"
white_rule_handle=$(nft --handle list chain inet fw4 input 2>/dev/null | awk -v rule="$ip_version saddr @${ipset_name/blacklist/whitelist} counter" 'index($0, rule) && /accept comment "!wechatpush Accept rule"/ {print $NF; exit}')
input_rule="insert rule inet fw4 input $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Drop rule\""
[ -n "$white_rule_handle" ] && input_rule="add rule inet fw4 input position $white_rule_handle $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Drop rule\""
# nft 原子事务:切换集合失败时保留原有封禁规则。
if [ -n "$set_exists" ] && [ "$set_has_timeout" != "$want_timeout" ]; then
local rule_deletions="" element_commands="" forward_rule="" chain handle ip elements
elements=$(nft -j list set inet fw4 "$ipset_name") || return 1
elements=$(jq -r '.nftables[] | .set?.elem[]? | if type == "object" then .elem.val else . end | strings' <<<"$elements") || return 1
while IFS= read -r ip; do
[ -z "$ip" ] && continue
if [[ "$ip" =~ ^[0-9a-fA-F:.]+$ ]]; then
element_commands="${element_commands}add element inet fw4 $ipset_name { $ip"
[ -n "$want_timeout" ] && element_commands="${element_commands} timeout ${login_ip_black_timeout}s"
element_commands="${element_commands} }"$'\n'
else
log_change "[ERROR] Invalid blacklist element in $ipset_name: $ip"
return 1
fi
done <<<"$elements"
for chain in input forward; do
for handle in $(nft --handle list chain inet fw4 "$chain" 2>/dev/null | awk -v set_name="@$ipset_name" 'index($0, set_name " ") && /comment "!wechatpush/ && /Drop rule/ {print $NF}'); do
rule_deletions="${rule_deletions}delete rule inet fw4 $chain handle $handle"$'\n'
done
done
[ "$block_blacklisted_forwarding" = "1" ] && forward_rule="insert rule inet fw4 forward $ip_version saddr @$ipset_name counter drop comment \"!wechatpush Forward Drop rule\""
if ! nft -f - >/dev/null 2>&1 <<EOF
${rule_deletions}delete set inet fw4 $ipset_name
add set inet fw4 $ipset_name { $set_options }
${element_commands}${input_rule}
$forward_rule
EOF
then
log_change "[ERROR] Failed to switch blacklist set $ipset_name; existing firewall rules retained"
return 1
fi
if [ -n "$forward_rule" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] && clear_ip_black_flows "$ip"
done <<<"$elements"
fi
# 集合元素已逐个清理连接,集合级清理义务同步完成。
rm -f "$pending_clear_marker"
return 0
fi
if [ -z "$set_exists" ]; then
if [ -n "$want_timeout" ]; then
nft add set inet fw4 "$ipset_name" { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; } || return 1
else
nft add set inet fw4 "$ipset_name" { type ${1}_addr\; } || return 1
fi
fi
if ! nft list chain inet fw4 input | awk -v rule="$ip_version saddr @$ipset_name counter" 'index($0, rule) && / drop comment "!wechatpush Drop rule"/ {found=1} END {exit !found}'; then
if [ -n "$white_rule_handle" ]; then
nft add rule inet fw4 input position "$white_rule_handle" "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist input rule for $ipset_name"
return 1
}
else
nft insert rule inet fw4 input "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist input rule for $ipset_name"
return 1
}
fi
fi
if [ "$block_blacklisted_forwarding" = "1" ]; then
if ! nft list chain inet fw4 forward | awk -v rule="$ip_version saddr @$ipset_name counter" 'index($0, rule) && / drop comment "!wechatpush Forward Drop rule"/ {found=1} END {exit !found}'; then
nft insert rule inet fw4 forward "$ip_version" saddr "@$ipset_name" counter drop comment '"!wechatpush Forward Drop rule"' >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist forward rule for $ipset_name"
return 1
}
touch "$pending_clear_marker"
fi
else
delete_nft_ip_black_forward_rule
fi
else
ipset list $ipset_name >/dev/null 2>&1 || ipset create $ipset_name hash:ip timeout $login_ip_black_timeout $nat_table_cmd >/dev/null 2>&1
${ip_version}tables -C INPUT -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1
if [ "$block_blacklisted_forwarding" = "1" ]; then
if ! ${ip_version}tables -C FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1; then
${ip_version}tables -I FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1 || {
log_change "[ERROR] Failed to add blacklist forward rule for $ipset_name"
return 1
}
touch "$pending_clear_marker"
fi
else
${ip_version}tables -D FORWARD -m set --match-set $ipset_name src -j DROP >/dev/null 2>&1
fi
fi
if [ "$block_blacklisted_forwarding" = "1" ] && [ -f "$pending_clear_marker" ]; then
clear_ip_black_set_flows || return 1
rm -f "$pending_clear_marker"
elif [ "$block_blacklisted_forwarding" != "1" ]; then
rm -f "$pending_clear_marker" "${dir}"/pending_black_flow_*
fi
}
# nft 输出可能以 :: 开头或结尾;只接受单个压缩段及合法的十六进制分组。
valid_ipv6_blacklist_ip() {
local ip="$1" left right group count=0
if [[ "$ip" == *::* ]]; then
left=${ip%%::*}
right=${ip#*::}
[[ "$right" != *::* ]] || return 1
[[ -z "$left" || "$left" =~ ^([[:xdigit:]]{1,4}:)*[[:xdigit:]]{1,4}$ ]] || return 1
[[ -z "$right" || "$right" =~ ^([[:xdigit:]]{1,4}:)*[[:xdigit:]]{1,4}$ ]] || return 1
for group in ${ip//:/ }; do ((count+=1)); done
((count < 8))
else
[[ "$ip" =~ ^([[:xdigit:]]{1,4}:){7}[[:xdigit:]]{1,4}$ ]]
fi
}
# 按 nft 对点分 IPv4 文本的解析规则转为集合输出格式。
# 带前导零的分组按八进制解析;不带前导零的分组按十进制解析。
normalize_ipv4_blacklist_ip() {
local ip="$1" octet value normalized=""
local -a octets=()
IFS=. read -r -a octets <<<"$ip"
[ "${#octets[@]}" -eq 4 ] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1
if [[ "$octet" == 0* ]]; then
[[ "$octet" =~ ^0[0-7]{0,2}$ ]] || return 1
value=$((8#$octet))
else
value=$((10#$octet))
fi
((value <= 255)) || return 1
normalized+="$value."
done
printf '%s\n' "${normalized%.}"
}
normalize_ipv6_blacklist_ip() {
local ip="$1" left right group count=0 missing normalized=""
if [[ "$ip" == *::* ]]; then
left=${ip%%::*}
right=${ip#*::}
else
left="$ip"
right=""
fi
for group in ${left//:/ }; do ((count+=1)); done
for group in ${right//:/ }; do ((count+=1)); done
missing=$((8 - count))
for group in ${left//:/ }; do printf -v group '%04x' "0x$group"; normalized+="${group}:"; done
for ((count=0; count<missing; count++)); do normalized+="0000:"; done
for group in ${right//:/ }; do printf -v group '%04x' "0x$group"; normalized+="${group}:"; done
printf '%s\n' "${normalized%:}"
} }
# 添加黑名单 # 添加黑名单
add_ip_black() { add_ip_black() {
local login_ip=$1 local login_ip=$1 element_timeout="${login_ip_black_timeout}s"
[ -z "$login_ip" ] && return 0 [ -z "$login_ip" ] && return 0
echo "$login_ip_white_list" | grep -w -q "$login_ip" && return 1 echo "$login_ip_white_list" | grep -w -q "$login_ip" && return 1
# 检查 IP 版本 # 检查 IP 版本
unset ipset_name unset ipset_name
(echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist" (echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
(echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6" (echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
if [ -n "$nftables_version" ] && [[ "$login_ip" == *:* ]]; then
valid_ipv6_blacklist_ip "$login_ip" && ipset_name="wechatpush_blacklistv6" || unset ipset_name
fi
[ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log_change "[WARN] $(translate "Failed to add to blacklist, invalid IP format: %s (removed from list)" "$login_ip")" && return 1 [ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log_change "[WARN] $(translate "Failed to add to blacklist, invalid IP format: %s (removed from list)" "$login_ip")" && return 1
! cat "$ip_blacklist_path" | grep -q -w -i $login_ip && echo "$login_ip timeout $login_ip_black_timeout" >>"$ip_blacklist_path" ! cat "$ip_blacklist_path" | grep -q -w -i $login_ip && echo "$login_ip timeout $login_ip_black_timeout" >>"$ip_blacklist_path"
[ -n "$nftables_version" ] && { if [ -n "$nftables_version" ]; then
nft list set inet fw4 ${ipset_name} | grep -qw "${login_ip}" && return 1 # IP 已存在 nft get element inet fw4 "$ipset_name" { "$login_ip" } >/dev/null 2>&1 && return 1 # 按地址值判断,兼容 IPv6 的非规范写法
nft add element inet fw4 $ipset_name { $login_ip expires ${login_ip_black_timeout}s } >/dev/null 2>&1 # 先记录清理义务,若在加入集合与清理连接之间中断,下次循环仍会重试。
} || { [ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && write_pending_black_flow "$login_ip"
ipset -exist add $ipset_name $login_ip timeout ${login_ip_black_timeout} >/dev/null 2>&1 [[ "$2" =~ ^([0-9]+(ms|[smhdw]))+$ ]] && element_timeout="$2"
} [ -z "$element_timeout" ] && element_timeout="${login_ip_black_timeout}s"
if [ "$login_ip_black_timeout" -eq 0 ]; then
nft add element inet fw4 "$ipset_name" { "$login_ip" } >/dev/null 2>&1 || return 1
else
nft add element inet fw4 "$ipset_name" { "$login_ip" timeout "$element_timeout" } >/dev/null 2>&1 || return 1
fi
# 新封禁可能覆盖磁盘中已到期的旧记录;写回当前封禁,再由集合同步剩余时间。
if [ "$login_ip_black_timeout" -gt 0 ]; then
remove_ip_black_file_entry "$login_ip" || return 1
echo "$login_ip expires $element_timeout" >>"$ip_blacklist_path"
fi
else
ipset test "$ipset_name" "$login_ip" >/dev/null 2>&1 && return 1
[ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && write_pending_black_flow "$login_ip"
ipset -exist add "$ipset_name" "$login_ip" timeout "$login_ip_black_timeout" >/dev/null 2>&1 || return 1
fi
[ "$login_web_black" = "1" ] && [ "$block_blacklisted_forwarding" = "1" ] && clear_ip_black_flows "$login_ip"
return 0
}
remove_ip_black_file_entry() {
local escaped_ip=${1//./\\.}
sed -i -e "/^[[:space:]]*${escaped_ip}[[:space:]]/d" -e "/^[[:space:]]*${escaped_ip}$/d" "$ip_blacklist_path"
} }
# 移出黑名单 # 移出黑名单
del_ip_black() { del_ip_black() {
[ -z "$1" ] && return [ -z "$1" ] && return
sed -i "/^${1}/d" ${ip_blacklist_path} remove_ip_black_file_entry "$1" || return 1
rm -f "${dir}/pending_black_flow_${1//[:.]/_}"
# 检查 IP 版本 # 检查 IP 版本
unset ipset_name unset ipset_name
(echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist" (echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
(echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6" (echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
[ -n "$nftables_version" ] && [[ "$1" == *:* ]] && valid_ipv6_blacklist_ip "$1" && ipset_name="wechatpush_blacklistv6"
if [ -n "$nftables_version" ] && [[ "$1" == *:* ]]; then
canonical_ip=$(nft get element inet fw4 "$ipset_name" { "$1" } 2>/dev/null | awk -F'elements = [{][[:space:]]*' 'NF > 1 {split($2, fields, /[[:space:],}]/); print fields[1]; exit}')
[ -n "$canonical_ip" ] && [ "$canonical_ip" != "$1" ] && remove_ip_black_file_entry "$canonical_ip"
fi
[ -z "$ipset_name" ] && return [ -z "$ipset_name" ] && return
[ -n "$nftables_version" ] && { [ -n "$nftables_version" ] && {
@@ -2368,23 +2694,104 @@ set_ip_black() {
[ $(tail -n1 "${ip_blacklist_path}" | wc -l) -eq "0" ] && echo -e >>${ip_blacklist_path} [ $(tail -n1 "${ip_blacklist_path}" | wc -l) -eq "0" ] && echo -e >>${ip_blacklist_path}
# 从 ip_blacklist 文件逐行添加黑名单,add_ip_black() 处验证是否重复,此处不在验证 # 从 ip_blacklist 文件逐行添加黑名单,add_ip_black() 处验证是否重复,此处不在验证
for ip_black in $(cat ${ip_blacklist_path} | awk '{print $1}'); do local saved_remaining
add_ip_black "$ip_black" while IFS= read -r ip_black_info; do
done ip_black=$(awk '{print $1}' <<<"$ip_black_info")
[ -z "$ip_black" ] && continue
saved_remaining=""
if [ -n "$nftables_version" ] && [[ "$ip_black_info" == *" expires "* ]]; then
saved_remaining=${ip_black_info##* expires }
saved_remaining=${saved_remaining//[[:space:]]/}
fi
if [[ "$saved_remaining" == 0s || "$saved_remaining" == 0ms ]]; then
if [[ "$ip_black" == *:* ]]; then
ipset_name="wechatpush_blacklistv6"
else
ipset_name="wechatpush_blacklist"
fi
if ! nft get element inet fw4 "$ipset_name" { "$ip_black" } >/dev/null 2>&1; then
del_ip_black "$ip_black"
rm -f "${dir}/pending_black_flow_${ip_black//[:.]/_}"
fi
continue
fi
add_ip_black "$ip_black" "$saved_remaining"
done <<<"$(cat "$ip_blacklist_path")"
# 当 ip_blacklist 文件清除 IP 时,从集合中清除 IP # 当 ip_blacklist 文件清除 IP 时,从集合中清除 IP
[ -n "$nftables_version" ] && fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//') if [ -n "$nftables_version" ] && [ "$login_ip_black_timeout" -eq 0 ]; then
[ -n "$nftables_version" ] && fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//') fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | sed -e 's/.*{//' -e 's/}//' | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | sed -e 's/.*{//' -e 's/}//' | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
elif [ -n "$nftables_version" ]; then
fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
fi
[ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list wechatpush_blacklist | grep "timeout" 2>/dev/null) [ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list wechatpush_blacklist | grep "timeout" 2>/dev/null)
[ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list wechatpush_blacklistv6 | grep "timeout" 2>/dev/null) [ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list wechatpush_blacklistv6 | grep "timeout" 2>/dev/null)
[ -n "$fw_info_blacklist" ] && [ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${fw_info_blacklist}\n${fw_info_blacklistv6}" combined_fw_info_blacklist="$fw_info_blacklist"
[ -z "$fw_info_blacklist" ] && combined_fw_info_blacklist="${fw_info_blacklistv6}" || combined_fw_info_blacklist="${fw_info_blacklist}" [ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${combined_fw_info_blacklist}${combined_fw_info_blacklist:+$'\n'}${fw_info_blacklistv6}"
# nft 按地址值查询并返回规范写法,避免文件中的 IPv6 缩写差异导致误删集合元素。
local configured_ips="" configured_ip configured_set canonical_ip escaped_ip octet
if [ -n "$nftables_version" ]; then
for configured_ip in $(awk '{print $1}' "$ip_blacklist_path"); do
[[ "$configured_ip" =~ ^[0-9a-fA-F:.]+$ ]] || continue
case "$configured_ip" in
*:*) valid_ipv6_blacklist_ip "$configured_ip" || continue; configured_set="wechatpush_blacklistv6" ;;
*)
[[ "$configured_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || continue
for octet in ${configured_ip//./ }; do
((10#$octet <= 255)) || continue 2
done
configured_set="wechatpush_blacklist"
;;
esac
if [[ "$configured_ip" == *:* ]]; then
configured_canonical=$(normalize_ipv6_blacklist_ip "$configured_ip")
whitelist_match=""
for whitelist_ip in $login_ip_white_list; do
valid_ipv6_blacklist_ip "$whitelist_ip" || continue
[ "$configured_canonical" = "$(normalize_ipv6_blacklist_ip "$whitelist_ip")" ] && whitelist_match="1" && break
done
[ "$whitelist_match" = "1" ] && continue
elif echo "$login_ip_white_list" | grep -w -q "$configured_ip"; then
continue
fi
# get element 即使加 -j 仍输出集合文本,按实际输出提取规范化地址。
canonical_ip=$(nft get element inet fw4 "$configured_set" { "$configured_ip" } 2>/dev/null) || return 1
canonical_ip=$(awk -F'elements = [{][[:space:]]*' 'NF > 1 {split($2, fields, /[[:space:],}]/); print fields[1]; exit}' <<<"$canonical_ip")
[[ "$canonical_ip" =~ ^[0-9a-fA-F:.]+$ ]] || return 1
configured_ips="${configured_ips}${canonical_ip}"$'\n'
if [ "$configured_ip" != "$canonical_ip" ]; then
escaped_ip=${configured_ip//./\\.}
sed -i -e "s/^[[:space:]]*${escaped_ip}$/${canonical_ip}/" -e "s/^[[:space:]]*${escaped_ip} /${canonical_ip} /" "$ip_blacklist_path" || return 1
fi
done
fi
while IFS= read -r ip_black_info; do while IFS= read -r ip_black_info; do
ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}") if [ -n "$nftables_version" ]; then
[ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}") ip_black=$(awk '{print $1}' <<<"$ip_black_info")
[ -z "$ip_black" ] && continue
else
ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
[ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
fi
[ -z "$ip_black" ] && continue [ -z "$ip_black" ] && continue
cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || del_ip_black ${ip_black} if [ -n "$nftables_version" ]; then
if grep -Fqx -- "$ip_black" <<<"$configured_ips"; then
remove_ip_black_file_entry "$ip_black" || return 1
# 按集合中的剩余时长写回,忽略 nft 为单个元素输出的 timeout 字段。
if [ "$login_ip_black_timeout" -gt 0 ]; then
echo "$ip_black expires ${ip_black_info##* expires }" >>"$ip_blacklist_path"
else
echo "$ip_black" >>"$ip_blacklist_path"
fi
else
del_ip_black "$ip_black"
fi
else
cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || del_ip_black ${ip_black}
fi
done <<<"$combined_fw_info_blacklist" done <<<"$combined_fw_info_blacklist"
} }
+3 -3
View File
@@ -17,12 +17,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wwand PKG_NAME:=luci-app-wwand
PKG_VERSION:=1.7.0_pre2 PKG_VERSION:=1.7.0_pre6
PKG_RELEASE:=41 PKG_RELEASE:=42
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
PKG_SOURCE_VERSION:=2f5176a88cdc03e6c93b1bcd1c0de30684355b53 PKG_SOURCE_VERSION:=3649f55a823e889e300ff37e317da9c7e58bfb40
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only PKG_LICENSE:=GPL-2.0-only
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-xclient PKG_NAME:=luci-app-xclient
PKG_VERSION:=v2.1 PKG_VERSION:=v2.1
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=xcode75 PKG_MAINTAINER:=xcode75
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
Binary file not shown.
@@ -32,31 +32,38 @@ function getServiceStatus() {
} }
function renderStatus(status) { function renderStatus(status) {
let clientHTML = ''; let clientBadge = status.client
let serverHTML = ''; ? E('em', {}, [
let spanGreen = '<em><span style="color:green"><strong>%s (PID %d)</strong></span></em>'; E('span', { 'style': 'color:green;' }, [
let spanRed = '<em><span style="color:grey"><strong>%s</strong></span></em>'; E('strong', {}, _('RUNNING') + ' (PID ' + status.client + ')')
])
])
: E('em', {}, [
E('span', { 'style': 'color:grey;' }, [
E('strong', {}, _('NOT RUNNING'))
])
]);
if (status.client) { let serverBadge = status.server
clientHTML = String.format(spanGreen, _('RUNNING'), status.client); ? E('em', {}, [
} else { E('span', { 'style': 'color:green;' }, [
clientHTML = String.format(spanRed, _('NOT RUNNING')); E('strong', {}, _('RUNNING') + ' (PID ' + status.server + ')')
} ])
])
: E('em', {}, [
E('span', { 'style': 'color:grey;' }, [
E('strong', {}, _('NOT RUNNING'))
])
]);
if (status.server) { return E('div', { 'class': 'cbi-map' },
serverHTML = String.format(spanGreen, _('RUNNING'), status.server); E('fieldset', { 'class': 'cbi-section' }, [
} else {
serverHTML = String.format(spanRed, _('NOT RUNNING'));
}
return E('div', { class: 'cbi-map' },
E('fieldset', { class: 'cbi-section' }, [
E('p', {}, [ E('p', {}, [
E('strong', {}, _('Client Daemon') + ': '), E('strong', {}, _('Client Daemon') + ': '),
E('span', {}, [clientHTML]), clientBadge,
E('span', { style: 'margin-left: 20px;' }, [ E('span', { 'style': 'margin-left: 20px;' }, [
E('strong', {}, _('Server Daemon') + ': '), E('strong', {}, _('Server Daemon') + ': '),
E('span', {}, [serverHTML]) serverBadge
]) ])
]) ])
]) ])
@@ -296,8 +303,8 @@ return view.extend({
o = s.taboption('general', form.ListValue, 'proto', _('Tunnel Protocol'), o = s.taboption('general', form.ListValue, 'proto', _('Tunnel Protocol'),
_('Protocol to tunnel: TCP (standard KCP stream) or UDP (direct low-latency datagram forwarding, e.g. for DNS).')); _('Protocol to tunnel: TCP (standard KCP stream) or UDP (direct low-latency datagram forwarding, e.g. for DNS).'));
o.value('tcp', 'TCP (TCP-over-KCP)'); o.value('tcp', _('TCP (TCP-over-KCP)'));
o.value('udp', 'UDP (Direct UDP Datagram)'); o.value('udp', _('UDP (Direct UDP Datagram)'));
o.default = 'tcp'; o.default = 'tcp';
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
+64
View File
@@ -363,3 +363,67 @@ msgstr "TCP (基于 KCP 可靠流)"
msgid "UDP (Direct UDP Datagram)" msgid "UDP (Direct UDP Datagram)"
msgstr "UDP (原生报文直传,适用于 DNS)" msgstr "UDP (原生报文直传,适用于 DNS)"
msgid "Add Fallback Tunnel"
msgstr "添加回退隧道"
msgid "Enable Client Daemon"
msgstr "启用客户端守护进程"
msgid "Enable Server Daemon"
msgstr "启用服务端守护进程"
msgid "Enable loss-driven AIMD send window adaptation. Default is disabled."
msgstr "启用基于丢包驱动的 AIMD 发送窗口自适应调整。默认禁用。"
msgid "Enable unified xkcptun client daemon."
msgstr "启用统一 xkcptun 客户端守护进程。"
msgid "Enable unified xkcptun server dynamic gateway daemon."
msgstr "启用统一 xkcptun 服务端动态网关守护进程。"
msgid "Frame all UDP datagrams with Reed-Solomon FEC header. Default is enabled."
msgstr "为所有 UDP 数据报文添加 Reed-Solomon FEC 前向纠错头。默认启用。"
msgid "Internal clock interval in milliseconds (10-5000). Default is 10ms."
msgstr "内部协议循环时钟间隔,单位毫秒 (10-5000)。默认为 10ms。"
msgid "Listen Interface"
msgstr "监听接口"
msgid "Listen Port"
msgstr "监听端口"
msgid "Listen Port (UDP)"
msgstr "监听端口 (UDP)"
msgid "Local port to listen for incoming application traffic (e.g. 9088)."
msgstr "监听传入应用程序流量的本地端口(例如 9088)。"
msgid "Max KCP segments per flush tick to smooth send bursts (0 = off). Default is 0."
msgstr "每次 flush 周期允许发送的最大 KCP 分片数,平滑突发流量 (0 = 禁用)。默认 0。"
msgid "Optional fallback static target rules for legacy clients that do not send dynamic destination headers. Modern dynamic clients do not require any server tunnels configured here."
msgstr "适用于未发送动态目标头的旧版客户端的可选静态回退目标规则。现代动态客户端无需在此配置任何服务端隧道。"
msgid "Server Fallback Tunnels (Optional)"
msgstr "服务端回退隧道(可选)"
msgid "Server Global Settings (Dynamic Gateway)"
msgstr "服务端全局设置(动态网关)"
msgid "Target host on the server to connect/forward to (default: 127.0.0.1)."
msgstr "服务端连接/转发的目标主机(默认:127.0.0.1)。"
msgid "Target port on the server to forward traffic to (e.g. 22, 53, 80, 443)."
msgstr "服务端转发流量的目标端口(例如 22、53、80、443)。"
msgid "UDP port to listen for incoming client KCP connections (default: 9089)."
msgstr "监听客户端传入 KCP 连接的 UDP 端口(默认:9089)。"
msgid "UDP socket receive/send buffer size in bytes. Default is 16777216 (16MB)."
msgstr "UDP 套接字接收与发送缓冲区大小,单位字节。默认为 16777216 (16MB)。"
msgid "Unified xkcptun server dynamic gateway. The server listens on a single UDP port and dynamically routes each client tunnel to its requested backend target."
msgstr "统一 xkcptun 服务端动态网关。服务端在单个 UDP 端口上监听,并动态将每个客户端隧道路由到其请求的后端目标。"
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-zerotier PKG_NAME:=luci-app-zerotier
PKG_VERSION:=2.2 PKG_VERSION:=2.2
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI for ZeroTier LUCI_TITLE:=LuCI for ZeroTier
+39
View File
@@ -97,6 +97,8 @@ root/etc/zerotier/
root/usr/share/rpcd/acl.d/ # ACL groups (UCI access + RPC methods) root/usr/share/rpcd/acl.d/ # ACL groups (UCI access + RPC methods)
root/usr/share/ucitrack/ # Reload-on-apply registration root/usr/share/ucitrack/ # Reload-on-apply registration
root/lib/upgrade/keep.d/ # Sysupgrade backup list (/etc/zerotier*) root/lib/upgrade/keep.d/ # Sysupgrade backup list (/etc/zerotier*)
po/zh_Hans/ # Simplified Chinese translation + gen_tr.py (regenerates the client catalog)
htdocs/luci-static/zerotier/ # Client-side translation catalog (generated, see po/zh_Hans/gen_tr.py)
``` ```
## Firewall Rules (when NAT=1) ## Firewall Rules (when NAT=1)
@@ -210,6 +212,43 @@ stderr and continues. Two consequences are handled explicitly:
## Changelog ## Changelog
### v2.2-r42
**Simplified Chinese, end to end**
- **227 strings translated** (104 inherited from the original app's partial
catalog, 123 new -- the entire Remote Controller page had never been
translated). One po, two delivery paths, because on this firmware
translation reaches the user through two different mechanisms that were
both broken for this app.
- **The server path**: the ucode dispatcher loads `<domain>.<lang>.lmo` from
`/usr/lib/lua/luci/i18n/` per request and translates menu titles with it.
This app had no `.lmo`, so its menu entries were the lone English items in
an otherwise Chinese menu. The `po2lmo` tool no longer exists at any
fetchable upstream path, so the binary format was reverse-engineered from
the firmware's own `base.zh-cn.lmo`: 16-byte big-endian index entries
`(hash, 1, offset, length)`, sorted by hash, plural expression at hash 0,
trailing u32 = index offset. Verified against six known pairs (Log in→登录,
Dismiss→忽略, …) before anything was built with it.
- **The client path**: JS views translate through cbi.js's `_()`, which looks
up `window.TR[sfh(trimws(s))]` -- and on this firmware **nothing ever
populates window.TR**. Not the dispatcher, not the header templates, not
any theme asset; the login page is Chinese only because it renders
server-side. So the app now ships its own catalog (`tr-zh-cn.js`, keys in
the exact hex form cbi.js hashes) and installs it from each view before
the first string is needed, gated on the document's language. Idempotent
and harmless on firmware that grows a bridge.
- Verified with the router's **real cbi.js `_()`/`sfh` code** run against
the generated catalog: 226 of 227 round-trip exactly (the 227th is
`Remove`/`Remove ` colliding under trimws -- same word, trailing space).
That harness is also what caught the first attempt shipping a broken
catalog: the keys were decimal where cbi.js hashes hex, and an empty
intermediate got deployed before the check existed. The check runs last
now, not first.
- `po/zh_Hans/gen_tr.py` regenerates the client catalog from the po
(idempotent; committed so the next string addition does not need the
reverse-engineering again).
### v2.2-r41 ### v2.2-r41
**Three hardening items from the review, one honestly retired** **Three hardening items from the review, one honestly retired**
@@ -6,7 +6,19 @@
'require poll'; 'require poll';
'require rpc'; 'require rpc';
var callLuciZerotierStatus = rpc.declare({
/* This firmware ships no client-side i18n bridge: cbi.js's _() reads
* window.TR but nothing populates it, so JS views render English even with
* .lmo catalogs installed. Install our own catalog before the first string
* is needed; idempotent and harmless on firmware that does bridge. */
var ZT_TR = /^zh/i.test(document.documentElement.lang || '')
? new Promise(function(resolve) {
var s = document.createElement('script');
s.onload = s.onerror = function() { resolve(); };
s.src = L.env.resource + '/zerotier/tr-zh-cn.js';
document.head.appendChild(s);
})
: Promise.resolve();var callLuciZerotierStatus = rpc.declare({
object: 'luci-zerotier', object: 'luci-zerotier',
method: 'status' method: 'status'
}); });
@@ -35,6 +47,7 @@ var maskSecret = function(s) {
return view.extend({ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
ZT_TR,
uci.load('zerotier') uci.load('zerotier')
]); ]);
}, },
@@ -4,7 +4,19 @@
'require poll'; 'require poll';
'require rpc'; 'require rpc';
var callLuciZerotierStatus = rpc.declare({
/* This firmware ships no client-side i18n bridge: cbi.js's _() reads
* window.TR but nothing populates it, so JS views render English even with
* .lmo catalogs installed. Install our own catalog before the first string
* is needed; idempotent and harmless on firmware that does bridge. */
var ZT_TR = /^zh/i.test(document.documentElement.lang || '')
? new Promise(function(resolve) {
var s = document.createElement('script');
s.onload = s.onerror = function() { resolve(); };
s.src = L.env.resource + '/zerotier/tr-zh-cn.js';
document.head.appendChild(s);
})
: Promise.resolve();var callLuciZerotierStatus = rpc.declare({
object: 'luci-zerotier', object: 'luci-zerotier',
method: 'status' method: 'status'
}); });
@@ -45,7 +57,7 @@ var callLuciZerotierSync = rpc.declare({
return view.extend({ return view.extend({
load: function() { load: function() {
return Promise.resolve(); return ZT_TR;
}, },
render: function() { render: function() {
@@ -4,7 +4,19 @@
'require rpc'; 'require rpc';
'require uci'; 'require uci';
/*
/* This firmware ships no client-side i18n bridge: cbi.js's _() reads
* window.TR but nothing populates it, so JS views render English even with
* .lmo catalogs installed. Install our own catalog before the first string
* is needed; idempotent and harmless on firmware that does bridge. */
var ZT_TR = /^zh/i.test(document.documentElement.lang || '')
? new Promise(function(resolve) {
var s = document.createElement('script');
s.onload = s.onerror = function() { resolve(); };
s.src = L.env.resource + '/zerotier/tr-zh-cn.js';
document.head.appendChild(s);
})
: Promise.resolve();/*
* Remote Controller / Moon management. * Remote Controller / Moon management.
* *
* The LuCI page lives on the OpenWrt router, but the Controller and Moon root * The LuCI page lives on the OpenWrt router, but the Controller and Moon root
@@ -1041,7 +1053,9 @@ function moonPanel(section) {
/* --------------------------------------------------------------------- page */ /* --------------------------------------------------------------------- page */
return view.extend({ return view.extend({
load: function() {}, load: function() {
return ZT_TR;
},
render: function() { render: function() {
var listBox = E('div', {}); var listBox = E('div', {});
@@ -0,0 +1,5 @@
/* Generated from po/zh_Hans/zerotier.po - do not edit by hand.
* Keys are sfh(trimws(msgid)) in hex, exactly what cbi.js _() looks up.
* This firmware ships no window.TR bridge of its own. */
window.TR = window.TR || {};
Object.assign(window.TR, {"7ba57fa5":"(控制器端口 ","005fb0ba":" —— 成员将不会从此地址池获得地址。","928f10ca":"节点地址必须为 10 位十六进制数字","4f25ffed":"地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。","623124cd":"ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。","038a5486":"操作","e733b525":"主动桥接","300fe5c8":"添加","3698d6ed":"添加远程主机","5598797e":"加入 Moon","d252deef":"添加地址池","e5b147b7":"添加远程主机","ea8fae46":"添加路由","71677946":"地址","a4a2e2e7":"高级设置","6ee64833":"允许默认路由","d24721ab":"允许 DNS","51aaeef3":"允许全局","e575f233":"允许管理","d6d5a047":"允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)","0b830d67":"正在分配地址…","2e4746c8":"授权","18bbde5a":"授权失败","ececebcc":"已授权","e3f664db":"自动允许客户端 NAT","2014c910":"自动 NAT: 已禁用","228abc80":"自动 NAT: 已启用","f2ffec55":"正在备份...","bdabf869":"备份失败","5eca6072":"立即备份","379bb677":"备份请求失败","f9fb6815":"基本设置","6b3b39f7":"广播","fcf89e62":"构建中…","ae5deb3a":"取消","08f23f6b":"无法连接到 ZeroTier 服务","4bd163d1":"无法读取 moon(ZeroTier 服务是否在运行?)","c06f3259":"正在收集数据...","8462484d":"正在获取地址...","cd2f6c29":"运行配置目录","8f48fe99":"配置","6e72b101":"已配置的主机","f1d119dc":"控制器端口","5d359cf2":"控制器可经隧道访问","41e439d8":"控制器状态存在","00a35b8a":"运行配置目录保护","bd7030be":"使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)","88720483":"无法构建方案","a1a69329":"无法获取成员列表","c53244dc":"无法连接控制器","2c150360":"无法读取此网络的任何成员","c1dfeb1a":"以下网络无法读取,因此未显示:","f2248b2d":"无法刷新成员列表;表格保持不变","cbb3b741":"无法保存主机","56370eb4":"创建并签署 Moon","e3a2eb47":"创建失败","eb9bb863":"创建网络","6c94b9b0":"点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)","9a74d36d":"删除","b3b872fe":"删除失败","51d36b3d":"删除网络 ","f89b7324":"退出失败","88643bc2":"退出请求失败","be021833":"设备","a0c974c7":"诊断","6469d71b":"诊断失败","00c6b479":"将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。","799c81a7":"下载 .moon","000e380f":"编辑","2f2c0439":"编辑远程主机","814db5a8":"留空 = 直连","78c2105b":"启用","17074f70":"端点","037e2ea1":"错误","be7c6b14":"加载 moon 列表出错","ef0771c3":"加载网络信息失败","eef2dd18":"加载节点信息失败","a285946d":"读取 moon 列表出错","b4b44266":"服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload","d0e77240":"按名称、地址或 IP 筛选","9865b44d":"起始 IP","a62c536c":"常规设置","764447db":"全局设置","8686381d":"隐藏","24553b14":"远程管理的工作原理","42933f28":"接口信息","d08681fe":"IP 地址","ad69d4ef":"IP 分配","fbb92dcf":"IP 地址池","7e11b65e":"标签","f2929f6f":"结束 IP","c6ce32d3":"延迟","22038f1d":"退出","4e72d9f1":"留空则保留已存储的密钥","2b1fb9a6":"正在退出 moon...","dee10879":"已退出 Moon","9c0e750d":"链路","10e6bd0b":"加载中...","c2b71057":"本地配置文件","b259d7d8":"管理","949429e8":"托管 IP 必须为 IPv4 地址,以逗号分隔","cdd88b7c":"成员 ID","cdc6d88f":"成员名称","16916c1d":"成员","9c03b236":"个成员","a0ddf226":"Moon","b7ca31c5":"Moon 已创建:","c2e07f85":"Moon 创建失败","bbf90d15":"Moon ID","1af43404":"Moon ID 必须是 10 位十六进制字符(例如 123456abcd)","119eb928":"Moon 种子节点","793bcbfa":"Moon 种子必须是该 moon 某个根节点的 10 位地址","2e8cf7ed":"本节点并未加入该 moon","9a9a1e82":"主机上的 moon.json","7ab90fdf":"Moon","ff21d2cf":"MTU","a59f3806":"多播限制","8d8bb35f":"我的控制器","d785d68e":"名称","1cbaf231":"网络 ID","fadba037":"网络 ID 必须是 16 位十六进制字符(0-9, a-f)","e4aff468":"网络","3eaa20dd":"从未连接","53386700":"否","20961bde":"没有可 Ping 的 IP","70454625":"尚无成员加入此网络。","0887813c":"没有匹配的成员。","867914a1":"未加入任何 moLine truncated
+50
View File
@@ -0,0 +1,50 @@
import os, re, json, sys
HERE = os.path.dirname(os.path.abspath(__file__))
PO = os.path.join(HERE, 'zerotier.po')
OUT = os.path.join(HERE, '..', '..', 'htdocs', 'luci-static', 'zerotier', 'tr-zh-cn.js')
def sfh(s):
def u16(b, o): return ((b[o+1] << 8) + b[o]) & 0xFFFFFFFF
def s8(b, o): return b[o] if b[o] < 128 else b[o] - 256
b = s.encode(); M = 0xFFFFFFFF
h, ln, off = len(b) & M, len(b) // 4, 0
while ln:
ln -= 1
h = (h + u16(b, off)) & M
t = ((u16(b, off+2) << 11) ^ h) & M
h = ((h << 16) ^ t) & M
h = (h + (h >> 11)) & M
off += 4
r = len(b) & 3
if r == 3:
h = (h + u16(b, off)) & M; h = (h ^ ((h << 16) & M)) & M
h = (h ^ ((s8(b, off+2) << 18) & M)) & M; h = (h + (h >> 11)) & M
elif r == 2:
h = (h + u16(b, off)) & M; h = (h ^ ((h << 11) & M)) & M; h = (h + (h >> 17)) & M
elif r == 1:
h = (h + s8(b, off)) & M; h = (h ^ ((h << 10) & M)) & M; h = (h + (h >> 1)) & M
for s_, a in ((3, 5), (4, 17), (25, 6)):
h = (h ^ ((h << s_) & M)) & M; h = (h + (h >> a)) & M
return format(h, '08x')
def unesc(s): return s.replace('\\"', '"').replace('\\n', '\n').replace('\\t', '\t')
def trimws(s): return re.sub(r'[ \t\n]+', ' ', s.strip())
pairs = re.findall(r'msgid "((?:[^"\\]|\\.)*)"\nmsgstr "((?:[^"\\]|\\.)*)"', open(PO, encoding='utf-8').read())
msgs = [(unesc(a), unesc(b)) for a, b in pairs if a]
assert len(msgs) == 227, len(msgs)
tr = {}
for mid, t in msgs:
tr[sfh(trimws(mid))] = t
assert len(tr) >= 220, len(tr)
assert sfh('Remote Controller') in tr
assert tr[sfh('Remote Controller')] == '远程控制器'
body = ('/* Generated from po/zh_Hans/zerotier.po - do not edit by hand.\n'
' * Keys are sfh(trimws(msgid)) in hex, exactly what cbi.js _() looks up.\n'
' * This firmware ships no window.TR bridge of its own. */\n'
'window.TR = window.TR || {};\n'
'Object.assign(window.TR, ' + json.dumps(tr, ensure_ascii=False, separators=(',', ':')) + ');\n')
open(OUT, 'w', encoding='utf-8').write(body)
print('OK', len(tr), '键,字节数', len(body.encode()))
+607 -225
View File
@@ -1,164 +1,59 @@
msgid "Zerotier is an open source, cross-platform and easy to use virtual LAN" msgid ""
msgstr "Zerotier 是一个开源,跨平台,而且适合内网穿透互联的傻瓜配置虚拟 VPN LAN" msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: \n"
"PO-Revision-Date: 2026-10-10 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Auto NAT Clients" msgid " (controller port "
msgstr "自动允许客户端 NAT" msgstr "(控制器端口 "
msgid "Allow zerotier clients access your LAN network" msgid " — members will not receive an address from this pool."
msgstr "允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)" msgstr " —— 成员将不会从此地址池获得地址。"
msgid "Create or manage your zerotier network, and auth clients who could access" msgid "A node address is exactly 10 hex digits"
msgstr "点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)" msgstr "节点地址必须为 10 位十六进制数字"
msgid "Base Setting" msgid "A pool needs a route covering the same subnet, otherwise members receive no address."
msgstr "基本设置" msgstr "地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。"
msgid "Interface Info" msgid "A ZeroTier controller or moon root must live on a server with a fixed public IP, reachable by SSH, with passwordless sudo for that user. The controller API has no TLS and refuses every non-loopback caller, so this page never talks to it directly: it opens an SSH tunnel from this router to the remote loopback interface and sends API calls through it. The controller authtoken is read over SSH on demand and is never stored here."
msgstr "接口信息" msgstr "ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。"
msgid "General options" msgid "Actions"
msgstr "常规设置" msgstr "操作"
msgid "Advanced options" msgid "Active bridge"
msgstr "高级设置" msgstr "主动桥接"
msgid "Port of zerotier service, default 9993" msgid "Add"
msgstr "ZeroTier 运行端口,默认是 9993" msgstr "添加"
msgid "Secret" msgid "Add a remote host"
msgstr "节点密钥" msgstr "添加远程主机"
msgid "Secret of zerotier client. Displayed masked for security; paste a new identity.secret to replace it, or clear the field to remove it (a new identity is generated on next start)." msgid "Add Moon"
msgstr "客户端密钥(identity.secret)。为安全起见显示为掩码;粘贴新密钥可替换,清空输入框则删除(下次启动时自动生成新身份)。" msgstr "加入 Moon"
msgid "Configuration folder" msgid "Add pool"
msgstr "运行配置目录" msgstr "添加地址池"
msgid "Persistent configuration folder (for ZT controller mode)" msgid "Add remote host"
msgstr "持久化运行配置目录(一般用于控制器模式),默认为空" msgstr "添加远程主机"
msgid "Copy configuration folder" msgid "Add route"
msgstr "运行配置目录保护" msgstr "添加路由"
msgid "Copy configuration folder to RAM to prevent writing to flash (for ZT controller mode)"
msgstr "使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)"
msgid "Local configuration"
msgstr "本地配置文件"
msgid "Path to the local.conf. Imported into the configuration folder on save & apply."
msgstr "local.conf 文件路径。保存并应用时会导入到配置目录中。"
msgid "Global Settings"
msgstr "全局设置"
msgid "Address" msgid "Address"
msgstr "地址" msgstr "地址"
msgid "Network ID" msgid "Advanced options"
msgstr "网络 ID" msgstr "高级设置"
msgid "Name"
msgstr "名称"
msgid "Status"
msgstr "状态"
msgid "Device"
msgstr "设备"
msgid "IP Address"
msgstr "IP 地址"
msgid "Version"
msgstr "版本"
msgid "Role"
msgstr "角色"
msgid "Latency"
msgstr "延迟"
msgid "Link"
msgstr "链路"
msgid "Path"
msgstr "路径"
msgid "Networks"
msgstr "网络"
msgid "Peers"
msgstr "节点"
msgid "Ping All"
msgstr "全部 Ping"
msgid "Pinging..."
msgstr "正在 Ping..."
msgid "Online"
msgstr "在线"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Collecting data..."
msgstr "正在收集数据..."
msgid "Collecting identity..."
msgstr "正在获取地址..."
msgid "Loading..."
msgstr "加载中..."
msgid "No networks joined"
msgstr "未加入任何网络"
msgid "No peers"
msgstr "无节点"
msgid "No IPs to ping"
msgstr "没有可 Ping 的 IP"
msgid "Error"
msgstr "错误"
msgid "Error loading networks"
msgstr "加载网络信息失败"
msgid "Error loading peers"
msgstr "加载节点信息失败"
msgid "Auto NAT: Enabled"
msgstr "自动 NAT: 已启用"
msgid "Auto NAT: Disabled"
msgstr "自动 NAT: 已禁用"
msgid "Service reload failed: "
msgstr "服务重载失败: "
msgid "Unknown error"
msgstr "未知错误"
msgid "Failed to reload service, please run manually: /etc/init.d/luci-zerotier reload"
msgstr "服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload"
msgid "Path must be within /etc/zerotier, or /tmp"
msgstr "路径必须在 /etc/zerotier 或 /tmp 目录下"
msgid "Path must be within /etc/zerotier"
msgstr "路径必须在 /etc/zerotier 目录下"
msgid "Allow Managed"
msgstr "允许管理"
msgid "Allow Global"
msgstr "允许全局"
msgid "Allow Default Route" msgid "Allow Default Route"
msgstr "允许默认路由" msgstr "允许默认路由"
@@ -166,77 +61,137 @@ msgstr "允许默认路由"
msgid "Allow DNS" msgid "Allow DNS"
msgstr "允许 DNS" msgstr "允许 DNS"
msgid "Network ID must be 16 hex characters (0-9, a-f)" msgid "Allow Global"
msgstr "网络 ID 必须是 16 位十六进制字符(0-9, a-f)" msgstr "允许全局"
msgid "Scan already in progress, please wait" msgid "Allow Managed"
msgstr "扫描正在进行中,请等待完成" msgstr "允许管理"
msgid "Backup Now" msgid "Allow zerotier clients access your LAN network"
msgstr "立即备份" msgstr "允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)"
msgid "assigning address..."
msgstr "正在分配地址…"
msgid "Authorize"
msgstr "授权"
msgid "Authorize failed"
msgstr "授权失败"
msgid "Authorized"
msgstr "已授权"
msgid "Auto NAT Clients"
msgstr "自动允许客户端 NAT"
msgid "Auto NAT: Disabled"
msgstr "自动 NAT: 已禁用"
msgid "Auto NAT: Enabled"
msgstr "自动 NAT: 已启用"
msgid "Backing up..." msgid "Backing up..."
msgstr "正在备份..." msgstr "正在备份..."
msgid "Save the current runtime state to the persistent configuration folder. Recommended after adding a moon with zerotier orbit, and essential for controller nodes (member data lives only in the runtime directory)."
msgstr "将当前运行时状态保存到持久化配置目录。建议在用 zerotier orbit 添加 moon 后备份;对 controller 节点尤为关键(成员数据仅存于运行时目录)。"
msgid "Runtime state backed up to persistent storage"
msgstr "运行时状态已备份到持久化存储"
msgid "Backup failed" msgid "Backup failed"
msgstr "备份失败" msgstr "备份失败"
msgid "Backup Now"
msgstr "立即备份"
msgid "Backup request failed" msgid "Backup request failed"
msgstr "备份请求失败" msgstr "备份请求失败"
msgid "Moons" msgid "Base Setting"
msgstr "Moon" msgstr "基本设置"
msgid "Orbiting a moon adds its roots to this node's root set. ZeroTier keeps using the planetary roots as fallback. Note that upstream ZeroTier no longer recommends private moons and does not support them under its SLA." msgid "Broadcast"
msgstr "加入 moon 会把它的根节点加入本节点的根集合,ZeroTier 仍会回落到 planet 根节点。注意:上游 ZeroTier 已不再推荐私有 moon,且不在其 SLA 支持范围内。" msgstr "广播"
msgid "Add Moon" msgid "Building..."
msgstr "加入 Moon" msgstr "构建中…"
msgid "Moon ID" msgid "Cancel"
msgstr "Moon ID" msgstr "取消"
msgid "Moon Seed" msgid "Cannot reach the ZeroTier service"
msgstr "Moon 种子节点" msgstr "无法连接到 ZeroTier 服务"
msgid "The 10-digit address of one of the moon's roots. ZeroTier fetches the moon definition from it." msgid "Cannot read moons (is the ZeroTier service running?)"
msgstr "moon 某个根节点的 10 位地址,ZeroTier 将从它获取 moon 定义文件。" msgstr "无法读取 moon(ZeroTier 服务是否在运行?)"
msgid "Moon ID must be 10 hexadecimal digits (e.g. 123456abcd)" msgid "Collecting data..."
msgstr "Moon ID 必须是 10 位十六进制字符(例如 123456abcd)" msgstr "正在收集数据..."
msgid "Moon Seed must be the 10-digit address of one of the moon's roots" msgid "Collecting identity..."
msgstr "Moon 种子必须是该 moon 某个根节点的 10 位地址" msgstr "正在获取地址..."
msgid "Orbiting..." msgid "Configuration folder"
msgstr "正在加入..." msgstr "运行配置目录"
msgid "Orbited Moon" msgid "Configure"
msgstr "已加入 Moon" msgstr "配置"
msgid "Orbit failed" msgid "Configured hosts"
msgstr "加入失败" msgstr "已配置的主机"
msgid "Orbit request failed" msgid "Controller port"
msgstr "加入请求失败" msgstr "控制器端口"
msgid "Left Moon" msgid "Controller reachable via tunnel"
msgstr "已退出 Moon" msgstr "控制器可经隧道访问"
msgid "Leaving moon..." msgid "Controller state present"
msgstr "正在退出 moon..." msgstr "控制器状态存在"
msgid "Leave" msgid "Copy configuration folder"
msgstr "退出" msgstr "运行配置目录保护"
msgid "Moon was not orbited" msgid "Copy configuration folder to RAM to prevent writing to flash (for ZT controller mode)"
msgstr "本节点并未加入该 moon" msgstr "使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)"
msgid "Could not build a plan"
msgstr "无法构建方案"
msgid "Could not list members"
msgstr "无法获取成员列表"
msgid "Could not reach the controller"
msgstr "无法连接控制器"
msgid "Could not read any member of this network"
msgstr "无法读取此网络的任何成员"
msgid "Could not read these networks, so they are not shown: "
msgstr "以下网络无法读取,因此未显示:"
msgid "Could not refresh the member list; the table is unchanged"
msgstr "无法刷新成员列表;表格保持不变"
msgid "Could not save the host"
msgstr "无法保存主机"
msgid "Create and sign the moon"
msgstr "创建并签署 Moon"
msgid "Create failed"
msgstr "创建失败"
msgid "Create network"
msgstr "创建网络"
msgid "Create or manage your zerotier network, and auth clients who could access"
msgstr "点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)"
msgid "Delete"
msgstr "删除"
msgid "Delete failed"
msgstr "删除失败"
msgid "Delete network "
msgstr "删除网络 "
msgid "Deorbit failed" msgid "Deorbit failed"
msgstr "退出失败" msgstr "退出失败"
@@ -244,53 +199,305 @@ msgstr "退出失败"
msgid "Deorbit request failed" msgid "Deorbit request failed"
msgstr "退出请求失败" msgstr "退出请求失败"
msgid "Roots" msgid "Device"
msgstr "根节点数" msgstr "设备"
msgid "Diagnostics"
msgstr "诊断"
msgid "Diagnostics failed"
msgstr "诊断失败"
msgid "Distribute this file to members, then use Interface Info → Moons → Add Moon on each node. The signing secret never left the remote host."
msgstr "将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。"
msgid "Download .moon"
msgstr "下载 .moon"
msgid "Edit"
msgstr "编辑"
msgid "Edit remote host"
msgstr "编辑远程主机"
msgid "empty = direct"
msgstr "留空 = 直连"
msgid "Enable"
msgstr "启用"
msgid "Endpoints" msgid "Endpoints"
msgstr "端点" msgstr "端点"
msgid "Storage" msgid "Error"
msgstr "持久化" msgstr "错误"
msgid "persistent"
msgstr "已持久化"
msgid "waiting for the moon definition - this orbit is not persistent yet"
msgstr "正在等待 moon 定义 — 该 join 尚未持久化"
msgid "No moons orbited"
msgstr "未加入任何 moon"
msgid "Cannot read moons (is the ZeroTier service running?)"
msgstr "无法读取 moon(ZeroTier 服务是否在运行?)"
msgid "Error reading moon list"
msgstr "读取 moon 列表出错"
msgid "Error loading moons" msgid "Error loading moons"
msgstr "加载 moon 列表出错" msgstr "加载 moon 列表出错"
msgid "Registering the orbit..." msgid "Error loading networks"
msgstr "正在注册 orbit..." msgstr "加载网络信息失败"
msgid "Error loading peers"
msgstr "加载节点信息失败"
msgid "Error reading moon list"
msgstr "读取 moon 列表出错"
msgid "Failed to reload service, please run manually: /etc/init.d/luci-zerotier reload"
msgstr "服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload"
msgid "Filter by name, address or IP"
msgstr "按名称、地址或 IP 筛选"
msgid "First IP"
msgstr "起始 IP"
msgid "General options"
msgstr "常规设置"
msgid "Global Settings"
msgstr "全局设置"
msgid "Hide"
msgstr "隐藏"
msgid "How remote management works"
msgstr "远程管理的工作原理"
msgid "Interface Info"
msgstr "接口信息"
msgid "IP Address"
msgstr "IP 地址"
msgid "IP assignment"
msgstr "IP 分配"
msgid "IP assignment pools"
msgstr "IP 地址池"
msgid "Label"
msgstr "标签"
msgid "Last IP"
msgstr "结束 IP"
msgid "Latency"
msgstr "延迟"
msgid "Leave"
msgstr "退出"
msgid "Leave blank to keep the stored key"
msgstr "留空则保留已存储的密钥"
msgid "Leaving moon..."
msgstr "正在退出 moon..."
msgid "Left Moon"
msgstr "已退出 Moon"
msgid "Link"
msgstr "链路"
msgid "Loading..."
msgstr "加载中..."
msgid "Local configuration"
msgstr "本地配置文件"
msgid "Manage"
msgstr "管理"
msgid "Managed IPs must be IPv4 addresses, comma-separated"
msgstr "托管 IP 必须为 IPv4 地址,以逗号分隔"
msgid "Member ID"
msgstr "成员 ID"
msgid "Member name"
msgstr "成员名称"
msgid "Members"
msgstr "成员"
msgid "members"
msgstr "个成员"
msgid "Moon"
msgstr "Moon"
msgid "Moon created: "
msgstr "Moon 已创建:"
msgid "Moon creation failed"
msgstr "Moon 创建失败"
msgid "Moon ID"
msgstr "Moon ID"
msgid "Moon ID must be 10 hexadecimal digits (e.g. 123456abcd)"
msgstr "Moon ID 必须是 10 位十六进制字符(例如 123456abcd)"
msgid "Moon Seed"
msgstr "Moon 种子节点"
msgid "Moon Seed must be the 10-digit address of one of the moon's roots"
msgstr "Moon 种子必须是该 moon 某个根节点的 10 位地址"
msgid "Moon was not orbited"
msgstr "本节点并未加入该 moon"
msgid "moon.json on host"
msgstr "主机上的 moon.json"
msgid "Moons"
msgstr "Moon"
msgid "MTU"
msgstr "MTU"
msgid "Multicast limit"
msgstr "多播限制"
msgid "My controller"
msgstr "我的控制器"
msgid "Name"
msgstr "名称"
msgid "Network ID"
msgstr "网络 ID"
msgid "Network ID must be 16 hex characters (0-9, a-f)"
msgstr "网络 ID 必须是 16 位十六进制字符(0-9, a-f)"
msgid "Networks"
msgstr "网络"
msgid "never connected"
msgstr "从未连接"
msgid "no"
msgstr "否"
msgid "No IPs to ping"
msgstr "没有可 Ping 的 IP"
msgid "No members have joined this network yet."
msgstr "尚无成员加入此网络。"
msgid "No members match."
msgstr "没有匹配的成员。"
msgid "No moons orbited"
msgstr "未加入任何 moon"
msgid "No networks joined"
msgstr "未加入任何网络"
msgid "No peers"
msgstr "无节点"
msgid "no pool assigned"
msgstr "未分配地址池"
msgid "No pool — members get no managed address"
msgstr "无地址池——成员不会获得托管地址"
msgid "No remote hosts configured yet. Add one to manage a Controller or Moon that runs on a separate server."
msgstr "尚未配置远程主机。添加一台以管理运行在其它服务器上的控制器或 Moon。"
msgid "No route"
msgstr "无路由"
msgid "No route covers "
msgstr "无路由覆盖 "
msgid "No SSH key — add one before use"
msgstr "无 SSH 密钥——请先添加再使用"
msgid "no — required"
msgstr "否——必需"
msgid "Node address"
msgstr "节点地址"
msgid "Node address (10 hex)"
msgstr "节点地址(10 位十六进制)"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Online"
msgstr "在线"
msgid "Operating system"
msgstr "操作系统"
msgid "Orbit failed"
msgstr "加入失败"
msgid "Orbit registered. The moon definition is still being fetched from the seed - it will appear below once it arrives." msgid "Orbit registered. The moon definition is still being fetched from the seed - it will appear below once it arrives."
msgstr "orbit 已注册。moon 定义仍在从种子节点获取中,到达后会显示在下方。" msgstr "orbit 已注册。moon 定义仍在从种子节点获取中,到达后会显示在下方。"
msgid "saving..." msgid "Orbit request failed"
msgstr "正在保存..." msgstr "加入请求失败"
msgid "Saving to the persistent folder automatically. Until then the orbit is lost on daemon restart. If it stays in this state, use \"Backup Now\" under Settings -> Advanced options." msgid "Orbited Moon"
msgstr "正在自动保存到持久化目录。在此之前,重启守护进程会丢失该 orbit。若长期停留在此状态,请到“基本设置 → 高级设置”中点击“立即备份”。" msgstr "已加入 Moon"
msgid "Add" msgid "Orbiting a moon adds its roots to this node's root set. ZeroTier keeps using the planetary roots as fallback. Note that upstream ZeroTier no longer recommends private moons and does not support them under its SLA."
msgstr "添加" msgstr "加入 moon 会把它的根节点加入本节点的根集合,ZeroTier 仍会回落到 planet 根节点。注意:上游 ZeroTier 已不再推荐私有 moon,且不在其 SLA 支持范围内。"
msgid "Cancel" msgid "Orbiting..."
msgstr "取消" msgstr "正在加入..."
msgid "Actions" msgid "Passwordless sudo"
msgstr "操作" msgstr "免密 sudo"
msgid "Path"
msgstr "路径"
msgid "Path must be within /etc/zerotier"
msgstr "路径必须在 /etc/zerotier 目录下"
msgid "Path must be within /etc/zerotier, or /tmp"
msgstr "路径必须在 /etc/zerotier 或 /tmp 目录下"
msgid "Path to the local.conf. Imported into the configuration folder on save & apply."
msgstr "local.conf 文件路径。保存并应用时会导入到配置目录中。"
msgid "Peer address / latency"
msgstr "对端地址 / 延迟"
msgid "Peer status"
msgstr "对端状态"
msgid "Peer status unavailable"
msgstr "对端状态不可用"
msgid "Peers"
msgstr "节点"
msgid "persistent"
msgstr "已持久化"
msgid "Persistent configuration folder (for ZT controller mode)"
msgstr "持久化运行配置目录(一般用于控制器模式),默认为空"
msgid "Ping All"
msgstr "全部 Ping"
msgid "Pinging..."
msgstr "正在 Ping..."
msgid "Pool bounds must be IPv4 addresses"
msgstr "地址池边界必须为 IPv4 地址"
msgid "Port"
msgstr "端口"
msgid "Port must be a number" msgid "Port must be a number"
msgstr "端口必须是数字" msgstr "端口必须是数字"
@@ -298,14 +505,189 @@ msgstr "端口必须是数字"
msgid "Port must be between 0 and 65535" msgid "Port must be between 0 and 65535"
msgstr "端口必须在 0 到 65535 之间" msgstr "端口必须在 0 到 65535 之间"
msgid "Cannot reach the ZeroTier service" msgid "Port of zerotier service, default 9993"
msgstr "无法连接到 ZeroTier 服务" msgstr "ZeroTier 运行端口,默认是 9993"
msgid "Private"
msgstr "私有"
msgid "Public IP or host"
msgstr "公网 IP 或主机名"
msgid "Refresh"
msgstr "刷新"
msgid "Registering the orbit..."
msgstr "正在注册 orbit..."
msgid "Remote Controller"
msgstr "远程控制器"
msgid "Remote host"
msgstr "远程主机"
msgid "Remote host report"
msgstr "远程主机报告"
msgid "Remove"
msgstr "移除"
msgid "Remove "
msgstr "移除 "
msgid "Remove member "
msgstr "移除成员 "
msgid "Rename"
msgstr "重命名"
msgid "Rename failed"
msgstr "重命名失败"
msgid "Required: paste the PRIVATE key of a keypair whose public half is in the remote ~/.ssh/authorized_keys"
msgstr "必填:粘贴密钥对的私钥,其公钥部分须已在远程主机的 ~/.ssh/authorized_keys 中"
msgid "Requirements: a fixed public IP · SSH access · a keypair whose public key is in the remote authorized_keys · passwordless sudo · ZeroTier built with the controller (1.14.2, or a 1.16 nonfree build)"
msgstr "要求:固定公网 IP · SSH 访问 · 公钥已加入远程 authorized_keys 的密钥对 · 免密 sudo · 带控制器支持的 ZeroTier(1.14.2 或 1.16 nonfree 构建)"
msgid "Role"
msgstr "角色"
msgid "Roots"
msgstr "根节点数"
msgid "Route target must be CIDR, e.g. 192.168.192.0/24"
msgstr "路由目标必须为 CIDR 格式,例如 192.168.192.0/24"
msgid "Route via must be an IPv4 address, or empty for a direct route"
msgstr "路由网关必须为 IPv4 地址,留空表示直连路由"
msgid "Routes"
msgstr "路由"
msgid "Run diagnostics"
msgstr "运行诊断"
msgid "RUNNING"
msgstr "运行中"
msgid "Running..."
msgstr "运行中…"
msgid "Runtime state backed up to persistent storage"
msgstr "运行时状态已备份到持久化存储"
msgid "Save"
msgstr "保存"
msgid "Save anyway?"
msgstr "仍然保存?"
msgid "Save the current runtime state to the persistent configuration folder. Recommended after adding a moon with zerotier orbit, and essential for controller nodes (member data lives only in the runtime directory)."
msgstr "将当前运行时状态保存到持久化配置目录。建议在用 zerotier orbit 添加 moon 后备份;对 controller 节点尤为关键(成员数据仅存于运行时目录)。"
msgid "Saving to the persistent folder automatically. Until then the orbit is lost on daemon restart. If it stays in this state, use \"Backup Now\" under Settings -> Advanced options."
msgstr "正在自动保存到持久化目录。在此之前,重启守护进程会丢失该 orbit。若长期停留在此状态,请到“基本设置 → 高级设置”中点击“立即备份”。"
msgid "saving..."
msgstr "正在保存..."
msgid "Scan already in progress, please wait"
msgstr "扫描正在进行中,请等待完成"
msgid "Secret"
msgstr "节点密钥"
msgid "Secret of zerotier client. Displayed masked for security; paste a new identity.secret to replace it, or clear the field to remove it (a new identity is generated on next start)."
msgstr "客户端密钥(identity.secret)。为安全起见显示为掩码;粘贴新密钥可替换,清空输入框则删除(下次启动时自动生成新身份)。"
msgid "Service reload failed: "
msgstr "服务重载失败: "
msgid "Service state"
msgstr "服务状态"
msgid "Show plan"
msgstr "查看方案"
msgid "Signed .moon files"
msgstr "已签署的 .moon 文件"
msgid "SSH key installed"
msgstr "SSH 密钥已安装"
msgid "SSH port"
msgstr "SSH 端口"
msgid "SSH private key"
msgstr "SSH 私钥"
msgid "SSH user"
msgstr "SSH 用户"
msgid "Status"
msgstr "状态"
msgid "Storage"
msgstr "持久化"
msgid "Target"
msgstr "目标"
msgid "The 10-digit address of one of the moon's roots. ZeroTier fetches the moon definition from it."
msgstr "moon 某个根节点的 10 位地址,ZeroTier 将从它获取 moon 定义文件。"
msgid "The controller allocates addresses from these ranges and reports them per member."
msgstr "控制器从这些地址段中分配地址,并按成员逐一报告。"
msgid "The following will run on the remote host:"
msgstr "以下命令将在远程主机上执行:"
msgid "The port the controller API listens on, e.g. 27893. Only reachable through the SSH tunnel."
msgstr "控制器 API 监听的端口,例如 27893。仅可经 SSH 隧道访问。"
msgid "The remote host becomes the single root of a new moon. Additional roots are not supported yet."
msgstr "远程主机将成为新 Moon 的唯一根节点。暂不支持额外根节点。"
msgid "This controller has no networks yet."
msgstr "此控制器尚无网络。"
msgid "This will create moon.json (holding the signing secret) and a signed .moon on the remote host. Continue?"
msgstr "这将在远程主机上创建 moon.json(含签署密钥)以及已签署的 .moon 文件。是否继续?"
msgid "Unknown error"
msgstr "未知错误"
msgid "unreadable"
msgstr "个不可读"
msgid "Update failed"
msgstr "更新失败"
msgid "updated"
msgstr "更新于"
msgid "Version"
msgstr "版本"
msgid "Via"
msgstr "网关"
msgid "waiting for the moon definition - this orbit is not persistent yet"
msgstr "正在等待 moon 定义 — 该 join 尚未持久化"
msgid "yes"
msgstr "是"
msgid "ZeroTier" msgid "ZeroTier"
msgstr "ZeroTier" msgstr "ZeroTier"
msgid "Enable" msgid "ZeroTier installed"
msgstr "启用" msgstr "ZeroTier 已安装"
msgid "Zerotier is an open source, cross-platform and easy to use virtual LAN"
msgstr "Zerotier 是一个开源,跨平台,而且适合内网穿透互联的傻瓜配置虚拟 VPN LAN"
msgid "ZeroTier version"
msgstr "ZeroTier 版本"
msgid "Port"
msgstr "端口"
+4
View File
@@ -12515,6 +12515,10 @@ msgstr ""
msgid "Very High" msgid "Very High"
msgstr "" msgstr ""
#: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:2934
msgid "Very Low"
msgstr ""
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229
msgid "Via" msgid "Via"
+4
View File
@@ -12929,6 +12929,10 @@ msgstr "正在校验上传的固件文件。"
msgid "Very High" msgid "Very High"
msgstr "非常高" msgstr "非常高"
#: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:2934
msgid "Very Low"
msgstr "很低"
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229
msgid "Via" msgid "Via"
+4
View File
@@ -12812,6 +12812,10 @@ msgstr "驗證上傳的映像檔中。"
msgid "Very High" msgid "Very High"
msgstr "超高速" msgstr "超高速"
#: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:2934
msgid "Very Low"
msgstr "很低"
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan.js:229
#: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229 #: protocols/luci-proto-vxlan/htdocs/luci-static/resources/protocol/vxlan6.js:229
msgid "Via" msgid "Via"
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Status Pages LUCI_TITLE:=LuCI Status Pages
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
PKG_RELEASE:=32 PKG_RELEASE:=33
PKG_BUILD_DEPENDS:=iwinfo PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
@@ -56,6 +56,9 @@ function addKnownPort(knownPorts, seenPorts, role, device, extra)
label: device label: device
}, extra || {}); }, extra || {});
if (port.label === 'cpu')
return;
if (!port.netdev && !port.swstate) if (!port.netdev && !port.swstate)
port.netdev = network.instantiateDevice(device); port.netdev = network.instantiateDevice(device);
@@ -211,7 +214,11 @@ function addResolvedPort(knownPorts, seenPorts, role, device, mapping, board, sw
}); });
} }
else { else {
addKnownPort(knownPorts, seenPorts, role, value); const deviceInfo = L.isObject(board) && L.isObject(board['network-device'])
? board['network-device'][value] : null;
addKnownPort(knownPorts, seenPorts, role, value,
L.isObject(deviceInfo) && isString(deviceInfo.label) ? { label: deviceInfo.label } : null);
} }
} }
} }
+3 -3
View File
@@ -20,12 +20,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-proto-wwand PKG_NAME:=luci-proto-wwand
PKG_VERSION:=1.6.9 PKG_VERSION:=1.7.0_pre1
PKG_RELEASE:=17 PKG_RELEASE:=18
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git
PKG_SOURCE_VERSION:=d2b2e85f5885581547f5863379fc44d64e9c9051 PKG_SOURCE_VERSION:=fb547fef6576e16ca0e83bd8379f82c27a040613
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only PKG_LICENSE:=GPL-2.0-only
+1 -1
View File
@@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme
LUCI_DEPENDS:= LUCI_DEPENDS:=
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_VERSION:=7.1 PKG_VERSION:=7.1
PKG_RELEASE:=10 PKG_RELEASE:=11
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=
CONFIG_LUCI_CSSTIDY:= CONFIG_LUCI_CSSTIDY:=
@@ -74,7 +74,7 @@
return Math.min(cardCount, 2); return Math.min(cardCount, 2);
} }
return Math.min(cardCount, 8); return Math.min(cardCount, 10);
} }
function syncOverviewPortStatus() { function syncOverviewPortStatus() {
+2 -2
View File
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437 MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise PKG_NAME:=mise
PKG_VERSION:=2026.10.5 PKG_VERSION:=2026.10.7
PKG_RELEASE:=31 PKG_RELEASE:=32
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/ PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip PKG_HASH:=skip
+1 -1
View File
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mptcp PKG_NAME:=mptcp
PKG_VERSION:=6.1 PKG_VERSION:=6.1
PKG_RELEASE:=22 PKG_RELEASE:=23
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org> PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+2 -2
View File
@@ -653,13 +653,13 @@ interface_multipath_settings() {
# every ifup (discussion #4368). 003-up adds the endpoint back in the # every ifup (discussion #4368). 003-up adds the endpoint back in the
# configured mode as soon as the recovery is confirmed (issue #4355). # configured mode as soon as the recovery is confirmed (issue #4355).
logger -t "MPTCP" "$iface tracker state is down, not adding it to MPTCP ($(multipath $iface))" logger -t "MPTCP" "$iface tracker state is down, not adding it to MPTCP ($(multipath $iface))"
elif ([ "$mode" = "master" ] || [ "$mode" = "on" ]) && [ -z "$(multipath $iface | grep default)" ]; then elif ([ "$mode" = "master" ] || [ "$mode" = "on" ]) && { [ -z "$(multipath $iface | grep default)" ] || ! multipath -i "$iface" "$id"; }; then
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)" logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
multipath "$iface" "on" "$id" multipath "$iface" "on" "$id"
elif ([ "$mode" = "off" ] || [ -z "$mode" ]) && [ -z "$(multipath $iface | grep deactivated)" ]; then elif ([ "$mode" = "off" ] || [ -z "$mode" ]) && [ -z "$(multipath $iface | grep deactivated)" ]; then
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)" logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
multipath "$iface" "$mode" "$id" multipath "$iface" "$mode" "$id"
elif [ "$mode" = "backup" ] && [ -z "$(multipath $iface | grep backup)" ]; then elif [ "$mode" = "backup" ] && { [ -z "$(multipath $iface | grep backup)" ] || ! multipath -i "$iface" "$id"; }; then
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)" logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
multipath "$iface" "$mode" "$id" multipath "$iface" "$mode" "$id"
fi fi
+94 -12
View File
@@ -17,6 +17,77 @@ else
exit 1 exit 1
fi fi
# From an "ip mptcp endpoint show" listing ($1), set id_used when an endpoint
# has the id $2 and id_holder to its device, and dev_ids to the endpoint ids
# of device $3. Lines are "ADDR [port P] id ID [FLAGS...] [dev DEV]".
endpoint_ids() {
local listing="$1" want="$2" dev="$3" ep_line ep_id ep_dev
id_used=""
id_holder=""
dev_ids=""
while IFS= read -r ep_line; do
ep_id=""
ep_dev=""
set -- $ep_line
while [ "$#" -gt 1 ]; do
case "$1" in
id) ep_id="$2" ;;
dev) ep_dev="$2" ;;
esac
shift
done
[ -n "$ep_id" ] || continue
[ "$ep_id" = "$want" ] && {
id_used=1
id_holder="$ep_dev"
}
[ -n "$dev" ] && [ "$ep_dev" = "$dev" ] && dev_ids="$dev_ids $ep_id"
done <<EOF
$listing
EOF
}
# Add an endpoint with the flags $@ for each global address ($IP) of $DEVICE.
# The explicit id ($ENDPOINT_ID, the WAN's metric) goes to the first address,
# the IPv4 one on a dual-stack WAN, the one the VPS pins a WAN by. Each
# endpoint needs its own id: with the same one for all, the IPv6 add failed
# ("Resource busy") and IPv6 was left out of MPTCP. The next addresses get
# id+128 then id+192, above the WAN metrics so that they never take the id
# of another WAN, and an address left without one is not added at all: an
# id picked by the kernel would be one of those WAN ids.
# When another device still holds the id of the first address, it is added
# with an id picked by the kernel instead of failing on every tracker check,
# and "multipath -i" tells the callers to fix it once the id is free.
add_endpoints() {
local addr addr_id n=0
for addr in $IP; do
addr_id=""
if [ -n "$ENDPOINT_ID" ]; then
case "$n" in
0) addr_id="$ENDPOINT_ID" ;;
1) addr_id=$((ENDPOINT_ID + 128)) ;;
2) addr_id=$((ENDPOINT_ID + 192)) ;;
*) addr_id=256 ;;
esac
endpoint_ids "$endpoint" "$addr_id" "$DEVICE"
if [ -n "$id_used" ] && [ "$id_holder" != "$DEVICE" ]; then
logger -t "MPTCP" "Endpoint id $addr_id for $addr on $DEVICE is used by ${id_holder:-another endpoint}"
addr_id=256
fi
if [ "$addr_id" -gt 255 ]; then
if [ "$n" -gt 0 ]; then
logger -t "MPTCP" "No endpoint id left for $addr on $DEVICE, not added to MPTCP"
n=$((n + 1))
continue
fi
addr_id=""
fi
fi
ip mptcp endpoint add "$addr" dev "$DEVICE" "$@" ${addr_id:+id "$addr_id"}
n=$((n + 1))
done
}
case $1 in case $1 in
"-h") "-h")
echo " Multipath-TCP configuration tool" echo " Multipath-TCP configuration tool"
@@ -34,6 +105,7 @@ case $1 in
echo "show MPTCP info: -m" echo "show MPTCP info: -m"
if [ -f /proc/sys/net/mptcp/enabled ]; then if [ -f /proc/sys/net/mptcp/enabled ]; then
echo "verify MPTCP interface: -v device" echo "verify MPTCP interface: -v device"
echo "check the endpoint id of an interface: -i device id"
fi fi
echo echo
echo "Flag on the device, to enable/disable MPTCP for this interface. The backup-flag" echo "Flag on the device, to enable/disable MPTCP for this interface. The backup-flag"
@@ -44,6 +116,8 @@ case $1 in
echo "(0-255) to pass to 'ip mptcp endpoint add ... id <id>' instead of letting the" echo "(0-255) to pass to 'ip mptcp endpoint add ... id <id>' instead of letting the"
echo "kernel auto-assign one. Needed for a stable id per WAN across reconnects, e.g." echo "kernel auto-assign one. Needed for a stable id per WAN across reconnects, e.g."
echo "so a peer (VPS) can pin traffic to this WAN via the subflow's remote_id." echo "so a peer (VPS) can pin traffic to this WAN via the subflow's remote_id."
echo "It goes to the first address of the device, the next ones get id+128 and"
echo "id+192 (IPv6 on a dual-stack WAN)."
echo echo
exit 0 ;; exit 0 ;;
"-c") "-c")
@@ -119,6 +193,22 @@ case $1 in
exit 1 exit 1
fi fi
;; ;;
"-i")
# Exit 1 when the endpoint of device $2 doesn't have the explicit id $3
# although no other endpoint holds it, so that recreating it gives that
# id back. An endpoint that kept an old id is never fixed by the mode
# checks of the callers, and it blocks the WAN that now has this id.
[ -f /proc/sys/net/mptcp/enabled ] || exit 0
case "$3" in
''|*[!0-9]*) exit 0 ;;
esac
[ "$3" -le 255 ] || exit 0
endpoint_ids "$(ip mptcp endpoint show)" "$3" "$2"
if [ -z "$dev_ids" ] || [ -n "$id_used" ]; then
exit 0
fi
exit 1
;;
"") "")
for ifpath in /sys/class/net/*; do for ifpath in /sys/class/net/*; do
$0 ${ifpath##*/} $0 ${ifpath##*/}
@@ -137,8 +227,6 @@ case "$ENDPOINT_ID" in
''|*[!0-9]*) ENDPOINT_ID="" ;; ''|*[!0-9]*) ENDPOINT_ID="" ;;
*) [ "$ENDPOINT_ID" -ge 0 ] 2>/dev/null && [ "$ENDPOINT_ID" -le 255 ] 2>/dev/null || ENDPOINT_ID="" ;; *) [ "$ENDPOINT_ID" -ge 0 ] 2>/dev/null && [ "$ENDPOINT_ID" -le 255 ] 2>/dev/null || ENDPOINT_ID="" ;;
esac esac
IDOPT=""
[ -n "$ENDPOINT_ID" ] && IDOPT="id $ENDPOINT_ID"
#FLAG_PATH=`find /sys/devices/ -path "*/net/$DEVICE/flags"` #FLAG_PATH=`find /sys/devices/ -path "*/net/$DEVICE/flags"`
[ -e "/sys/class/net/$DEVICE/" ] || { [ -e "/sys/class/net/$DEVICE/" ] || {
@@ -253,9 +341,7 @@ EOF
ip mptcp endpoint delete id $i >/dev/null 2>&1 ip mptcp endpoint delete id $i >/dev/null 2>&1
done done
} }
for i in $IP; do add_endpoints subflow fullmesh
ip mptcp endpoint add $i dev $DEVICE subflow fullmesh $IDOPT
done
exit 0;; exit 0;;
"signal") "signal")
[ -z "$IP" ] && { [ -z "$IP" ] && {
@@ -267,10 +353,8 @@ EOF
ip mptcp endpoint delete id $i >/dev/null 2>&1 ip mptcp endpoint delete id $i >/dev/null 2>&1
done done
} }
for i in $IP; do #add_endpoints signal subflow fullmesh
#ip mptcp endpoint add $i dev $DEVICE signal subflow fullmesh add_endpoints signal
ip mptcp endpoint add $i dev $DEVICE signal $IDOPT
done
exit 0;; exit 0;;
"backup") "backup")
[ -z "$IP" ] && { [ -z "$IP" ] && {
@@ -282,9 +366,7 @@ EOF
ip mptcp endpoint delete id $i >/dev/null 2>&1 ip mptcp endpoint delete id $i >/dev/null 2>&1
done done
} }
for i in $IP; do add_endpoints subflow backup fullmesh
ip mptcp endpoint add $i dev $DEVICE subflow backup fullmesh $IDOPT
done
exit 0;; exit 0;;
"") "")
case "$IFF" in case "$IFF" in
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more