🎨 Sync 2026-04-10 15:34:26

This commit is contained in:
github-actions[bot] 2026-04-10 15:34:26 +08:00
parent 03930f94a3
commit c2374cbb98
66 changed files with 6841 additions and 2210 deletions

View File

@ -1,16 +1,16 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=hickory-dns
PKG_VERSION:=2026.04.02~54a81de
PKG_RELEASE:=10
PKG_VERSION:=2026.04.09~34a4cca
PKG_RELEASE:=11
HICKORY_COMMIT:=54a81deefb24bb97df466b9032d5928345fffdde
HICKORY_COMMIT:=34a4cca1a707b642052b427938b7d1e8aaa71e3a
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/54a81deefb24bb97df466b9032d5928345fffdde?
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/34a4cca1a707b642052b427938b7d1e8aaa71e3a?
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-54a81deefb24bb97df466b9032d5928345fffdde
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-34a4cca1a707b642052b427938b7d1e8aaa71e3a
PKG_BUILD_DEPENDS:=rust/host
PKG_BUILD_PARALLEL:=1

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.1
PKG_RELEASE:=3
PKG_VERSION:=1.2.0
PKG_RELEASE:=4
LUCI_TITLE:=LuCI Support for momo
LUCI_DEPENDS:=+luci-base +momo

View File

@ -114,7 +114,7 @@ return view.extend({
o = s.option(form.Flag, 'scheduled_restart', _('Scheduled Restart'));
o.rmempty = false;
o = s.option(form.Value, 'cron_expression', _('Cron Expression'));
o = s.option(form.Value, 'scheduled_restart_cron', _('Scheduled Restart Cron'));
o.retain = true;
o.rmempty = false;
o.depends('scheduled_restart', '1');

View File

@ -24,35 +24,31 @@ return view.extend({
m = new form.Map('momo');
s = m.section(form.NamedSection, 'log', 'log', _('Log Cleanup'));
s = m.section(form.NamedSection, 'log', 'log', _('Log'));
o = s.option(form.Flag, 'log_cleanup_enabled', _('Scheduled Log Cleanup'));
s.tab('log_config', _('Log Config'));
o = s.taboption('log_config', form.Flag, 'scheduled_clear', _('Scheduled Clear'));
o.rmempty = false;
o = s.option(form.Value, 'log_cleanup_cron_expression', _('Log Cleanup Cron Expression'));
o = s.taboption('log_config', form.Value, 'scheduled_clear_cron', _('Scheduled Clear Cron'));
o.retain = true;
o.rmempty = false;
o.placeholder = '0 4 * * *';
o.depends('log_cleanup_enabled', '1');
o.description = _('Run unconditional log cleanup at the configured cron schedule.');
o.depends('scheduled_clear', '1');
o = s.option(form.Flag, 'log_cleanup_size_enabled', _('Size-based Log Cleanup'));
o.rmempty = false;
o = s.option(form.Value, 'log_cleanup_size_check_cron_expression', _('Log Size Check Cron Expression'));
o = s.taboption('log_config', form.Value, 'scheduled_clear_size_limit', _('Scheduled Clear Size Limit'));
o.retain = true;
o.rmempty = false;
o.placeholder = '*/30 * * * *';
o.depends('log_cleanup_size_enabled', '1');
o.description = _('Check log size at the configured cron schedule before cleaning up.');
o = s.option(form.Value, 'log_cleanup_size_mb', _('Log Cleanup Size Threshold (MB)'));
o.datatype = 'uinteger';
o.placeholder = '50';
o.depends('log_cleanup_size_enabled', '1');
o.description = _('Clear app, core and debug logs when their total size reaches this threshold.');
o.depends('scheduled_clear', '1');
s = m.section(form.NamedSection, 'placeholder', 'placeholder', _('Log'));
o = s.taboption('log_config', form.ListValue, 'scheduled_clear_size_limit_unit', _('Scheduled Clear Size Limit Unit'));
o.retain = true;
o.rmempty = false;
o.depends('scheduled_clear', '1');
o.value('KB', 'KB');
o.value('MB', 'MB');
o.value('GB', 'GB');
s.tab('app_log', _('App Log'));

View File

@ -0,0 +1,154 @@
'use strict';
'require form';
'require view';
'require uci';
'require fs';
'require network';
'require poll';
'require tools.widgets as widgets';
'require tools.momo as momo';
return view.extend({
load: function () {
return Promise.all([
uci.load('momo'),
]);
},
render: function (data) {
let m, s, o;
m = new form.Map('momo');
s = m.section(form.NamedSection, 'mixin', 'mixin', _('Mixin Option'));
s.tab('log', _('Log Config'));
o = s.taboption('log', form.ListValue, 'log_disabled', _('Log Disabled'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('log', form.ListValue, 'log_level', _('Log Level'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('panic');
o.value('fatal');
o.value('error');
o.value('warn');
o.value('info');
o.value('debug');
o.value('trace');
o = s.taboption('log', form.ListValue, 'log_timestamp', _('Log Timestamp'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('log', form.Value, 'log_output', _('Log Output'));
o.placeholder = _('Unmodified');
s.tab('dns', _('DNS Config'));
o = s.taboption('dns', form.ListValue, 'dns_strategy', _('DNS Strategy'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('prefer_ipv4', _('Prefer IPv4'));
o.value('prefer_ipv6', _('Prefer IPv6'));
o.value('ipv4_only', _('IPv4 Only'));
o.value('ipv6_only', _('IPv6 Only'));
o = s.taboption('dns', form.ListValue, 'dns_disable_cache', _('DNS Disable Cache'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_disable_expire', _('DNS Disable Expire'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_independent_cache', _('DNS Independent Cache'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.Value, 'dns_cache_capacity', _('DNS Cache Capacity'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
o = s.taboption('dns', form.ListValue, 'dns_reverse_mapping', _('DNS Reverse Mapping'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
s.tab('ntp', _('NTP Config'));
o = s.taboption('ntp', form.ListValue, 'ntp_enabled', _('NTP Enabled'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('ntp', form.Value, 'ntp_server', _('NTP Server'));
o.placeholder = _('Unmodified');
o = s.taboption('ntp', form.Value, 'ntp_server_port', _('NTP Server Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o = s.taboption('ntp', form.Value, 'ntp_interval', _('NTP Interval'));
o.placeholder = _('Unmodified');
s.tab('cache', _('Cache Config'));
o = s.taboption('cache', form.ListValue, 'cache_enabled', _('Cache Enabled'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('cache', form.Value, 'cache_path', _('Cache Path'));
o.placeholder = _('Unmodified');
o = s.taboption('cache', form.ListValue, 'cache_store_fakeip', _('Cache Store FakeIP'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('cache', form.ListValue, 'cache_store_rdrc', _('Cache Store RDRC'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
s.tab('external_control', _('External Control Config'));
o = s.taboption('external_control', form.Value, 'external_control_ui_path', _('UI Path'));
o.placeholder = _('Unmodified');
o = s.taboption('external_control', form.Value, 'external_control_ui_download_url', _('UI Download Url'));
o.placeholder = _('Unmodified');
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip', 'Zashboard (CDN Fonts)');
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip', 'Zashboard');
o.value('https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip', 'MetaCubeXD');
o.value('https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip', 'YACD');
o.value('https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip', 'Razord');
o = s.taboption('external_control', form.Value, 'external_control_api_listen', _('API Listen'));
o.datatype = 'ipaddrport(1)';
o.placeholder = _('Unmodified');
o = s.taboption('external_control', form.Value, 'external_control_api_secret', _('API Secret'));
o.password = true;
o.placeholder = _('Unmodified');
return m.render();
}
});

View File

@ -1,6 +1,14 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:144
msgid "API Listen"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:148
msgid "API Secret"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:69
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:122
msgid "Access Control"
@ -24,7 +32,7 @@ msgstr ""
msgid "App Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
msgid "App Log"
msgstr ""
@ -56,6 +64,26 @@ msgstr ""
msgid "CGroup"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:108
msgid "Cache Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:110
msgid "Cache Enabled"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:116
msgid "Cache Path"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:119
msgid "Cache Store FakeIP"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:125
msgid "Cache Store RDRC"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:27
msgid "Choose File"
msgstr ""
@ -64,8 +92,8 @@ msgstr ""
msgid "Choose Profile"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:33
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:57
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:90
msgid "Clear Log"
msgstr ""
@ -74,7 +102,7 @@ msgstr ""
msgid "Commonly Used Port"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:62
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:86
msgid "Core Log"
msgstr ""
@ -90,16 +118,40 @@ msgstr ""
msgid "Core Version"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
msgid "Cron Expression"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:165
msgid "DNS"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:95
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:80
msgid "DNS Cache Capacity"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:52
msgid "DNS Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:62
msgid "DNS Disable Cache"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:68
msgid "DNS Disable Expire"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:74
msgid "DNS Independent Cache"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:84
msgid "DNS Reverse Mapping"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:54
msgid "DNS Strategy"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:119
msgid "Debug Log"
msgstr ""
@ -111,6 +163,16 @@ msgstr ""
msgid "Destination UDP Port to Proxy"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:46
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:65
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:71
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:77
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:87
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:95
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:113
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:122
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:128
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:53
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:60
msgid "Disable"
@ -121,11 +183,21 @@ msgid "Edit Subscription"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:25
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
msgid "Editor"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:96
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:30
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:47
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:72
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:78
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:88
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:96
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:114
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:123
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:129
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:33
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:77
@ -138,6 +210,10 @@ msgstr ""
msgid "Expire At"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:131
msgid "External Control Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:48
msgid "Fake-IP Ping Hijack"
msgstr ""
@ -155,7 +231,7 @@ msgstr ""
msgid "General Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:123
msgid "Generate & Download"
msgstr ""
@ -183,6 +259,10 @@ msgstr ""
msgid "IPv4 DNS Hijack"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:59
msgid "IPv4 Only"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:42
msgid "IPv4 Proxy"
msgstr ""
@ -191,6 +271,10 @@ msgstr ""
msgid "IPv6 DNS Hijack"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:60
msgid "IPv6 Only"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:45
msgid "IPv6 Proxy"
msgstr ""
@ -208,19 +292,68 @@ msgid "Local"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:53
msgid "Log"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:24
msgid "Log Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:26
msgid "Log Disabled"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:32
msgid "Log Level"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:49
msgid "Log Output"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:43
msgid "Log Timestamp"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:194
msgid "Misc"
msgstr ""
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
msgid "Mixin Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:22
msgid "Mixin Option"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:38
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:3
msgid "Momo"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:90
msgid "NTP Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:92
msgid "NTP Enabled"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:105
msgid "NTP Interval"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:98
msgid "NTP Server"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:101
msgid "NTP Server Port"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:15
msgid "Not Running"
msgstr ""
@ -241,6 +374,14 @@ msgstr ""
msgid "Prefer"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:57
msgid "Prefer IPv4"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:58
msgid "Prefer IPv6"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:21
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:21
msgid "Profile"
@ -257,7 +398,7 @@ msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:31
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
msgid "Proxy Config"
msgstr ""
@ -297,12 +438,32 @@ msgstr ""
msgid "Running"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:31
msgid "Scheduled Clear"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:34
msgid "Scheduled Clear Cron"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
msgid "Scheduled Clear Size Limit"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:45
msgid "Scheduled Clear Size Limit Unit"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:114
msgid "Scheduled Restart"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:56
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:89
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
msgid "Scheduled Restart Cron"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:80
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:113
msgid "Scroll To Bottom"
msgstr ""
@ -385,6 +546,14 @@ msgstr ""
msgid "UDP Mode"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:136
msgid "UI Download Url"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:133
msgid "UI Path"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:139
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:143
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:147
@ -394,6 +563,31 @@ msgstr ""
msgid "Unlimited"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:28
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:34
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:45
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:50
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:56
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:64
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:70
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:76
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:82
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:86
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:94
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:103
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:106
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:112
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:117
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:121
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:127
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:134
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:137
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:146
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:150
msgid "Unmodified"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:63
msgid "Update"
msgstr ""
@ -425,39 +619,3 @@ msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:128
msgid "procd Config"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:47
msgid "Check log size at the configured cron schedule before cleaning up."
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
msgid "Clear app, core and debug logs when their total size reaches this threshold."
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
msgid "Log Cleanup"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:32
msgid "Log Cleanup Cron Expression"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:49
msgid "Log Cleanup Size Threshold (MB)"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:42
msgid "Log Size Check Cron Expression"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:37
msgid "Run unconditional log cleanup at the configured cron schedule."
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
msgid "Scheduled Log Cleanup"
msgstr ""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
msgid "Size-based Log Cleanup"
msgstr ""

View File

@ -8,6 +8,14 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:144
msgid "API Listen"
msgstr "API 监听"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:148
msgid "API Secret"
msgstr "API 密码"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:69
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:122
msgid "Access Control"
@ -31,7 +39,7 @@ msgstr "全部端口"
msgid "App Config"
msgstr "插件配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
msgid "App Log"
msgstr "插件日志"
@ -63,6 +71,26 @@ msgstr "绕过 FWMark"
msgid "CGroup"
msgstr "控制组"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:108
msgid "Cache Config"
msgstr "缓存配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:110
msgid "Cache Enabled"
msgstr "启用缓存"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:116
msgid "Cache Path"
msgstr "缓存文件路径"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:119
msgid "Cache Store FakeIP"
msgstr "缓存 FakeIP"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:125
msgid "Cache Store RDRC"
msgstr "缓存 RDRC"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:27
msgid "Choose File"
msgstr "选择文件"
@ -71,8 +99,8 @@ msgstr "选择文件"
msgid "Choose Profile"
msgstr "选择配置文件"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:33
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:57
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:90
msgid "Clear Log"
msgstr "清空日志"
@ -81,7 +109,7 @@ msgstr "清空日志"
msgid "Commonly Used Port"
msgstr "常用端口"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:62
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:86
msgid "Core Log"
msgstr "核心日志"
@ -97,16 +125,40 @@ msgstr "核心状态"
msgid "Core Version"
msgstr "核心版本"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
msgid "Cron Expression"
msgstr "Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:165
msgid "DNS"
msgstr "DNS"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:95
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:80
msgid "DNS Cache Capacity"
msgstr "DNS 缓存容量"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:52
msgid "DNS Config"
msgstr "DNS 配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:62
msgid "DNS Disable Cache"
msgstr "禁用 DNS 缓存"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:68
msgid "DNS Disable Expire"
msgstr "禁用 DNS 过期"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:74
msgid "DNS Independent Cache"
msgstr "DNS 独立缓存"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:84
msgid "DNS Reverse Mapping"
msgstr "DNS 反向映射"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:54
msgid "DNS Strategy"
msgstr "DNS 策略"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:119
msgid "Debug Log"
msgstr "调试日志"
@ -118,6 +170,16 @@ msgstr "要代理的 TCP 目标端口"
msgid "Destination UDP Port to Proxy"
msgstr "要代理的 UDP 目标端口"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:46
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:65
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:71
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:77
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:87
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:95
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:113
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:122
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:128
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:53
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:60
msgid "Disable"
@ -128,11 +190,21 @@ msgid "Edit Subscription"
msgstr "编辑订阅"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:25
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
msgid "Editor"
msgstr "编辑器"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:96
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:30
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:47
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:72
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:78
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:88
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:96
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:114
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:123
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:129
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:33
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:66
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:77
@ -145,6 +217,10 @@ msgstr "启用"
msgid "Expire At"
msgstr "到期时间"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:131
msgid "External Control Config"
msgstr "外部控制配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:48
msgid "Fake-IP Ping Hijack"
msgstr "Fake-IP Ping 劫持"
@ -162,7 +238,7 @@ msgstr "文件:"
msgid "General Config"
msgstr "全局配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:123
msgid "Generate & Download"
msgstr "生成并下载"
@ -190,6 +266,10 @@ msgstr "使用说明"
msgid "IPv4 DNS Hijack"
msgstr "IPv4 DNS 劫持"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:59
msgid "IPv4 Only"
msgstr "仅 IPv4"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:42
msgid "IPv4 Proxy"
msgstr "IPv4 代理"
@ -198,6 +278,10 @@ msgstr "IPv4 代理"
msgid "IPv6 DNS Hijack"
msgstr "IPv6 DNS 劫持"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:60
msgid "IPv6 Only"
msgstr "仅 IPv6"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:45
msgid "IPv6 Proxy"
msgstr "IPv6 代理"
@ -215,19 +299,68 @@ msgid "Local"
msgstr "本地"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:53
msgid "Log"
msgstr "日志"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:24
msgid "Log Config"
msgstr "日志配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:26
msgid "Log Disabled"
msgstr "禁用日志"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:32
msgid "Log Level"
msgstr "日志级别"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:49
msgid "Log Output"
msgstr "日志输出文件路径"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:43
msgid "Log Timestamp"
msgstr "打印时间戳"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:194
msgid "Misc"
msgstr "杂项"
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
msgid "Mixin Config"
msgstr "混入配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:22
msgid "Mixin Option"
msgstr "混入选项"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:38
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:3
msgid "Momo"
msgstr "Momo"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:90
msgid "NTP Config"
msgstr "NTP 配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:92
msgid "NTP Enabled"
msgstr "启用 NTP"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:105
msgid "NTP Interval"
msgstr "NTP 时间同步间隔"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:98
msgid "NTP Server"
msgstr "NTP 服务地址"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:101
msgid "NTP Server Port"
msgstr "NTP 服务端口"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:15
msgid "Not Running"
msgstr "未在运行"
@ -248,6 +381,14 @@ msgstr "打开面板"
msgid "Prefer"
msgstr "优先"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:57
msgid "Prefer IPv4"
msgstr "优先 IPv4"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:58
msgid "Prefer IPv6"
msgstr "优先 IPv6"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:21
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:21
msgid "Profile"
@ -264,7 +405,7 @@ msgstr "代理"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:29
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:31
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
msgid "Proxy Config"
msgstr "代理配置"
@ -304,12 +445,32 @@ msgstr "路由器代理"
msgid "Running"
msgstr "运行中"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:31
msgid "Scheduled Clear"
msgstr "定时清除""
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:34
msgid "Scheduled Clear Cron"
msgstr "Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
msgid "Scheduled Clear Size Limit"
msgstr "日志大小上限"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:45
msgid "Scheduled Clear Size Limit Unit"
msgstr "日志大小上限单位"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:114
msgid "Scheduled Restart"
msgstr "定时重启"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:56
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:89
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
msgid "Scheduled Restart Cron"
msgstr "Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:80
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:113
msgid "Scroll To Bottom"
msgstr "滚动到底部"
@ -392,6 +553,14 @@ msgstr "在 OpenWrt 上使用 sing-box 进行透明代理"
msgid "UDP Mode"
msgstr "UDP 模式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:136
msgid "UI Download Url"
msgstr "UI 下载地址"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:133
msgid "UI Path"
msgstr "UI 路径"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:139
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:143
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:147
@ -401,6 +570,31 @@ msgstr "UDP 模式"
msgid "Unlimited"
msgstr "无限制"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:28
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:34
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:45
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:50
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:56
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:64
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:70
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:76
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:82
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:86
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:94
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:99
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:103
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:106
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:112
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:117
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:121
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:127
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:134
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:137
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:146
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:150
msgid "Unmodified"
msgstr "不修改"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:63
msgid "Update"
msgstr "更新"
@ -433,38 +627,34 @@ msgstr "用户代理UA"
msgid "procd Config"
msgstr "procd 配置"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:47
msgid "Check log size at the configured cron schedule before cleaning up."
msgstr "按设定的 Cron 周期检查日志大小,满足条件后再清理。"
#~ msgid "Cron Expression"
#~ msgstr "Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
msgid "Clear app, core and debug logs when their total size reaches this threshold."
msgstr "当插件日志、核心日志和调试日志总大小达到该阈值时自动清理。"
#~ msgid "Check log size at the configured cron schedule before cleaning up."
#~ msgstr "按设定的 Cron 周期检查日志大小,满足条件后再清理。"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
msgid "Log Cleanup"
msgstr "日志清理"
#~ msgid ""
#~ "Clear app, core and debug logs when their total size reaches this "
#~ "threshold."
#~ msgstr "当插件日志、核心日志和调试日志总大小达到该阈值时自动清理。"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:32
msgid "Log Cleanup Cron Expression"
msgstr "日志清理 Cron 表达式"
#~ msgid "Log Cleanup"
#~ msgstr "日志清理"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:49
msgid "Log Cleanup Size Threshold (MB)"
msgstr "日志清理大小阈值MB"
#~ msgid "Log Cleanup Cron Expression"
#~ msgstr "日志清理 Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:42
msgid "Log Size Check Cron Expression"
msgstr "日志大小检查 Cron 表达式"
#~ msgid "Log Cleanup Size Threshold (MB)"
#~ msgstr "日志清理大小阈值MB"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:37
msgid "Run unconditional log cleanup at the configured cron schedule."
msgstr "按设定的 Cron 时间无条件执行日志清理。"
#~ msgid "Log Size Check Cron Expression"
#~ msgstr "日志大小检查 Cron 表达式"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
msgid "Scheduled Log Cleanup"
msgstr "定时日志清理"
#~ msgid "Run unconditional log cleanup at the configured cron schedule."
#~ msgstr "按设定的 Cron 时间无条件执行日志清理。"
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
msgid "Size-based Log Cleanup"
msgstr "按大小触发日志清理"
#~ msgid "Scheduled Log Cleanup"
#~ msgstr "定时日志清理"
#~ msgid "Size-based Log Cleanup"
#~ msgstr "按大小触发日志清理"

View File

@ -25,9 +25,17 @@
"path": "momo/profile"
}
},
"admin/services/momo/mixin": {
"title": "Mixin Config",
"order": 30,
"action": {
"type": "view",
"path": "momo/mixin"
}
},
"admin/services/momo/proxy": {
"title": "Proxy Config",
"order": 30,
"order": 40,
"action": {
"type": "view",
"path": "momo/proxy"
@ -35,7 +43,7 @@
},
"admin/services/momo/editor": {
"title": "Editor",
"order": 40,
"order": 50,
"action": {
"type": "view",
"path": "momo/editor"
@ -43,7 +51,7 @@
},
"admin/services/momo/log": {
"title": "Log",
"order": 50,
"order": 60,
"action": {
"type": "view",
"path": "momo/log"

View File

@ -2,8 +2,8 @@
'use strict';
import { access, popen, readfile, writefile } from 'fs';
import { get_paths, merge_exists, get_users, get_groups, get_cgroups } from '/etc/momo/ucode/include.uc';
import { access, popen, readfile } from 'fs';
import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile, save_profile } from '/etc/momo/ucode/include.uc';
const methods = {
get_paths: {
@ -40,9 +40,8 @@ const methods = {
profile: {
args: { defaults: {} },
call: function(req) {
const paths = get_paths();
const defaults = req.args?.defaults ?? {};
const profile = json(readfile(paths.run_profile_path));
const profile = load_profile();
return merge_exists(defaults, profile);
}
},
@ -67,8 +66,7 @@ const methods = {
const query = req.args?.query;
const body = req.args?.body;
const paths = get_paths();
const profile = json(readfile(paths.run_profile_path));
const profile = load_profile();
const api_listen = profile['experimental']['clash_api']['external_controller'];
const api_secret = profile['experimental']['clash_api']['secret'];

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.25.3
PKG_RELEASE:=3
PKG_VERSION:=1.26.0
PKG_RELEASE:=4
LUCI_TITLE:=LuCI Support for nikki
LUCI_DEPENDS:=+luci-base +nikki

View File

@ -13,7 +13,6 @@ return view.extend({
return Promise.all([
uci.load('nikki'),
network.getNetworks(),
]);
},
render: function (data) {

View File

@ -4,6 +4,124 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
## [2.0.4] - 2026-04-10
### 适配 OpenClaw v2026.4.9
本次更新适配 OpenClaw 最新稳定版本 (2026.4.9),包含多项破坏性配置变更处理和安全策略适配。
#### 版本变更
- **OC_TESTED_VERSION**: 从 2026.3.28 更新到 2026.4.9 (跨 12 个版本)
- **Node.js 版本**: 保持 22.16.0 (v2026.4.x 最低要求 >= 22.14.0)
#### 更新日志 UI 深度优化
参照 GitHub Releases 和 App Store 更新页面的设计规范,重构「检测升级」按钮触发的更新日志显示区域:
- **Markdown 渲染引擎重构** (`ocMarkdownToHtml` 函数):
- 中英文混排字体栈优化:`PingFang SC` / `Microsoft YaHei` / `Noto Sans SC` + 系统字体回退
- 字体渲染增强:`text-rendering: optimizeLegibility` + 抗锯齿平滑
- 标题层级比例重构:一级标题 (20px) → 二级 (17px) → 三级 (15px) → 四级 (14px)
- 代码块样式:圆角边框 + 等宽字体栈 (`SF Mono`, Consolas, Menlo)
- 链接悬停效果:底部边框渐显动画
- 列表标记美化:统一的 `list-style-position: inside` + 行高优化 (1.75)
- **版本状态徽章设计**:
- 「有新版本」状态:渐变金色背景 + 橙色边框
- 「已是最新」状态:渐变绿色背景 + 绿色边框
- 「无法检查」状态:灰色背景 + 中性边框
- 版本号使用等宽字体徽章样式,增强可读性
- **更新日志容器设计**:
- 卡片式布局:圆角 + 微阴影 + 白色背景
- 版本标题栏:渐变背景 + 蓝色版本徽章 + GitHub 跳转链接
- 内容区滚动条美化:`scrollbar-width: thin` + 自定义颜色
- 操作按钮区:分组设计 + 阴影增强
- **视觉细节优化**:
- 段落间距12px 统一间距
- 行高优化:中文场景 1.75-1.8
- 字间距微调:标题 `-0.02em`,正文 `0.01em`
- 颜色层级:主标题 `#1f2328` → 正文 `#32383f` → 次要文本 `#656d76`
#### 破坏性变更处理 (v2026.4.5)
OpenClaw v2026.4.5 移除了多项旧版配置别名,本版本新增自动清理逻辑:
- **已废弃配置清理**: `sync_uci_to_json()` 新增清理以下废弃字段
- `talk.voiceId` / `talk.apiKey` — 语音功能配置迁移
- `browser.ssrfPolicy.allowPrivateNetwork` — SSRF 策略重构
- `hooks.internal.handlers` — 内部钩子迁移
- `channel.*.allow` / `group.*.allow` / `room.*.allow` — 迁移到 `enabled` 字段
- `agents.defaults.cliBackends` — CLI 后端配置废弃
- **配置迁移工具集成**: 服务启动时自动调用 `openclaw doctor --fix` 迁移遗留配置
#### 权限模型适配 (v2026.4.7/4.9)
OpenClaw 新增了严格的环境变量安全检查,阻止危险环境变量覆盖:
- **环境变量安全审计**: `start_service()` 注入环境变量前进行路径安全验证
- 阻止包含特殊字符 (`$`, `` ` ``, `|`, `&`) 的路径
- 阻止 `/proc/*`, `/sys/*`, `/dev/*` 等危险目录
- 确保 `JAVA_HOME`, `RUST_*`, `GIT_*`, `KUBECONFIG`, `AWS_*` 等危险变量不被意外注入
- **命令授权策略更新**: 适配 v2026.4.7 的权限收紧
- `/allowlist add/remove` 现需要所有者授权 (LuCI 会话验证已满足)
- `gateway config.apply` 阻止修改 exec 审批路径
#### 安全更新
- **依赖安全**: `basic-ftp` 强制升级到 5.2.1 (修复 CRLF 命令注入漏洞 CVE)
- **浏览器 SSRF**: 新增安全策略配置选项 (默认不启用内网访问白名单)
#### 改进
- **配置验证增强**: 服务启动时先执行配置 schema 验证,失败时自动尝试修复
- **iframe 安全头修补**: 适配 v2026.4.x 新的安全头位置
- v2026.4.x 将 `X-Frame-Options``frame-ancestors` 设置从 `gateway-cli-*.js` 迁移到 `server.impl-*.js`
- `patch_iframe_headers()` 函数现已支持两个位置的安全头修补
- 解决 Web 控制台 iframe 嵌入被阻止的问题
- **错误提示优化**: 配置迁移失败时输出详细的诊断信息
- **移除 Gemini CLI 安装**: OpenClaw v2026.4.x 已废弃 `google-gemini-cli-auth` 插件
- 节省约 155MB 磁盘空间
- 减少安装时间约 30~60 秒
- 用户配置 Google Gemini 请使用 API Key 方式 (推荐)
- **修复微信插件加载失败**: 清理 jiti 缓存目录解决权限冲突
- jiti 编译 TypeScript 时在 `/tmp/jiti` 创建缓存
- 服务启动前自动清理,避免 openclaw 用户无法写入
#### 修复 (2026-04-10)
- **权限修复 EACCES 错误**: `doctor --fix` 以 root 运行后创建的文件导致 Gateway 无法写入
- 根因: `doctor --fix` 在服务启动时以 root 身份执行,可能创建 root 所有的配置文件
- 症状: `EACCES: permission denied, open '.../agents/main/agent/models.json.xxx.tmp'`
- 修复: `init.d/openclaw``doctor --fix` 后自动修复非 extensions 目录权限
- 同时修复 `openclaw-env factory-reset` 后的权限问题
- **OpenWrt 兼容性**: 无 `ss` 命令时 Gateway 重启检测失败
- 根因: `oc-config-interactive.js` 直接调用 `ss` 命令,部分精简固件未安装
- 修复: 优先使用 `ss`,不存在时回退到 `netstat`
- **新增交互式菜单引擎**: `oc-menu-engine.js`
- 方向键导航、搜索过滤、粘贴支持 (Bracketed Paste Mode)
- 纯 Node.js 实现,零外部依赖
- 用于 SSH 命令行配置体验优化
#### 技术细节
**新增函数**:
- `_validate_path()`: 环境变量路径安全验证
- `_run_config_migration()`: 执行 OpenClaw 官方配置迁移工具
**修改文件**:
- `root/etc/init.d/openclaw`: 配置同步逻辑、环境变量注入、权限修复
- `root/usr/bin/openclaw-env`: OC_TESTED_VERSION 版本号、factory-reset 权限修复
- `root/usr/share/openclaw/oc-config-interactive.js`: ss/netstat 兼容性
- `root/usr/share/openclaw/oc-menu-engine.js`: 新增交互式菜单引擎
---
## [2.0.3] - 2026-04-03
### 修复

View File

@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclaw
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
PKG_RELEASE:=9
PKG_RELEASE:=10
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
PKG_LICENSE:=GPL-3.0

View File

@ -30,15 +30,19 @@
无需 SDK适用于已安装好的系统。
```bash
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.run
sh luci-app-openclaw.run
# 下载最新版本(自动获取版本号)
VER=$(curl -sI "https://github.com/10000ge10000/luci-app-openclaw/releases/latest" 2>/dev/null | grep -i "location:" | sed 's/.*tag\/v\{0,1\}//' | tr -d '\r\n')
wget "https://github.com/10000ge10000/luci-app-openclaw/releases/download/v${VER}/luci-app-openclaw_${VER}.run"
sh "luci-app-openclaw_${VER}.run"
```
### 方式二:.ipk 安装
```bash
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.ipk
opkg install luci-app-openclaw.ipk
# 下载最新版本(自动获取版本号)
VER=$(curl -sI "https://github.com/10000ge10000/luci-app-openclaw/releases/latest" 2>/dev/null | grep -i "location:" | sed 's/.*tag\/v\{0,1\}//' | tr -d '\r\n')
wget "https://github.com/10000ge10000/luci-app-openclaw/releases/download/v${VER}/luci-app-openclaw_${VER}-1_all.ipk"
opkg install "luci-app-openclaw_${VER}-1_all.ipk"
```
### 方式三:集成到固件编译
@ -72,23 +76,6 @@ make package/luci-app-openclaw/compile V=s
find bin/ -name "luci-app-openclaw*.ipk"
```
### 方式四:手动安装
```bash
git clone https://github.com/10000ge10000/luci-app-openclaw.git
cd luci-app-openclaw
cp -r root/* /
mkdir -p /usr/lib/lua/luci/controller /usr/lib/lua/luci/model/cbi/openclaw /usr/lib/lua/luci/view/openclaw
cp luasrc/controller/openclaw.lua /usr/lib/lua/luci/controller/
cp luasrc/model/cbi/openclaw/*.lua /usr/lib/lua/luci/model/cbi/openclaw/
cp luasrc/view/openclaw/*.htm /usr/lib/lua/luci/view/openclaw/
chmod +x /etc/init.d/openclaw /usr/bin/openclaw-env /usr/share/openclaw/oc-config.sh
sh /etc/uci-defaults/99-openclaw
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/*
```
## 🔰 首次使用

View File

@ -1 +1 @@
2.0.3
2.0.4

View File

@ -188,11 +188,27 @@ act.cfgvalue = function(self, section)
html[#html+1] = '});'
html[#html+1] = '}'
-- 轮询安装日志
-- 轮询安装日志 (智能滚动: 用户向上滚动时暂停自动滚动,滚动到底部时恢复)
html[#html+1] = 'var _lastLogLen=0;'
html[#html+1] = 'var _autoScrollEnabled=true;' -- 智能滚动状态标志
html[#html+1] = 'function ocPollSetupLog(){'
html[#html+1] = 'if(_setupTimer)clearInterval(_setupTimer);'
html[#html+1] = '_lastLogLen=0;'
html[#html+1] = '_autoScrollEnabled=true;' -- 初始状态: 启用自动滚动
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
-- 绑定滚动事件监听器 (只绑定一次)
html[#html+1] = 'if(!logEl._scrollListenerAttached){'
html[#html+1] = 'logEl.addEventListener("scroll",function(){'
html[#html+1] = 'var el=this;'
html[#html+1] = 'var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<5;'
html[#html+1] = 'if(atBottom){'
html[#html+1] = '_autoScrollEnabled=true;' -- 滚动到底部: 恢复自动滚动
html[#html+1] = '}else{'
html[#html+1] = '_autoScrollEnabled=false;' -- 用户向上滚动: 暂停自动滚动
html[#html+1] = '}'
html[#html+1] = '});'
html[#html+1] = 'logEl._scrollListenerAttached=true;'
html[#html+1] = '}'
html[#html+1] = '_setupTimer=setInterval(function(){'
html[#html+1] = '(new XHR()).get("' .. log_url .. '",null,function(x){'
html[#html+1] = 'try{'
@ -204,7 +220,10 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'logEl.textContent+=newLog;'
html[#html+1] = '_lastLogLen=r.log.length;'
html[#html+1] = '}'
-- 智能滚动: 仅在自动滚动启用时滚动到底部
html[#html+1] = 'if(_autoScrollEnabled){'
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
html[#html+1] = '}'
html[#html+1] = 'if(r.state==="running"){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = '}else if(r.state==="success"){'
@ -295,35 +314,38 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
html[#html+1] = '});}'
-- 简单的 Markdown 转 HTML 函数 (用于渲染 GitHub Release Notes)
-- Markdown 转 HTML 函数 (优化版 - 用于渲染 GitHub Release Notes)
-- 特性:中英文字体栈、代码块样式、链接悬停效果、列表美化
html[#html+1] = 'function ocMarkdownToHtml(md){'
html[#html+1] = 'if(!md)return "";'
-- 转义 HTML 特殊字符
html[#html+1] = 'var html=md.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");'
-- 代码块 (```code```)
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:10px 14px;border-radius:6px;overflow-x:auto;font-size:13px;line-height:1.5;\\"><code>"+code.trim()+"</code></pre>";});'
-- 代码块 (```code```) - 圆角边框 + 等宽字体栈
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:12px 16px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.6;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #d0d7de;\\"><code>"+code.trim()+"</code></pre>";});'
-- 行内代码 (`code`)
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:3px;font-size:13px;\\">$1</code>");'
-- 标题 (### ## #)
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#24292f;\\">$1</h4>");'
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:16px;font-weight:600;color:#24292f;\\">$1</h3>");'
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:17px;font-weight:600;color:#24292f;border-bottom:1px solid #d0d7de;padding-bottom:6px;\\">$1</h2>");'
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:4px;font-size:13px;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #e1e4e8;\\">$1</code>");'
-- 标题层级 (优化比例: h1=20px, h2=17px, h3=15px, h4=14px)
html[#html+1] = 'html=html.replace(/^#### (.+)$/gm,"<h5 style=\\"margin:12px 0 6px;font-size:14px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h5>");'
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h4>");'
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:17px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h3>");'
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:20px;font-weight:600;color:#1f2328;border-bottom:1px solid #d0d7de;padding-bottom:8px;letter-spacing:-0.02em;\\">$1</h2>");'
-- 粗体和斜体
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong>$1</strong>");'
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong style=\\"font-weight:600;\\">$1</strong>");'
html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"<em>$1</em>");'
-- 链接 [text](url)
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;\\">$1</a>");'
-- 链接 [text](url) - 悬停效果
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;border-bottom:1px solid transparent;transition:border-color 0.2s;\\">$1</a>");'
-- 无序列表 (- 或 *)
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;line-height:1.7;\\">$1</li>");'
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\">$1</li>");'
-- 有序列表 (1. 2. 等)
html[#html+1] = 'html=html.replace(/^\\d+\\. (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;list-style-type:decimal;line-height:1.7;\\">$1</li>");'
html[#html+1] = 'html=html.replace(/^(\\d+)\\. (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\"><span style=\\"color:#656d76;margin-right:4px;\\">$1.</span>$2</li>");'
-- 水平线 (--- 或 ***)
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:14px 0;\\"/>");'
-- 段落 (连续的非空行合并)
html[#html+1] = 'html=html.replace(/\\n\\n/g,"</p><p style=\\"margin:10px 0;line-height:1.7;\\">");'
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:16px 0;\\"/>");'
-- 段落: 连续空行合并为段落分隔 (简化处理,避免生成未闭合标签)
html[#html+1] = 'html=html.replace(/\\n\\n+/g,"<br/><br/>");'
-- 换行
html[#html+1] = 'html=html.replace(/\\n/g,"<br/>");'
html[#html+1] = 'return"<div style=\\"font-size:14px;color:#24292f;line-height:1.7;\\">"+html+"</div>";'
-- 外层容器 - 中英文字体栈
html[#html+1] = 'return"<div style=\\"font-size:14px;line-height:1.75;color:#32383f;font-family:PingFang SC,Microsoft YaHei,Noto Sans SC,sans-serif;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;\\">"+html+"</div>";'
html[#html+1] = '}'
-- 检测升级 (只检查插件版本,有新版本时显示更新内容)
@ -337,29 +359,31 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'var dot=document.getElementById("update-dot");if(dot)dot.style.display="none";'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'var msgs=[];'
-- 插件版本检查
-- 版本状态徽章样式
html[#html+1] = 'var badgeNew="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#fff7e6 0%,#ffe7ba 100%);color:#9a6700;border:1px solid #f5a623;";'
html[#html+1] = 'var badgeLatest="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e6f7e6 0%,#c8f7c8 100%);color:#1a7f1a;border:1px solid #28a745;";'
html[#html+1] = 'var badgeUnknown="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:#f6f8fa;color:#656d76;border:1px solid #d0d7de;";'
html[#html+1] = 'var verBadge="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-family:SF Mono,Consolas,Menlo,monospace;background:#e1e4e8;color:#24292f;margin-left:4px;";'
-- 插件版本检查 (带渐变徽章)
html[#html+1] = 'if(r.plugin_current){'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\"color:#e36209\\">🔌 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本)</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\"color:green\\">✅ 插件: v"+r.plugin_current+" (已是最新)</span>");}'
html[#html+1] = 'else{msgs.push("<span style=\\"color:#999\\">🔌 插件: v"+r.plugin_current+" (无法检查最新版本)</span>");}'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🔌 有新版本</span> v"+r.plugin_current+" → <span style=\\""+verBadge+"\\">v"+r.plugin_latest+"</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\""+badgeLatest+"\\">✅ 已是最新</span> v"+r.plugin_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🔌 无法检查</span> v"+r.plugin_current);}'
html[#html+1] = '}'
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\"color:#999\\">无法获取版本信息</span>");'
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\""+badgeUnknown+"\\">无法获取版本信息</span>");'
html[#html+1] = 'el.innerHTML=msgs.join("<br/>");'
-- 插件有更新时: release notes + 一键升级按钮 + GitHub 下载链接
-- 插件有更新时: 卡片式更新日志 + 操作按钮
html[#html+1] = 'if(r.plugin_has_update){'
html[#html+1] = 'act.style.display="block";'
html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;'
html[#html+1] = 'var notesHtml="";'
html[#html+1] = 'if(r.release_notes){'
html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);'
html[#html+1] = 'notesHtml=\'<div style="margin:10px 0 8px;padding:12px 16px;background:#f6f8fa;border:1px solid #d0d7de;border-radius:8px;max-height:400px;overflow-y:auto;">\''
html[#html+1] = '+\'<div style="font-size:13px;font-weight:600;color:#24292f;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #d0d7de;">📋 v\'+r.plugin_latest+\' 更新内容</div>\''
html[#html+1] = '+rendered'
html[#html+1] = '+\'</div>\';'
-- 卡片式容器: 圆角边框 + 微阴影 + 版本标题栏
html[#html+1] = 'notesHtml="<div style=\\"margin:12px 0;border:1px solid #d0d7de;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);\\"><div style=\\"background:linear-gradient(135deg,#f6f8fa 0%,#ffffff 100%);padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;justify-content:space-between;\\"><span style=\\"font-size:14px;font-weight:600;color:#24292f;\\">📋 更新日志</span><span style=\\"display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e3f2fd 0%,#bbdefb 100%);color:#1565c0;border:1px solid #64b5f6;\\">v"+r.plugin_latest+"</span></div><div style=\\"padding:16px;max-height:450px;overflow-y:auto;background:#fff;\\">"+rendered+"</div></div>";'
html[#html+1] = '}'
html[#html+1] = 'act.innerHTML=notesHtml'
html[#html+1] = '+\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocPluginUpgrade()" id="btn-plugin-upgrade">⬆️ 升级插件 v\'+r.plugin_latest+\'</button>\''
html[#html+1] = '+\' <a href="https://github.com/10000ge10000/luci-app-openclaw/releases/latest" target="_blank" rel="noopener" class="btn cbi-button cbi-button-action" style="text-decoration:none;">📥 手动下载</a>\';'
-- 操作按钮区: 分组设计
html[#html+1] = 'act.innerHTML=notesHtml+"<div style=\\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;\\"><button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocPluginUpgrade()\\" id=\\"btn-plugin-upgrade\\" style=\\"box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级 v"+r.plugin_latest+"</button><a href=\\"https://github.com/10000ge10000/luci-app-openclaw/releases/latest\\" target=\\"_blank\\" rel=\\"noopener\\" class=\\"btn cbi-button cbi-button-action\\" style=\\"text-decoration:none;\\">📥 GitHub 下载</a></div>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
html[#html+1] = '});}'
@ -390,10 +414,12 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}'
-- 轮询插件升级日志 (带容错: 安装时文件被替换可能导致API暂时不可用)
-- 复用安装日志的智能滚动机制
html[#html+1] = 'var _pluginPollErrors=0;'
html[#html+1] = 'function ocPollPluginUpgradeLog(){'
html[#html+1] = 'if(_pluginUpgradeTimer)clearInterval(_pluginUpgradeTimer);'
html[#html+1] = '_pluginPollErrors=0;'
html[#html+1] = '_autoScrollEnabled=true;' -- 重置: 启用自动滚动
html[#html+1] = '_pluginUpgradeTimer=setInterval(function(){'
html[#html+1] = '(new XHR()).get("' .. plugin_upgrade_log_url .. '",null,function(x){'
html[#html+1] = 'try{'
@ -402,7 +428,10 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
-- 智能滚动: 仅在自动滚动启用时滚动到底部
html[#html+1] = 'if(_autoScrollEnabled){'
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
html[#html+1] = '}'
html[#html+1] = 'if(r.state==="running"){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 插件升级中...</span>";'
html[#html+1] = '}else if(r.state==="success"){'

View File

@ -77,8 +77,20 @@ rm -f "$_tmpf"
patch_iframe_headers() {
# 移除 OpenClaw 网关的 X-Frame-Options 和 frame-ancestors 限制,允许 LuCI iframe 嵌入
# v2026.3.8: 资产路径可能通过符号链接解析,需同时搜索 OC_GLOBAL 和 NODE_BASE
local gw_js
# v2026.4.x: 安全头设置迁移到 server.impl-*.js 文件
local patched=0
# 1. 搜索 server.impl-*.js (v2026.4.x 新位置)
for f in $(find "${OC_GLOBAL}/lib/node_modules/openclaw/dist" -name "server.impl-*.js" -type f 2>/dev/null); do
if grep -q "X-Frame-Options.*DENY\|frame-ancestors.*none" "$f" 2>/dev/null; then
sed -i 's|res\.setHeader("X-Frame-Options", "DENY")|res.setHeader("X-Frame-Options", "ALLOW-FROM *") // patched by luci-app-openclaw|g' "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
# 2. 兼容旧版本: 搜索 gateway-cli-*.js (v2026.3.x)
for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
[ -d "$search_root" ] || continue
for f in $(find "$search_root" -name "gateway-cli-*.js" -type f 2>/dev/null); do
@ -86,14 +98,17 @@ for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
sed -i "s|res.setHeader(\"X-Frame-Options\", \"DENY\");|// res.setHeader(\"X-Frame-Options\", \"DENY\"); // patched by luci-app-openclaw|g" "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
done
[ $patched -eq 1 ] && logger -t openclaw "iframe headers patched successfully"
}
sync_uci_to_json() {
# 将 UCI 配置同步到 openclaw.json同时确保 token 双向同步
local port bind token
local port bind token json_token
port=$(uci -q get openclaw.main.port || echo "18789")
bind=$(uci -q get openclaw.main.bind || echo "lan")
token=$(uci -q get openclaw.main.token || echo "")
@ -104,14 +119,28 @@ if [ ! -f "$CONFIG_FILE" ]; then
echo '{}' > "$CONFIG_FILE"
fi
# UCI 没有 token 时,尝试从已有的 JSON 读取
if [ -z "$token" ] && [ -x "$NODE_BIN" ]; then
token=$("$NODE_BIN" -e "
# 尝试从 JSON 读取 token (用于双向同步检测)
if [ -x "$NODE_BIN" ]; then
json_token=$("$NODE_BIN" -e "
try{const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gateway.auth.token);}catch(e){}
" 2>/dev/null)
fi
# v2026.4.10: 双向同步策略 - JSON 中的 token 优先
# 原因: doctor --fix 或其他操作可能修改 JSON 中的 token
# 如果 JSON 有 token 且与 UCI 不同,以 JSON 为准
if [ -n "$json_token" ] && [ "$json_token" != "$token" ]; then
token="$json_token"
logger -t openclaw "Token 同步: JSON -> UCI"
fi
# UCI 和 JSON 都没有 token 时,生成一个新的
if [ -z "$token" ]; then
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
logger -t openclaw "Token 生成: 新 token"
fi
# 如果仍然没有 token生成一个新的
if [ -z "$token" ]; then
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
@ -162,6 +191,37 @@ d.update.checkOnStart=false;
if(d.models&&d.models.providers&&d.models.providers.ollama){const ol=d.models.providers.ollama;if(ol.api==='openai-chat-completions'||ol.api==='openai-completions')ol.api='ollama';if(ol.baseUrl&&ol.baseUrl.endsWith('/v1'))ol.baseUrl=ol.baseUrl.replace(/\/v1$/,'');if(ol.apiKey==='ollama')ol.apiKey='ollama-local';}
// v2026.3.7: 清理已废弃的顶层配置键 (loadConfig() 现在会严格校验)
['cli','commands.native','commands.nativeSkills','commands.ownerDisplay'].forEach(k=>{const ks=k.split('.');let o=d;for(let i=0;i<ks.length-1;i++){if(!o[ks[i]])return;o=o[ks[i]];}delete o[ks[ks.length-1]];});
// v2026.4.5: 清理已废弃的配置别名 (BREAKING CHANGE)
// 1. talk.* 命名空间废弃 (voiceId, apiKey)
if(d.talk){delete d.talk.voiceId;delete d.talk.apiKey;}
// 2. browser.ssrfPolicy.allowPrivateNetwork 废弃
if(d.browser&&d.browser.ssrfPolicy){delete d.browser.ssrfPolicy.allowPrivateNetwork;}
// 3. hooks.internal.handlers 废弃
if(d.hooks&&d.hooks.internal){delete d.hooks.internal.handlers;}
// 4. channel/group/room 的 allow 字段迁移到 enabled
['channel','group','room'].forEach(k=>{if(d[k]){Object.keys(d[k]).forEach(id=>{if(d[k][id]&&typeof d[k][id].allow!=='undefined'){d[k][id].enabled=d[k][id].allow;delete d[k][id].allow;}});}});
// 5. agents.defaults.cliBackends 废弃
if(d.agents&&d.agents.defaults){delete d.agents.defaults.cliBackends;}
// v2026.4.9: 自动同步 plugins.allow (解决 "plugins.allow is empty" 警告)
// 扫描已安装的插件并添加到 allow 列表
if(!d.plugins)d.plugins={};
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
// 从 plugins.installs 中提取已安装插件
// 注意: 使用 installPath 中的目录名作为插件 ID (与 openclaw.plugin.json 中的 id 一致)
if(d.plugins.installs){
Object.keys(d.plugins.installs).forEach(key=>{
const inst=d.plugins.installs[key];
if(inst&&inst.installPath){
// 从 installPath 提取插件目录名 (如 openclaw-weixin)
const match=inst.installPath.match(/\/([^\/]+)\/?$/);
if(match&&match[1]&&!d.plugins.allow.includes(match[1])){
d.plugins.allow.push(match[1]);
}
}
});
}
// 始终信任 copilot-proxy (内置插件)
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
fs.writeFileSync(f,JSON.stringify(d,null,2));
" 2>/dev/null
fi
@ -252,6 +312,98 @@ fi
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
# v2026.4.9: 修复插件目录权限 (OpenClaw 要求插件目录属主为 root)
# 详见: https://github.com/nicepkg/openclaw/releases/tag/v2026.4.9
local ext_dir="${OC_DATA}/.openclaw/extensions"
if [ -d "$ext_dir" ]; then
chown -R root:root "$ext_dir" 2>/dev/null || true
fi
# v2026.4.x: 清理 jiti 缓存目录 (修复微信插件加载权限问题)
# jiti 编译 TypeScript 时会在 /tmp/jiti 创建缓存,如果由 root 创建则 openclaw 用户无法写入
rm -rf /tmp/jiti 2>/dev/null || true
# v2026.4.5: 执行 OpenClaw 官方配置迁移工具
# 自动迁移遗留配置到新版本格式
_run_config_migration() {
local oc_entry="$1"
[ -z "$oc_entry" ] && return
logger -t openclaw "执行配置迁移 (doctor --fix)..."
OPENCLAW_HOME="$OC_DATA" OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
"$NODE_BIN" "$oc_entry" doctor --fix 2>/dev/null && \
logger -t openclaw "配置迁移完成" || \
logger -t openclaw "配置迁移失败,请手动检查"
}
_run_config_migration "$oc_entry"
# doctor --fix 后再次修复权限 (防止 root 创建的文件导致 EACCES)
# 注意: extensions 目录保持 root 权限 (OpenClaw 安全要求)
find "$OC_DATA" -user root ! -path "${OC_DATA}/.openclaw/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
# v2026.4.10: doctor --fix 后确保关键配置字段存在
# doctor --fix 可能会删除或遗漏某些必要字段,需要补充
_ensure_critical_config() {
[ ! -f "$CONFIG_FILE" ] && return
[ ! -x "$NODE_BIN" ] && return
"$NODE_BIN" -e "
const fs = require('fs');
const f = '${CONFIG_FILE}';
let d = {};
try { d = JSON.parse(fs.readFileSync(f, 'utf8')); } catch(e) {}
// 确保 gateway.mode 存在
if (!d.gateway) d.gateway = {};
if (!d.gateway.mode) d.gateway.mode = 'local';
// 确保端口和绑定配置
if (!d.gateway.port) d.gateway.port = 18789;
if (!d.gateway.bind) d.gateway.bind = 'lan';
// 确保 controlUi 安全配置存在 (允许非 loopback 绑定和 LuCI iframe 嵌入)
if (!d.gateway.controlUi) d.gateway.controlUi = {};
if (d.gateway.controlUi.allowInsecureAuth !== true) d.gateway.controlUi.allowInsecureAuth = true;
if (d.gateway.controlUi.dangerouslyDisableDeviceAuth !== true) d.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
// 确保认证配置
if (!d.gateway.auth) d.gateway.auth = {};
if (!d.gateway.auth.mode) d.gateway.auth.mode = 'token';
// 禁用 ACP dispatch (路由器内存有限)
if (!d.acp) d.acp = {};
if (!d.acp.dispatch) d.acp.dispatch = {};
d.acp.dispatch.enabled = false;
// 工具配置
if (!d.tools) d.tools = {};
d.tools.profile = 'coding';
// 禁用更新检查
if (!d.update) d.update = {};
d.update.checkOnStart = false;
fs.writeFileSync(f, JSON.stringify(d, null, 2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
}
_ensure_critical_config
# v2026.4.10: 最终权限修复 - 确保 Gateway 启动前所有数据目录权限正确
# doctor --fix 或 _ensure_critical_config 可能创建新的目录/文件
# 关键: agents 目录下的 agent 子目录必须可被 openclaw 用户写入
find "${OC_STATE_DIR}" -user root -type d -exec chown openclaw:openclaw {} \; 2>/dev/null || true
find "${OC_STATE_DIR}" -user root -type f -exec chown openclaw:openclaw {} \; 2>/dev/null || true
# 特别确保 agents/main/agent 目录权限 (模型配置写入目录)
local agent_dir="${OC_STATE_DIR}/agents/main/agent"
if [ -d "$agent_dir" ]; then
chown openclaw:openclaw "$agent_dir" 2>/dev/null || true
chmod 755 "$agent_dir" 2>/dev/null || true
fi
# extensions 目录除外 - 必须保持 root 权限 (OpenClaw 安全要求)
if [ -d "${OC_STATE_DIR}/extensions" ]; then
chown -R root:root "${OC_STATE_DIR}/extensions" 2>/dev/null || true
fi
# Patch iframe 安全头,允许 LuCI 嵌入
patch_iframe_headers
@ -305,19 +457,32 @@ _ensure_port_free() {
_ensure_port_free "$port"
# 启动 OpenClaw Gateway (主服务, 前台运行)
procd_open_instance "gateway"
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
--port "$port" --bind "$gw_bind"
procd_set_param env \
HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NODE_BASE="$NODE_BASE" \
OC_GLOBAL="$OC_GLOBAL" \
OC_DATA="$OC_DATA" \
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# v2026.4.7/4.9: 环境变量安全验证 (阻止危险变量注入)
_validate_path() {
local p="$1"
case "$p" in
*'$'*|*'`'*|*'|'*|*'&'*) return 1 ;;
/proc/*|/sys/*|/dev/*) return 1 ;;
*) return 0 ;;
esac
}
for _dir in "$NODE_BASE" "$OC_GLOBAL" "$OC_DATA"; do
_validate_path "$_dir" || { logger -t openclaw "安全警告: 路径验证失败: $_dir"; return 1; }
done
procd_open_instance "gateway"
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
--port "$port" --bind "$gw_bind"
procd_set_param env \
HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NODE_BASE="$NODE_BASE" \
OC_GLOBAL="$OC_GLOBAL" \
OC_DATA="$OC_DATA" \
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
procd_set_param user openclaw
procd_set_param respawn 3600 10 5
procd_set_param stdout 1

View File

@ -90,4 +90,16 @@ if [ -z "$CURRENT_PTY_TOKEN" ]; then
uci commit openclaw
fi
# 启用服务自启动 (创建 /etc/rc.d/S99openclaw 符号链接)
# 这一步确保安装/升级后服务能随系统启动
# 注意: 仅当 UCI 中 enabled='1' 或尚未设置 enabled 时才启用
# 如果用户明确设置 enabled='0' (禁用),则尊重用户选择
if [ -x /etc/init.d/openclaw ]; then
OC_ENABLED=$(uci -q get openclaw.main.enabled)
# enabled='1' 或未设置时启用自启动enabled='0' 时不操作
if [ "$OC_ENABLED" != "0" ]; then
/etc/init.d/openclaw enable 2>/dev/null
fi
fi
exit 0

View File

@ -19,7 +19,7 @@ NODE_VERSION_V1="22.15.1"
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
# 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试)
OC_TESTED_VERSION="2026.3.28"
OC_TESTED_VERSION="2026.4.9"
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
OC_VERSION="${OC_VERSION:-}"
@ -375,18 +375,6 @@ install_openclaw() {
log_error "OpenClaw 安装验证失败"
exit 1
fi
# 安装 Gemini CLI (官方模型配置向导的 Google Gemini OAuth 依赖)
if [ -x "$NPM_BIN" ]; then
echo ""
echo "=== 安装 Gemini CLI (Google OAuth 依赖) ==="
"$NPM_BIN" install -g @google/gemini-cli --prefix="$OC_GLOBAL" $install_flags 2>&1 | tail -3
if command -v gemini >/dev/null 2>&1 || [ -x "$OC_GLOBAL/bin/gemini" ]; then
log_info "Gemini CLI 安装成功"
else
log_warn "Gemini CLI 安装失败 (不影响核心功能,仅影响 Google Gemini OAuth 登录)"
fi
fi
}
init_openclaw() {
@ -635,7 +623,10 @@ do_factory_reset() {
log_info "新认证令牌: $new_token"
fi
# 7. 重启服务
# 7. 确保数据目录权限正确 (防止 root 操作留下的文件)
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
# 8. 重启服务
/etc/init.d/openclaw start >/dev/null 2>&1 &
log_info "出厂设置已恢复Gateway 重启中..."
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,10 @@
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
# ── 交互式菜单引擎路径 ──
OC_MENU_ENGINE="/usr/share/openclaw/oc-menu-engine.js"
OC_INTERACTIVE="/usr/share/openclaw/oc-config-interactive.js"
# ── 端口检查兼容函数 (ss 或 netstat) ──
# check_port_listening <port> — 检查端口是否在监听,返回 0/1
check_port_listening() {
@ -200,10 +204,21 @@ enable_auth_plugins() {
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
if(!d.plugins)d.plugins={};if(!d.plugins.entries)d.plugins.entries={};
const e=d.plugins.entries;
['qwen-portal-auth','copilot-proxy','google-gemini-cli-auth','minimax-portal-auth'].forEach(p=>{
// 启用有效的认证插件
['copilot-proxy'].forEach(p=>{
if(!e[p])e[p]={};e[p].enabled=true;
});
// 清理已废弃或未安装的插件配置 (v2026.4.9: 这些插件已移除或更名)
delete e['qwen-portal-auth'];
delete e['google-gemini-cli-auth'];
delete e['minimax-portal-auth'];
delete e['google-antigravity-auth'];
delete e['openclaw-weixin']; // 微信插件有独立安装流程,不应在 entries 中
// 清理过时的 installs 配置
if(d.plugins && d.plugins.installs){
delete d.plugins.installs['google-gemini-cli-auth'];
delete d.plugins.installs['minimax-portal-auth'];
}
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
}catch(e){}
" 2>/dev/null
@ -641,45 +656,48 @@ show_current_config() {
# ══════════════════════════════════════════════════════════════
configure_model() {
echo ""
echo -e " ${BOLD}🤖 配置 AI 模型提供商${NC}"
echo -e " ${BOLD}🤖 配置 AI 模型提供商${NC}"
echo ""
echo -e " ${GREEN}${BOLD}--- 推荐 ---${NC}"
echo -e " ${CYAN}1)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
echo -e " ${GREEN}${BOLD}🌟 ── 推荐 ──${NC}"
echo -e " ${CYAN}w)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
echo ""
echo -e " ${BOLD}--- 快速配置 ---${NC}"
echo -e " ${CYAN}2)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
echo -e " ${CYAN}3)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
echo -e " ${CYAN}4)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
echo -e " ${CYAN}5)${NC} OpenRouter (聚合多家模型)"
echo -e " ${CYAN}6)${NC} DeepSeek (DeepSeek-V3/R1)"
echo -e " ${CYAN}7)${NC} GitHub Copilot (需要 Copilot 订阅)"
echo -e " ${CYAN}8)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
echo -e " ${CYAN}9)${NC} xAI Grok (Grok-3/3-mini)"
echo -e " ${CYAN}10)${NC} Groq (Llama 4, Llama 3.3)"
echo -e " ${CYAN}11)${NC} 硅基流动 SiliconFlow"
echo -e " ${CYAN}12)${NC} Ollama (本地模型,无需 API Key)"
echo -e " ${CYAN}13)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
echo -e " ${CYAN}14)${NC} Mistral AI (Mistral Large, Codestral)"
echo -e " ${CYAN}15)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)"
echo -e " ${CYAN}16)${NC} 自定义 OpenAI 兼容 API"
echo -e " ${CYAN}0)${NC} 返回"
echo -e " ${BOLD}🌍 ── 国外模型提供商 ──${NC}"
echo -e " ${CYAN}a)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
echo -e " ${CYAN}b)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
echo -e " ${CYAN}c)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
echo -e " ${CYAN}d)${NC} OpenRouter (聚合多家模型)"
echo -e " ${CYAN}e)${NC} GitHub Copilot (需要 Copilot 订阅)"
echo -e " ${CYAN}f)${NC} xAI Grok (Grok-4/3)"
echo ""
prompt_with_default "请选择" "1" choice
echo -e " ${BOLD}🇨🇳 ── 国内模型提供商 ──${NC}"
echo -e " ${CYAN}g)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
echo -e " ${CYAN}h)${NC} 硅基流动 SiliconFlow"
echo -e " ${CYAN}i)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
echo -e " ${CYAN}j)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)"
echo -e " ${CYAN}k)${NC} 智谱 GLM / Z.AI (GLM-5.1, GLM-5)"
echo ""
echo -e " ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}"
echo -e " ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)"
echo -e " ${CYAN}m)${NC} 自定义 OpenAI 兼容 API"
echo ""
echo -e " ${CYAN}q)${NC} 返回"
echo ""
prompt_with_default "请选择" "w" choice
case "$choice" in
1)
w)
echo ""
echo -e " ${CYAN}启动官方完整模型配置向导...${NC}"
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
echo -e " ${CYAN}预启用模型认证插件...${NC}"
echo -e " ${CYAN}清理过时插件配置...${NC}"
enable_auth_plugins
echo ""
(oc_cmd configure --section model) || echo -e " ${YELLOW}配置向导已退出${NC}"
echo ""
ask_restart
;;
2)
a)
echo ""
echo -e " ${BOLD}OpenAI 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://platform.openai.com/api-keys${NC}"
@ -712,7 +730,7 @@ configure_model() {
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}"
fi
;;
3)
b)
echo ""
echo -e " ${BOLD}Anthropic 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.anthropic.com/settings/keys${NC}"
@ -743,7 +761,7 @@ configure_model() {
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}"
fi
;;
4)
c)
echo ""
echo -e " ${BOLD}Google Gemini 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://aistudio.google.com/apikey${NC}"
@ -774,7 +792,7 @@ configure_model() {
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}"
fi
;;
5)
d)
echo ""
echo -e " ${BOLD}OpenRouter 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://openrouter.ai/keys${NC}"
@ -808,33 +826,7 @@ configure_model() {
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}"
fi
;;
6)
echo ""
echo -e " ${BOLD}DeepSeek 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://platform.deepseek.com/api_keys${NC}"
echo ""
prompt_with_default "请输入 DeepSeek API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} deepseek-chat — DeepSeek-V3 (通用对话)"
echo -e " ${CYAN}b)${NC} deepseek-reasoner — DeepSeek-R1 (深度推理)"
echo -e " ${CYAN}c)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="deepseek-chat" ;;
b) model_name="deepseek-reasoner" ;;
c) prompt_with_default "请输入模型名称" "deepseek-chat" model_name ;;
*) model_name="deepseek-chat" ;;
esac
auth_set_apikey deepseek "$api_key"
register_custom_provider deepseek "https://api.deepseek.com/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "deepseek/${model_name}"
echo -e " ${GREEN}✅ DeepSeek 已配置,活跃模型: deepseek/${model_name}${NC}"
fi
;;
7)
e)
echo ""
echo -e " ${BOLD}GitHub Copilot 配置${NC}"
echo -e " ${YELLOW}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${NC}"
@ -889,7 +881,7 @@ configure_model() {
echo -e " ${YELLOW}OAuth 授权已退出或失败${NC}"
fi
;;
8)
g)
echo ""
echo -e " ${BOLD}阿里云通义千问 Qwen 配置${NC}"
echo ""
@ -1016,7 +1008,7 @@ configure_model() {
;;
esac
;;
9)
f)
echo ""
echo -e " ${BOLD}xAI Grok 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.x.ai${NC}"
@ -1049,41 +1041,7 @@ configure_model() {
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}"
fi
;;
10)
echo ""
echo -e " ${BOLD}Groq 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.groq.com/keys${NC}"
echo -e " ${YELLOW}Groq 提供超快推理速度${NC}"
echo ""
prompt_with_default "请输入 Groq API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} meta-llama/llama-4-maverick-17b-128e-instruct — Llama 4 Maverick (推荐)"
echo -e " ${CYAN}b)${NC} meta-llama/llama-4-scout-17b-16e-instruct — Llama 4 Scout"
echo -e " ${CYAN}c)${NC} moonshotai/kimi-k2-instruct — Kimi K2"
echo -e " ${CYAN}d)${NC} qwen/qwen3-32b — 通义千问 Qwen3 32B"
echo -e " ${CYAN}e)${NC} llama-3.3-70b-versatile — Llama 3.3 70B"
echo -e " ${CYAN}f)${NC} llama-3.1-8b-instant — Llama 3.1 8B (极速)"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
b) model_name="meta-llama/llama-4-scout-17b-16e-instruct" ;;
c) model_name="moonshotai/kimi-k2-instruct" ;;
d) model_name="qwen/qwen3-32b" ;;
e) model_name="llama-3.3-70b-versatile" ;;
f) model_name="llama-3.1-8b-instant" ;;
g) prompt_with_default "请输入模型名称" "meta-llama/llama-4-maverick-17b-128e-instruct" model_name ;;
*) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
esac
auth_set_apikey groq "$api_key"
register_and_set_model "groq/${model_name}"
echo -e " ${GREEN}✅ Groq 已配置,活跃模型: groq/${model_name}${NC}"
fi
;;
11)
h)
echo ""
echo -e " ${BOLD}硅基流动 SiliconFlow 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://cloud.siliconflow.cn/account/ak${NC}"
@ -1130,7 +1088,7 @@ configure_model() {
echo -e " ${GREEN}✅ SiliconFlow 已配置,活跃模型: siliconflow/${model_name}${NC}"
fi
;;
12)
l)
echo ""
echo -e " ${BOLD}🦙 Ollama 本地模型配置${NC}"
echo -e " ${YELLOW}Ollama 在本地或局域网运行大模型,无需 API Key${NC}"
@ -1246,7 +1204,7 @@ configure_model() {
fi
fi
;;
13)
i)
echo ""
echo -e " ${BOLD}腾讯云大模型 Coding Plan 套餐配置${NC}"
echo ""
@ -1295,37 +1253,7 @@ configure_model() {
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
fi
;;
14)
echo ""
echo -e " ${BOLD}Mistral AI 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.mistral.ai/api-keys/${NC}"
echo ""
prompt_with_default "请输入 Mistral API Key" "" api_key
if [ -n "$api_key" ]; then
auth_set_apikey mistral "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} mistral-large-latest — 旗舰模型,最强性能 (推荐)"
echo -e " ${CYAN}b)${NC} mistral-medium-latest — 均衡模型"
echo -e " ${CYAN}c)${NC} codestral-latest — 代码专用,极速补全"
echo -e " ${CYAN}d)${NC} mistral-small-latest — 轻量快速"
echo -e " ${CYAN}e)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="mistral-large-latest" ;;
b) model_name="mistral-medium-latest" ;;
c) model_name="codestral-latest" ;;
d) model_name="mistral-small-latest" ;;
e) prompt_with_default "请输入模型名称" "mistral-large-latest" model_name ;;
*) model_name="mistral-large-latest" ;;
esac
register_custom_provider mistral "https://api.mistral.ai/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "mistral/${model_name}"
echo -e " ${GREEN}✅ Mistral AI 已配置,活跃模型: mistral/${model_name}${NC}"
fi
;;
15)
j)
echo ""
echo -e " ${BOLD}百度千帆大模型配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${NC}"
@ -1359,7 +1287,70 @@ configure_model() {
echo -e " ${GREEN}✅ 百度千帆已配置,活跃模型: qianfan/${model_name}${NC}"
fi
;;
16)
k)
echo ""
echo -e " ${BOLD}智谱 GLM / Z.AI 配置${NC}"
echo ""
echo -e " ${CYAN}认证方式:${NC}"
echo -e " ${CYAN}a)${NC} CN (open.bigmodel.cn) ${GREEN}★ 国内用户推荐${NC}"
echo -e " ${CYAN}b)${NC} Coding-Plan-CN (智谱 Coding Plan 套餐)"
echo -e " ${CYAN}c)${NC} Global (api.z.ai)"
echo -e " ${CYAN}d)${NC} 手动输入 API Key"
echo ""
prompt_with_default "请选择认证方式" "a" zai_method
case "$zai_method" in
a)
zai_base_url="https://open.bigmodel.cn/api/paas/v4"
echo -e " ${YELLOW}获取 API Key: https://open.bigmodel.cn/api-key${NC}"
;;
b)
zai_base_url="https://open.bigmodel.cn/api/coding/paas/v4"
echo -e " ${YELLOW}Coding Plan 套餐 API Key (sk-...)${NC}"
;;
c)
zai_base_url="https://api.z.ai/api/paas/v4"
echo -e " ${YELLOW}全球版 API Key${NC}"
;;
d)
prompt_with_default "请输入 Base URL" "https://open.bigmodel.cn/api/paas/v4" zai_base_url
;;
*)
zai_base_url="https://open.bigmodel.cn/api/paas/v4"
;;
esac
echo ""
prompt_with_default "请输入智谱 API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} glm-5.1 — GLM-5.1 (推荐)"
echo -e " ${CYAN}b)${NC} glm-5 — GLM-5"
echo -e " ${CYAN}c)${NC} glm-4.7 — GLM-4.7"
echo -e " ${CYAN}d)${NC} glm-4.7-flash — GLM-4.7 Flash"
echo -e " ${CYAN}e)${NC} glm-4.5 — GLM-4.5"
echo -e " ${CYAN}f)${NC} glm-4.5-flash — GLM-4.5 Flash (免费额度)"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="glm-5.1" ;;
b) model_name="glm-5" ;;
c) model_name="glm-4.7" ;;
d) model_name="glm-4.7-flash" ;;
e) model_name="glm-4.5" ;;
f) model_name="glm-4.5-flash" ;;
g) prompt_with_default "请输入模型名称" "glm-5.1" model_name ;;
*) model_name="glm-5.1" ;;
esac
# 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)
auth_set_apikey zai "$api_key"
register_custom_provider zai "$zai_base_url" "$api_key" "$model_name" "$model_name" "128000" "4096"
register_and_set_model "zai/${model_name}"
echo -e " ${GREEN}✅ 智谱 GLM 已配置,活跃模型: zai/${model_name}${NC}"
echo -e " ${DIM} Base URL: ${zai_base_url}${NC}"
fi
;;
m)
echo ""
echo -e " ${BOLD}自定义 OpenAI 兼容 API${NC}"
echo -e " ${YELLOW}支持任何兼容 OpenAI API 格式的服务商${NC}"
@ -1374,7 +1365,7 @@ configure_model() {
echo -e " ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}"
fi
;;
0) return ;;
q) return ;;
esac
if [ "$choice" != "0" ] && [ "$choice" != "1" ]; then
@ -2227,7 +2218,7 @@ reset_to_defaults() {
echo -e " ${CYAN}已取消${NC}"
fi
;;
4)
c)
echo ""
echo -e " ${RED}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${RED}║ ⚠️ 完全恢复出厂设置 ║${NC}"
@ -2409,7 +2400,7 @@ backup_restore_menu() {
oc_cmd backup verify "$latest" 2>&1
fi
;;
4)
c)
echo ""
if [ -d "$backup_dir" ]; then
local count=$(ls "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | wc -l)
@ -2427,7 +2418,7 @@ backup_restore_menu() {
echo ""
echo -e " ${DIM}备份目录: ${backup_dir}${NC}"
;;
5)
d)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
@ -2479,7 +2470,60 @@ backup_restore_menu() {
esac
}
# ══════════════════════════════════════════════════════════════════════════
# 交互式菜单 (方向键导航)
# ══════════════════════════════════════════════════════════════════════════
# 检测是否支持交互模式 (需要 Node.js + TTY 终端)
can_use_interactive() {
[ -x "$NODE_BIN" ] || return 1
[ -f "$OC_INTERACTIVE" ] || return 1
[ -f "$OC_MENU_ENGINE" ] || return 1
# 检测是否为真实 TTY (排除某些非交互环境)
[ -t 0 ] && [ -t 1 ] || return 1
return 0
}
# 启动交互式菜单 (方向键导航版本)
launch_interactive_menu() {
if ! can_use_interactive; then
echo -e " ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}"
return 1
fi
# 调用 Node.js 交互式前端
"$NODE_BIN" "$OC_INTERACTIVE" 2>&1
local rc=$?
# 返回后刷新配置权限
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
return $rc
}
# 启动交互式模型配置
launch_interactive_model_config() {
if ! can_use_interactive; then
echo -e " ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}"
configure_model # 回退到传统菜单
return $?
fi
# 调用 Node.js 交互式前端的模型配置模块
"$NODE_BIN" "$OC_INTERACTIVE" model 2>&1
local rc=$?
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
return $rc
}
main_menu() {
# 直接启动交互模式 (如果支持)
if can_use_interactive && [ "${OC_FORCE_TRADITIONAL:-0}" != "1" ]; then
launch_interactive_menu
return $?
fi
# 传统菜单 (回退方案)
while true; do
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
@ -2693,6 +2737,29 @@ advanced_menu() {
# ── 支持命令行参数 ──
case "${1:-}" in
--tui|--interactive)
# 强制启动交互式菜单 (方向键导航)
if can_use_interactive; then
launch_interactive_menu
else
echo -e "${YELLOW}⚠️ 当前环境不支持交互模式${NC}"
echo "需要: Node.js + TTY 终端"
exit 1
fi
;;
--traditional)
# 强制使用传统菜单
export OC_FORCE_TRADITIONAL=1
main_menu
;;
--model)
# 直接进入模型配置
if can_use_interactive; then
launch_interactive_model_config
else
configure_model
fi
;;
--set)
if [ -n "${2:-}" ] && [ -n "${3:-}" ]; then
json_set "$2" "$3"
@ -2727,12 +2794,21 @@ case "${1:-}" in
echo "OpenClaw AI Gateway — OpenWrt 配置管理工具"
echo ""
echo "用法:"
echo " oc-config.sh # 进入交互式菜单"
echo " oc-config.sh # 进入交互式菜单 (自动检测)"
echo " oc-config.sh --tui # 强制使用方向键交互模式"
echo " oc-config.sh --traditional # 强制使用传统数字菜单"
echo " oc-config.sh --model # 直接进入模型配置"
echo " oc-config.sh --set K V # 设置配置项"
echo " oc-config.sh --get K # 读取配置项"
echo " oc-config.sh --restart # 重启 Gateway"
echo " oc-config.sh --status # 查看状态"
echo ""
echo "交互模式快捷键:"
echo " ↑↓ 导航选项"
echo " 回车 确认选择"
echo " ESC/q 返回上级"
echo " 字符 搜索过滤"
echo ""
;;
"")
main_menu

View File

@ -0,0 +1,727 @@
/**
* ============================================================================
* OpenClaw 配置工具 交互式菜单引擎
* ============================================================================
*
* 功能特性:
* - 方向键 () 导航菜单选项
* - 回车/空格 确认选择
* - Tab 在多列菜单中切换
* - ESC/q 返回上级菜单
* - 搜索过滤 (输入字符实时过滤)
* - Node.js 实现零外部依赖
* - 完全兼容 xterm.js / Web PTY 环境
*
* 使用方式:
* const menu = require('./oc-menu-engine');
* const choice = await menu.select({
* title: '选择模型提供商',
* items: [
* { key: 'a', label: 'OpenAI', desc: 'GPT-5, GPT-4.1' },
* { key: 'b', label: 'Anthropic', desc: 'Claude' },
* ]
* });
*/
// ANSI 颜色代码
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
italic: '\x1b[3m',
underline: '\x1b[4m',
// 前景色
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
// 亮色
brightRed: '\x1b[91m',
brightGreen: '\x1b[92m',
brightYellow: '\x1b[93m',
brightBlue: '\x1b[94m',
brightMagenta: '\x1b[95m',
brightCyan: '\x1b[96m',
// 背景色
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m',
// 光标控制
hide: '\x1b[?25l',
show: '\x1b[?25h',
save: '\x1b[s',
restore: '\x1b[u',
clearLine: '\x1b[2K',
clearDown: '\x1b[J',
// 移动
up: (n = 1) => `\x1b[${n}A`,
down: (n = 1) => `\x1b[${n}B`,
right: (n = 1) => `\x1b[${n}C`,
left: (n = 1) => `\x1b[${n}D`,
toCol: (n) => `\x1b[${n}G`,
toRow: (n) => `\x1b[${n}H`,
to: (row, col) => `\x1b[${row};${col}H`,
};
// ═══════════════════════════════════════════════════════════════════════════
// 终端输入处理
// ═══════════════════════════════════════════════════════════════════════════
/**
* 设置终端为原始模式 (逐字节读取)
*/
function setRawMode(enable) {
if (process.stdin.isTTY) {
process.stdin.setRawMode(enable);
}
}
/**
* stdin 读取单个按键
* 支持 Bracketed Paste Mode (粘贴检测)
* @returns {Promise<{name: string, sequence: string, char: string, paste?: string}>}
*/
function readKey() {
return new Promise((resolve) => {
const onData = (buffer) => {
process.stdin.off('data', onData);
const seq = buffer.toString('utf8');
// 解析按键
let key = { sequence: seq, char: '', name: '' };
// 检测 Bracketed Paste (粘贴操作)
// 终端在粘贴时发送: ESC[200~ <粘贴内容> ESC[201~
if (seq.startsWith('\x1b[200~') && seq.endsWith('\x1b[201~')) {
// 提取粘贴的内容 (去掉首尾的转义序列)
const pasteContent = seq.slice(6, -6);
key.name = 'paste';
key.paste = pasteContent;
key.char = '';
resolve(key);
return;
}
if (seq === '\r' || seq === '\n') {
key.name = 'return';
} else if (seq === '\t') {
key.name = 'tab';
} else if (seq === '\x1b') {
key.name = 'escape';
} else if (seq === '\x7f' || seq === '\x08') {
key.name = 'backspace';
} else if (seq === '\x1b[A') {
key.name = 'up';
} else if (seq === '\x1b[B') {
key.name = 'down';
} else if (seq === '\x1b[C') {
key.name = 'right';
} else if (seq === '\x1b[D') {
key.name = 'left';
} else if (seq === '\x1b[1;2A' || seq === '\x1b[1;2B') {
// Shift+Up/Down - 可用于快速滚动
key.name = seq.endsWith('A') ? 'shift-up' : 'shift-down';
} else if (seq === '\x1b[5~') {
key.name = 'pageup';
} else if (seq === '\x1b[6~') {
key.name = 'pagedown';
} else if (seq === '\x1b[H') {
key.name = 'home';
} else if (seq === '\x1b[F') {
key.name = 'end';
} else if (seq.length === 1 && seq >= ' ' && seq <= '~') {
key.char = seq;
key.name = seq.toLowerCase();
} else if (seq.startsWith('\x1b[') && seq.endsWith('~')) {
// 功能键 F1-F12
const code = seq.slice(2, -1);
if (code >= '11' && code <= '24') {
key.name = `f${parseInt(code) - 10}`;
}
}
resolve(key);
};
process.stdin.once('data', onData);
});
}
// ═══════════════════════════════════════════════════════════════════════════
// 交互式选择菜单
// ═══════════════════════════════════════════════════════════════════════════
// 记录上一次渲染的菜单行数,用于正确清除
let lastRenderLines = 0;
/**
* 重置渲染计数器 (在打印非菜单内容前调用)
*/
function resetRenderCount() {
lastRenderLines = 0;
}
/**
* 渲染菜单选项
* @param {Array} items 菜单项数组
* @param {number} selected 当前选中索引
* @param {string} searchFilter 搜索过滤字符串
* @param {object} options 配置选项
* @returns {Array} 过滤后的菜单项
*/
function renderMenu(items, selected, searchFilter = '', options = {}) {
const {
title = '请选择',
header = '',
footer = '',
showSearch = true,
showHelp = true,
maxWidth = 80,
} = options;
// 过滤项目
let filteredItems = items;
if (searchFilter) {
const filter = searchFilter.toLowerCase();
filteredItems = items.filter(item =>
(item.label || '').toLowerCase().includes(filter) ||
(item.desc || '').toLowerCase().includes(filter) ||
(item.key || '').toLowerCase().includes(filter)
);
}
// 输出缓冲
const lines = [];
// 标题
if (title) {
lines.push(`${C.bold}${C.cyan} ${title}${C.reset}`);
lines.push('');
}
// 头部信息
if (header) {
lines.push(` ${header}`);
lines.push('');
}
// 搜索框
if (showSearch && searchFilter !== undefined) {
const searchPrompt = searchFilter
? `${C.dim}搜索: ${C.reset}${C.yellow}${searchFilter}${C.reset}${C.dim}${C.reset}`
: `${C.dim}搜索: ${C.reset}${C.dim}(输入字符过滤ESC 清空)${C.reset}`;
lines.push(` ${searchPrompt}`);
lines.push('');
}
// 菜单项
filteredItems.forEach((item, idx) => {
const isSelected = idx === selected;
const isDisabled = item.disabled;
// 选中态样式 - 使用背景色高亮整行
const cursor = isSelected ? `${C.cyan}${C.reset} ` : ' ';
const labelStyle = isSelected ? `${C.bold}${C.cyan}` : (isDisabled ? C.dim : '');
const descStyle = isSelected ? C.cyan : C.dim;
// 构建行
const keyLabel = item.key ? `${C.yellow}${item.key}${C.reset}` : '';
const mainLabel = `${labelStyle}${item.label || ''}${C.reset}`;
const descLabel = item.desc ? `${descStyle}${item.desc}${C.reset}` : '';
// 拼接
let line = cursor;
if (keyLabel) line += `${C.dim}[${C.reset}${keyLabel}${C.dim}]${C.reset} `;
line += mainLabel;
if (descLabel && !isDisabled) line += ` ${descLabel}`;
lines.push(line);
});
// 空状态
if (filteredItems.length === 0) {
lines.push(` ${C.dim}(没有匹配的项目)${C.reset}`);
}
// 底部帮助
if (showHelp) {
lines.push('');
lines.push(` ${C.dim}↑↓ 导航 回车 确认 ESC/q 返回${showSearch ? ' 输入 搜索' : ''}${C.reset}`);
}
// 自定义底部
if (footer) {
lines.push('');
lines.push(` ${footer}`);
}
// 计算需要的清除行数
const totalLines = lines.length + 1;
// 清除之前的内容: 移动到开头,清除到屏幕底部
let clearSeq = '\r';
if (lastRenderLines > 0) {
// 移动到上一次渲染的第一行
clearSeq += `\x1b[${lastRenderLines}A`;
// 清除从此行到屏幕底部
clearSeq += C.clearDown;
}
lastRenderLines = totalLines;
// 输出: 清除旧内容 + 新内容
process.stdout.write(clearSeq + lines.join('\n') + '\n');
return filteredItems;
}
/**
* 交互式单选菜单
* @param {object} options 配置选项
* @returns {Promise<object|null>} 选中的项目或 null (取消)
*/
async function select(options = {}) {
const {
items = [],
title = '请选择',
header = '',
footer = '',
defaultIndex = 0,
showSearch = true,
allowCancel = true,
exitKeys = ['escape', 'q'],
onSelect = null, // 选择回调 (可用于预览)
} = options;
if (items.length === 0) {
return null;
}
// 重置渲染行计数
lastRenderLines = 0;
// 初始化状态
let selected = Math.min(defaultIndex, items.length - 1);
let searchFilter = '';
let filteredItems = items;
let done = false;
let result = null;
// 保存终端状态
setRawMode(true);
process.stdout.write(C.hide);
process.stdout.write(C.save);
try {
// 初始渲染
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
while (!done) {
const key = await readKey();
// 处理按键
if (key.name === 'return') {
// 确认选择
if (filteredItems.length > 0 && !filteredItems[selected].disabled) {
result = filteredItems[selected];
done = true;
}
} else if (key.name === 'up') {
// 上移
if (selected > 0) {
selected--;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
} else if (key.name === 'down') {
// 下移
if (selected < filteredItems.length - 1) {
selected++;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
} else if (key.name === 'pageup') {
// 上翻页
selected = Math.max(0, selected - 10);
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'pagedown') {
// 下翻页
selected = Math.min(filteredItems.length - 1, selected + 10);
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'home') {
selected = 0;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'end') {
selected = filteredItems.length - 1;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (exitKeys.includes(key.name) && allowCancel) {
// 取消/退出
done = true;
} else if (key.name === 'backspace') {
// 删除搜索字符
if (searchFilter.length > 0) {
searchFilter = searchFilter.slice(0, -1);
selected = 0; // 重置选中
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
} else if (key.char && key.char.length === 1) {
// 输入字符 -> 搜索
if (showSearch) {
searchFilter += key.char;
selected = 0;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
}
// 直接按键选择 (如果菜单项有 key 属性)
if (key.char && !showSearch) {
const matchByChar = items.find(item => (item.key || '').toLowerCase() === key.name);
if (matchByChar) {
result = matchByChar;
done = true;
}
}
}
} finally {
// 恢复终端状态
process.stdout.write(C.restore);
process.stdout.write(C.clearDown);
process.stdout.write(C.show);
setRawMode(false);
}
return result;
}
// ═══════════════════════════════════════════════════════════════════════════
// 其他交互组件
// ═══════════════════════════════════════════════════════════════════════════
/**
* 文本输入框
* 支持粘贴操作 (Bracketed Paste Mode)
* @param {object} options 配置选项
* @returns {Promise<string|null>} 输入的文本或 null (取消)
*/
async function input(options = {}) {
const {
prompt = '请输入',
defaultValue = '',
placeholder = '',
validate = null,
allowCancel = true,
hidden = false, // 密码输入模式
} = options;
let value = '';
let done = false;
let cancelled = false;
// 启用 Bracketed Paste Mode
process.stdout.write('\x1b[?2004h');
setRawMode(true);
process.stdout.write(C.hide);
try {
// 初始渲染
const renderInput = () => {
const displayValue = hidden ? '*'.repeat(value.length) : value;
const promptText = `${C.bold}${C.cyan}${prompt}:${C.reset} `;
const valueText = displayValue || `${C.dim}${placeholder}${C.reset}`;
process.stdout.write(`\r${C.clearLine}${promptText}${valueText}`);
// 光标定位
if (displayValue) {
process.stdout.write(C.left(displayValue.length - value.length));
}
};
renderInput();
while (!done) {
const key = await readKey();
if (key.name === 'return') {
// 确认
const finalValue = value || defaultValue;
if (validate) {
const error = validate(finalValue);
if (error) {
process.stdout.write(`\n${C.red}${error}${C.reset}\n`);
renderInput();
continue;
}
}
done = true;
} else if ((key.name === 'escape' || key.name === 'q') && allowCancel) {
done = true;
cancelled = true;
} else if (key.name === 'backspace') {
if (value.length > 0) {
value = value.slice(0, -1);
renderInput();
}
} else if (key.name === 'paste' && key.paste) {
// 处理粘贴操作 (Bracketed Paste Mode)
// 清理粘贴内容中的控制字符和换行符
const cleanPaste = key.paste
.replace(/[\r\n]+/g, ' ') // 换行符替换为空格
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // 移除 ANSI 转义序列
.replace(/[\x00-\x1f]/g, ''); // 移除控制字符
value += cleanPaste;
renderInput();
} else if (key.char && key.char.length === 1) {
value += key.char;
renderInput();
} else if (key.sequence && key.sequence.length > 1 && !key.name.startsWith('page') &&
!['up', 'down', 'left', 'right', 'home', 'end'].includes(key.name)) {
// 回退处理: 如果收到未知的多字符序列,可能是粘贴内容
// 尝试提取可打印字符
const extracted = key.sequence
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
.replace(/[\r\n]+/g, ' ')
.replace(/[\x00-\x1f]/g, '')
.replace(/[^\x20-\x7e]/g, '');
if (extracted.length > 0) {
value += extracted;
renderInput();
}
}
}
} finally {
// 禁用 Bracketed Paste Mode
process.stdout.write('\x1b[?2004l');
process.stdout.write(C.show);
setRawMode(false);
}
if (cancelled) return null;
// 显示最终值 (非隐藏模式)
if (!hidden) {
process.stdout.write(`\n`);
}
return value || defaultValue;
}
/**
* 确认对话框
* @param {object} options 配置选项
* @returns {Promise<boolean>} 用户选择
*/
async function confirm(options = {}) {
const {
prompt = '确认吗?',
defaultYes = true,
} = options;
let selected = defaultYes; // true = Yes, false = No
setRawMode(true);
process.stdout.write(C.hide);
const renderConfirm = () => {
const yesStyle = selected ? `${C.bold}${C.green}` : C.dim;
const noStyle = selected ? C.dim : `${C.bold}${C.red}`;
process.stdout.write(
`\r${C.clearLine}${C.bold}${C.cyan}${prompt}${C.reset} ` +
`${yesStyle}[Y] 是${C.reset} / ` +
`${noStyle}[N] 否${C.reset} ` +
`${C.dim}(←→ 切换, 回车 确认)${C.reset}`
);
};
renderConfirm();
try {
while (true) {
const key = await readKey();
if (key.name === 'return') {
process.stdout.write('\n');
return selected;
} else if (key.name === 'left' || key.name === 'y') {
selected = true;
renderConfirm();
} else if (key.name === 'right' || key.name === 'n') {
selected = false;
renderConfirm();
} else if (key.name === 'escape') {
process.stdout.write('\n');
return false;
}
}
} finally {
process.stdout.write(C.show);
setRawMode(false);
}
}
/**
* 多选菜单 (复选框)
* @param {object} options 配置选项
* @returns {Promise<Array>} 选中的项目数组
*/
async function multiselect(options = {}) {
const {
items = [],
title = '请选择 (空格 切换选中)',
defaultSelected = [],
} = options;
if (items.length === 0) return [];
const selectedSet = new Set(defaultSelected);
let cursor = 0;
let done = false;
let cancelled = false;
setRawMode(true);
process.stdout.write(C.hide);
const render = () => {
const lines = [`\r${C.bold}${C.cyan}${title}${C.reset}\n`];
items.forEach((item, idx) => {
const isCursor = idx === cursor;
const isChecked = selectedSet.has(idx);
const cursorStyle = isCursor ? `${C.bold}${C.cyan}` : ' ';
const checkChar = isChecked ? `${C.green}${C.reset}` : `${C.dim}${C.reset}`;
const labelStyle = isCursor ? C.bold : '';
lines.push(`\r${cursorStyle} ${checkChar} ${labelStyle}${item.label}${C.reset}`);
});
lines.push(`\r\n${C.dim}↑↓ 移动 空格 选中 回车 确认 ESC 取消${C.reset}`);
process.stdout.write(C.clearDown + lines.join('\n') + '\n');
};
render();
try {
while (!done) {
const key = await readKey();
if (key.name === 'return') {
done = true;
} else if (key.name === 'escape' || key.name === 'q') {
done = true;
cancelled = true;
} else if (key.name === 'up' && cursor > 0) {
cursor--;
render();
} else if (key.name === 'down' && cursor < items.length - 1) {
cursor++;
render();
} else if (key.name === ' ' || key.name === 'tab') {
if (selectedSet.has(cursor)) {
selectedSet.delete(cursor);
} else {
selectedSet.add(cursor);
}
render();
}
}
} finally {
process.stdout.write(C.show);
setRawMode(false);
}
if (cancelled) return [];
return Array.from(selectedSet).map(idx => items[idx]);
}
// ═══════════════════════════════════════════════════════════════════════════
// 进度指示器
// ═══════════════════════════════════════════════════════════════════════════
/**
* 加载动画 (Spinner)
*/
const spinners = {
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
line: ['-', '\\', '|', '/'],
circle: ['◜', '◠', '◝', '◞', '◡', '◟'],
};
function spinner(options = {}) {
const {
text = '加载中...',
style = 'dots',
} = options;
const frames = spinners[style] || spinners.dots;
let frame = 0;
let interval = null;
let stopped = false;
return {
start() {
process.stdout.write(C.hide);
interval = setInterval(() => {
if (stopped) return;
const symbol = `${C.cyan}${frames[frame]}${C.reset}`;
process.stdout.write(`\r${C.clearLine}${symbol} ${text}`);
frame = (frame + 1) % frames.length;
}, 80);
},
update(newText) {
if (interval && !stopped) {
const symbol = `${C.cyan}${frames[frame]}${C.reset}`;
process.stdout.write(`\r${C.clearLine}${symbol} ${newText}`);
}
},
stop(finalText = '') {
stopped = true;
if (interval) {
clearInterval(interval);
interval = null;
}
process.stdout.write(`\r${C.clearLine}${finalText}\n`);
process.stdout.write(C.show);
},
succeed(text) {
this.stop(`${C.green}${C.reset} ${text}`);
},
fail(text) {
this.stop(`${C.red}${C.reset} ${text}`);
},
};
}
// ═══════════════════════════════════════════════════════════════════════════
// 导出
// ═══════════════════════════════════════════════════════════════════════════
module.exports = {
// 颜色常量
C,
// 核心交互
select,
input,
confirm,
multiselect,
// 工具
spinner,
readKey,
setRawMode,
resetRenderCount,
// 底层渲染
renderMenu,
};

View File

@ -120,14 +120,6 @@ if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hy
translate("The configured type also applies to the core specified when manually importing nodes."))
end
o = s:option(ListValue, "domain_strategy", "Sing-box " .. translate("Domain Strategy"), translate("Set the default domain resolution strategy for the sing-box node."))
o.default = ""
o:value("", translate("Auto"))
o:value("prefer_ipv4", translate("Prefer IPv4"))
o:value("prefer_ipv6", translate("Prefer IPv6"))
o:value("ipv4_only", translate("IPv4 Only"))
o:value("ipv6_only", translate("IPv6 Only"))
---- Subscribe Delete All
o = s:option(DummyValue, "_stop", translate("Delete All Subscribe Node"))
o.rawhtml = true

View File

@ -133,6 +133,35 @@ o.validate = function(self, value)
return value:gsub("%s+", ""):gsub("%z", "")
end
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" ..
translate("Supports only Xray or Sing-box node types.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, "domain_resolver_dns", "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ domain_resolver = "tcp" })
o:depends({ domain_resolver = "udp" })
o = s:option(Value, "domain_resolver_dns_https", "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ domain_resolver = "https" })
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o = s:option(Flag, "allowInsecure", translate("allowInsecure"))
o.default = "1"
o.rmempty = false
@ -203,15 +232,6 @@ if #hysteria2_type > 0 then
end
end
o = s:option(ListValue, "domain_strategy", "Sing-box " .. translate("Domain Strategy"), translate("Set the default domain resolution strategy for the sing-box node."))
o.default = "global"
o:value("global", translate("Use global config"))
o:value("", translate("Auto"))
o:value("prefer_ipv4", translate("Prefer IPv4"))
o:value("prefer_ipv6", translate("Prefer IPv6"))
o:value("ipv4_only", translate("IPv4 Only"))
o:value("ipv6_only", translate("IPv6 Only"))
o = s:option(Flag, "boot_update", translate("Update Once on Boot"), translate("Updates the subscription the first time PassWall runs automatically after each system boot."))
o.default = 0

View File

@ -240,16 +240,6 @@ o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for index, value in ipairs(protocols) do
if not value:find("^_") then
s.fields[_n("address")]:depends({ [_n("protocol")] = value })
s.fields[_n("port")]:depends({ [_n("protocol")] = value })
end
end
end
o = s:option(Value, _n("uuid"), translate("ID"))
o.password = true
o:depends({ [_n("protocol")] = "vmess" })
@ -735,10 +725,48 @@ o.datatype = "uinteger"
o.placeholder = 0
o:depends({ [_n("protocol")] = "vless" })
for i, v in ipairs(s.fields[_n("protocol")].keylist) do
if not v:find("^_") and v ~= "hysteria2" then
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "tcp" })
o:depends({ [_n("domain_resolver")] = "udp" })
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "https" })
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
s.fields[_n("address")]:depends({ [_n("protocol")] = v })
s.fields[_n("port")]:depends({ [_n("protocol")] = v })
s.fields[_n("domain_resolver")]:depends({ [_n("protocol")] = v })
s.fields[_n("domain_strategy")]:depends({ [_n("protocol")] = v })
if v ~= "hysteria2" then
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
end
end
end
end
end

View File

@ -206,16 +206,6 @@ o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for index, value in ipairs(protocols) do
if not value:find("^_") then
s.fields[_n("address")]:depends({ [_n("protocol")] = value })
s.fields[_n("port")]:depends({ [_n("protocol")] = value })
end
end
end
o = s:option(Value, _n("uuid"), translate("ID"))
o.password = true
o:depends({ [_n("protocol")] = "vmess" })
@ -742,6 +732,28 @@ o:value("v2ray-plugin")
o = s:option(Value, _n("plugin_opts"), translate("opts"))
o:depends({ [_n("plugin_enabled")] = true })
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "tcp" })
o:depends({ [_n("domain_resolver")] = "udp" })
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "https" })
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
@ -749,17 +761,19 @@ o:value("prefer_ipv4", translate("Prefer IPv4"))
o:value("prefer_ipv6", translate("Prefer IPv6"))
o:value("ipv4_only", translate("IPv4 Only"))
o:value("ipv6_only", translate("IPv6 Only"))
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "wireguard" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "anytls" })
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
s.fields[_n("address")]:depends({ [_n("protocol")] = v })
s.fields[_n("port")]:depends({ [_n("protocol")] = v })
s.fields[_n("domain_resolver")]:depends({ [_n("protocol")] = v })
s.fields[_n("domain_strategy")]:depends({ [_n("protocol")] = v })
end
end
end
end
-- [[ Normal single node End ]]

View File

@ -11,6 +11,10 @@ local ech_domain = {}
local local_version = api.get_app_version("sing-box"):match("[^v]+")
local version_ge_1_13_0 = api.compare_versions(local_version, ">=", "1.13.0")
local GLOBAL = {
DNS_SERVER = {}
}
local GEO_VAR = {
OK = nil,
DIR = nil,
@ -154,6 +158,47 @@ function gen_outbound(flag, node, tag, proxy_table)
detour = node.detour,
}
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
local dns_tag = node_id .. "_dns"
local dns_proto = node.domain_resolver
local server_address
local server_port
local server_path
if dns_proto == "https" then
local _a = api.parseURL(node.domain_resolver_dns_https)
if _a then
server_address = _a.hostname
if _a.port then
server_port = _a.port
else
server_port = 443
end
server_path = _a.pathname
end
else
server_address = node.domain_resolver_dns
server_port = 53
local split = api.split(server_address, ":")
if #split > 1 then
server_address = split[1]
server_port = tonumber(split[#split])
end
end
GLOBAL.DNS_SERVER[node_id] = {
server = {
tag = dns_tag,
type = dns_proto,
server = server_address,
server_port = server_port,
path = server_path,
domain_resolver = "direct",
detour = "direct"
},
domain = node.address
}
result.domain_resolver.server = dns_tag
end
local tls = nil
if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" then
node.tls = "1"
@ -1922,6 +1967,16 @@ function gen_config(var)
}
end
for i, v in pairs(GLOBAL.DNS_SERVER) do
table.insert(dns.servers, v.server)
if not dns.rules then dns.rules = {} end
table.insert(dns.rules, {
action = "route",
server = v.server.tag,
domain = v.domain,
})
end
if next(ech_domain) ~= nil then
table.insert(dns.servers, {
tag = "ech-dns",
@ -1983,6 +2038,12 @@ function gen_config(var)
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
end
if not value.detour and value.server then
value.detour = "direct"
end
if value.server and not api.datatypes.hostname(value.server) then
value.domain_resolver = nil
end
for k, v in pairs(config.outbounds[index]) do
if k:find("_") == 1 then
config.outbounds[index][k] = nil

View File

@ -6,6 +6,11 @@ local jsonc = api.jsonc
local appname = "passwall"
local fs = api.fs
local GLOBAL = {
DNS_SERVER = {},
DNS_HOSTNAME = {}
}
local xray_version = api.get_app_version("xray")
local function get_domain_excluded()
@ -134,6 +139,7 @@ function gen_outbound(flag, node, tag, proxy_table)
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria") and {
sockopt = {
mark = 255,
domainStrategy = node.domain_strategy or "UseIP",
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
tcpMptcp = (node.tcpMptcp == "1") and true or nil
},
@ -397,6 +403,48 @@ function gen_outbound(flag, node, tag, proxy_table)
result.streamSettings.tlsSettings.alpn = alpn
end
end
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
local dns_tag = node_id .. "_dns"
local dns_proto = node.domain_resolver
local config_address
local config_port
if dns_proto == "https" then
local _a = api.parseURL(node.domain_resolver_dns_https)
if _a then
config_address = node.domain_resolver_dns_https
if _a.port then
config_port = _a.port
else
config_port = 443
end
if _a.hostname then
if api.datatypes.hostname(_a.hostname) then
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
end
end
end
else
local server_address = node.domain_resolver_dns
local config_port = 53
local split = api.split(server_address, ":")
if #split > 1 then
server_address = split[1]
config_port = tonumber(split[#split])
end
config_address = server_address
if dns_proto == "tcp" then
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
end
end
GLOBAL.DNS_SERVER[node_id] = {
tag = dns_tag,
queryStrategy = node.domain_strategy or "UseIP",
address = config_address,
port = config_port,
domains = {"full:" .. node.address}
}
end
end
return result
end
@ -1437,7 +1485,15 @@ function gen_config(var)
end
end
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) then
local node_dns = {}
for i, v in pairs(GLOBAL.DNS_SERVER) do
table.insert(node_dns, {
server = v,
outboundTag = "direct"
})
end
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) or #node_dns > 0 then
if not routing then
routing = {
domainStrategy = "IPOnDemand",
@ -1461,6 +1517,8 @@ function gen_config(var)
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
}
direct_dns_udp_server = (direct_dns_udp_server and direct_dns_udp_server ~= "") and direct_dns_udp_server or nil
if direct_dns_udp_server or direct_dns_tcp_server then
local domain = {}
local nodes_domain_text = sys.exec('uci show passwall | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
@ -1489,14 +1547,12 @@ function gen_config(var)
end
end
local _remote_dns = {
--tag = "dns-global-remote",
queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4",
}
local _remote_dns = {}
if remote_dns_udp_server then
_remote_dns.address = remote_dns_udp_server
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
else
elseif remote_dns_tcp_server then
_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
end
@ -1510,7 +1566,11 @@ function gen_config(var)
_remote_dns.port = tonumber(remote_dns_doh_port)
end
table.insert(dns.servers, _remote_dns)
if next(_remote_dns) then
-- _remote_dns.tag = "dns-global-remote"
_remote_dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
table.insert(dns.servers, _remote_dns)
end
local _remote_fakedns = {
--tag = "dns-global-remote-fakedns",
@ -1649,7 +1709,7 @@ function gen_config(var)
dns_server = nil
end
if dns_server then
dns_server.finalQuery = true
--dns_server.finalQuery = true
dns_server.domains = value.domain
if value.shunt_rule_name then
dns_server.tag = "dns-in-" .. value.shunt_rule_name
@ -1666,7 +1726,7 @@ function gen_config(var)
end
local _outboundTag
if not api.is_local_ip(_remote_dns.address) or dns_outbound_tag == "blackhole" then --dns为本地ip不走代理
if _remote_dns.address and not api.is_local_ip(_remote_dns.address) or dns_outbound_tag == "blackhole" then --dns为本地ip不走代理
_outboundTag = dns_outbound_tag
else
_outboundTag = "direct"
@ -1698,6 +1758,37 @@ function gen_config(var)
if dns_hosts_len == 0 then
dns.hosts = nil
end
-- 自定义节点 DNS
if #node_dns > 0 and #dns.servers < 1 then
dns.servers = { "localhost" }
end
for i = #node_dns, 1, -1 do
local value = node_dns[i]
table.insert(routing.rules, 1, {
inboundTag = {
value.server.tag
},
outboundTag = value.outboundTag,
})
table.insert(dns.servers, 2, value.server)
end
if next(GLOBAL.DNS_HOSTNAME) then
local hostname = {}
for line, _ in pairs(GLOBAL.DNS_HOSTNAME) do
table.insert(hostname, line)
end
table.insert(dns.servers, 2, {
tag = "bootstrap",
address = "223.5.5.5",
queryStrategy = "UseIPv4",
domains = hostname
})
table.insert(routing.rules, 1, {
inboundTag = { "bootstrap" },
outboundTag = "direct"
})
end
end
if inbounds or outbounds then

View File

@ -1972,6 +1972,18 @@ msgstr "延迟ms"
msgid "If is domain name, The requested domain name will be resolved to IP before connect."
msgstr "如果是域名,域名将在请求发出之前解析为 IP。"
msgid "Domain DNS Resolve"
msgstr "域名 DNS 解析"
msgid "If the node address is a domain name, this DNS will be used for resolution."
msgstr "如果节点地址是域名,则将使用此 DNS 进行解析。"
msgid "Note: For node-specific DNS only. Keep Auto to avoid extra overhead."
msgstr "注意:仅用于节点专用 DNS通常请保持自动以免增加开销。"
msgid "Supports only Xray or Sing-box node types."
msgstr "仅支持 Xray 或 Sing-box 类型节点。"
msgid "Chain Proxy"
msgstr "链式代理"
@ -1985,15 +1997,12 @@ msgid ""
"Chained proxy works only with Xray or Sing-box nodes.<br>"
"You can only use manual or imported nodes as chained nodes."
msgstr ""
"链式代理仅支持 Xray Sing-box 节点。<br>"
"链式代理仅支持 Xray Sing-box 节点。<br>"
"仅支持手动添加或导入的节点用作链式节点。"
msgid "Only work with using the %s node."
msgstr "与使用 %s 节点时生效。"
msgid "Set the default domain resolution strategy for the sing-box node."
msgstr "为 sing-box 节点设置默认的域名解析策略。"
msgid "Total Lines"
msgstr "总行数:"

View File

@ -186,7 +186,18 @@ run_singbox() {
[ -n "$no_run" ] && json_add_string "no_run" "1"
local _json_arg="$(json_dump)"
lua $UTIL_SINGBOX gen_config "${_json_arg}" > $config_file
[ -n "$no_run" ] || ln_run "$SINGBOX_BIN" "sing-box" $log_file run -c "$config_file"
[ -n "$no_run" ] && return
local test_log_file=$log_file
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
$SINGBOX_BIN check -c "$config_file" > $test_log_file 2>&1; local status=$?
if [ "${status}" = 0 ]; then
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
else
echolog "Sing-box 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
cat ${test_log_file} >> ${LOG_FILE}
fi
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
}
run_xray() {
@ -275,7 +286,18 @@ run_xray() {
[ -n "$no_run" ] && json_add_string "no_run" "1"
local _json_arg="$(json_dump)"
lua $UTIL_XRAY gen_config "${_json_arg}" > $config_file
[ -n "$no_run" ] || ln_run "$XRAY_BIN" "xray" $log_file run -c "$config_file"
[ -n "$no_run" ] && return
local test_log_file=$log_file
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
$XRAY_BIN run -test -c "$config_file" > $test_log_file; local status=$?
if [ "${status}" = 0 ]; then
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
else
echolog "Xray 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
cat ${test_log_file} >> ${LOG_FILE}
fi
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
}
run_dns2socks() {

View File

@ -42,8 +42,7 @@ local core_has = {
["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2
}
----
local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
local domain_strategy_node = ""
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- 判断是否过滤节点关键字
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
@ -1855,9 +1854,22 @@ local function update_node(manual)
if kkk ~= "group" or vvv ~= "default" then
uci:set(appname, cfgid, kkk, vvv)
end
-- sing-box 域名解析策略
if kkk == "type" and vvv == "sing-box" then
uci:set(appname, cfgid, "domain_strategy", domain_strategy_node)
-- sing-box/xray 节点域名解析
if kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
if domain_resolver then
uci:set(appname, cfgid, "domain_resolver", domain_resolver)
if domain_resolver_dns then
uci:set(appname, cfgid, "domain_resolver_dns", domain_resolver_dns)
elseif domain_resolver_dns_https then
uci:set(appname, cfgid, "domain_resolver_dns_https", domain_resolver_dns_https)
end
end
if domain_strategy then
if vvv == "sing-box" then
domain_strategy = (domain_strategy == "UseIPv4" and "ipv4_only") or (domain_strategy == "UseIPv6" and "ipv6_only") or domain_strategy
end
uci:set(appname, cfgid, "domain_strategy", domain_strategy)
end
end
-- 订阅组链式代理
if chain_node_type ~= "" and kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
@ -2069,12 +2081,11 @@ local execute = function()
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
hysteria2_type_default = hysteria2_type
end
local domain_strategy = value.domain_strategy or "global"
if domain_strategy ~= "global" then
domain_strategy_node = domain_strategy
else
domain_strategy_node = domain_strategy_default
end
domain_resolver = value.domain_resolver
domain_resolver_dns = value.domain_resolver_dns
domain_resolver_dns_https = value.domain_resolver_dns_https
domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil
-- 订阅组链式代理
local function valid_chain_node(node)

View File

@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.4.5
PKG_VERSION:=26.4.10
PKG_RELEASE:=1
PKG_PO_VERSION:=$(PKG_VERSION)

View File

@ -168,6 +168,19 @@ node_socks_bind_local:depends({ node = "", ["!reverse"] = true })
s:tab("DNS", translate("DNS"))
o = s:taboption("DNS", TextValue, "direct_dns_shunt", translate("Direct domain DNS routing"))
o.description = "<br /><ul>"
.. "<li>" .. translate("Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "</li>"
.. "<li>" .. translate("Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "</li>"
.. "<li>" .. translate("Such as:") .. "</li>"
.. "<li>" .. "domain:my-nodes.com tcp://223.5.5.5" .. "</li>"
.. "<li>" .. "domain:vpn.com udp://119.29.29.29:53" .. "</li>"
.. "<li>" .. "full:www.dnspod.com https://120.53.53.53/dns-query" .. "</li>"
.. "<li>" .. '<a style="color:red">' .. translate("Please note that the program will not start if the format is incorrect!") .. '</a>' .. "</li>"
.. "</ul>"
o.rows = 3
o.wrap = "off"
o = s:taboption("DNS", ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy"))
o.default = "UseIP"
o:value("UseIP")

View File

@ -91,6 +91,34 @@ local function convert_geofile()
convert(GEO_VAR.IP_PATH, "geoip", GEO_VAR.IP_TAGS)
end
function parseDNS(str)
local result_dns_server
-- [proto]://[ip]
-- [proto]://[ip]:[port]
-- https://[ip]/[path]
-- https://[ip]:[port]/[path]
local _a = api.parseURL(str)
if _a then
if _a.protocol == "tcp" or _a.protocol == "udp" or _a.protocol == "https" then
result_dns_server = {
type = _a.protocol,
server = _a.hostname
}
if _a.port then
result_dns_server.server_port = _a.port
else
if _a.protocol == "https" then
result_dns_server.server_port = 443
else
result_dns_server.server_port = 53
end
end
result_dns_server.path = _a.pathname
end
end
return result_dns_server
end
function gen_outbound(flag, node, tag, proxy_table)
local result = nil
if node then
@ -1754,6 +1782,13 @@ function gen_config(var)
tag = "local"
})
local direct_strategy = "prefer_ipv6"
if direct_dns_query_strategy == "UseIPv4" then
direct_strategy = "ipv4_only"
elseif direct_dns_query_strategy == "UseIPv6" then
direct_strategy = "ipv6_only"
end
if dns_listen_port then
local dns_host = ""
if flag == "global" then
@ -1891,13 +1926,6 @@ function gen_config(var)
})
end
direct_strategy = "prefer_ipv6"
if direct_dns_query_strategy == "UseIPv4" then
direct_strategy = "ipv4_only"
elseif direct_dns_query_strategy == "UseIPv6" then
direct_strategy = "ipv6_only"
end
local server_port = tonumber(direct_dns_udp_port) or 53
table.insert(dns.servers, {
@ -2229,7 +2257,155 @@ function gen_proto_config(var)
return jsonc.stringify(config, 1)
end
function gen_front_dns_config(var)
local dns_listen_port = var["dns_listen_port"]
local direct_dns_udp_server = var["direct_dns_udp_server"]
local direct_dns_udp_port = var["direct_dns_udp_port"]
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
local default_dns_udp_server = var["default_dns_udp_server"]
local default_dns_udp_port = var["default_dns_udp_port"]
local dns = {
servers = {},
rules = {}
}
local inbounds = {}
local outbounds = {}
local route = {}
local direct_strategy = "prefer_ipv6"
if direct_dns_query_strategy == "UseIPv4" then
direct_strategy = "ipv4_only"
elseif direct_dns_query_strategy == "UseIPv6" then
direct_strategy = "ipv6_only"
end
table.insert(outbounds, {
type = "direct",
tag = "direct",
routing_mark = 255,
domain_resolver = {
server = "direct",
strategy = direct_strategy
}
})
local direct_dns_shunt = uci:get(appname, "@global[0]", "direct_dns_shunt") or ""
if #direct_dns_shunt > 0 then
local dns_server = {}
string.gsub(direct_dns_shunt, '[^' .. "\r\n" .. ']+', function(w)
if w:find("#") == 1 then return end
local domain = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w))
local dns = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w))
if domain ~= "" and dns ~= "" then
local new_dns_server = parseDNS(dns)
if new_dns_server then
if not dns_server[dns] then
dns_server[dns] = {}
end
if not dns_server[dns].server then
dns_server[dns].server = new_dns_server
dns_server[dns].server.tag = dns
dns_server[dns].server.detour = "direct"
end
if not dns_server[dns].rule then
dns_server[dns].rule = {
action = "route",
server = dns,
domain = {},
domain_suffix = {},
domain_keyword = {}
}
end
if domain:find("full:") == 1 then
table.insert(dns_server[dns].rule.domain, domain:sub(1 + #"full:"))
elseif domain:find("domain:") == 1 then
table.insert(dns_server[dns].rule.domain_suffix, domain:sub(1 + #"domain:"))
else
table.insert(dns_server[dns].rule.domain_keyword, domain)
end
end
end
end)
for k, v in pairs(dns_server) do
table.insert(dns.servers, v.server)
if #v.rule.domain == 0 then v.rule.domain = nil end
if #v.rule.domain_suffix == 0 then v.rule.domain_suffix = nil end
if #v.rule.domain_keyword == 0 then v.rule.domain_keyword = nil end
table.insert(dns.rules, v.rule)
end
end
if direct_dns_udp_server then
table.insert(dns.servers, {
tag = "direct",
type = "udp",
server = direct_dns_udp_server,
server_port = tonumber(direct_dns_udp_port) or 53,
detour = "direct"
})
local node_domain = {}
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
table.insert(node_domain, w)
end)
if #node_domain > 0 then
table.insert(dns.rules, {
action = "route",
server = "direct",
domain = node_domain
})
end
end
if default_dns_udp_server then
table.insert(dns.servers, {
tag = "default",
type = "udp",
server = default_dns_udp_server,
server_port = tonumber(default_dns_udp_port) or 53,
detour = "direct"
})
dns.final = "default"
else
dns.final = "direct"
end
local dns_in_inbound = {
type = "direct",
tag = "dns-in",
listen = "127.0.0.1",
listen_port = tonumber(dns_listen_port),
}
table.insert(inbounds, dns_in_inbound)
route.rules = {}
table.insert(route.rules, {
action = "hijack-dns",
inbound = dns_in_inbound.tag
})
table.insert(route.rules, {
action = "sniff",
inbound = dns_in_inbound.tag
})
route.final = "direct"
local config = {
log = {
disabled = true,
level = "debug",
timestamp = true,
},
dns = dns,
inbounds = inbounds,
outbounds = outbounds,
route = route
}
return jsonc.stringify(config, 1)
end
_G.gen_config = gen_config
_G.gen_front_dns_config = gen_front_dns_config
_G.gen_proto_config = gen_proto_config
_G.geo_convert_srs = geo_convert_srs

View File

@ -28,6 +28,35 @@ local function get_domain_excluded()
return hosts
end
function parseDNS(str)
local result_dns_server
-- [proto]://[ip]
-- [proto]://[ip]:[port]
-- https://[ip]/[path]
-- https://[ip]:[port]/[path]
local _a = api.parseURL(str)
if _a then
if _a.protocol == "tcp" or _a.protocol == "udp" or _a.protocol == "https" then
result_dns_server = {
address = str
}
if _a.protocol == "udp" then
result_dns_server.address = _a.hostname
end
if _a.port then
result_dns_server.port = _a.port
else
if _a.protocol == "https" then
result_dns_server.port = 443
else
result_dns_server.port = 53
end
end
end
end
return result_dns_server
end
function gen_outbound(flag, node, tag, proxy_table)
local result = nil
if node then
@ -1451,12 +1480,22 @@ function gen_config(var)
queryStrategy = "UseIP"
}
table.insert(dns_servers, {
server = {
tag = "local",
address = "localhost"
local _direct_dns = nil
if direct_dns_udp_server then
_direct_dns = {
tag = direct_dns_tag,
address = direct_dns_udp_server,
port = tonumber(direct_dns_udp_port) or 53,
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
}
})
if _direct_dns.address then
table.insert(dns_servers, {
outboundTag = "direct",
server = _direct_dns
})
end
end
if dns_listen_port then
local _remote_dns_proto = "tcp"
@ -1556,8 +1595,7 @@ function gen_config(var)
server = _remote_fakedns
})
end
local _direct_dns = nil
if direct_dns_udp_server then
local domain = {}
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
@ -1565,24 +1603,10 @@ function gen_config(var)
table.insert(domain, "full:" .. w)
end)
if #domain > 0 then
table.insert(dns_domain_rules, 1, {
shunt_rule_name = "logic-vpslist",
outboundTag = "direct",
domain = domain
})
end
_direct_dns = {
tag = direct_dns_tag,
address = direct_dns_udp_server,
port = tonumber(direct_dns_udp_port) or 53,
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP",
}
if _direct_dns.address then
table.insert(dns_servers, {
outboundTag = "direct",
server = _direct_dns
table.insert(dns.servers, 1, {
tag = "local",
address = "localhost",
domains = domain
})
end
end
@ -1773,6 +1797,21 @@ function gen_config(var)
end
end
local has_default_dns_server = "0"
for index, value in ipairs(dns.servers) do
if value.domains == nil then
has_default_dns_server = "1"
break
end
end
if #dns.servers == 0 or not has_default_server == "0" then
table.insert(dns.servers, 1, {
tag = "local",
address = "localhost"
})
end
if redir_port then
local inbound = {
port = tonumber(redir_port),
@ -1994,250 +2033,154 @@ function gen_proto_config(var)
return jsonc.stringify(config, 1)
end
function gen_dns_config(var)
function gen_front_dns_config(var)
local dns_listen_port = var["dns_listen_port"]
local dns_out_tag = var["dns_out_tag"]
local direct_dns_udp_server = var["direct_dns_udp_server"]
local direct_dns_udp_port = var["direct_dns_udp_port"]
local direct_dns_tcp_server = var["direct_dns_tcp_server"]
local direct_dns_tcp_port = var["direct_dns_tcp_port"]
local direct_dns_doh_url = var["direct_dns_doh_url"]
local direct_dns_doh_host = var["direct_dns_doh_host"]
local direct_dns_doh_ip = var["direct_dns_doh_ip"]
local direct_dns_doh_port = var["direct_dns_doh_port"]
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
local remote_dns_udp_server = var["remote_dns_udp_server"]
local remote_dns_udp_port = var["remote_dns_udp_port"]
local remote_dns_tcp_server = var["remote_dns_tcp_server"]
local remote_dns_tcp_port = var["remote_dns_tcp_port"]
local remote_dns_doh_url = var["remote_dns_doh_url"]
local remote_dns_doh_host = var["remote_dns_doh_host"]
local remote_dns_doh_ip = var["remote_dns_doh_ip"]
local remote_dns_doh_port = var["remote_dns_doh_port"]
local remote_dns_query_strategy = var["remote_dns_query_strategy"]
local remote_dns_detour = var["remote_dns_detour"]
local remote_dns_client_ip = var["remote_dns_client_ip"]
local remote_dns_outbound_socks_address = var["remote_dns_outbound_socks_address"]
local remote_dns_outbound_socks_port = var["remote_dns_outbound_socks_port"]
local dns_cache = var["dns_cache"]
local loglevel = var["loglevel"] or "warning"
local default_dns_udp_server = var["default_dns_udp_server"]
local default_dns_udp_port = var["default_dns_udp_port"]
local queryStrategy = "UseIP"
local dns = {
tag = "dns-global-direct",
disableCache = false,
disableFallback = true,
disableFallbackIfMatch = true,
queryStrategy = queryStrategy,
servers = {}
}
local inbounds = {}
local outbounds = {}
local dns = nil
local routing = nil
local routing = {
rules = {}
}
if dns_listen_port then
routing = {
domainStrategy = "IPOnDemand",
rules = {}
}
dns = {
tag = "dns-global",
hosts = {},
disableCache = (dns_cache == "1") and false or true,
disableFallback = true,
disableFallbackIfMatch = true,
servers = {},
clientIp = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
}
local other_type_dns_proto, other_type_dns_server, other_type_dns_port
if dns_out_tag == "remote" then
dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
if remote_dns_detour == "direct" then
dns_out_tag = "direct"
table.insert(outbounds, 1, {
tag = dns_out_tag,
protocol = "freedom",
settings = {
domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
},
streamSettings = {
sockopt = {
mark = 255
}
}
})
else
if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then
table.insert(outbounds, 1, {
tag = dns_out_tag,
protocol = "socks",
streamSettings = {
network = "tcp",
security = "none"
},
settings = {
servers = {
{
address = remote_dns_outbound_socks_address,
port = tonumber(remote_dns_outbound_socks_port)
}
}
}
})
end
end
local _remote_dns = {
tag = "dns-in-remote"
table.insert(outbounds, {
tag = "direct",
protocol = "freedom",
settings = {
domainStrategy = queryStrategy
},
streamSettings = {
sockopt = {
mark = 255
}
if remote_dns_udp_server then
_remote_dns.address = remote_dns_udp_server
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
other_type_dns_proto = "udp"
other_type_dns_server = remote_dns_udp_server
other_type_dns_port = _remote_dns.port
end
if remote_dns_tcp_server then
_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
_remote_dns.port = tonumber(remote_dns_tcp_port) or 53
other_type_dns_proto = "tcp"
other_type_dns_server = remote_dns_tcp_server
other_type_dns_port = _remote_dns.port
end
if remote_dns_doh_url and remote_dns_doh_host then
if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then
dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip
}
})
if default_dns_udp_server then
table.insert(dns.servers, {
tag = "default",
address = default_dns_udp_server,
port = tonumber(default_dns_udp_port) or 53,
queryStrategy = queryStrategy,
})
end
local direct_dns_shunt = uci:get(appname, "@global[0]", "direct_dns_shunt") or ""
if #direct_dns_shunt > 0 then
local dns_server = {}
string.gsub(direct_dns_shunt, '[^' .. "\r\n" .. ']+', function(w)
if w:find("#") == 1 then return end
local domain = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w))
local dns = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w))
if domain ~= "" and dns ~= "" then
local new_dns_server = parseDNS(dns)
if new_dns_server then
if not dns_server[dns] then
dns_server[dns] = {}
end
dns_server[dns].tag = dns
dns_server[dns].queryStrategy = queryStrategy
dns_server[dns].address = new_dns_server.address
dns_server[dns].port = new_dns_server.port
dns_server[dns].finalQuery = true
if not dns_server[dns].domains then
dns_server[dns].domains = {}
end
table.insert(dns_server[dns].domains, domain)
end
_remote_dns.address = remote_dns_doh_url
_remote_dns.port = tonumber(remote_dns_doh_port) or 443
end
table.insert(dns.servers, _remote_dns)
elseif dns_out_tag == "direct" then
dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
table.insert(outbounds, 1, {
tag = dns_out_tag,
protocol = "freedom",
settings = {
domainStrategy = dns.queryStrategy
end)
for k, v in pairs(dns_server) do
table.insert(dns.servers, v)
table.insert(routing.rules, {
inboundTag = {
v.tag
},
streamSettings = {
sockopt = {
mark = 255
}
}
outboundTag = "direct"
})
local _direct_dns = {
tag = "dns-in-direct"
}
if direct_dns_udp_server then
_direct_dns.address = direct_dns_udp_server
_direct_dns.port = tonumber(direct_dns_udp_port) or 53
table.insert(routing.rules, 1, {
ip = {
direct_dns_udp_server
},
port = tonumber(direct_dns_udp_port) or 53,
network = "udp",
outboundTag = "direct"
})
other_type_dns_proto = "udp"
other_type_dns_server = direct_dns_udp_server
other_type_dns_port = _direct_dns.port
end
if direct_dns_tcp_server then
_direct_dns.address = "tcp+local://" .. direct_dns_tcp_server
_direct_dns.port = tonumber(direct_dns_tcp_port) or 53
other_type_dns_proto = "tcp"
other_type_dns_server = direct_dns_tcp_server
other_type_dns_port = _direct_dns.port
end
if direct_dns_doh_url and direct_dns_doh_host then
if direct_dns_doh_ip and direct_dns_doh_host ~= direct_dns_doh_ip and not api.is_ip(direct_dns_doh_host) then
dns.hosts[direct_dns_doh_host] = direct_dns_doh_ip
end
_direct_dns.address = direct_dns_doh_url:gsub("https://", "https+local://")
_direct_dns.port = tonumber(direct_dns_doh_port) or 443
end
table.insert(dns.servers, _direct_dns)
end
local dns_hosts_len = 0
for key, value in pairs(dns.hosts) do
dns_hosts_len = dns_hosts_len + 1
end
if dns_hosts_len == 0 then
dns.hosts = nil
end
table.insert(inbounds, {
listen = "127.0.0.1",
port = tonumber(dns_listen_port),
protocol = "dokodemo-door",
tag = "dns-in",
settings = {
address = other_type_dns_server or "1.1.1.1",
port = other_type_dns_port or 53,
network = "tcp,udp"
}
})
table.insert(outbounds, {
tag = "dns-out",
protocol = "dns",
proxySettings = {
tag = dns_out_tag
},
settings = {
address = other_type_dns_server or "1.1.1.1",
port = other_type_dns_port or 53,
network = other_type_dns_proto or "tcp",
nonIPQuery = "reject"
}
})
table.insert(routing.rules, 1, {
inboundTag = {
"dns-in"
},
outboundTag = "dns-out"
})
table.insert(routing.rules, {
inboundTag = {
"dns-global"
},
outboundTag = dns_out_tag
})
end
if inbounds or outbounds then
local config = {
log = {
--dnsLog = true,
loglevel = loglevel
},
dns = dns,
inbounds = inbounds,
outbounds = outbounds,
routing = routing
if direct_dns_udp_server then
local node_domain = {}
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
table.insert(node_domain, "full:" .. w)
end)
if #node_domain > 0 then
table.insert(dns.servers, {
tag = "direct",
address = direct_dns_udp_server,
port = tonumber(direct_dns_udp_port) or 53,
queryStrategy = queryStrategy,
domains = node_domain
})
end
end
table.insert(inbounds, {
tag = "dns-in",
listen = "127.0.0.1",
port = tonumber(dns_listen_port),
protocol = "dokodemo-door",
settings = {
address = "0.0.0.0",
network = "tcp,udp"
}
return jsonc.stringify(config, 1)
end
})
table.insert(outbounds, {
tag = "dns-out",
protocol = "dns",
proxySettings = {
tag = "direct"
},
settings = {
address = direct_dns_udp_server,
port = tonumber(direct_dns_udp_port) or 53,
network = "udp",
nonIPQuery = "skip",
blockTypes = {
65
}
}
})
table.insert(routing.rules, {
inboundTag = {
"dns-in"
},
outboundTag = "dns-out"
})
local config = {
log = {
dnsLog = true,
loglevel = "debug"
},
dns = dns,
inbounds = inbounds,
outbounds = outbounds,
routing = routing
}
return jsonc.stringify(config, 1)
end
_G.gen_config = gen_config
_G.gen_front_dns_config = gen_front_dns_config
_G.gen_proto_config = gen_proto_config
_G.gen_dns_config = gen_dns_config
if arg[1] then
local func =_G[arg[1]]

View File

@ -121,6 +121,12 @@ msgstr "0 به معنای عدم استفاده است"
msgid "Current node: %s"
msgstr "گره فعلی: %s"
msgid "Direct domain DNS routing"
msgstr "مسیریابی مستقیم DNS دامنه"
msgid "Please note that the program will not start if the format is incorrect!"
msgstr "لطفا توجه داشته باشید که اگر قالب نادرست باشد، برنامه شروع نخواهد شد!"
msgid "IP:Port mode acceptable, multi value split with english comma."
msgstr "حالت IP:Port قابل قبول است، مقادیر متعدد را با کاما انگلیسی جدا کنید."

View File

@ -118,6 +118,12 @@ msgstr "0 — отключить функцию"
msgid "Current node: %s"
msgstr "Текущий узел: %s"
msgid "Direct domain DNS routing"
msgstr "Прямая маршрутизация DNS домена"
msgid "Please note that the program will not start if the format is incorrect!"
msgstr "Обратите внимание, что программа не запустится, если формат указан неверно!"
msgid "IP:Port mode acceptable, multi value split with english comma."
msgstr "Допустим формат IP:Port. Несколько значений указывайте через запятую."

View File

@ -118,6 +118,12 @@ msgstr "0为不使用"
msgid "Current node: %s"
msgstr "当前节点:%s"
msgid "Direct domain DNS routing"
msgstr "直连域名 DNS 分流"
msgid "Please note that the program will not start if the format is incorrect!"
msgstr "请注意,格式不正确将无法启动!"
msgid "IP:Port mode acceptable, multi value split with english comma."
msgstr "接受 IP:Port 形式的输入,多个以英文逗号分隔。"

View File

@ -118,6 +118,12 @@ msgstr "0為不使用"
msgid "Current node: %s"
msgstr "當前節點:%s"
msgid "Direct domain DNS routing"
msgstr "直連網域 DNS 分流"
msgid "Please note that the program will not start if the format is incorrect!"
msgstr "請注意,格式不正確將無法啟動!"
msgid "IP:Port mode acceptable, multi value split with english comma."
msgstr "接受 IP:Port 形式的輸入,多個以英文逗号分隔。"

View File

@ -106,14 +106,6 @@ run_xray() {
[ -n "$dns_listen_port" ] && {
json_add_string "dns_listen_port" "${dns_listen_port}"
[ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}"
local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
DIRECT_DNS_UDP_SERVER=${_dns_address}
DIRECT_DNS_UDP_PORT=${_dns_port}
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
[ "${write_ipset_direct}" = "1" ] && {
direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp)
@ -188,38 +180,17 @@ run_xray() {
[ -n "$remote_dns_client_ip" ] && json_add_string "remote_dns_client_ip" "${remote_dns_client_ip}"
local _json2_arg="$(json_dump)"
local independent_dns
if [ -z "${independent_dns}" ]; then
local _json2_keys key
json_load "${_json2_arg}"
json_get_keys _json2_keys
for key in ${_json2_keys}; do
json_get_var "_json2_$key" "$key"
done
json_load "${_json1_arg}"
for key in ${_json2_keys}; do
eval "local _v=\$_json2_$key"
json_add_string "$key" "$_v"
done
else
dns_remote_listen_port=$(get_new_port $(expr ${direct_dnsmasq_listen_port:-${dns_listen_port}} + 1) udp)
V2RAY_DNS_REMOTE_CONFIG="${TMP_PATH}/${flag}_dns_remote.json"
V2RAY_DNS_REMOTE_LOG="${TMP_PATH}/${flag}_dns_remote.log"
V2RAY_DNS_REMOTE_LOG="/dev/null"
json_load "${_json2_arg}"
json_add_string "dns_out_tag" "remote"
json_add_string "dns_listen_port" "${dns_remote_listen_port}"
json_add_string "remote_dns_outbound_socks_address" "127.0.0.1"
json_add_string "remote_dns_outbound_socks_port" "${socks_port}"
_json2_arg="$(json_dump)"
lua $UTIL_XRAY gen_dns_config "${_json2_arg}" > $V2RAY_DNS_REMOTE_CONFIG
ln_run "$XRAY_BIN" "xray" $V2RAY_DNS_REMOTE_LOG run -c "$V2RAY_DNS_REMOTE_CONFIG"
json_load "${_json1_arg}"
json_add_string "remote_dns_udp_port" "${dns_remote_listen_port}"
json_add_string "remote_dns_udp_server" "127.0.0.1"
json_add_string "remote_dns_query_strategy" "${remote_dns_query_strategy}"
fi
local _json2_keys key
json_load "${_json2_arg}"
json_get_keys _json2_keys
for key in ${_json2_keys}; do
json_get_var "_json2_$key" "$key"
done
json_load "${_json1_arg}"
for key in ${_json2_keys}; do
eval "local _v=\$_json2_$key"
json_add_string "$key" "$_v"
done
}
[ -n "${redir_port}" ] && {
@ -290,13 +261,6 @@ run_singbox() {
}
}
[ -n "$dns_listen_port" ] && {
local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
DIRECT_DNS_UDP_SERVER=${_dns_address}
DIRECT_DNS_UDP_PORT=${_dns_port}
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
[ "${write_ipset_direct}" = "1" ] && {
direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp)
@ -633,8 +597,6 @@ run_global() {
TYPE=$(echo $(config_n_get $NODE type) | tr 'A-Z' 'a-z')
[ -z "$TYPE" ] && return 1
mkdir -p ${GLOBAL_ACL_PATH}
if [ $PROXY_IPV6 == "1" ]; then
log_i18n 0 "To enable experimental IPv6 transparent proxy (TProxy), please ensure your node and type support IPv6!"
fi
@ -712,8 +674,62 @@ run_global() {
return 1
fi
set_cache_var "ACL_GLOBAL_node" "$NODE"
set_cache_var "ACL_GLOBAL_redir_port" "$REDIR_PORT"
}
run_front_dns() {
local switch=0
direct_dns_shunt=$(config_t_get global direct_dns_shunt)
direct_dns_shunt=$(echo "${direct_dns_shunt}" | grep -v "^#")
[ -n "${direct_dns_shunt}" ] && switch=1
[ "${switch}" == "1" ] && {
local config_file="${TMP_PATH}/direct_dns.json"
local log_file="${TMP_PATH}/direct_dns.log"
log_file="/dev/null"
local listen_port=$(get_new_port 10553)
json_init
json_add_string "dns_listen_port" "${listen_port}"
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
json_add_string "direct_dns_query_strategy" "${DIRECT_DNS_QUERY_STRATEGY}"
[ -n "${ACL_GLOBAL_node}" ] && [ -n "${TUN_DNS_PORT}" ] && {
json_add_string "default_dns_udp_server" "127.0.0.1"
json_add_string "default_dns_udp_port" "${TUN_DNS_PORT}"
}
local _json_arg="$(json_dump)"
local preferred_core=""
[ -n "${XRAY_BIN}" ] && preferred_core="xray"
[ -n "${SINGBOX_BIN}" ] && preferred_core="sing-box"
if [ "${preferred_core}" = "xray" ]; then
lua $UTIL_XRAY gen_front_dns_config "${_json_arg}" > $config_file
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
elif [ "${preferred_core}" = "sing-box" ]; then
lua $UTIL_SINGBOX gen_front_dns_config "${_json_arg}" > $config_file
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
else
return 1
fi
FRONT_DNS_SERVER="127.0.0.1"
FRONT_DNS_PORT="${listen_port}"
}
}
run_global_dnsmasq() {
[ -z "${ACL_GLOBAL_node}" ] && [ -z "${FRONT_DNS_PORT}" ] && return
local RUN_NEW_DNSMASQ=1
RUN_NEW_DNSMASQ=${DNS_REDIRECT}
DNSMASQ_DEFAULT_DNS="${AUTO_DNS}"
DNSMASQ_LOCAL_DNS="${LOCAL_DNS:-${AUTO_DNS}}"
DNSMASQ_TUN_DNS="${TUN_DNS}"
[ -n "${FRONT_DNS_PORT}" ] && {
DNSMASQ_DEFAULT_DNS="${FRONT_DNS_SERVER}#${FRONT_DNS_PORT}"
DNSMASQ_LOCAL_DNS="${DNSMASQ_DEFAULT_DNS}"
DNSMASQ_TUN_DNS="${DNSMASQ_DEFAULT_DNS}"
}
if [ "${RUN_NEW_DNSMASQ}" == "0" ]; then
#The old logic will be removed in the future.
#Run a copy dnsmasq instance, DNS hijack that don't need a proxy devices.
@ -738,9 +754,9 @@ run_global() {
json_add_string "FLAG" "default"
json_add_string "TMP_DNSMASQ_PATH" "${GLOBAL_DNSMASQ_CONF_PATH}"
json_add_string "DNSMASQ_CONF_FILE" "${GLOBAL_DNSMASQ_CONF}"
json_add_string "DEFAULT_DNS" "${AUTO_DNS}"
json_add_string "LOCAL_DNS" "${LOCAL_DNS:-${AUTO_DNS}}"
json_add_string "TUN_DNS" "${TUN_DNS}"
json_add_string "DEFAULT_DNS" "${DNSMASQ_DEFAULT_DNS}"
json_add_string "LOCAL_DNS" "${DNSMASQ_LOCAL_DNS}"
json_add_string "TUN_DNS" "${DNSMASQ_TUN_DNS}"
json_add_string "NFTFLAG" "${nftflag:-0}"
json_add_string "NO_LOGIC_LOG" "${NO_LOGIC_LOG:-0}"
lua $APP_PATH/helper_dnsmasq.lua add_rule "$(json_dump)"
@ -753,14 +769,11 @@ run_global() {
else
#Run a copy dnsmasq instance, DNS hijack for that need proxy devices.
GLOBAL_DNSMASQ_PORT=$(get_new_port 11400)
run_copy_dnsmasq flag="default" listen_port=$GLOBAL_DNSMASQ_PORT tun_dns="${TUN_DNS}"
run_copy_dnsmasq flag="default" listen_port=$GLOBAL_DNSMASQ_PORT tun_dns="${DNSMASQ_TUN_DNS}"
DNS_REDIRECT_PORT=${GLOBAL_DNSMASQ_PORT}
#dhcp.leases to hosts
$APP_PATH/lease2hosts.sh > /dev/null 2>&1 &
fi
set_cache_var "ACL_GLOBAL_node" "$NODE"
set_cache_var "ACL_GLOBAL_redir_port" "$REDIR_PORT"
}
start_socks() {
@ -1188,7 +1201,10 @@ start() {
lua $APP_PATH/helper_dnsmasq.lua restart "$(json_dump)"
}
fi
mkdir -p ${GLOBAL_ACL_PATH}
[ "$ENABLED_DEFAULT_ACL" == 1 ] && run_global
run_front_dns
run_global_dnsmasq
[ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh start
set_cache_var "USE_TABLES" "$USE_TABLES"
if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then
@ -1312,6 +1328,12 @@ get_config() {
[ -z "${DEFAULT_DNS}" ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | head -2 | tr '\n' ',' | sed 's/,$//')
AUTO_DNS=${DEFAULT_DNS:-119.29.29.29}
local AUTO_DNS2=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
local AUTO_DNS_ADDRESS=$(echo ${AUTO_DNS2} | awk -F ':' '{print $1}')
local AUTO_DNS_PORT=$(echo ${AUTO_DNS2} | awk -F ':' '{print $2}')
DIRECT_DNS_UDP_SERVER=${AUTO_DNS_ADDRESS}
DIRECT_DNS_UDP_PORT=${AUTO_DNS_PORT}
DNSMASQ_CONF_DIR=/tmp/dnsmasq.d
DEFAULT_DNSMASQ_CFGID="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then

View File

@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=190
PKG_RELEASE:=8
PKG_RELEASE:=9
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \

View File

@ -5,6 +5,7 @@ require "nixio.fs"
require "luci.sys"
require "luci.http"
require "luci.jsonc"
local nixio = require "nixio"
require "luci.model.uci"
local uci = require "luci.model.uci".cursor()
@ -146,11 +147,11 @@ local function set_apply_on_parse(map)
end
end
local has_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
local has_trojan = is_finded("trojan")
local has_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local server_table = {}
local encrypt_methods = {
@ -277,25 +278,63 @@ o.value = sid
-- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心
o = s:option(ListValue, "_xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
-- 设置默认 Xray 或 Hysteria2 核心
-- 动态添加选项
if has_xray then
o:value("v2ray", translate("Xray (Hysteria2)"))
end
-- 注意Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
o:value("__auto__", translate("Auto"))
if has_hysteria2 then
o:value("hysteria2", translate("Hysteria2"))
o:value("hysteria2", translate("Hysteria2"))
end
if has_xray then
o:value("v2ray", translate("Xray (Hysteria2)"))
end
-- 读取全局 xray_hy2_type
o.cfgvalue = function(self, section)
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type") or "hysteria2"
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
if val == nil or val == "" then
return "__auto__" -- 对应 Auto 选项
end
return val
end
o.rmempty = true
-- 保存时更新全局配置
o.write = function(self, section, value)
-- 更新 server_subscribe 的 xray_hy2_type
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
if old_value ~= value then
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
end
if value == "__auto__" then
-- 删除全局配置
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
else
-- 设置具体值
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
end
end
-- 新增一个选择框,用于选择 Xray 或 Trojan 核心
o = s:option(ListValue, "_xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
-- 注意Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
o:value("__auto__", translate("Auto"))
if has_hysteria2 then
o:value("trojan", translate("Trojan"))
end
if has_xray then
o:value("v2ray", translate("Xray (Trojan)"))
end
-- 读取全局 xray_tj_type
o.cfgvalue = function(self, section)
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
if val == nil or val == "" then
return "__auto__" -- 对应 Auto 选项
end
return val
end
o.rmempty = true
-- 保存时更新全局配置
o.write = function(self, section, value)
if value == "__auto__" then
-- 删除全局配置
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
else
-- 设置具体值
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_tj_type", value)
end
end
o = s:option(ListValue, "type", translate("Server Node Type"))
@ -329,6 +368,29 @@ end
if is_finded("redsocks2") then
o:value("tun", translate("Network Tunnel"))
end
local old_cfgvalue = o.cfgvalue
o.cfgvalue = function(self, section)
local val = self.map.uci:get("shadowsocksr", section, "type")
if val == "ss-rust" or val == "ss-libev" then
return "ss"
end
if old_cfgvalue then
return old_cfgvalue(self, section)
end
return val
end
-- 重写 write当用户选择 "ss" 时不写入(由 _ss_core 负责写入具体核心)
local old_write = o.write
o.write = function(self, section, value)
if value == "ss" then
return -- 不做任何写入,等待 _ss_core 写入
end
if old_write then
old_write(self, section, value)
else
self.map.uci:set("shadowsocksr", section, "type", value)
end
end
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
@ -343,29 +405,37 @@ end
o:depends("type", "tun")
o.description = translate("Redirect traffic to this network interface")
-- 新增一个选择框,用于选择 Shadowsocks 版本
o = s:option(ListValue, "_has_ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
-- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示)
o = s:option(ListValue, "_ss_core", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
o.description = translate("Selection ShadowSocks Node Use Version.")
-- 设置默认 Shadowsocks 版本
-- 动态添加选项
if has_ss_rust then
o:value("ss-rust", translate("ShadowSocks-rust Version"))
o:value("ss-rust", translate("ShadowSocks-rust Version"))
end
if has_ss_libev then
o:value("ss-libev", translate("ShadowSocks-libev Version"))
o:value("ss-libev", translate("ShadowSocks-libev Version"))
end
-- 读取全局 ss_type
o.cfgvalue = function(self, section)
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type") or "ss-rust"
-- 读取当前节点的 type 值,如果已经是具体核心则显示对应的选项
local node_type = self.map.uci:get("shadowsocksr", section, "type")
if node_type == "ss-rust" or node_type == "ss-libev" then
return node_type
end
-- 如果全局 ss_type 有值且为具体核心则返回该值
local ss_type = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
if ss_type == "ss-rust" or ss_type == "ss-libev" then
return ss_type
end
-- 如果节点 type 是旧的 "ss",则返回空,手动选择
return nil
end
-- 显示条件:当节点类型为 "ss" 或其具体核心时显示
o:depends("type", "ss")
o.rmempty = true
-- 保存时,将选择的值直接写入当前节点的 type 字段
o.write = function(self, section, value)
-- 更新 server_subscribe 的 ss_type
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
if old_value ~= value then
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "ss_type", value)
end
if value and value ~= "" then
self.map.uci:set("shadowsocksr", section, "type", value)
end
end
o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
@ -539,7 +609,7 @@ o = s:option(Value, "port_range", translate("Port hopping range"))
o.description = translate("Format as 10000:20000 or 10000-20000 Multiple groups are separated by commas (,).")
o:depends({type = "hysteria2", flag_port_hopping = true})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_port_hopping = true})
--o.datatype = "portrange"
o.datatype = "or(uinteger,portrange)"
o.rmempty = true
o = s:option(Flag, "flag_transport", translate("Enable Transport Protocol Settings"))
@ -554,9 +624,10 @@ o.default = "udp"
o.rmempty = true
o = s:option(Value, "hopinterval", translate("Port Hopping Interval(Unit:Second)"))
o.description = translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")
o:depends({type = "hysteria2", flag_transport = true, flag_port_hopping = true})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_port_hopping = true})
o.datatype = "uinteger"
o.datatype = "or(uinteger,portrange)"
o.rmempty = true
o.default = "30"

View File

@ -295,4 +295,3 @@ end
m:section(SimpleSection).template = "shadowsocksr/status_bottom"
return m

View File

@ -70,9 +70,14 @@ local function set_apply_on_parse(map)
end
end
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
local has_trojan = is_finded("trojan")
local has_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local ss_type_list = {}
local tj_type_list = {}
local hy2_type_list = {}
if has_hysteria2 then
@ -82,19 +87,12 @@ if has_xray then
table.insert(hy2_type_list, { id = "v2ray", name = translate("Xray (Hysteria2)") })
end
-- 如果用户没有手动设置,则自动选择
if not xray_hy2_type or xray_hy2_type == "" then
if has_hysteria2 then
xray_hy2_type = "hysteria2"
elseif has_xray then
xray_hy2_type = "v2ray"
end
if has_trojan then
table.insert(tj_type_list, { id = "trojan", name = translate("Trojan") })
end
if has_xray then
table.insert(tj_type_list, { id = "v2ray", name = translate("Xray (Trojan)") })
end
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
local ss_type_list = {}
if has_ss_rust then
table.insert(ss_type_list, { id = "ss-rust", name = translate("ShadowSocks-rust Version") })
@ -106,17 +104,6 @@ if has_xray then
table.insert(ss_type_list, { id = "v2ray", name = translate("Xray (ShadowSocks)") })
end
-- 如果用户没有手动设置,则自动选择
if not ss_type or ss_type == "" then
if has_ss_rust then
ss_type = "ss-rust"
elseif has_ss_libev then
ss_type = "ss-libev"
elseif has_xray then
ss_type = "v2ray"
end
end
uci:foreach("shadowsocksr", "servers", function(s)
server_count = server_count + 1
end)
@ -162,22 +149,87 @@ o:depends("auto_update", "1")
-- 确保 hy2_type_list 不为空
if #hy2_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "xray_hy2_type")
if old_val and old_val ~= "" then
if (old_val == "hysteria2" and not has_hysteria2) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "xray_hy2_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
o:value("", translate("Auto"))
for _, v in ipairs(hy2_type_list) do
o:value(v.id, v.name) -- 存储 "Xray" / "Hysteria2",但 UI 显示完整名称
end
o.default = xray_hy2_type -- 设置默认值
end
-- 确保 tj_type_list 不为空
if #tj_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "xray_tj_type")
if old_val and old_val ~= "" then
if (old_val == "trojan" and not has_trojan) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "xray_tj_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
o:value("", translate("Auto"))
for _, v in ipairs(tj_type_list) do
o:value(v.id, v.name) -- 存储 "Xray" / "Trojan",但 UI 显示完整名称
end
end
-- 确保 ss_type_list 不为空
if #ss_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "ss_type")
if old_val and old_val ~= "" then
if (old_val == "ss-rust" and not has_ss_rust) or
(old_val == "ss-libev" and not has_ss_libev) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "ss_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
o.description = translate("Selection ShadowSocks Node Use Version.")
o:value("", translate("Auto"))
for _, v in ipairs(ss_type_list) do
o:value(v.id, v.name) -- 存储 "ss-libev" / "ss-rust",但 UI 显示完整名称
end
o.default = ss_type -- 设置默认值
end
o = s:option(DynamicList, "subscribe_url", translate("Subscribe URL"))

View File

@ -3,11 +3,24 @@
local map = self.map
local ss_type = map:get("@server_subscribe[0]", "ss_type")
local xray_hy2_type = map:get("@server_subscribe[0]", "xray_hy2_type")
local xray_tj_type = map:get("@server_subscribe[0]", "xray_tj_type")
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
local has_ss_libev = luci.sys.exec('type -t -p ss-redir 2>/dev/null || type -t -p ss-local 2>/dev/null') ~= ""
local has_hysteria = luci.sys.exec('type -t -p hysteria 2>/dev/null') ~= ""
local has_trojan = luci.sys.exec('type -t -p trojan 2>/dev/null') ~= ""
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
-%>
<script type="text/javascript">
//<![CDATA[
let ss_type = "<%=ss_type%>"
let xray_hy2_type = "<%=xray_hy2_type%>"
let xray_tj_type = "<%=xray_tj_type%>"
let has_ss_rust = "<%=has_ss_rust%>"
let has_ss_libev = "<%=has_ss_libev%>"
let has_hysteria = "<%=has_hysteria%>"
let has_trojan = "<%=has_trojan%>"
let has_xray = "<%=has_xray%>"
function padright(str, cnt, pad) {
return str + Array(cnt + 1).join(pad);
@ -112,48 +125,105 @@ function import_ssr_url(btn, urlname, sid) {
return false;
}
if (xray_hy2_type === "hysteria2") {
// 普通 Hysteria2 导入函数
function importAsNormalHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
if (params.get("protocol") && params.get("protocol").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].checked = true; // 设置 flag_transport 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport_protocol')[0].value = params.get("protocol") || "udp";
}
if (params.get("lazy") === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
}
if (params.get("sni") || params.get("alpn")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
if (params.get("sni")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
}
if (params.get("alpn") && params.get("alpn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
}
}
if (params.get("pinSHA256") && params.get("pinSHA256").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.pinsha256')[0].value = params.get("pinSHA256") || "";
}
}
// Xray Hysteria2 导入函数
function importAsXrayHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
if (params.get("security") === "tls" || params.get("sni") || params.get("alpn") || (params.get("pcs") || params.get("vcn"))) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
if (params.get("sni")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
}
if (params.get("alpn") && params.get("alpn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
}
if (params.get("pcs") && params.get("pcs").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertSha')[0].value = params.get("pcs") || "";
}
if (params.get("vcn") && params.get("vcn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertByName')[0].value = params.get("vcn") || "";
}
}
if (params.get("fm") && params.get("fm").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].checked = true; // 设置 enable_finalmask 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.finalmask')[0].value = params.get("fm") || "";
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
} else {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
}
// 主逻辑:智能核心选择(支持回退)
function hasCore(core) {
if (core === "hysteria") return has_hysteria === "true";
if (core === "xray") return has_xray === "true";
return false;
}
let finalMode = null; // true=Xray, false=普通, null=无核心
if (xray_hy2_type === "v2ray") {
if (hasCore("xray")) finalMode = true;
else if (hasCore("hysteria")) finalMode = false;
else finalMode = null;
} else if (xray_hy2_type === "hysteria2") {
if (hasCore("hysteria")) finalMode = false;
else if (hasCore("xray")) finalMode = true;
else finalMode = null;
} else {
// auto 或空Hysteria2 无 type 参数,优先普通 Hysteria2
if (hasCore("hysteria")) finalMode = false;
else if (hasCore("xray")) finalMode = true;
else finalMode = null;
}
if (finalMode === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Hysteria2 or Xray) to import this node.%></font>";
return false;
}
if (finalMode === true) importAsXrayHy2();
else importAsNormalHy2();
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "443";
if (params.get("lazy") === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
if (params.get("mport")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].checked = true; // 设置 flag_port_hopping 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.port_range')[0].value = params.get("mport") || "";
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.uplink_capacity')[0].value =
(params.get("upmbps") && params.get("upmbps").match(/\d+/)) ? params.get("upmbps").match(/\d+/)[0] : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.downlink_capacity')[0].value =
@ -165,25 +235,6 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.obfs_type')[0].value = params.get("obfs");
document.getElementsByName('cbid.shadowsocksr.' + sid + '.salamander')[0].value = params.get("obfs-password") || params.get("obfs_password");
}
if (params.get("security") === "tls" || params.get("sni") || params.get("alpn")
|| (xray_hy2_type !== "hysteria2" && (params.get("pcs") || params.get("vcn")))) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
if (params.get("sni") && params.get("protocol").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
}
if (params.get("alpn") && params.get("alpn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
}
if (xray_hy2_type !== "hysteria2") {
if (params.get("pcs") && params.get("pcs").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertSha')[0].value = params.get("pcs") || "";
}
if (params.get("vcn") && params.get("vcn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertByName')[0].value = params.get("vcn") || "";
}
}
}
var allowInsecureValue = params.get("allowInsecure") || params.get("insecure");
if (allowInsecureValue === "1" || allowInsecureValue === "true") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].checked = true; // 设置 insecure 为 true
@ -218,7 +269,7 @@ function import_ssr_url(btn, urlname, sid) {
var params = Object.fromEntries(new URLSearchParams(query));
if ( ss_type !== "v2ray") {
function importAsNormalSS() {
// 普通 SS 导入逻辑
// 判断是否 SIP002 格式(即含 @
if (url0.indexOf("@") !== -1) {
@ -226,15 +277,28 @@ function import_ssr_url(btn, urlname, sid) {
var sipIndex = url0.indexOf("@");
// 先 URL 解码 base64 再解码
var userInfoB64 = decodeURIComponent(url0.substring(0, sipIndex));
// console.log("userInfoB64:", userInfoB64);
var userInfo = b64decsafe(userInfoB64);
if (userInfo && userInfo.indexOf(":") !== -1) {
var userInfo = userInfo;
} else {
var userInfo = userInfoB64;
}
// 如果没有冒号,且不是 base64 格式,我们可以尝试补齐它
if (userInfo.indexOf(":") === -1) {
userInfo = "none:" + userInfo;
}
// console.log("userInfo after decode:", userInfo);
var userInfoSplitIndex = userInfo.indexOf(":");
if(userInfoSplitIndex < 0) {
// 格式错误
s.innerHTML = "<font style='color:red'><%:Userinfo format error.%></font>";
break;
return false;
}
var method = userInfo.substring(0, userInfoSplitIndex);
// console.log("method:", method);
var password = userInfo.substring(userInfoSplitIndex + 1);
// console.log("password:", password);
var serverPart = url0.substring(sipIndex + 1);
var serverInfo = serverPart.split(":");
@ -254,7 +318,7 @@ function import_ssr_url(btn, urlname, sid) {
var sstr = b64decsafe(decodedUrl0);
if (!sstr) {
s.innerHTML = "<font style='color:red'><%:Base64 sstr failed.%></font>";
break;
return false;
}
// 支持 SS2022 / 普通格式
@ -276,7 +340,7 @@ function import_ssr_url(btn, urlname, sid) {
var port = mNormal[4];
} else {
s.innerHTML = "<font style='color:red'><%:SS URL base64 sstr format not recognized.%></font>";
break;
return false;
}
var plugin = "", pluginOpts = "";
@ -314,9 +378,17 @@ function import_ssr_url(btn, urlname, sid) {
// === 填充配置项 ===
//var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
// 设置 _ss_core 的值
var ssCore = (ss_type === "ss-rust" || ss_type === "ss-libev")
? ss_type
: (has_ss_rust ? "ss-rust" : (has_ss_libev ? "ss-libev" : "ss-libev"));
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].value = ssCore;
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].dispatchEvent(event);
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
@ -351,8 +423,10 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURIComponent(param);
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return false;
} else {
return true;
}
function importAsXraySS() {
try {
// Xray SS 导入逻辑
// 拆分 @,判断是否是 base64 userinfo 的格式
@ -366,7 +440,14 @@ function import_ssr_url(btn, urlname, sid) {
password = userinfo.slice(sepIndex + 1); //一些链接用明文uuid做密码
}
}
var url = new URL("http://" + url0 + (param ? "#" + encodeURIComponent(param) : ""));
// 已编码的别名
// var url = new URL("http://" + url0 + (param ? "#" + encodeURIComponent(param) : ""));
// 未编码的别名
// var url = new URL("http://" + url0 + (param ? "#" + param : ""));
// 兼容已编码和未编码的别名
var remarks = param ? decodeURIComponent(param) : "";
var encodedremarks = encodeURIComponent(remarks);
var url = new URL("http://" + url0 + (encodedremarks ? "#" + encodedremarks : ""));
} catch(e) {
alert(e);
@ -411,7 +492,7 @@ function import_ssr_url(btn, urlname, sid) {
}
if (params.fm && params.fm.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true); // 设置 enable_finalmask 为 true
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.fm || "");
}
@ -514,8 +595,46 @@ function import_ssr_url(btn, urlname, sid) {
break;
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
// 主逻辑:智能核心选择(支持回退)
function hasSSCore() { return has_ss_rust === "true" || has_ss_libev === "true"; }
function hasXrayCore() { return has_xray === "true"; }
let finalSSType = null; // true=Xray, false=普通SS, null=无核心
if (ss_type === "v2ray") {
if (hasXrayCore()) finalSSType = true;
else if (hasSSCore()) finalSSType = false;
else finalSSType = null;
} else if (ss_type === "ss-rust" || ss_type === "ss-libev") {
let userSSCore = (ss_type === "ss-rust" && has_ss_rust === "true") || (ss_type === "ss-libev" && has_ss_libev === "true");
if (userSSCore) finalSSType = false;
else {
let otherSSCore = (ss_type === "ss-rust" && has_ss_libev === "true") || (ss_type === "ss-libev" && has_ss_rust === "true");
if (otherSSCore) finalSSType = false;
else if (hasXrayCore()) finalSSType = true;
else finalSSType = null;
}
} else {
// auto 或空:根据 type 参数决定
let hasType = params.type && params.type !== "";
if (hasType) {
if (hasXrayCore()) finalSSType = true;
else if (hasSSCore()) finalSSType = false;
else finalSSType = null;
} else {
if (hasSSCore()) finalSSType = false;
else if (hasXrayCore()) finalSSType = true;
else finalSSType = null;
}
}
if (finalSSType === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
return false;
}
if (finalSSType === true) importAsXraySS();
else importAsNormalSS();
return false;
case "ssr":
var sstr = b64decsafe((ssu[1] || "").replace(/#.*/, "").trim());
var ploc = sstr.indexOf("/?");
@ -561,8 +680,8 @@ function import_ssr_url(btn, urlname, sid) {
return false;
}
if (!params.get("type")) {
// 普通 Trojan 导入逻辑
// 普通 Trojan 导入逻辑
function importAsNormalTrojan() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "trojan";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
@ -585,9 +704,11 @@ function import_ssr_url(btn, urlname, sid) {
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return false;
} else {
// Xray Trojan 导入逻辑
return true;
}
// Xray Trojan 导入逻辑
function importAsXrayTrojan() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
@ -690,8 +811,40 @@ function import_ssr_url(btn, urlname, sid) {
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
// 主逻辑:智能核心选择(支持回退)
function hasTrojanCore() { return has_trojan === "true"; }
function hasXrayCore() { return has_xray === "true"; }
let finalTrojanMode = null;
if (xray_tj_type === "v2ray") {
if (hasXrayCore()) finalTrojanMode = true;
else if (hasTrojanCore()) finalTrojanMode = false;
else finalTrojanMode = null;
} else if (xray_tj_type === "trojan") {
if (hasTrojanCore()) finalTrojanMode = false;
else if (hasXrayCore()) finalTrojanMode = true;
else finalTrojanMode = null;
} else {
let hasType = params.get("type") && params.get("type") !== "";
if (hasType) {
if (hasXrayCore()) finalTrojanMode = true;
else if (hasTrojanCore()) finalTrojanMode = false;
else finalTrojanMode = null;
} else {
if (hasTrojanCore()) finalTrojanMode = false;
else if (hasXrayCore()) finalTrojanMode = true;
else finalTrojanMode = null;
}
}
if (finalTrojanMode === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Trojan or Xray) to import this node.%></font>";
return false;
}
if (finalTrojanMode === true) importAsXrayTrojan();
else importAsNormalTrojan();
return false;
case "vmess":
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim());
var ploc = sstr.indexOf("/?");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -611,15 +611,18 @@ gen_config_file() { #server1 type2 code3 local_port4 socks_port5 chain6 threads5
}
start_udp() {
local type=$(uci_get_by_name $UDP_RELAY_SERVER type)
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
local udp_relay_server_type=$(uci_get_by_name $UDP_RELAY_SERVER type)
local type=$udp_relay_server_type
if [ "$udp_relay_server_type" = "ss-rust" ] || [ "$udp_relay_server_type" = "ss-libev" ]; then
type="ss"
fi
redir_udp=1
case "$type" in
ss | ssr)
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$udp_relay_server_type" = "ss-libev" ] || [ "$udp_relay_server_type" = "ssr" ]; then
ss_program="$(first_type ${type}-redir)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$udp_relay_server_type" = "ss-rust" ]; then
ss_program="$(first_type ${type}local)"
fi
echolog "$(get_name $type) program is: $ss_program"
@ -858,14 +861,17 @@ shunt_dns_config_file_port() {
}
start_shunt() {
local type=$(uci_get_by_name $SHUNT_SERVER type)
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
local shunt_server_type=$(uci_get_by_name $SHUNT_SERVER type)
local type=$shunt_server_type
if [ "$shunt_server_type" = "ss-rust" ] || [ "$shunt_server_type" = "ss-libev" ]; then
type="ss"
fi
case "$type" in
ss | ssr)
gen_config_file $SHUNT_SERVER $type 3 $tmp_shunt_port
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$shunt_server_type" = "ss-libev" ] || [ "$shunt_server_type" = "ssr" ]; then
ss_program="$(first_type ${type}-redir)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$shunt_server_type" = "ss-rust" ]; then
ss_program="$(first_type ${type}local)"
fi
echolog "$(get_name $type) program is: $ss_program"
@ -880,9 +886,9 @@ start_shunt() {
local tmp_port=$tmp_local_port
else
local tmp_port=$tmp_shunt_local_port
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$shunt_server_type" = "ss-libev" ] || [ "$shunt_server_type" = "ssr" ]; then
dns_ss_program="$(first_type ${type}-local)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$shunt_server_type" = "ss-rust" ]; then
dns_ss_program="$(first_type ${type}local)"
fi
# 获取当前软链接指向的执行文件路径
@ -1009,14 +1015,17 @@ start_local() {
[ "$LOCAL_SERVER" = "nil" ] && return 1
local local_port=$(uci_get_by_type socks5_proxy local_port)
[ "$LOCAL_SERVER" == "$SHUNT_SERVER" ] && tmp_local_port=$local_port
local type=$(uci_get_by_name $LOCAL_SERVER type)
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
local local_server_type=$(uci_get_by_name $LOCAL_SERVER type)
local type=$local_server_type
if [ "$local_server_type" = "ss-rust" ] || [ "$local_server_type" = "ss-libev" ]; then
type="ss"
fi
case "$type" in
ss | ssr)
gen_config_file $LOCAL_SERVER $type 4 $local_port
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$local_server_type" = "ss-libev" ] || [ "$local_server_type" = "ssr" ]; then
ss_program="$(first_type ${type}-local)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$local_server_type" = "ss-rust" ]; then
ss_program="$(first_type ${type}local)"
fi
echolog "$(get_name $type) program is: $ss_program"
@ -1124,14 +1133,17 @@ Start_Run() {
#}
fi
local tcp_port=$(uci_get_by_name $GLOBAL_SERVER local_port)
local type=$(uci_get_by_name $GLOBAL_SERVER type)
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
local global_server_type=$(uci_get_by_name $GLOBAL_SERVER type)
local type=$global_server_type
if [ "$global_server_type" = "ss-rust" ] || [ "$global_server_type" = "ss-libev" ]; then
type="ss"
fi
case "$type" in
ss | ssr)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$global_server_type" = "ss-libev" ] || [ "$global_server_type" = "ssr" ]; then
ss_program="$(first_type ${type}-redir)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$global_server_type" = "ss-rust" ]; then
ss_program="$(first_type ${type}local)"
fi
echolog "$(get_name $type) program is: $ss_program"
@ -1367,14 +1379,17 @@ start_server() {
fi
fi
fi
local type=$(uci_get_by_name $1 type)
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
local node_type=$(uci_get_by_name $1 type)
local type=$node_type
if [ "$node_type" = "ss-rust" ] || [ "$node_type" = "ss-libev" ]; then
type="ss"
fi
case "$type" in
ss | ssr)
gen_service_file ${type} $1 $TMP_PATH/ssr-server$server_count.json
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
if [ "$node_type" = "ss-libev" ] || [ "$node_type" = "ssr" ]; then
ss_program="$(first_type ${type}-server)"
elif [ "$has_ss_type" = "ss-rust" ]; then
elif [ "$node_type" = "ss-rust" ]; then
ss_program="$(first_type ${type}server)"
fi
# 获取当前软链接指向的执行文件路径
@ -1506,7 +1521,7 @@ start_xhttp_addr() {
# MD5 不同更新 download_address 地址文件
if [ "$md5_new" != "$md5_old" ]; then
mv -f "$tmp_file" "$xhttp_addr_file"
logger -t ssrplus-xhttp "xhttp_address.txt updated"
logger -t ssrplus-xhttp "download_address.txt updated"
else
rm -f "$tmp_file"
fi

View File

@ -1241,7 +1241,6 @@ compare_rules() {
fi
# Generate temporary file for current rules
local temp_file=$(mktemp)
local rules_file=$(mktemp)
loger 7 "DEBUG: Temporary file path: $rules_file"
@ -1255,18 +1254,18 @@ compare_rules() {
# Check if current rules were exported successfully
if [ ! -s "$rules_file" ] || ! grep -q "table" "$rules_file" 2>/dev/null; then
loger 4 "Failed to export current rules"
rm -f "$temp_file" "$rules_file"
rm -f "$rules_file"
return 1 # Export failed, need update
fi
# Compare current rules with rules in persistence file
if ! cmp -s "$rules_file" "$NFTABLES_RULES_FILE"; then
loger 6 "Rules differ, update needed"
rm -f "$temp_file" "$rules_file"
rm -f "$rules_file"
return 1 # Need update
fi
rm -f "$temp_file" "$rules_file"
rm -f "$rules_file"
loger 6 "Rules unchanged, no update needed"
return 0 # No update needed
}
@ -1277,21 +1276,17 @@ persist_nftables_rules() {
return 0
fi
# If mode unchanged and persistence file exists, skip update
if [ "$MODE_CHANGED" = "0" ] && [ -f "$NFTABLES_RULES_FILE" ]; then
loger 6 "Mode unchanged and persistence file exists, skipping update"
return 0
fi
# Force update: skip comparison check and delete old file
if [ "$FORCE_UPDATE" = "1" ]; then
loger 6 "Force update requested, removing old persistence file"
rm -f "$NFTABLES_RULES_FILE" 2>/dev/null
# Non-force update: compare rules
# Otherwise, if persistence file exists, compare rules
elif [ -f "$NFTABLES_RULES_FILE" ]; then
if compare_rules; then
loger 6 "Rules unchanged, skipping persistence update"
return 0
else
loger 6 "Rules changed, updating persistence"
fi
fi
@ -1299,7 +1294,7 @@ persist_nftables_rules() {
mkdir -p "$NFTABLES_RULES_DIR" 2>/dev/null
# Generate nftables rule file
cat <<-'EOF' >>$NFTABLES_RULES_FILE
cat <<-'EOF' > "$NFTABLES_RULES_FILE"
#!/usr/sbin/nft -f
# ShadowsocksR nftables rules
@ -1374,17 +1369,18 @@ start_auto_update_daemon() {
echo $$ > "/var/run/ssr-rules-daemon.pid"
while true; do
sleep 300
sleep "$AUTO_UPDATE_INTERVAL"
if [ -x "/usr/bin/ssr-rules" ]; then
# -C returns 0 if rules are OK, non-zero if need update
if /usr/bin/ssr-rules -C >/dev/null 2>&1; then
logger -t ssr-rules[daemon] "Rules status OK, no update needed"
else
logger -t ssr-rules[daemon] "Rules changed or missing, updating persistence"
if /usr/bin/ssr-rules -P >/dev/null 2>&1; then
logger -t ssr-rules[daemon] "Persistence rules updated successfully"
else
logger -t ssr-rules[daemon] "Failed to update persistence"
fi
else
logger -t ssr-rules[daemon] "Rules status OK, no update needed"
fi
else
logger -t ssr-rules[daemon] "Script not found, exiting daemon"

View File

@ -25,6 +25,10 @@ local remarks = server.alias or ""
local b64decode = nixio.bin.b64decode
local b64encode = nixio.bin.b64encode
if server.type == "ss-rust" or server.type == "ss-libev" then
server.type = "ss"
end
-- base64 解码
local function base64Decode(text)
local raw = text
@ -527,6 +531,18 @@ end
udpHop = (server.flag_port_hopping == "1") and {
ports = string.gsub(server.port_range, ":", "-"),
interval = (function(v)
if not v then return 30 end
if v:find("-", 1, true) then
local min, max = v:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
return min .. "-" .. max
end
return 30
end
v = tonumber((v or "30"):match("^%d+"))
return (v and v >= 5) and v or 30
end)(server.hopinterval)
@ -736,12 +752,30 @@ local hysteria2 = {
listen = "0.0.0.0:" .. tonumber(socks_port),
disableUDP = false
} or nil,
transport = server.transport_protocol and {
transport = {
type = server.transport_protocol or "udp",
udp = (server.port_range and (server.hopinterval) and {
hopInterval = (server.port_range and (tonumber(server.hopinterval) .. "s") or nil)
} or nil)
} or nil,
udp = server.port_range and (function()
local udp = {}
local t = server.hopinterval
if not t then return nil end
if t:find("-", 1, true) then
local min, max = t:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
udp.minHopInterval = min .. "s"
udp.maxHopInterval = max .. "s"
return udp
end
end
t = tonumber((t or "30"):match("^%d+"))
t = (t and t >= 5) and t or 30
udp.hopInterval = t .. "s"
return udp
end)() or nil
},
--[[
tcpTProxy = (proto:find("tcp") and local_port ~= "0") and {
listen = "0.0.0.0:" .. tonumber(local_port)

View File

@ -27,10 +27,8 @@ config server_subscribe
option auto_update_week_time '*'
option auto_update_day_time '2'
option auto_update_min_time '0'
option ss_type 'ss-rust'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option xray_hy2_type 'hysteria2'
option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
config access_control

View File

@ -30,54 +30,20 @@ local subscribe_url = ucic:get_first(name, 'server_subscribe', 'subscribe_url',
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期时间/剩余流量')
local save_words = ucic:get_first(name, 'server_subscribe', 'save_words', '')
local user_agent = ucic:get_first(name, 'server_subscribe', 'user_agent', 'v2rayN/9.99')
-- 读取 ss_type 设置
local ss_type = ucic:get_first(name, 'server_subscribe', 'ss_type', 'ss-rust')
-- 根据 ss_type 选择对应的程序
local ss_program = "sslocal"
if ss_type == "ss-rust" then
ss_program = "sslocal"
elseif ss_type == "ss-libev" then
ss_program = "ss-redir"
elseif ss_type == "v2ray" then
ss_program = "xray"
end
-- 从 UCI 配置读取 xray_hy2_type 设置
local xray_hy2_type = ucic:get_first(name, 'server_subscribe', 'xray_hy2_type', 'hysteria2')
local xray_hy2_program = "hysteria"
if xray_hy2_type == "v2ray" then
xray_hy2_program = "xray" -- Hysteria2 使用 Xray
elseif xray_hy2_type == "hysteria2" then
xray_hy2_program = "hysteria" -- Hysteria2 使用 Hysteria
end
local v2_ss_exists = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= ""
-- 初始化变量
local v2_ss = nil
local has_v2_ss_type = nil
if v2_ss_exists then
if ss_type == "v2ray" then
-- 使用 Xray
v2_ss = "v2ray"
has_v2_ss_type = "shadowsocks"
else
-- 使用 SS (rust 或 libev)
v2_ss = "ss"
end
end
local v2_tj = luci.sys.exec('type -t -p trojan') ~= "" and "trojan" or "v2ray"
-- 检查程序是否存在
local program_exists = luci.sys.exec('type -t -p ' .. xray_hy2_program .. ' 2>/dev/null') ~= ""
-- 初始化变量
local hy2_type = nil
local has_xray_hy2_type = nil
if program_exists then
-- 设置节点类型
if xray_hy2_type == "hysteria2" then
hy2_type = "hysteria2"
else
hy2_type = "v2ray" -- 当使用 Xray 时,节点类型是 "v2ray"
has_xray_hy2_type = "hysteria2" -- 可用的协议类型是 Hysteria2
end
end
local ss_type = ucic:get_first(name, 'server_subscribe', 'ss_type')
-- 读取 xray_hy2_type 设置
local xray_hy2_type = ucic:get_first(name, 'server_subscribe', 'xray_hy2_type')
-- 读取 xray_tj_type 设置
local xray_tj_type = ucic:get_first(name, 'server_subscribe', 'xray_tj_type')
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
local has_ss_libev = luci.sys.exec('type -t -p ss-redir 2>/dev/null || type -t -p ss-local 2>/dev/null') ~= ""
local has_hysteria = luci.sys.exec('type -t -p hysteria 2>/dev/null') ~= ""
local has_trojan = luci.sys.exec('type -t -p trojan 2>/dev/null') ~= ""
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
local tuic_type = luci.sys.exec('type -t -p tuic-client') ~= "" and "tuic"
local log = function(...)
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
@ -246,67 +212,66 @@ local function processData(szType, content, cfgid)
-- log(k.."="..v)
-- end
-- 如果 hy2 或 Xray 程序未安装则跳过订阅
if not hy2_type then
-- 自动决定模式true=Xray, false=普通)
local xray_hy2_mode = false -- 默认普通模式
if xray_hy2_type == "v2ray" then
-- Xray 模式
if has_xray then
xray_hy2_mode = true
elseif has_hysteria then
xray_hy2_mode = false -- 回退到普通 Hysteria2
else
xray_hy2_mode = nil
end
elseif xray_hy2_type == "hysteria2" then
-- 普通 Hysteria2 模式
if has_hysteria then
xray_hy2_mode = false
elseif has_xray then
xray_hy2_mode = true -- 回退到 Xray
else
xray_hy2_mode = nil
end
else
-- auto 或空:优先普通 Hysteria2若不存在则使用 Xray
if has_hysteria then
xray_hy2_mode = false
elseif has_xray then
xray_hy2_mode = true -- 回退到 Xray
else
xray_hy2_mode = nil
end
end
-- 如果无法确定模式,跳过该订阅
if xray_hy2_mode == nil then
return nil
end
if xray_hy2_type == "hysteria2" then
if params.protocol and params.protocol ~= "" then
result.flag_transport = "1"
result.transport_protocol = params.protocol
else
result.flag_transport = "1"
result.transport_protocol = "udp"
end
if xray_hy2_mode then
result.type = "v2ray"
result.v2ray_protocol = "hysteria2"
if params.fm and params.fm ~= "" then
result.enable_finalmask = "1"
result.finalmask = base64Encode(params.fm)
end
if params.pinSHA256 and params.pinSHA256 ~= "" then
result.pinsha256 = params.pinSHA256
end
else
result.v2ray_protocol = has_xray_hy2_type
end
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
result.raw_alias = raw_alias -- 新增
result.alias = raw_alias -- 临时赋值(后面会被覆盖)
result.type = hy2_type
result.server = url.host
result.server_port = url.port or 443
result.hy2_auth = url.user
result.uplink_capacity = tonumber((params.upmbps or ""):match("^(%d+)")) or nil
result.downlink_capacity = tonumber((params.downmbps or ""):match("^(%d+)")) or nil
if params.mport then
result.flag_port_hopping = "1"
result.port_range = params.mport
end
if params.obfs and params.obfs ~= "none" then
result.flag_obfs = "1"
result.obfs_type = params.obfs
result.salamander = params["obfs-password"] or params["obfs_password"]
end
if (params.security and params.security:lower() == "tls")
or (params.sni and params.sni ~= "")
or (params.alpn and params.alpn ~= "")
or (xray_hy2_type == "hysteria2" and (params.pcs or params.vcn)) then
result.tls = "1"
if params.sni then
result.tls_host = params.sni
end
if params.alpn and params.alpn ~= "" then
local alpn = {}
for v in params.alpn:gmatch("[^,;|%s]+") do
table.insert(alpn, v)
if (params.security and params.security:lower() == "tls")
or (params.sni and params.sni ~= "")
or (params.alpn and params.alpn ~= "")
or (params.pcs or params.vcn) then
result.tls = "1"
if params.sni then
result.tls_host = params.sni
end
if #alpn > 0 then
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
if params.alpn and params.alpn ~= "" then
local alpn = {}
for v in params.alpn:gmatch("[^,;|%s]+") do
table.insert(alpn, v)
end
if #alpn > 0 then
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
end
end
end
if xray_hy2_type ~= "hysteria2" then
if params.pcs then
result.tls_CertSha = params.pcs
end
@ -314,6 +279,55 @@ local function processData(szType, content, cfgid)
result.tls_CertByName = params.vcn
end
end
else
result.type = "hysteria2"
if params.protocol and params.protocol ~= "" then
result.flag_transport = "1"
result.transport_protocol = params.protocol
else
result.flag_transport = "1"
result.transport_protocol = "udp"
end
if params.lazy and params.lazy ~= "" then
result.lazy_mode = "1"
end
if (params.sni and params.sni ~= "") or (params.alpn and params.alpn ~= "") then
result.tls = "1"
if params.sni then
result.tls_host = params.sni
end
if params.alpn and params.alpn ~= "" then
local alpn = {}
for v in params.alpn:gmatch("[^,;|%s]+") do
table.insert(alpn, v)
end
if #alpn > 0 then
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
end
end
end
if params.pinSHA256 and params.pinSHA256 ~= "" then
result.pinsha256 = params.pinSHA256
end
end
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
result.raw_alias = raw_alias -- 新增
result.alias = raw_alias -- 临时赋值(后面会被覆盖)
result.server = url.host
result.server_port = url.port or 443
result.hy2_auth = url.user
if params.mport then
result.flag_port_hopping = "1"
result.port_range = params.mport
end
result.uplink_capacity = tonumber((params.upmbps or ""):match("^(%d+)")) or nil
result.downlink_capacity = tonumber((params.downmbps or ""):match("^(%d+)")) or nil
if params.obfs and params.obfs ~= "none" then
result.flag_obfs = "1"
result.obfs_type = params.obfs
result.salamander = params["obfs-password"] or params["obfs_password"]
end
if params.allowInsecure or params.insecure then
local insecure = params.allowInsecure or params.insecure
@ -543,153 +557,69 @@ local function processData(szType, content, cfgid)
result.fast_open = params.tfo
end
if v2_ss ~= "v2ray" then
local is_old_format = find_index:find("@") and not find_index:find("://.*@")
local old_base64, host_port, userinfo, server, port, method, password
if is_old_format then
-- 旧格式base64(method:pass)@host:port
old_base64, host_port = find_index:match("^([^@]+)@(.-)$")
log("SS 节点旧格式解析:", old_base64)
if not old_base64 or not host_port then
log("SS 节点旧格式解析失败:", find_index)
return nil
end
local decoded = base64Decode(UrlDecode(old_base64))
if not decoded then
log("SS base64 解码失败(旧格式):", old_base64)
return nil
end
userinfo = decoded
-- 自动决定模式true=Xray, false=普通 SS
local xray_ss_mode = false
if ss_type == "v2ray" then
-- Xray 模式
if has_xray then
xray_ss_mode = true
elseif has_ss_rust or has_ss_libev then
xray_ss_mode = false -- 回退到普通 SS
else
-- 新格式base64(method:pass@host:port)
local decoded = base64Decode(UrlDecode(find_index))
if not decoded then
log("SS base64 解码失败(新格式):", find_index)
return nil
end
userinfo, host_port = decoded:match("^(.-)@(.-)$")
if not userinfo or not host_port then
log("SS 解码内容缺失 @ 分隔:", decoded)
return nil
end
xray_ss_mode = nil
end
-- 解析加密方式和密码(允许密码包含冒号)
local meth_pass = userinfo:find(":")
if not meth_pass then
log("SS 用户信息格式错误:", userinfo)
return nil
end
method = userinfo:sub(1, meth_pass - 1)
password = userinfo:sub(meth_pass + 1)
-- 判断密码是否经过url编码
local function isURLEncodedPassword(pwd)
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
return false
end
local ok, decoded = pcall(UrlDecode, pwd)
return ok and urlEncode(decoded) == pwd
end
local decoded = UrlDecode(password)
if isURLEncodedPassword(password) and decoded then
password = decoded
end
-- 解析服务器地址和端口(兼容 IPv6
if host_port:find("^%[.*%]:%d+$") then
server, port = host_port:match("^%[(.*)%]:(%d+)$")
elseif ss_type == "ss-rust" or ss_type == "ss-libev" then
-- 普通 SS 模式
local user_core = (ss_type == "ss-rust" and has_ss_rust) or (ss_type == "ss-libev" and has_ss_libev)
if user_core then
xray_ss_mode = false -- 否则普通 SS
else
server, port = host_port:match("^(.-):(%d+)$")
end
if not server or not port then
log("SS 节点服务器信息格式错误:", host_port)
return nil
end
-- 如果 SS 程序未安装则跳过订阅
if not v2_ss then
return nil
end
-- 填充 result
result.type = v2_ss
result.encrypt_method_ss = method
result.password = password
result.server = server
result.server_port = port
-- 插件处理
if params.plugin then
local plugin_info = UrlDecode(params.plugin)
local idx_pn = plugin_info:find(";")
if idx_pn then
result.plugin = plugin_info:sub(1, idx_pn - 1)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
-- 指定的核心不存在,尝试另一个 SS 核心
local other_core = (ss_type == "ss-rust" and has_ss_libev) or (ss_type == "ss-libev" and has_ss_rust)
if other_core then
xray_ss_mode = false -- 使用存在的另一个 SS 核心
elseif has_xray then
xray_ss_mode = true -- 回退到 Xray
else
result.plugin = plugin_info
result.plugin_opts = ""
xray_ss_mode = nil
end
-- 部分机场下发的插件名为 simple-obfs这里应该改为 obfs-local
if result.plugin == "simple-obfs" then
result.plugin = "obfs-local"
end
-- 如果插件不为 none确保 enable_plugin 为 1
if result.plugin ~= "none" and result.plugin ~= "" then
result.enable_plugin = 1
end
elseif has_ss_type and has_ss_type ~= "ss-libev" then
if params["shadow-tls"] then
-- 特别处理 shadow-tls 作为插件
-- log("原始 shadow-tls 参数:", params["shadow-tls"])
local decoded_tls = base64Decode(UrlDecode(params["shadow-tls"]))
--log("SS 节点 shadow-tls 解码后:", decoded_tls or "nil")
if decoded_tls then
local ok, st = pcall(jsonParse, decoded_tls)
if ok and st then
result.plugin = "shadow-tls"
result.enable_plugin = 1
local version_flag = ""
if st.version and tonumber(st.version) then
version_flag = string.format("v%s=1;", st.version)
end
-- 合成 plugin_opts 格式v%s=1;host=xxx;password=xxx
result.plugin_opts = string.format("%shost=%s;passwd=%s",
version_flag,
st.host or "",
st.password or "")
else
log("shadow-tls JSON 解析失败")
end
end
end
else
if params["shadow-tls"] then
log("错误ShadowSocks-libev 不支持使用 shadow-tls 插件")
return nil, "ShadowSocks-libev 不支持使用 shadow-tls 插件"
end
end
-- 检查加密方法是否受支持
if not checkTabValue(encrypt_methods_ss)[method] then
-- 1202 年了还不支持 SS AEAD 的屑机场
-- log("不支持的SS加密方法:", method)
result.server = nil
end
else
-- ss_type 为空或 auto根据链接中是否有 type 参数决定
local has_type = params.type and params.type ~= ""
if has_type then
-- 有 type 参数,优先 Xray
if has_xray then
xray_ss_mode = true
elseif has_ss_rust or has_ss_libev then
xray_ss_mode = false -- 回退到普通 SS
else
xray_ss_mode = nil
end
else
-- 无 type 参数,优先普通 SS
if has_ss_rust or has_ss_libev then
-- 普通 SS 模式
xray_ss_mode = false
elseif has_xray then
xray_ss_mode = true -- 回退到 Xray
else
xray_ss_mode = nil
end
end
end
-- 如果最终无可用核心,跳过该订阅
if xray_ss_mode == nil then
return nil
end
if xray_ss_mode then
local url = URL.parse("http://" .. info)
local params = url.query
-- 如果 Xray 程序未安装则跳过订阅
if not v2_ss then
return nil
end
result.type = v2_ss
result.v2ray_protocol = has_v2_ss_type
result.type = "v2ray"
result.v2ray_protocol = "shadowsocks"
result.server = url.host
result.server_port = url.port
@ -816,6 +746,143 @@ local function processData(szType, content, cfgid)
result.tcp_path = params.path and UrlDecode(params.path) or nil
end
end
else
local is_old_format = find_index:find("@") and not find_index:find("://.*@")
local old_base64, host_port, userinfo, server, port, method, password
if is_old_format then
-- 旧格式base64(method:pass)@host:port
old_base64, host_port = find_index:match("^([^@]+)@(.-)$")
log("SS 节点旧格式解析:", old_base64)
if not old_base64 or not host_port then
log("SS 节点旧格式解析失败:", find_index)
return nil
end
local decoded = base64Decode(UrlDecode(old_base64))
if not decoded then
log("SS base64 解码失败(旧格式):", old_base64)
return nil
end
userinfo = decoded
else
-- 新格式base64(method:pass@host:port)
local decoded = base64Decode(UrlDecode(find_index))
if not decoded then
log("SS base64 解码失败(新格式):", find_index)
return nil
end
userinfo, host_port = decoded:match("^(.-)@(.-)$")
if not userinfo or not host_port then
log("SS 解码内容缺失 @ 分隔:", decoded)
return nil
end
end
-- 解析加密方式和密码(允许密码包含冒号)
local meth_pass = userinfo:find(":")
if not meth_pass then
log("SS 用户信息格式错误:", userinfo)
return nil
end
method = userinfo:sub(1, meth_pass - 1)
password = userinfo:sub(meth_pass + 1)
-- 判断密码是否经过url编码
local function isURLEncodedPassword(pwd)
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
return false
end
local ok, decoded = pcall(UrlDecode, pwd)
return ok and urlEncode(decoded) == pwd
end
local decoded = UrlDecode(password)
if isURLEncodedPassword(password) and decoded then
password = decoded
end
-- 解析服务器地址和端口(兼容 IPv6
if host_port:find("^%[.*%]:%d+$") then
server, port = host_port:match("^%[(.*)%]:(%d+)$")
else
server, port = host_port:match("^(.-):(%d+)$")
end
if not server or not port then
log("SS 节点服务器信息格式错误:", host_port)
return nil
end
-- 填充 result
local xray_ss_type
if ss_type == "ss-rust" or ss_type == "ss-libev" then
xray_ss_type = ss_type
else
xray_ss_type = has_ss_rust and "ss-rust" or "ss-libev"
end
result.type = xray_ss_type
result.encrypt_method_ss = method
result.password = password
result.server = server
result.server_port = port
-- 插件处理
if params.plugin then
local plugin_info = UrlDecode(params.plugin)
local idx_pn = plugin_info:find(";")
if idx_pn then
result.plugin = plugin_info:sub(1, idx_pn - 1)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
else
result.plugin = plugin_info
result.plugin_opts = ""
end
-- 部分机场下发的插件名为 simple-obfs这里应该改为 obfs-local
if result.plugin == "simple-obfs" then
result.plugin = "obfs-local"
end
-- 如果插件不为 none确保 enable_plugin 为 1
if result.plugin ~= "none" and result.plugin ~= "" then
result.enable_plugin = 1
end
elseif has_ss_type and has_ss_type ~= "ss-libev" then
if params["shadow-tls"] then
-- 特别处理 shadow-tls 作为插件
-- log("原始 shadow-tls 参数:", params["shadow-tls"])
local decoded_tls = base64Decode(UrlDecode(params["shadow-tls"]))
--log("SS 节点 shadow-tls 解码后:", decoded_tls or "nil")
if decoded_tls then
local ok, st = pcall(jsonParse, decoded_tls)
if ok and st then
result.plugin = "shadow-tls"
result.enable_plugin = 1
local version_flag = ""
if st.version and tonumber(st.version) then
version_flag = string.format("v%s=1;", st.version)
end
-- 合成 plugin_opts 格式v%s=1;host=xxx;password=xxx
result.plugin_opts = string.format("%shost=%s;passwd=%s",
version_flag,
st.host or "",
st.password or "")
else
log("shadow-tls JSON 解析失败")
end
end
end
else
if params["shadow-tls"] then
log("错误ShadowSocks-libev 不支持使用 shadow-tls 插件")
return nil, "ShadowSocks-libev 不支持使用 shadow-tls 插件"
end
end
-- 检查加密方法是否受支持
if not checkTabValue(encrypt_methods_ss)[method] then
-- 1202 年了还不支持 SS AEAD 的屑机场
-- log("不支持的SS加密方法:", method)
result.server = nil
end
end
elseif szType == "sip008" then
result.type = v2_ss
@ -936,102 +1003,143 @@ local function processData(szType, content, cfgid)
result.server_port = port
end
-- 如果 Tojan 程序未安装则跳过订阅
if not v2_tj or v2_tj == "" then
-- 自动决定模式true=Xray, false=普通 Trojan
local xray_tj_mode = false
if xray_tj_type == "v2ray" then
-- Xray 模式
if has_xray then
xray_tj_mode = true
elseif has_trojan then
xray_tj_mode = false -- 回退到普通 Trojan
else
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
end
elseif xray_tj_type == "trojan" then
-- 普通 Trojan 模式
if has_trojan then
xray_tj_mode = false
elseif has_xray then
xray_tj_mode = true -- 回退到 Xray
else
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
end
else
-- 全局配置为空或 auto根据链接中是否有 type 参数决定
local has_type = params.type and params.type ~= ""
-- 有 type 参数,优先 Xray
if has_type then
if has_xray then
xray_tj_mode = true -- 有 type 参数使用 Xray
elseif has_trojan then
xray_tj_mode = false -- 否则普通 Trojan
else
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
end
else
-- 无 type 参数,优先普通 Trojan
if has_trojan then
xray_tj_mode = false -- 普通 Trojan
elseif has_xray then
xray_tj_mode = true -- 否则使用 Xray
else
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
end
end
end
-- 如果最终无可用核心,跳过该订阅
if xray_tj_mode == nil then
return nil
end
if params.type and params.type ~= "" then
v2_tj = "v2ray"
result.type = v2_tj
if xray_tj_mode then
result.type = "v2ray"
result.v2ray_protocol = "trojan"
if v2_tj ~= "trojan" then
if params.fp then
-- 处理 fingerprint 参数
result.fingerprint = params.fp
end
-- 处理 ech 参数
if params.ech and params.ech ~= "" then
result.enable_ech = "1"
result.ech_config = params.ech
end
-- 检查 finalmaskg 参数是否存在且非空
if params.fm and params.fm ~= "" then
result.enable_finalmask = "1"
result.finalmaskg = base64Encode(params.fm)
end
-- 处理传输协议
result.transport = params.type or "raw" -- 默认传输协议为 raw
if result.transport == "tcp" then
result.transport = "raw"
end
if result.transport == "splithttp" then
result.transport = "xhttp"
end
if params.pcs and params.pcs ~= "" then
result.tls_CertSha = params.pcs
end
if params.vcn and params.vcn ~= "" then
result.tls_CertByName = params.vcn
end
if result.transport == "ws" then
result.ws_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
result.ws_path = params.path and UrlDecode(params.path) or "/"
elseif result.transport == "httpupgrade" then
result.httpupgrade_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
result.httpupgrade_path = params.path and UrlDecode(params.path) or "/"
elseif result.transport == "xhttp" or result.transport == "splithttp" then
result.xhttp_mode = params.mode or "auto"
result.xhttp_host = params.host and UrlDecode(params.host) or nil
result.xhttp_path = params.path and UrlDecode(params.path) or "/"
-- 检查 extra 参数是否存在且非空
if params.extra and params.extra ~= "" then
result.enable_xhttp_extra = "1"
result.xhttp_extra = base64Encode(params.extra)
end
-- 尝试解析 JSON 数据
local success, Data = pcall(jsonParse, params.extra or "")
if success and type(Data) == "table" then
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
or (Data.downloadSettings and Data.downloadSettings.address)
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
else
-- 如果解析失败,清空下载地址
result.download_address = nil
end
elseif result.transport == "http" or result.transport == "h2" then
result.transport = "h2"
result.h2_host = params.host and UrlDecode(params.host) or nil
result.h2_path = params.path and UrlDecode(params.path) or nil
elseif result.transport == "kcp" then
result.kcp_guise = params.headerType or "none"
if params.headerType and params.headerType == "dns" then
result.kcp_domain = params.host or ""
end
result.seed = params.seed
result.mtu = 1350
result.tti = 50
result.uplink_capacity = 5
result.downlink_capacity = 20
result.read_buffer_size = 2
result.write_buffer_size = 2
elseif result.transport == "quic" then
result.quic_guise = params.headerType or "none"
result.quic_security = params.quicSecurity or "none"
result.quic_key = params.key
elseif result.transport == "grpc" then
result.serviceName = params.serviceName
result.grpc_mode = params.mode or "gun"
elseif result.transport == "tcp" or result.transport == "raw" then
result.tcp_guise = params.headerType and params.headerType ~= "" and params.headerType or "none"
if result.tcp_guise == "http" then
result.tcp_host = params.host and UrlDecode(params.host) or nil
result.tcp_path = params.path and UrlDecode(params.path) or nil
end
end
else
result.type = v2_tj
if params.fp then
-- 处理 fingerprint 参数
result.fingerprint = params.fp
end
-- 处理 ech 参数
if params.ech and params.ech ~= "" then
result.enable_ech = "1"
result.ech_config = params.ech
end
-- 检查 finalmaskg 参数是否存在且非空
if params.fm and params.fm ~= "" then
result.enable_finalmask = "1"
result.finalmaskg = base64Encode(params.fm)
end
-- 处理传输协议
result.transport = params.type or "raw" -- 默认传输协议为 raw
if result.transport == "tcp" then
result.transport = "raw"
end
if result.transport == "splithttp" then
result.transport = "xhttp"
end
if params.pcs and params.pcs ~= "" then
result.tls_CertSha = params.pcs
end
if params.vcn and params.vcn ~= "" then
result.tls_CertByName = params.vcn
end
if result.transport == "ws" then
result.ws_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
result.ws_path = params.path and UrlDecode(params.path) or "/"
elseif result.transport == "httpupgrade" then
result.httpupgrade_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
result.httpupgrade_path = params.path and UrlDecode(params.path) or "/"
elseif result.transport == "xhttp" or result.transport == "splithttp" then
result.xhttp_mode = params.mode or "auto"
result.xhttp_host = params.host and UrlDecode(params.host) or nil
result.xhttp_path = params.path and UrlDecode(params.path) or "/"
-- 检查 extra 参数是否存在且非空
if params.extra and params.extra ~= "" then
result.enable_xhttp_extra = "1"
result.xhttp_extra = base64Encode(params.extra)
end
-- 尝试解析 JSON 数据
local success, Data = pcall(jsonParse, params.extra or "")
if success and type(Data) == "table" then
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
or (Data.downloadSettings and Data.downloadSettings.address)
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
else
-- 如果解析失败,清空下载地址
result.download_address = nil
end
elseif result.transport == "http" or result.transport == "h2" then
result.transport = "h2"
result.h2_host = params.host and UrlDecode(params.host) or nil
result.h2_path = params.path and UrlDecode(params.path) or nil
elseif result.transport == "kcp" then
result.kcp_guise = params.headerType or "none"
if params.headerType and params.headerType == "dns" then
result.kcp_domain = params.host or ""
end
result.seed = params.seed
result.mtu = 1350
result.tti = 50
result.uplink_capacity = 5
result.downlink_capacity = 20
result.read_buffer_size = 2
result.write_buffer_size = 2
elseif result.transport == "quic" then
result.quic_guise = params.headerType or "none"
result.quic_security = params.quicSecurity or "none"
result.quic_key = params.key
elseif result.transport == "grpc" then
result.serviceName = params.serviceName
result.grpc_mode = params.mode or "gun"
elseif result.transport == "tcp" or result.transport == "raw" then
result.tcp_guise = params.headerType and params.headerType ~= "" and params.headerType or "none"
if result.tcp_guise == "http" then
result.tcp_host = params.host and UrlDecode(params.host) or nil
result.tcp_path = params.path and UrlDecode(params.path) or nil
end
end
else
result.type = "trojan"
end
elseif szType == "vless" then
local url = URL.parse("http://" .. content)

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=momo
PKG_VERSION:=2026.03.29
PKG_RELEASE:=5
PKG_VERSION:=2026.04.09
PKG_RELEASE:=6
PKG_LICENSE:=GPL-3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
@ -35,6 +35,7 @@ define Package/momo/install
$(INSTALL_DIR) $(1)/etc/momo/run
$(INSTALL_BIN) $(CURDIR)/files/ucode/include.uc $(1)/etc/momo/ucode/include.uc
$(INSTALL_BIN) $(CURDIR)/files/ucode/mixin.uc $(1)/etc/momo/ucode/mixin.uc
$(INSTALL_BIN) $(CURDIR)/files/ucode/hijack.ut $(1)/etc/momo/ucode/hijack.ut
$(INSTALL_BIN) $(CURDIR)/files/scripts/include.sh $(1)/etc/momo/scripts/include.sh
@ -52,6 +53,7 @@ define Package/momo/install
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/firewall.sh $(1)/etc/uci-defaults/99_firewall_momo
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/init.sh $(1)/etc/uci-defaults/99_init_momo
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/migrate.sh $(1)/etc/uci-defaults/99_migrate_momo
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d

View File

@ -4,17 +4,10 @@ config config 'config'
option 'profile' 'file:example.json'
option 'start_delay' '0'
option 'scheduled_restart' '0'
option 'cron_expression' '0 3 * * *'
option 'scheduled_restart_cron' '0 3 * * *'
option 'test_profile' '1'
option 'core_only' '0'
config log 'log'
option 'log_cleanup_enabled' '0'
option 'log_cleanup_cron_expression' '0 4 * * *'
option 'log_cleanup_size_enabled' '0'
option 'log_cleanup_size_check_cron_expression' '*/30 * * * *'
option 'log_cleanup_size_mb' '50'
config procd 'procd'
option 'fast_reload' '0'
@ -25,6 +18,18 @@ config core 'core'
option 'dns_inbound_tag' 'dns-in'
option 'fake_ip_dns_server_tag' 'fake-ip-dns-server'
config mixin 'mixin'
option 'log_disabled' '0'
option 'log_level' 'info'
option 'log_timestamp' '1'
option 'dns_independent_cache' '1'
option 'dns_reverse_mapping' '1'
option 'cache_enabled' '1'
option 'cache_store_fakeip' '1'
option 'cache_store_rdrc' '1'
option 'external_control_ui_path' 'ui'
option 'external_control_ui_download_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
config proxy 'proxy'
option 'enabled' '1'
option 'ipv4_dns_hijack' '1'
@ -115,4 +120,10 @@ config routing 'routing'
option 'cgroup_name' 'momo'
option 'dummy_device' 'momo-dummy'
config log 'log'
option 'scheduled_clear' '1'
option 'scheduled_clear_cron' '*/5 * * * *'
option 'scheduled_clear_size_limit' '1'
option 'scheduled_clear_size_limit_unit' 'MB'
config placeholder 'placeholder'

View File

@ -9,8 +9,7 @@ PROG="/usr/bin/sing-box"
. "$IPKG_INSTROOT/etc/momo/scripts/include.sh"
extra_command 'update_subscription' 'Update subscription by section id'
extra_command 'cleanup_logs' 'Cleanup logs immediately'
extra_command 'check_log_cleanup' 'Cleanup logs if size threshold is reached'
extra_command 'clear_logs' 'Clear logs if size exceeds the limit'
boot_func() {
# prepare files
@ -30,7 +29,7 @@ boot_func() {
}
boot() {
boot_func &
boot_func < /dev/null > /dev/null 2>&1 &
}
start_service() {
@ -64,11 +63,6 @@ start_service() {
config_get profile "config" "profile"
config_get_bool test_profile "config" "test_profile" 0
config_get_bool core_only "config" "core_only" 0
config_get_bool log_cleanup_enabled "log" "log_cleanup_enabled" 0
config_get log_cleanup_cron_expression "log" "log_cleanup_cron_expression"
config_get_bool log_cleanup_size_enabled "log" "log_cleanup_size_enabled" 0
config_get log_cleanup_size_check_cron_expression "log" "log_cleanup_size_check_cron_expression"
config_get log_cleanup_size_mb "log" "log_cleanup_size_mb" 0
## procd config
### general
local fast_reload
@ -97,6 +91,10 @@ start_service() {
config_get_bool ipv6_dns_hijack "proxy" "ipv6_dns_hijack" 0
config_get tcp_mode "proxy" "tcp_mode"
config_get udp_mode "proxy" "udp_mode"
## log config
local log_scheduled_clear log_scheduled_clear_cron
config_get_bool log_scheduled_clear "log" "scheduled_clear" 0
config_get log_scheduled_clear_cron "log" "scheduled_clear_cron"
# get profile
local profile_type; profile_type=$(echo "$profile" | cut -d ':' -f 1)
local profile_id; profile_id=$(echo "$profile" | cut -d ':' -f 2)
@ -131,6 +129,11 @@ start_service() {
log "App" "Exit."
return
fi
# mixin
if [ "$core_only" = 0 ]; then
log "Mixin" "Mixin config."
ucode -S "$MIXIN_UC"
fi
# check profile
if [ "$core_only" = 0 ] && [ "$proxy_enabled" = 1 ]; then
log "Profile" "Checking..."
@ -202,23 +205,18 @@ start_service() {
procd_close_instance
# cron
local cron_reload; cron_reload=0
if [ "$scheduled_restart" = 1 ] && [ -n "$cron_expression" ]; then
local reload_cron; reload_cron=0
if [ "$scheduled_restart" = 1 ] && [ -n "$scheduled_restart_cron" ]; then
log "App" "Set scheduled restart."
echo "$cron_expression /etc/init.d/momo restart #momo_scheduled_restart" >> "/etc/crontabs/root"
cron_reload=1
echo "$scheduled_restart_cron /etc/init.d/momo restart #momo scheduled restart" >> "/etc/crontabs/root"
reload_cron=1
fi
if [ "$log_cleanup_enabled" = 1 ] && [ -n "$log_cleanup_cron_expression" ]; then
log "Log" "Set scheduled log cleanup."
echo "$log_cleanup_cron_expression /etc/init.d/momo cleanup_logs > /dev/null 2>&1 #momo_log_cleanup_time" >> "/etc/crontabs/root"
cron_reload=1
if [ "$log_scheduled_clear" = 1 ] && [ -n "$log_scheduled_clear_cron" ]; then
log "App" "Set log scheduled clear."
echo "$log_scheduled_clear_cron /etc/init.d/momo clear_logs #momo log scheduled clear" >> "/etc/crontabs/root"
reload_cron=1
fi
if [ "$log_cleanup_size_enabled" = 1 ] && [ -n "$log_cleanup_size_check_cron_expression" ] && [ "${log_cleanup_size_mb:-0}" -gt 0 ]; then
log "Log" "Set size-based log cleanup check."
echo "$log_cleanup_size_check_cron_expression /etc/init.d/momo check_log_cleanup > /dev/null 2>&1 #momo_log_cleanup_size" >> "/etc/crontabs/root"
cron_reload=1
fi
if [ "$cron_reload" = 1 ]; then
if [ "$reload_cron" = 1 ]; then
/etc/init.d/cron restart
fi
# set started flag
@ -379,8 +377,6 @@ service_triggers() {
}
cleanup() {
# clear log
clear_log
# load config
config_load momo
# get config
@ -427,44 +423,39 @@ cleanup() {
/etc/init.d/cron restart
}
cleanup_logs() {
prepare_files
run_log_cleanup "scheduled trigger"
}
check_log_cleanup() {
prepare_files
clear_logs() {
# load config
config_load momo
local log_cleanup_size_enabled log_cleanup_size_mb total_size_bytes threshold_size_bytes
config_get_bool log_cleanup_size_enabled "log" "log_cleanup_size_enabled" 0
config_get log_cleanup_size_mb "log" "log_cleanup_size_mb" 0
if [ "$log_cleanup_size_enabled" != 1 ] || [ "${log_cleanup_size_mb:-0}" -le 0 ]; then
return
# get config
## log config
local log_scheduled_clear_size_limit log_scheduled_clear_size_limit_unit
config_get log_scheduled_clear_size_limit "log" "scheduled_clear_size_limit" 1
config_get log_scheduled_clear_size_limit_unit "log" "scheduled_clear_size_limit_unit" "MB"
# prepare config
local log_scheduled_clear_size_limit_in_bytes; log_scheduled_clear_size_limit_in_bytes=0
case "$log_scheduled_clear_size_limit_unit" in
B)
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit))
;;
KB)
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024))
;;
MB)
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024 * 1024))
;;
GB)
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024 * 1024 * 1024))
;;
esac
# clear logs
if [ -f "$APP_LOG_PATH" ] && [ "$(wc -c < "$APP_LOG_PATH")" -ge "$log_scheduled_clear_size_limit_in_bytes" ]; then
echo -n > "$APP_LOG_PATH"
log "Log" "App log is cleared due to scheduled clear and exceeding size limits."
fi
total_size_bytes=$(get_total_log_size)
threshold_size_bytes=$((log_cleanup_size_mb * 1024 * 1024))
if [ "$total_size_bytes" -lt "$threshold_size_bytes" ]; then
return
if [ -f "$CORE_LOG_PATH" ] && [ "$(wc -c < "$CORE_LOG_PATH")" -ge "$log_scheduled_clear_size_limit_in_bytes" ]; then
echo -n > "$CORE_LOG_PATH"
log "Log" "Core log is cleared due to scheduled clear and exceeding size limits."
fi
run_log_cleanup "size threshold ${log_cleanup_size_mb} MB reached" "$total_size_bytes"
}
run_log_cleanup() {
local reason previous_size
reason="$1"
previous_size="$2"
if [ -z "$previous_size" ]; then
previous_size=$(get_total_log_size)
fi
clear_log
log "Log" "Cleanup executed: ${reason}. Previous total size: $(format_filesize "$previous_size")."
}
update_subscription() {

View File

@ -118,39 +118,11 @@ prepare_files() {
if [ ! -f "$CORE_LOG_PATH" ]; then
touch "$CORE_LOG_PATH"
fi
if [ ! -f "$DEBUG_LOG_PATH" ]; then
touch "$DEBUG_LOG_PATH"
fi
if [ ! -d "$TEMP_DIR" ]; then
mkdir -p "$TEMP_DIR"
fi
}
clear_log() {
echo -n > "$APP_LOG_PATH"
echo -n > "$CORE_LOG_PATH"
echo -n > "$DEBUG_LOG_PATH"
}
get_filesize() {
local path; path="$1"
if [ ! -f "$path" ]; then
echo 0
return
fi
wc -c < "$path" | tr -d '[:space:]'
}
get_total_log_size() {
local total_size path path_size
total_size=0
for path in "$APP_LOG_PATH" "$CORE_LOG_PATH" "$DEBUG_LOG_PATH"; do
path_size=$(get_filesize "$path")
total_size=$((total_size + path_size))
done
echo "$total_size"
}
log() {
echo "[$(date "+%Y-%m-%d %H:%M:%S")] [$1] $2" >> "$APP_LOG_PATH"
}

View File

@ -0,0 +1,21 @@
#!/bin/sh
. "$IPKG_INSTROOT/etc/momo/scripts/include.sh"
# check momo.config.init
init=$(uci -q get momo.config.init); [ -z "$init" ] && return
# generate random string for api secret and authentication password
random=$(awk 'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}')
# set momo.mixin.api_secret
uci set momo.mixin.external_control_api_secret="$random"
# remove momo.config.init
uci del momo.config.init
# commit
uci commit momo
# exit with 0
exit 0

View File

@ -27,42 +27,29 @@ dummy_device=$(uci -q get momo.routing.dummy_device); [ -z "$dummy_device" ] &&
# since v1.1.2
section_log=$(uci -q get momo.log); [ -z "$section_log" ] && uci set momo.log=log
config_scheduled_restart_cron=$(uci -q get momo.config.scheduled_restart_cron); [ -z "$config_scheduled_restart_cron" ] && uci rename momo.config.cron_expression="scheduled_restart_cron"
log_cleanup_enabled=$(uci -q get momo.log.log_cleanup_enabled)
[ -z "$log_cleanup_enabled" ] && {
log_cleanup_enabled=$(uci -q get momo.config.log_cleanup_enabled)
[ -n "$log_cleanup_enabled" ] && uci set momo.log.log_cleanup_enabled=$log_cleanup_enabled || uci set momo.log.log_cleanup_enabled=0
section_log=$(uci -q get momo.log); [ -z "$section_log" ] && {
uci set momo.log=log
uci set momo.log.scheduled_clear=1
uci set momo.log.scheduled_clear_cron="*/5 * * * *"
uci set momo.log.scheduled_clear_size_limit=1
uci set momo.log.scheduled_clear_size_limit_unit=MB
}
uci -q del momo.config.log_cleanup_enabled
log_cleanup_cron_expression=$(uci -q get momo.log.log_cleanup_cron_expression)
[ -z "$log_cleanup_cron_expression" ] && {
log_cleanup_cron_expression=$(uci -q get momo.config.log_cleanup_cron_expression)
[ -n "$log_cleanup_cron_expression" ] && uci set momo.log.log_cleanup_cron_expression="$log_cleanup_cron_expression" || uci set momo.log.log_cleanup_cron_expression='0 4 * * *'
section_mixin=$(uci -q get momo.mixin); [ -z "$section_mixin" ] && {
uci set momo.mixin=mixin
uci set momo.mixin.log_disabled='0'
uci set momo.mixin.log_level='info'
uci set momo.mixin.log_timestamp='1'
uci set momo.mixin.dns_independent_cache='1'
uci set momo.mixin.dns_reverse_mapping='1'
uci set momo.mixin.cache_enabled='1'
uci set momo.mixin.cache_store_fakeip='1'
uci set momo.mixin.cache_store_rdrc='1'
uci set momo.mixin.external_control_ui_path='ui'
uci set momo.mixin.external_control_ui_download_url='https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
}
uci -q del momo.config.log_cleanup_cron_expression
log_cleanup_size_enabled=$(uci -q get momo.log.log_cleanup_size_enabled)
[ -z "$log_cleanup_size_enabled" ] && {
log_cleanup_size_enabled=$(uci -q get momo.config.log_cleanup_size_enabled)
[ -n "$log_cleanup_size_enabled" ] && uci set momo.log.log_cleanup_size_enabled=$log_cleanup_size_enabled || uci set momo.log.log_cleanup_size_enabled=0
}
uci -q del momo.config.log_cleanup_size_enabled
log_cleanup_size_check_cron_expression=$(uci -q get momo.log.log_cleanup_size_check_cron_expression)
[ -z "$log_cleanup_size_check_cron_expression" ] && {
log_cleanup_size_check_cron_expression=$(uci -q get momo.config.log_cleanup_size_check_cron_expression)
[ -n "$log_cleanup_size_check_cron_expression" ] && uci set momo.log.log_cleanup_size_check_cron_expression="$log_cleanup_size_check_cron_expression" || uci set momo.log.log_cleanup_size_check_cron_expression='*/30 * * * *'
}
uci -q del momo.config.log_cleanup_size_check_cron_expression
log_cleanup_size_mb=$(uci -q get momo.log.log_cleanup_size_mb)
[ -z "$log_cleanup_size_mb" ] && {
log_cleanup_size_mb=$(uci -q get momo.config.log_cleanup_size_mb)
[ -n "$log_cleanup_size_mb" ] && uci set momo.log.log_cleanup_size_mb=$log_cleanup_size_mb || uci set momo.log.log_cleanup_size_mb=50
}
uci -q del momo.config.log_cleanup_size_mb
# commit
uci commit momo

View File

@ -1,4 +1,4 @@
import { readfile, popen } from 'fs';
import { readfile, writefile, popen } from 'fs';
export function get_paths() {
let result = {};
@ -120,4 +120,14 @@ export function get_cgroups() {
}
}
return result;
};
export function load_profile() {
const paths = get_paths();
return json(readfile(paths.run_profile_path));
};
export function save_profile(obj) {
const paths = get_paths();
return writefile(paths.run_profile_path, obj);
};

48
momo/files/ucode/mixin.uc Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/ucode
'use strict';
import { cursor } from 'uci';
import { uci_bool, uci_int, uci_array, merge, trim_all, load_profile, save_profile } from '/etc/momo/ucode/include.uc';
const uci = cursor();
const config = {};
config['log'] = {};
config['log']['disabled'] = uci_bool(uci.get('momo', 'mixin', 'log_disabled'));
config['log']['level'] = uci.get('momo', 'mixin', 'log_level');
config['log']['timestamp'] = uci_bool(uci.get('momo', 'mixin', 'log_timestamp'));
config['log']['output'] = uci.get('momo', 'mixin', 'log_output');
config['dns'] = {};
config['dns']['strategy'] = uci.get('momo', 'mixin', 'dns_strategy');
config['dns']['disable_cache'] = uci_bool(uci.get('momo', 'mixin', 'dns_disable_cache'));
config['dns']['disable_expire'] = uci_bool(uci.get('momo', 'mixin', 'dns_disable_expire'));
config['dns']['independent_cache'] = uci_bool(uci.get('momo', 'mixin', 'dns_independent_cache'));
config['dns']['cache_capacity'] = uci_int(uci.get('momo', 'mixin', 'dns_cache_capacity'));
config['dns']['reverse_mapping'] = uci_bool(uci.get('momo', 'mixin', 'dns_reverse_mapping'));
config['ntp'] = {};
config['ntp']['enabled'] = uci_bool(uci.get('momo', 'mixin', 'ntp_enabled'));
config['ntp']['server'] = uci.get('momo', 'mixin', 'ntp_server');
config['ntp']['server_port'] = uci_int(uci.get('momo', 'mixin', 'ntp_server_port'));
config['ntp']['interval'] = uci.get('momo', 'mixin', 'ntp_interval');
config['experimental'] = {};
config['experimental']['cache_file'] = {};
config['experimental']['cache_file']['enabled'] = uci_bool(uci.get('momo', 'mixin', 'cache_enabled'));
config['experimental']['cache_file']['path'] = uci.get('momo', 'mixin', 'cache_path');
config['experimental']['cache_file']['store_fakeip'] = uci_bool(uci.get('momo', 'mixin', 'cache_store_fakeip'));
config['experimental']['cache_file']['store_rdrc'] = uci_bool(uci.get('momo', 'mixin', 'cache_store_rdrc'));
config['experimental']['clash_api'] = {};
config['experimental']['clash_api']['external_ui'] = uci.get('momo', 'mixin', 'external_control_ui_path');
config['experimental']['clash_api']['external_ui_download_url'] = uci.get('momo', 'mixin', 'external_control_ui_download_url');
config['experimental']['clash_api']['external_controller'] = uci.get('momo', 'mixin', 'external_control_api_listen');
config['experimental']['clash_api']['secret'] = uci.get('momo', 'mixin', 'external_control_api_secret');
const profile = load_profile();
save_profile(merge(profile, trim_all(config)));

View File

@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mptcp
PKG_VERSION:=6.1
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)

View File

@ -1,5 +1,5 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2018 Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
# Copyright (C) 2018-2026 Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
# Released under GPL 3. See LICENSE for the full terms.
START=50
@ -41,10 +41,10 @@ global_multipath_settings() {
# Global MPTCP configuration
if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
sysctl -qew net.mptcp.mptcp_enabled="$multipath_status"
if [ -z "$(grep net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf)" ]; then
echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
else
if grep -q net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
sed -i "s:^net.mptcp.mptcp_enabled=[0-1]*:net.mptcp.mptcp_enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf
else
echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
fi
[ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager"
[ -z "$mptcp_scheduler" ] || sysctl -qew net.mptcp.mptcp_scheduler="$mptcp_scheduler"
@ -59,10 +59,10 @@ global_multipath_settings() {
[ -z "$mptcp_version" ] || sysctl -qew net.mptcp.mptcp_version="$mptcp_version"
elif [ -f /proc/sys/net/mptcp/enabled ]; then
sysctl -qew net.mptcp.enabled="$multipath_status"
if [ -z "$(grep net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf)" ]; then
echo "net.mptcp.enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
else
if grep -q net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
sed -i "s:^net.mptcp.enabled=[0-1]*:net.mptcp.enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf
else
echo "net.mptcp.enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
fi
ip mptcp limits set add_addr_accepted $mptcp_add_addr_accepted subflows $mptcp_subflows >/dev/null 2>&1
#[ -z "$mptcp_debug" ] || sysctl -qew net.mptcp.mptcp_debug="$mptcp_debug"
@ -70,15 +70,16 @@ global_multipath_settings() {
[ -z "$mptcp_checksum" ] || sysctl -qew net.mptcp.checksum_enabled="$mptcp_checksum"
[ -z "$mptcp_stale_loss_cnt" ] || sysctl -qew net.mptcp.stale_loss_cnt="$mptcp_stale_loss_cnt"
[ -z "$mptcp_pm_type" ] || sysctl -qew net.mptcp.pm_type="$mptcp_pm_type"
[ -z "$mptcp_allow_join_initial_addr_port" ] || sysctl -qew net.mptcp.allow_join_initial_addr_port="$mptcp_allow_initial_addr_port"
[ -z "$mptcp_allow_join_initial_addr_port" ] || sysctl -qew net.mptcp.allow_join_initial_addr_port="$mptcp_allow_join_initial_addr_port"
[ -z "$mptcp_close_timeout" ] || sysctl -qew net.mptcp.close_timeout="$mptcp_close_timeout"
[ -z "$mptcp_syn_retrans_before_tcp_fallback" ] || sysctl -qew net.mptcp.syn_retrans_before_tcp_fallback="$mptcp_syn_retrans_before_tcp_fallback"
[ -z "$mptcp_blackhole_timeout" ] || sysctl -qew net.mptcp.blackhole_timeout="$mptcp_blackhole_timeout"
if [ -n "$mptcp_scheduler" ] && [ -d /usr/share/bpf/scheduler ]; then
for scheduler in $(ls -1 /usr/share/bpf/scheduler/*.o); do
bpftool struct_ops register $scheduler >/dev/null 2>&1
for scheduler in /usr/share/bpf/scheduler/*.o; do
[ -f "$scheduler" ] && bpftool struct_ops register "$scheduler" >/dev/null 2>&1
done
sysctl -qew net.mptcp.scheduler="$(echo $mptcp_scheduler | sed -e 's/mptcp_//' -e 's/.o//')" >/dev/null 2>&1
local sched="${mptcp_scheduler#mptcp_}"; sched="${sched%.o}"
sysctl -qew net.mptcp.scheduler="$sched" >/dev/null 2>&1
fi
fi
[ -z "$congestion" ] || sysctl -qew net.ipv4.tcp_congestion_control="$congestion"
@ -88,44 +89,52 @@ interface_macaddr_count() {
local intf="$1"
local dmacaddr="$2"
config_get macaddr "$intf" macaddr
[ "$macaddr" = "$dmacaddr" ] && [ -z "$(echo $intf | grep '\.')" ] && nbmac=$((nbmac+1))
[ "$macaddr" = "$dmacaddr" ] && [ "${intf%.*}" = "$intf" ] && nbmac=$((nbmac+1))
}
interface_max_metric() {
local config="$1"
if [ "$1" != "omrvpn" ] && [ "$1" != "omr6in4" ] && [ "$1" != "lan" ] && [ "$1" != "loopback" ]; then
config_get metric "$config" ip4table
if [ "$metric" -gt "$count" ] && [ "$metric" -lt "1000" ]; then
count=$metric
fi
elif [ "$1" = "omrvpn" ]; then
uci -q batch <<-EOF >/dev/null
set network.${config}.metric=1500
set network.${config}.ip4table=1500
set network.${config}.ip6table=61500
commit network
set openmptcprouter.${config}.metric=1500
commit openmptcprouter
EOF
elif [ "$1" = "omr6in4" ]; then
uci -q batch <<-EOF >/dev/null
set network.${config}.metric=1201
set network.${config}.ip4table=1201
set network.${config}.ip6table=61201
commit network
set openmptcprouter.${config}.metric=1201
commit openmptcprouter
EOF
elif [ "$1" = "lan" ]; then
sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables
fi
case "$1" in
omrvpn)
uci -q batch <<-EOF >/dev/null
set network.${config}.metric=1500
set network.${config}.ip4table=1500
set network.${config}.ip6table=61500
commit network
set openmptcprouter.${config}.metric=1500
commit openmptcprouter
EOF
;;
omr6in4)
uci -q batch <<-EOF >/dev/null
set network.${config}.metric=1201
set network.${config}.ip4table=1201
set network.${config}.ip6table=61201
commit network
set openmptcprouter.${config}.metric=1201
commit openmptcprouter
EOF
;;
lan)
sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables
;;
loopback)
;;
*)
config_get metric "$config" ip4table
if [ "$metric" -gt "$count" ] && [ "$metric" -lt "1000" ]; then
count=$metric
fi
;;
esac
}
interface_multipath_settings() {
local mode iface proto metric ip4table
local config="$1"
local intf="$2"
local enabled
local enabled uci_route
uci_route=$(uci -q get openmptcprouter.settings.uci_route)
network_get_device iface $config
[ -z "$iface" ] && network_get_physdev iface $config
@ -135,7 +144,7 @@ interface_multipath_settings() {
config_get enabled "$config" auto "1"
config_get txqueuelen "$config" txqueuelen
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]')
case "$iface" in *@*) iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]') ;; esac
if [ "$(uci -q get openmptcprouter.${config}.metric)" = "" ] || [ "$(uci -q get openmptcprouter.${config}.metric)" = "1" ]; then
count=$((count+1))
metric=$count
@ -164,21 +173,12 @@ interface_multipath_settings() {
ip4table=$metric
ip6table=6$metric
[ -n "$iface" ] && {
gro=$(uci -q get network.${config}.gro)
[ "$gro" = "1" ] && ethtool -K $iface gro on >/dev/null 2>&1
[ "$gro" = "0" ] && ethtool -K $iface gro off >/dev/null 2>&1
gso=$(uci -q get network.${config}.gso)
[ "$gso" = "1" ] && ethtool -K $iface gso on >/dev/null 2>&1
[ "$gso" = "0" ] && ethtool -K $iface gso off >/dev/null 2>&1
lro=$(uci -q get network.${config}.lro)
[ "$lro" = "1" ] && ethtool -K $iface lro on >/dev/null 2>&1
[ "$lro" = "0" ] && ethtool -K $iface lro off >/dev/null 2>&1
ufo=$(uci -q get network.${config}.ufo)
[ "$ufo" = "1" ] && ethtool -K $iface ufo on >/dev/null 2>&1
[ "$ufo" = "0" ] && ethtool -K $iface ufo off >/dev/null 2>&1
tso=$(uci -q get network.${config}.tso)
[ "$tso" = "1" ] && ethtool -K $iface tso on >/dev/null 2>&1
[ "$tso" = "0" ] && ethtool -K $iface tso off >/dev/null 2>&1
local offload val
for offload in gro gso lro ufo tso; do
val=$(uci -q get network.${config}.${offload})
[ "$val" = "1" ] && ethtool -K $iface $offload on >/dev/null 2>&1
[ "$val" = "0" ] && ethtool -K $iface $offload off >/dev/null 2>&1
done
}
[ "$mode" = "" ] && {
mode="$(uci -q get openmptcprouter.${config}.multipath)"
@ -199,7 +199,7 @@ interface_multipath_settings() {
[ -z "$mptcpmintf" ] && mptcpmintf="$config"
uci -q set network.${config}.defaultroute=0
uci -q set network.${config}.peerdns=0
uci -q delete network.${config}.dns=0
uci -q delete network.${config}.dns
echo '' > /etc/resolv.conf >/dev/null 2>&1
}
[ "$mode" = "master" ] && {
@ -233,9 +233,9 @@ interface_multipath_settings() {
#[ "$config" = "omrvpn" ] && return 0
[ "$config" = "omrvpn" ] && mode="off"
#[ -n "$(ifconfig | grep $iface)" ] || return 0
[ -n "$(ip link show dev $iface)" ] || return 0
[ "$(echo $iface | grep _dev)" != "" ] && return 0
[ "$(echo $iface | grep '^if')" != "" ] && return 0
ip link show dev "$iface" >/dev/null 2>&1 || return 0
case "$iface" in *_dev*) return 0 ;; esac
case "$iface" in if*) return 0 ;; esac
[ "$iface" = "lo" ] && return 0
status=`ifstatus $config | jsonfilter -q -e "@.up" | tr -d "\n"`
[ "$status" = "false" ] && return 0
@ -293,7 +293,7 @@ interface_multipath_settings() {
[ -n "$ipaddr" ] && [ -n "$netmask" ] && netmask=`ipcalc.sh $ipaddr/$netmask | sed -n '/PREFIX=/{;s/.*=//;s/ .*//;p;}'`
[ -n "$ipaddr" ] && [ -n "$netmask" ] && network=`ipcalc.sh $ipaddr/$netmask | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
fi
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
if [ "$uci_route" = "1" ]; then
uci -q batch <<-EOF >/dev/null
delete network.${config}_rule
delete network.${config}_route
@ -301,12 +301,12 @@ interface_multipath_settings() {
commit network
EOF
else
[ -n "$(ip rule list | grep $id)" ] && ip rule del table $ip4table > /dev/null 2>&1
ip rule show | grep -q " $id " && ip rule del table $ip4table > /dev/null 2>&1
ip route flush $id > /dev/null 2>&1
fi
if [ -n "$gateway" ] && [ -n "$network" ]; then
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
if [ "$uci_route" = "1" ]; then
uci -q batch <<-EOF >/dev/null
delete network.${config}_rule
set network.${config}_rule=rule
@ -360,7 +360,7 @@ interface_multipath_settings() {
fi
fi
if [ -z "$gateway" ] && [ -n "$network" ]; then
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
if [ "$uci_route" = "1" ]; then
uci -q batch <<-EOF >/dev/null
delete network.${config}_rule
set network.${config}_rule=rule
@ -424,11 +424,11 @@ interface_multipath_settings() {
#fi
gateway6=$(echo $gateway6 | cut -d/ -f1 | tr -d "\n")
netmask6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@["ipv6-prefix"][0].mask' | tr -d "\n")
network6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@[".ipv6-prefix"][0].address' | tr -d "\n")
network6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@["ipv6-prefix"][0].address' | tr -d "\n")
[ -z "$netmask6" ] && [ -n "$ip6addr" ] && netmask6=$(ip -6 addr show dev $iface | grep $ip6addr | awk '/inet6/ {print $2; exit}' | cut -d/ -f2 | tr -d "\n")
[ -z "$network6" ] && [ -n "$ip6addr" ] && [ -n "$netmask6" ] && network6=`/usr/sbin/ipcalc -n ${ip6addr}/${netmask6} | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
fi
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
if [ "$uci_route" = "1" ]; then
uci -q batch <<-EOF >/dev/null
delete network.${config}_rule6
delete network.${config}_route6
@ -436,12 +436,12 @@ interface_multipath_settings() {
commit network
EOF
else
[ -n "$(ip -6 rule list | grep 6$id)" ] && ip -6 rule del table $ip6table > /dev/null 2>&1
ip -6 rule show | grep -q " 6$id " && ip -6 rule del table $ip6table > /dev/null 2>&1
ip -6 route flush 6$id > /dev/null 2>&1
fi
if [ -n "$gateway6" ] && [ -n "$network6" ]; then
#echo "gateway6: $gateway6 - network6: $network6 -> ok"
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
if [ "$uci_route" = "1" ]; then
uci -q batch <<-EOF >/dev/null
delete network.${config}_rule6
set network.${config}_rule6=rule6
@ -466,7 +466,7 @@ interface_multipath_settings() {
EOF
else
[ -n "$ip6addr" ] && ip -6 rule add from $ip6addr table $ip6table pref 0 >/dev/null 2>&1
[ -z "$(ip rule show pref 0 table $ip6table oif $iface)" ] && ip rule add oif $iface table $ip6table pref 0
[ -z "$(ip -6 rule show pref 0 table $ip6table oif $iface)" ] && ip -6 rule add oif $iface table $ip6table pref 0
ip -6 route replace $network6/$netmask6 dev $iface scope link metric 6$id $initcwrwnd >/dev/null 2>&1
ip -6 route replace $network6/$netmask6 dev $iface scope link table $ip6table $initcwrwnd >/dev/null 2>&1
ip -6 route replace $gateway6 dev $iface table $ip6table $initcwrwnd >/dev/null 2>&1
@ -516,17 +516,16 @@ load_interfaces() {
}
set_multipath() {
ls -1 /sys/class/net/ | while read iface; do
exist=0
for ifacemptcp in $mptcpintf; do
if [ "$iface" = "$ifacemptcp" ]; then
exist=1
fi
done
[ "$exist" = "0" ] && [ -z "$(multipath $iface | grep deactivated)" ] && [ "$iface" != "bonding_master" ] && [ -n "$(multipath $iface)" ] && {
logger -t "MPTCP" "Disabling MPTCP on interface $iface not found in enabled multipath list"
multipath $iface off >/dev/null 2>&1
}
local iface mp
for iface in /sys/class/net/*; do
iface="${iface##*/}"
case " $mptcpintf " in *" $iface "*) continue ;; esac
mp=$(multipath "$iface" 2>/dev/null)
[ "$iface" = "bonding_master" ] && continue
[ -z "$mp" ] && continue
case "$mp" in *deactivated*) continue ;; esac
logger -t "MPTCP" "Disabling MPTCP on interface $iface not found in enabled multipath list"
multipath "$iface" off >/dev/null 2>&1
done
}
@ -550,10 +549,10 @@ add_route() {
config_get type "$1" type
[ -n "$type" ] && routeset="$routeset type $type"
config_get table "$1" table
[ -n "$table" ] && routeset="table $table"
[ -n "$table" ] && routeset="$routeset table $table"
config_get interface "$1" interface
iface=$(ifstatus "$interface" | jsonfilter -q -e '@["l3_device"]')
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]')
case "$iface" in *@*) iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]') ;; esac
[ -n "$iface" ] && routeset="$routeset dev $iface"
logger -t "MPTCP" "Add route $routeset"
[ -n "$routeset" ] && {
@ -575,10 +574,10 @@ add_route6() {
config_get type "$1" type
[ -n "$type" ] && routeset="$routeset type $type"
config_get table "$1" table
[ -n "$table" ] && routeset="table $table"
[ -n "$table" ] && routeset="$routeset table $table"
config_get interface "$1" interface
iface=$(ifstatus "$interface" | jsonfilter -q -e '@["l3_device"]')
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]')
case "$iface" in *@*) iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]') ;; esac
[ -n "$iface" ] && routeset="$routeset dev $iface"
logger -t "MPTCP" "Add IPv6 route $routeset"
[ -n "$routeset" ] && {
@ -599,13 +598,18 @@ start_service() {
#[ -n "$intf" ] && multipath "${intf}" off >/dev/null 2>&1
[ -z "$intf" ] && global_multipath_settings
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep '3-model-b')" ] && [ "$(ip link show eth0 | grep UP)" = "" ] && {
# RPI 3 workaround no network at boot
ethtool eth0 > /dev/null 2>&1
ethtool -s eth0 autoneg off > /dev/null 2>&1
ip link set eth0 up > /dev/null 2>&1
ethtool -s eth0 autoneg on > /dev/null 2>&1
}
local board_name
board_name=$(ubus call system board | jsonfilter -e '@.board_name')
case "$board_name" in *3-model-b*)
[ "$(ip link show eth0 | grep UP)" = "" ] && {
# RPI 3 workaround no network at boot
ethtool eth0 > /dev/null 2>&1
ethtool -s eth0 autoneg off > /dev/null 2>&1
ip link set eth0 up > /dev/null 2>&1
ethtool -s eth0 autoneg on > /dev/null 2>&1
}
;; esac
mptcpintf=""
mptcpmintf=""
@ -637,7 +641,7 @@ start_service() {
# If no master is defined, one interface is defined as master
if [ "$master" = "" ] && [ -z "$intf" ]; then
intfmaster="$mptcpmintf"
[ "$intfmaster" != "" ] && [ "$(uci -q show network | grep \"multipath='master'\")" ] && {
[ "$intfmaster" != "" ] && [ -z "$(uci -q show network | grep \"multipath='master'\")" ] && {
logger -t "MPTCP" "No master multipath defined, setting it to $intfmaster"
uci -q set network.${intfmaster}.multipath="master"
uci -q set openmptcprouter.${intfmaster}.multipath="master"
@ -646,13 +650,15 @@ start_service() {
fi
[ -n "$(uci -q changes network)" ] && uci -q commit network
[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep raspberry)" ] && [ -z "$(ubus call system board | jsonfilter -e '@.board_name' | grep '4-model-b')" ] && [ -z "$(ubus call system board | jsonfilter -e '@.board_name' | grep '5')" ] && {
ethtool --offload eth0 rx off tx off > /dev/null 2>&1
}
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep -i r2s)" ] && {
case "$board_name" in
*raspberry*) case "$board_name" in *4-model-b*|*5*) ;; *)
ethtool --offload eth0 rx off tx off > /dev/null 2>&1
;; esac ;;
esac
case "$board_name" in *[Rr]2[Ss]*)
ethtool -K eth0 rx off tx off > /dev/null 2>&1
ethtool -K eth1 rx off tx off > /dev/null 2>&1
}
;; esac
}
reload_service() {

View File

@ -65,7 +65,7 @@ case $1 in
fi
if [ -f /proc/sys/net/mptcp/mptcp_checksum ]; then
echo Use checksum: `cat /proc/sys/net/mptcp/mptcp_checksum`
else
elif [ -f /proc/sys/net/mptcp/checksum_enabled ]; then
echo Use checksum: `cat /proc/sys/net/mptcp/checksum_enabled`
fi
if [ -f /proc/sys/net/mptcp/mptcp_scheduler ]; then
@ -98,7 +98,12 @@ case $1 in
echo "jsonfilter or jq are required"
exit 1
fi
[ -n "$IP" ] && [ -n "$endpoint" ] && ID=$(echo "${endpoint}" show | grep "$IP " | awk '{print $3}')
if [ -n "$IP" ] && [ -n "$endpoint" ]; then
for i in $IP; do
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
[ -n "$ID" ] && break
done
fi
fi
if [ -n "$ID" ]; then
echo "OK"
@ -159,18 +164,18 @@ else
endpoint="$(ip mptcp endpoint show)"
if [ -n "$TYPE" ]; then
# Remove not needed if* interfaces in MPTCP
[ -n "$endpoint" ] && oldintfs=$(echo "${endpoint}" | grep "dev if" | awk '{ print $3 }')
[ -n "$oldintfs" ] && {
for oldintf in $oldintfs; do
ip mptcp endpoint delete id $oldintf >/dev/null 2>&1
[ -n "$endpoint" ] && oldids=$(echo "${endpoint}" | grep "dev if" | awk '{ print $3 }')
[ -n "$oldids" ] && {
for oldid in $oldids; do
ip mptcp endpoint delete id $oldid >/dev/null 2>&1
done
endpoint="$(ip mptcp endpoint show)"
}
fi
if [ -n "$endpoint" ]; then
ID=$(echo "${endpoint}" | sort | grep "dev $DEVICE " | awk '{print $3}')
IFF=$(echo "${endpoint}" | sort | grep -m 1 -E "dev $DEVICE " | awk '{print $4; exit}')
IFF2=$(echo "${endpoint}" | sort | grep -m 1 -E "dev $DEVICE " | awk '{print $5; exit}')
ID=$(echo "${endpoint}" | sort | grep -F "dev $DEVICE " | awk '{print $3}')
IFF=$(echo "${endpoint}" | sort | grep -F -m 1 "dev $DEVICE " | awk '{print $4; exit}')
IFF2=$(echo "${endpoint}" | sort | grep -F -m 1 "dev $DEVICE " | awk '{print $5; exit}')
fi
if [ -n "$TYPE" ]; then
#IP=$(ip a show $DEVICE | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p')
@ -182,7 +187,12 @@ else
echo "jsonfilter or jq are required"
exit 1
fi
[ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ] && ID=$(echo "${endpoint}" show | grep "$IP " | awk '{print $3}')
if [ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ]; then
for i in $IP; do
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
[ -n "$ID" ] && break
done
fi
[ -n "$endpoint" ] && RMID=$(echo "${endpoint}" | grep '::ffff' | awk '{ print $3 }')
[ -n "$RMID" ] && ip mptcp endpoint delete id $RMID >/dev/null 2>&1
fi
@ -195,6 +205,10 @@ else
}
exit 0;;
"on")
[ -z "$IP" ] && {
echo "No global IP found on $DEVICE"
exit 1
}
[ -n "$ID" ] && {
for i in $ID; do
ip mptcp endpoint delete id $i >/dev/null 2>&1
@ -205,6 +219,10 @@ else
done
exit 0;;
"signal")
[ -z "$IP" ] && {
echo "No global IP found on $DEVICE"
exit 1
}
[ -n "$ID" ] && {
for i in $ID; do
ip mptcp endpoint delete id $i >/dev/null 2>&1
@ -216,6 +234,10 @@ else
done
exit 0;;
"backup")
[ -z "$IP" ] && {
echo "No global IP found on $DEVICE"
exit 1
}
[ -n "$ID" ] && {
for i in $ID; do
ip mptcp endpoint delete id $i >/dev/null 2>&1

View File

@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20251024
PKG_RELEASE:=2
PKG_VERSION:=20260313
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git

View File

@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=netdata
PKG_VERSION:=2.9.0
PKG_RELEASE:=5
PKG_VERSION:=2.10.0
PKG_RELEASE:=6
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
PKG_LICENSE:=GPL-3.0-or-later

View File

@ -7,9 +7,9 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=openlist2
PKG_VERSION:=4.2.0
PKG_WEB_VERSION:=4.1.10
PKG_RELEASE:=2
PKG_VERSION:=4.2.1
PKG_WEB_VERSION:=4.2.1
PKG_RELEASE:=3
PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)?