diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile
new file mode 100644
index 000000000..7c813cc82
--- /dev/null
+++ b/hickory-dns/Makefile
@@ -0,0 +1,42 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=hickory-dns
+PKG_VERSION:=master
+PKG_RELEASE:=1
+
+PKG_SOURCE_PROTO:=git
+PKG_SOURCE_URL:=https://github.com/hickory-dns/hickory-dns.git
+PKG_SOURCE_VERSION:=f9da0e946dd1bc62259a2b7dc5c05d7b740fd9a7
+PKG_BUILD_DEPENDS:=rust/host
+PKG_BUILD_PARALLEL:=1
+
+PKG_BUILD_FLAGS:=no-lto
+RUST_PKG_FEATURES:=resolver,dns-over-https-rustls,dns-over-quic,dns-over-h3,native-certs,dnssec-ring,recursor
+
+include $(INCLUDE_DIR)/package.mk
+include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk
+
+define Package/hickory-dns
+ SECTION:=net
+ CATEGORY:=Network
+ SUBMENU:=IP Addresses and Names
+ TITLE:=A plug-in DNS forwarder/splitter
+ URL:=https://github.com/hickory-dns/hickory-dns
+ DEPENDS:=$(RUST_ARCH_DEPENDS)
+endef
+
+define Build/Compile
+ $(call Build/Compile/Cargo,bin,--no-default-features)
+endef
+
+define Package/hickory-dns/install
+ $(INSTALL_DIR) $(1)/usr/bin/
+ $(INSTALL_BIN) $(PKG_INSTALL_DIR)/bin/* $(1)/usr/bin/
+ $(INSTALL_DIR) $(1)/etc/init.d/
+ $(INSTALL_DIR) $(1)/etc/hickory-dns/
+ $(INSTALL_BIN) ./files/etc/init.d/hickory-dns $(1)/etc/init.d/hickory-dns
+ $(INSTALL_BIN) ./files/etc/hickory-dns/forwarder.toml $(1)/etc/hickory-dns/forwarder.toml
+endef
+
+$(eval $(call RustBinPackage,hickory-dns))
+$(eval $(call BuildPackage,hickory-dns))
diff --git a/hickory-dns/files/etc/hickory-dns/forwarder.toml b/hickory-dns/files/etc/hickory-dns/forwarder.toml
new file mode 100644
index 000000000..69788b9cc
--- /dev/null
+++ b/hickory-dns/files/etc/hickory-dns/forwarder.toml
@@ -0,0 +1,12 @@
+listen_addrs_ipv6 = ["::0"]
+
+[[zones]]
+zone = "."
+
+zone_type = "Forward"
+stores = { type = "forward", name_servers = [
+ { socket_addr = "[2400:3200::1]:443", protocol = "h3", trust_nx_responses = true, tls_dns_name = "dns.alidns.com" },
+ { socket_addr = "[2400:3200:baba::1]:443", protocol = "h3", trust_nx_responses = true, tls_dns_name = "dns.alidns.com" },
+ { socket_addr = "1.12.12.12:443", protocol = "https", trust_nx_responses = true, tls_dns_name = "1.12.12.12" },
+ { socket_addr = "120.53.53.53:443", protocol = "https", trust_nx_responses = true, tls_dns_name = "120.53.53.53" },
+ ], options = { rotate = true, edns0 = true, ip_strategy = "Ipv6thenIpv4", cache_size = 0, use_hosts_file = true, server_ordering_strategy = "QueryStatistics", shuffle_dns_servers = true }}
diff --git a/hickory-dns/files/etc/init.d/hickory-dns b/hickory-dns/files/etc/init.d/hickory-dns
new file mode 100755
index 000000000..56e325e3f
--- /dev/null
+++ b/hickory-dns/files/etc/init.d/hickory-dns
@@ -0,0 +1,18 @@
+#!/bin/sh /etc/rc.common
+
+USE_PROCD=1
+START=99
+
+PROG=/usr/bin/hickory-dns
+CONF=/etc/hickory-dns/forwarder.toml
+
+start_service() {
+ procd_open_instance hickory-dns
+ procd_set_param command $PROG -c $CONF -p 5335
+ procd_set_param env RUST_LOG=error
+ procd_set_param user root
+ procd_set_param stdout 1
+ procd_set_param stderr 1
+ procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}"
+ procd_close_instance hickory-dns
+}
diff --git a/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js
index 86285ed30..410f99845 100644
--- a/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js
+++ b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js
@@ -212,6 +212,7 @@ return view.extend({
o = s.option(widgets.NetworkSelect, 'wan_interfaces', _('WAN Interfaces'));
o.multiple = true;
o.optional = false;
+ o.retain = true;
o.rmempty = false;
o.depends('transparent_proxy', '1');
diff --git a/luci-app-sblite/po/templates/sblite.pot b/luci-app-sblite/po/templates/sblite.pot
new file mode 100644
index 000000000..06042538c
--- /dev/null
+++ b/luci-app-sblite/po/templates/sblite.pot
@@ -0,0 +1,39 @@
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2024-06-19 13:42+0800\n"
+"PO-Revision-Date: 2024-06-19 11:44+0800\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.4.4\n"
+"X-Poedit-Basepath: ../../root\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: www/luci-static/resources/view/sblite/main.js:12
+msgid "Main Settings"
+msgstr ""
+
+#: www/luci-static/resources/view/sblite/main.js:53
+msgid "Enable Server"
+msgstr ""
+
+#: www/luci-static/resources/view/sblite/main.js:55
+msgid "Include Interface"
+msgstr ""
+
+#: www/luci-static/resources/view/sblite/main.js:55
+msgid "A list of interfaces for which the transparent proxy takes effect"
+msgstr ""
+
+#: www/luci-static/resources/view/sblite/main.js:61
+msgid "DNS Listen Port"
+msgstr ""
+
+#: www/luci-static/resources/view/sblite/main.js:61
+msgid "The port number on which the DNS service runs"
+msgstr ""
diff --git a/luci-app-sblite/po/zh-cn b/luci-app-sblite/po/zh-cn
new file mode 120000
index 000000000..8d69574dd
--- /dev/null
+++ b/luci-app-sblite/po/zh-cn
@@ -0,0 +1 @@
+zh_Hans
\ No newline at end of file
diff --git a/luci-app-sblite/po/zh_Hans/sblite.po b/luci-app-sblite/po/zh_Hans/sblite.po
new file mode 100644
index 000000000..a0a89414f
--- /dev/null
+++ b/luci-app-sblite/po/zh_Hans/sblite.po
@@ -0,0 +1,50 @@
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2024-06-19 13:42+0800\n"
+"PO-Revision-Date: 2024-06-19 13:42+0800\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.4.4\n"
+"X-Poedit-Basepath: ../../root\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: www/luci-static/resources/view/sblite/main.js:12
+msgid "Main Settings"
+msgstr "基本设置"
+
+#: www/luci-static/resources/view/sblite/main.js:53
+msgid "Enable Server"
+msgstr "启用服务"
+
+#: www/luci-static/resources/view/sblite/main.js:55
+msgid "Include Interface"
+msgstr "包含的接口"
+
+#: www/luci-static/resources/view/sblite/main.js:55
+msgid "A list of interfaces for which the transparent proxy takes effect"
+msgstr "透明代理生效的内网接口列表"
+
+#: www/luci-static/resources/view/sblite/main.js:61
+#, fuzzy
+#| msgid "DNS Port"
+msgid "DNS Listen Port"
+msgstr "DNS 监听端口"
+
+#: www/luci-static/resources/view/sblite/main.js:61
+msgid "The port number on which the DNS service runs"
+msgstr "DNS 服务运行的端口号"
+
+#~ msgid "TProxy Listen Port"
+#~ msgstr "透明代理监听端口"
+
+#~ msgid "The port number on which the proxy service runs"
+#~ msgstr "透明代理服务运行的端口号"
+
+#~ msgid "sing-box lite"
+#~ msgstr "sing-box lite"
diff --git a/luci-app-sblite/root/usr/share/luci/menu.d/luci-app-sblite.json b/luci-app-sblite/root/usr/share/luci/menu.d/luci-app-sblite.json
new file mode 100644
index 000000000..f2f2e462b
--- /dev/null
+++ b/luci-app-sblite/root/usr/share/luci/menu.d/luci-app-sblite.json
@@ -0,0 +1,18 @@
+{
+ "admin/services/sblite": {
+ "title": "sing-box lite",
+ "order": 60,
+ "action": {
+ "type": "view",
+ "path": "sblite/main"
+ },
+ "depends": {
+ "acl": [
+ "luci-app-sblite"
+ ],
+ "uci": {
+ "sblite": true
+ }
+ }
+ }
+}
diff --git a/luci-app-sblite/root/usr/share/rpcd/acl.d/luci-app-sblite.json b/luci-app-sblite/root/usr/share/rpcd/acl.d/luci-app-sblite.json
new file mode 100644
index 000000000..ef23ffb75
--- /dev/null
+++ b/luci-app-sblite/root/usr/share/rpcd/acl.d/luci-app-sblite.json
@@ -0,0 +1,25 @@
+{
+ "luci-app-sblite": {
+ "description": "Grant access to sblite configuration",
+ "read": {
+ "file": {
+ "/tmp/log/sblite.log": [
+ "read"
+ ]
+ },
+ "ubus": {
+ "luci.sblite": [
+ "*"
+ ]
+ },
+ "uci": [
+ "sblite"
+ ]
+ },
+ "write": {
+ "uci": [
+ "sblite"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/luci-app-sblite/root/usr/share/rpcd/ucode/luci.sblite b/luci-app-sblite/root/usr/share/rpcd/ucode/luci.sblite
new file mode 100644
index 000000000..364be7b7a
--- /dev/null
+++ b/luci-app-sblite/root/usr/share/rpcd/ucode/luci.sblite
@@ -0,0 +1,14 @@
+'use strict';
+
+import * as app from '/usr/share/sblite/export.uc';
+
+const methods = {
+ subscribe: {
+ args: { section_id: 'section_id' },
+ call: function (req) {
+ return app.subscribe(req.args?.section_id);
+ }
+ },
+};
+
+return { 'luci.sblite': methods };
diff --git a/luci-app-sblite/root/www/luci-static/resources/view/sblite/main.js b/luci-app-sblite/root/www/luci-static/resources/view/sblite/main.js
new file mode 100644
index 000000000..0b77125fc
--- /dev/null
+++ b/luci-app-sblite/root/www/luci-static/resources/view/sblite/main.js
@@ -0,0 +1,1149 @@
+'use strict';
+'require dom';
+'require form';
+'require fs';
+'require rpc';
+'require uci';
+'require ui';
+'require network';
+'require view';
+'require validation';
+
+const config_name = 'sblite';
+const ENABLE_CONFIG_NAME = 'enable';
+const REJECT_OUTBOUND_TAG = 'reject';
+const DNS_OUTBOUND_TAG = 'dns-out';
+const DNS_FAKE_IP_TAG = 'fakeip';
+const DNS_BLOCK_TAG = 'block';
+
+const callSubscribe = rpc.declare({
+ object: 'luci.sblite',
+ method: 'subscribe',
+ params: ['section_id'],
+ expect: { '': {} }
+});
+
+const modalStyle = ' height:50%; width:50%; position:absolute; top:20%; left:25%; background-color:#800080; border-radius: 15px;';
+
+function unique_tag(section_type, section_id, value) {
+ let sections = uci.sections(config_name, section_type);
+ for (let s of sections) {
+ if (s['.name'] != section_id && s.tag === value) {
+ return _('Tag conflicts with other sections');
+ }
+ }
+
+ return true;
+}
+
+return view.extend({
+ load: function () {
+ return Promise.all(
+ [
+ async function () {
+ await uci.load(config_name);
+ return;
+ }(),
+
+ async function () {
+ const data = await Promise.all(
+ [
+ network.getNetworks(),
+ network.getWANNetworks(),
+ ]
+ );
+
+ let lanInterfaces = data[0];
+ let wanInterfaceNames = [];
+ let wanInterfaces = [];
+
+ data[1].forEach(element => {
+ wanInterfaceNames.push(element.getIfname());
+ wanInterfaces.push(element);
+ });
+
+ lanInterfaces = lanInterfaces.filter(element => !wanInterfaceNames.includes(element.getIfname()));
+
+ return {
+ wanInterfaces: wanInterfaces,
+ lanInterfaces: lanInterfaces,
+ };
+ }(),
+ ]
+ );
+ },
+
+ render: function (data) {
+ const map = new form.Map(config_name);
+
+ let wanInterfaces = data[1].wanInterfaces;
+ let lanInterfaces = data[1].lanInterfaces;
+
+ let s, o;
+
+ s = map.section(form.NamedSection, 'main', 'sing_box', _('sing-box lite'), '');
+ s.addremove = false;
+
+ const tabName = 'main';
+
+ s.tab(tabName, _('Main Settings'));
+
+ o = s.taboption(tabName, form.Flag, ENABLE_CONFIG_NAME, _('Enable Server'));
+ o.rmempty = false;
+
+ o = s.taboption(tabName, form.Flag, 'log', _('Enable Logger'));
+ o.rmempty = false;
+ o.default = true;
+
+ o = s.taboption(tabName, form.ListValue, 'loglevel', _('Log Level'));
+ o.depends('log', '1');
+ o.rmempty = false;
+ o.value('trace', _('Trace'));
+ o.value('debug', _('Debug'));
+ o.value('info', _('Info'));
+ o.value('warn', _('Warn'));
+ o.value('error', _('Error'));
+ o.value('fatal', _('Fatal'));
+ o.value('panic', _('Panic'));
+ o.default = 'error';
+
+ render_rules(s.taboption(
+ tabName,
+ form.SectionValue,
+ 'route_rule',
+ form.NamedSection,
+ 'route',
+ 'sing_box',
+ _('Route Settings')
+ ).subsection);
+
+ render_access_control_tab(s);
+ render_dns_tab(s);
+ render_outbound_tab(s, wanInterfaces, lanInterfaces);
+ render_nodes_tab(s);
+ render_subscription_tab(s);
+ render_rule_set_tab(s);
+
+ return map.render();
+ },
+});
+
+function render_rules(parent) {
+ let s, o;
+
+ o = parent.option(form.Flag, 'custom_default', _('Custom Default Outbound'), _('The first outbound will be used if not set.'));
+ o = parent.option(form.ListValue, 'final', _('Default Outbound'), _('Default outbound tag.'));
+ o.depends('custom_default', '1');
+ o.value(REJECT_OUTBOUND_TAG, _('Reject'));
+ uci.sections(config_name, 'outbound', s => o.value(s.tag));
+
+ s = parent.option(
+ form.SectionValue,
+ 'route_rule',
+ form.GridSection,
+ 'route_rule',
+ _('Route Rules'))
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+ s.description = E('div', { style: 'color:red' }, _('Please note attention to the priority, the higher the order, the higher the priority.'));
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('Outbound Configuration') + ' » ' + (name ?? _('new rule'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Rule Tag'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => unique_tag('route_rule', section_id, value);
+ }
+
+ o = s.option(form.ListValue, 'outbound', _('Outbound'));
+ o.value(REJECT_OUTBOUND_TAG, _('Reject'));
+ uci.sections(config_name, 'outbound', s => o.value(s.tag));
+
+ o = s.option(form.Flag, 'invert', _('Invert'), _('Invert match result.'));
+ o.rmempty = false;
+
+ const protocol_selections = {
+ 'HTTP': _('QUIC'),
+ 'TLS': _('TLS'),
+ 'QUIC': _('QUIC'),
+ 'STUN': _('STUN'),
+ 'BitTorrent': _('BitTorrent'),
+ };
+
+ o = s.option(form.MultiValue, 'protocol', _('Protocol'));
+ Object.keys(protocol_selections).forEach(key => o.value(key, protocol_selections[key]));
+
+ o = s.option(form.MultiValue, 'rule_set', _('Rule Sets'));
+ uci.sections(config_name, 'rule_set', s => {
+ if (s.sub !== '1' && s.type !== 'headless') {
+ o.value(s.tag);
+ }
+ });
+ };
+
+ o = s.option(form.DummyValue, 'outbound');
+ o.modalonly = false;
+ o.textvalue = section_id => uci.get(config_name, section_id, 'outbound');
+
+ o = s.option(form.DummyValue, 'protocol');
+ o.modalonly = false;
+ o.textvalue = section_id => {
+ const protocols = uci.get(config_name, section_id, 'protocol');
+ if (protocols) {
+ return `${_('Protocol')}: ` + protocols.join(', ');
+ } else {
+ return '';
+ }
+ }
+
+ o = s.option(form.DummyValue, 'rule_set');
+ o.modalonly = false;
+ o.textvalue = section_id => {
+ const rule_sets = uci.get(config_name, section_id, 'rule_set');
+ if (rule_sets) {
+ const text = `${_('Rule Set')}: ` + rule_sets.join(', ');
+ if (uci.get(config_name, section_id, 'invert') === '1') {
+ return E('div', `${_('Not Match')} ${text}`);
+ }
+
+ return text;
+ } else {
+ return '';
+ }
+ }
+}
+
+function render_access_control_tab(parent) {
+ const tabName = 'access_control';
+ let s, o;
+ parent.tab(tabName, _('Access Control'));
+
+ o = parent.taboption(tabName, form.DummyValue, '_description_text', '');
+ o.cfgvalue = function () { return E('div', _('Packets are shunted ahead of time before they enter the sing-box core')); };
+ o.write = function () { };
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'access_control',
+ form.NamedSection,
+ 'access_control',
+ 'sing_box',
+ '')
+ .subsection;
+
+ o = s.option(form.ListValue, 'mode', _('Filter Mode'));
+ o.value('0', _('Disable'));
+ o.value('1', _('Blacklist'));
+ o.value('2', _('Whitelist'));
+
+ o = s.option(form.DynamicList, 'black_ip4addr', _('Blacklist IPv4 Address'));
+ o.rmempty = false;
+ o.depends('mode', '1');
+ o.datatype = 'ip4addr';
+ o = s.option(form.DynamicList, 'black_ip6addr', _('Blacklist IPv6 Address'));
+ o.depends('mode', '1');
+ o.datatype = 'ip6addr';
+ o = s.option(form.DynamicList, 'black_macaddr', _('Blacklist MAC Address'));
+ o.depends('mode', '1');
+ o.datatype = 'macaddr';
+
+ o = s.option(form.DynamicList, 'white_ip4addr', _('Whitelist IPv4 Address'));
+ o.depends('mode', '2');
+ o.datatype = 'ip4addr';
+ o = s.option(form.DynamicList, 'white_ip6addr', _('Whitelist IPv6 Address'));
+ o.depends('mode', '2');
+ o.datatype = 'ip6addr';
+ o = s.option(form.DynamicList, 'white_macaddr', _('Whitelist MAC Address'));
+ o.depends('mode', '2');
+ o.datatype = 'macaddr';
+}
+
+function render_dns_tab(parent) {
+ const strategys = {
+ 'prefer_ipv4': _('Perfer IPv4'),
+ 'prefer_ipv6': _('Perfer IPv6'),
+ 'ipv4_only': _('IPv4 Only'),
+ 'ipv6_only': _('IPv6 Only'),
+ };
+
+ const tabName = 'dns';
+ let s, o;
+
+ parent.tab(tabName, _('DNS Settings'));
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'dns',
+ form.NamedSection,
+ 'dns',
+ 'sing_box',
+ '')
+ .subsection;
+
+ o = s.option(form.Value, 'listen_port', _('Listen Port'), _('The port number on which the DNS service runs'));
+ o.datatype = 'port';
+ o.default = 7535;
+
+ o = s.option(form.Flag, 'fake_ip', _('Use Fake IP'));
+
+ o = s.option(form.Value, 'fake_ip_inet4_range', _('Fake IP IPv4 Address Range'));
+ o.rmempty = false;
+ o.depends('fake_ip', '1');
+ o.datatype = 'ip4addr';
+ o.default = '198.18.0.0/15';
+
+ o = s.option(form.Value, 'fake_ip_inet6_range', _('Fake IP IPv6 Address Range'));
+ o.rmempty = false;
+ o.depends('fake_ip', '1');
+ o.datatype = 'ip6addr';
+ o.default = 'fc00::/18';
+
+ o = s.option(form.Flag, 'custom_default', _('Custom Default DNS'), _('The first dns server will be used if not set.'));
+ o = s.option(form.ListValue, 'final', _('Default DNS'), _('Default dns server tag.'));
+ o.depends('custom_default', '1');
+ uci.sections(config_name, 'dns_server', s => o.value(s.tag));
+
+ o = s.option(form.ListValue, 'strategy', _('Default Strategy'), _('Default domain strategy for resolving the domain names.'));
+ Object.keys(strategys).forEach(key => o.value(key, strategys[key]));
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'dns_servers',
+ form.GridSection,
+ 'dns_server',
+ _('DNS Servers'))
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('DNS Server Configuration') + ' » ' + (name ?? _('new dns server'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Server Tag'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => {
+ if (value !== DNS_FAKE_IP_TAG || value !== DNS_BLOCK_TAG || value.startsWith('DHCP')) {
+ return unique_tag('dns_server', section_id, value);
+ }
+
+ return _(`DNS Server tag couldn\'t be "${value}"`);
+ }
+ }
+
+ o = s.option(form.ListValue, 'proto', _('Protocol'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.value('TCP', _('TCP'));
+ o.value('UDP', _('UDP'));
+ o.value('TLS', _('TLS'));
+ o.value('HTTPS', _('HTTPS'));
+ o.value('QUIC', _('QUIC'));
+ o.value('HTTP3', _('HTTP3')); // validation.types
+ o.default = 'UDP';
+
+ o = s.option(form.Value, 'ip_address', _('Address'), _('IP Address'));
+ o.depends('proto', 'TCP');
+ o.depends('proto', 'UDP');
+ o.depends('proto', undefined);
+ o.depends({ proto: 'TLS', resolver: '0' });
+ o.depends({ proto: 'HTTPS', resolver: '0' });
+ o.depends({ proto: 'QUIC', resolver: '0' });
+ o.depends({ proto: 'HTTP3', resolver: '0' });
+ o.depends({ proto: 'TLS', resolver: undefined });
+ o.depends({ proto: 'HTTPS', resolver: undefined });
+ o.depends({ proto: 'QUIC', resolver: undefined });
+ o.depends({ proto: 'HTTP3', resolver: undefined });
+ o.ucioption = 'address';
+
+ o = s.option(form.Value, 'domain', _('Address'), _('Domain Address'));
+ o.depends({ proto: 'TLS', resolver: '1' });
+ o.depends({ proto: 'HTTPS', resolver: '1' });
+ o.depends({ proto: 'QUIC', resolver: '1' });
+ o.depends({ proto: 'HTTP3', resolver: '1' });
+ o.ucioption = 'address';
+
+ o = s.option(form.ListValue, 'strategy', _('Strategy'), _('Default domain strategy for resolving the domain names.'));
+ o.rmempty = false;
+ Object.keys(strategys).forEach(key => o.value(key, strategys[key]));
+
+ o = s.option(form.Flag, 'custom_detour', _('Custom Outbound'), _('Use custom outbound to the dns server.'));
+ o = s.option(form.ListValue, 'detour', _('Outbound'), _('Tag of an outbound for connecting to the dns server.'));
+ o.depends('custom_detour', '1');
+ o.rmempty = false;
+ uci.sections(config_name, 'outbound', s => o.value(s.tag));
+
+ o = s.option(form.Flag, 'resolver', _('Resolver'), _('Required if address contains domain'));
+ o.rmempty = false;
+ o.depends('proto', 'TLS');
+ o.depends('proto', 'HTTPS');
+ o.depends('proto', 'QUIC');
+ o.depends('proto', 'HTTP3');
+
+ o = s.option(form.ListValue, 'resolver_tag', _('Resolver Tag'), _('Tag of a another server to resolve the domain name in the address.'));
+ o.rmempty = false;
+ o.depends('resolver', '1');
+ o.value(_('DHCP'));
+ uci.sections(config_name, 'dns_server', s => {
+ if (s['.name'] !== section_id) {
+ o.value(s.tag);
+ }
+ });
+
+ o = s.option(form.ListValue, 'resolver_strategy', _('Resolver Strategy'), _('The domain strategy for resolving the domain name in the address.'));
+ o.rmempty = false;
+ o.depends('resolver', '1');
+ Object.keys(strategys).forEach(key => o.value(key, strategys[key]));
+ };
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'dns_rules',
+ form.GridSection,
+ 'dns_rule',
+ _('DNS Rules'))
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('DNS Rule Configuration') + ' » ' + (name ?? _('new dns rule'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Rule Tag'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => unique_tag('dns_server', section_id, value);
+ }
+
+ o = s.option(form.ListValue, 'server', _('Server'));
+ uci.sections(config_name, 'dns_server', s => o.value(s.tag));
+
+ o = s.option(form.MultiValue, 'rule_set', _('Rule Sets'));
+ uci.sections(config_name, 'rule_set', s => {
+ if (s.sub !== '1' && s.type !== 'headless') {
+ o.value(s.tag);
+ }
+ });
+ o.value(DNS_BLOCK_TAG, 'BLOCK');
+ }
+}
+
+function render_outbound_tab(parent, wanInterfaces, lanInterfaces) {
+ const tabName = 'outbound';
+ let s, o;
+
+ parent.tab(tabName, _('Outbound Settings'));
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'outbounds',
+ form.GridSection,
+ 'outbound')
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('Outbound Configuration') + ' » ' + (name ?? _('new outbound'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Outbound Tag'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => {
+ if (value !== 'any' || value !== DNS_OUTBOUND_TAG || value !== REJECT_OUTBOUND_TAG) {
+ return unique_tag('outbound', section_id, value);
+ }
+
+ return _(`Outbound tag couldn\'t be "${value}"`);
+ }
+ }
+
+ o = s.option(form.ListValue, 'type', _('Outbound Type'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.value('direct', _('Direct'));
+ o.value('node', _('Node'));
+ o.value('urltest', _('URLTest'));
+ o.default = 'direct';
+
+ o = s.option(form.ListValue, 'interface', _('Outbound Interface'));
+ o.rmempty = false;
+ o.depends('type', 'direct');
+ wanInterfaces.forEach(element => {
+ let ifname = element.getIfname();
+ o.value(ifname, `${element.sid} (${ifname})`);
+ });
+
+ o = s.option(form.ListValue, 'node', _('Outbound Node'));
+ o.rmempty = false;
+ o.depends('type', 'node');
+ uci.sections(config_name, 'node', (s, section_id) => {
+ let value = section_id;
+ if (s.group) {
+ value = s.hashkey;
+ }
+ o.value(value, s.alias);
+ });
+
+ o = s.option(form.ListValue, 'outbound', _('Outbound Tag'));
+ o.rmempty = false;
+ o.depends('type', 'node');
+ uci.sections(config_name, 'outbound', s => {
+ if (s.type == 'direct') {
+ o.value(s.tag);
+ }
+ });
+
+ o = s.option(form.MultiValue, 'include', _('Include Outbound'), _('List of outbound tags to test.'));
+ o.rmempty = false;
+ o.depends('type', 'urltest');
+ uci.sections(config_name, 'outbound', s => {
+ if (s.type !== 'urltest' && s['.name'] !== section_id) {
+ o.value(s.tag);
+ }
+ });
+
+ o = s.option(form.Value, 'url', _('Test Url'), _('The URL to test.'));
+ o.rmempty = false;
+ o.depends('type', 'urltest');
+ o.value('https://www.gstatic.com/generate_204');
+ o.value('https://www.apple.com/library/test/success.html');
+ o.value('https://connectivitycheck.platform.hicloud.com/generate_204');
+ o.value('https://wifi.vivo.com.cn/generate_204');
+ };
+
+ o = s.option(form.DummyValue, 'outbound_config');
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ const type = uci.get(config_name, section_id, 'type');
+ switch (type) {
+ case 'direct':
+ const ifname = uci.get(config_name, section_id, 'interface');
+ const element = wanInterfaces.find(element => element.getIfname() === ifname);
+ return `${element.sid} (${ifname})`;
+
+ case 'node':
+ let node = uci.get(config_name, section_id, 'node');
+ if (!node.startsWith('cfg')) {
+ uci.sections(config_name, 'node', (s, section_id) => {
+ if (s.group && s.hashkey === node) {
+ node = section_id;
+ }
+ });
+ }
+ return uci.get(config_name, node, 'alias');
+
+ case 'urltest':
+ return _('URLTest') + ': ' + uci.get(config_name, section_id, 'include').join(', ')
+ default: return 'unkown';
+ }
+ };
+}
+
+function render_nodes_tab(parent) {
+ const tabName = 'nodes';
+ let s, o;
+
+ parent.tab(tabName, _('Node List'));
+
+ o = parent.taboption(tabName, form.DummyValue, '_nodes_info', '');
+ o.cfgvalue = function (section_id) {
+ const nodes = uci.sections(config_name, 'node');
+
+ if (nodes && Array.isArray(nodes)) {
+ const groups = nodes.reduce((groups, outbound) => {
+ const key = outbound.group;
+ if (key) {
+ if (!groups[key]) {
+ groups[key] = 0;
+ }
+
+ groups[key]++;
+ }
+
+ return groups;
+ }, {});
+
+ const count = Object.values(groups).reduce((count, group) => count += group, 0);
+ let text = `${_('Manual Add')}: ${nodes.length - count}`;
+ for (let key in groups) {
+ text += ` ${key}: ${groups[key]}`;
+ }
+
+ return text;
+
+ } else {
+ return _('Node List is Empty');
+ }
+ };
+ o.write = function () { };
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'nodes',
+ form.GridSection,
+ 'node',
+ '')
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'alias');
+
+ s.renderRowActions = function (section_id) {
+ let tdEl = this.super('renderRowActions', [section_id, _('Edit')]);
+
+ const group = uci.get(config_name, section_id, 'group');
+
+ if (group !== undefined && group !== null && group !== '') {
+ dom.content(tdEl.lastChild, [
+ E('button', {
+ class: 'btn cbi-button-negative',
+ click: ui.createHandlerFn(this, function (section_id) {
+ uci.unset(config_name, section_id, 'group');
+ uci.unset(config_name, section_id, 'hashkey');
+ return parent.map.save(null, true);
+ }, section_id),
+ title: _('Unbind this node from the subscription'),
+ }, _('Unbind')),
+ ]);
+ }
+ else { }
+
+ return tdEl;
+ };
+
+ o = s.option(form.DummyValue, '_group', _('Subscription Group'));
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ const group_tag = uci.get(config_name, section_id, 'group');
+
+ if (group_tag) {
+ return group_tag;
+ } else {
+ return '';
+ }
+ };
+}
+
+function render_subscription_tab(parent) {
+ const tabName = 'subscription';
+ let s, o;
+
+ parent.tab(tabName, _('Node Subscribe'));
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ '_subscribe',
+ form.NamedSection,
+ 'subscribe',
+ 'sing_box',
+ '').subsection;
+ s.addremove = false;
+ s.anonymous = true;
+
+ const set_filter_mode = function (s, global) {
+ o = s.option(form.ListValue, 'filter_mode', _('Keyword Filter'));
+ o.value(0, _('Close'));
+ o.value(1, _('Discard List'));
+ o.value(2, _('Keep List'));
+ o.value(3, _('Discard List, But Keep List First'));
+ o.value(4, _('Keep List, But Discard List First'));
+
+ if (global) {
+ o.value(5, _('Global Filter'));
+ }
+
+ o = s.option(form.DynamicList, 'blacklist', _('Discard List'));
+ o.rmempty = false;
+ o.datatype = 'list(string)';
+ o.depends('filter_mode', '1');
+ o.depends('filter_mode', '3');
+ o.depends('filter_mode', '4');
+
+ o = s.option(form.DynamicList, 'whitelist', _('Keep List'));
+ o.rmempty = false;
+ o.datatype = 'list(string)';
+ o.depends('filter_mode', '2');
+ o.depends('filter_mode', '3');
+ o.depends('filter_mode', '4');
+ };
+
+ set_filter_mode(s, false);
+
+ o = s.option(form.DummyValue, '_manual_subscribe_button', _('Manual Subscribe'));
+ o.modalonly = false;
+ o.cfgvalue = function (section_id) {
+ return E('button', {
+ class: 'btn cbi-button-positive',
+ click: ui.createHandlerFn(this, function () {
+ const modal = document.getElementById('sblite-modal');
+ modal.style.display = 'flex';
+ }),
+ title: _('Manual Subscribe'),
+ }, _('Manual Subscribe'));
+ };
+ o.write = function () { };
+
+ s = parent.taboption(
+ tabName,
+ form.SectionValue,
+ 'subscribe_list',
+ form.GridSection,
+ 'subscription',
+ '')
+ .subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+ s.description = E('div', { style: 'color:red' }, _('Please input the subscription url first, save and submit before manual subscription.'))
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('Subscription Configuration') + ' » ' + (name ?? _('new subscription'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Subscription Name'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => unique_tag('subscription', section_id, value);
+ }
+
+ o = s.option(form.Value, 'subscribe_url', _('Subscription Url'));
+ o.rmempty = false;
+ o.datatype = 'string';
+
+ o = s.option(form.Flag, 'no_certificate', _('Allow Insecure Connections'), _('Whether or not to allow insecure connections. When checked, certificate verification is skipped.'));
+ o.rmempty = false;
+
+ set_filter_mode(s, true);
+
+ o = s.option(form.Flag, 'auto_subscribe', _('Auto Update Subscription'));
+ o.rmempty = false;
+
+ o = s.option(form.ListValue, 'auto_subscribe_weekly', _('Week update rules'));
+ o.rmempty = false;
+ o.depends('auto_subscribe', '1');
+ o.value(0, _('Every Day'));
+ o.value(1, _('Monday'));
+ o.value(2, _('Tuesday'));
+ o.value(3, _('Wednesday'));
+ o.value(4, _('Thursday'));
+ o.value(5, _('Friday'));
+ o.value(6, _('Saturday'));
+ o.value(7, _('Sunday'));
+
+ o = s.option(form.ListValue, 'auto_subscribe_daily', _('Day update rules'));
+ o.depends('auto_subscribe', '1');
+ for (let i = 0; i < 24; i++) {
+ o.value(i.toString(), `${i}:00`);
+ }
+
+ o = s.option(form.Value, 'ua', _('User-Agent'));
+ o.datatype = 'string';
+ o.value('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0');
+ o.value('sblite/OpenWrt');
+ };
+
+ o = s.option(form.DummyValue, '_subscribe_count', _('Count'));
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ const nodes = uci.sections(config_name, 'node');
+ const tag = uci.get(config_name, section_id, 'tag');
+ if (nodes && Array.isArray(nodes)) {
+ return nodes.filter(section => section.group === tag).length;
+ } else {
+ return 0;
+ }
+ };
+
+ o = s.option(form.Value, 'subscribe_url', _('Subscription Url'));
+ o.editable = true;
+ o.modalonly = false;
+
+ o = s.option(form.DummyValue, 'auto_subscribe', _('Auto Update'));
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ return uci.get(config_name, section_id, 'auto_subscribe') === '1' ? _('Yes') : _('No');
+ };
+
+ o = s.option(form.DummyValue, '_remove_this_subscriptions_button');
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ return E('button', {
+ class: 'btn cbi-button-negative',
+ click: ui.createHandlerFn(this, function (section_id) {
+ const outbounds = uci.sections(config_name, 'node', s => {
+ if (s.group === section_id) {
+ parent.map.data.remove(config_name, section_id);
+ }
+ });
+
+ return parent.map.save(null, true);
+ }, section_id),
+ title: _('Delete Subscribe Nodes'),
+ }, _('Delete Subscribe Nodes'));
+ };
+
+ o = s.option(form.DummyValue, '_manual_subscribe_button');
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ return E('button', {
+ class: 'btn cbi-button-positive',
+ click: ui.createHandlerFn(this, async function () {
+ try {
+ const res = await callSubscribe(section_id);
+ uci.unload(config_name);
+ await uci.load(config_name);
+ const arr = Object.keys(res).map(key => `${res[key].alias}`);
+ if (arr.length > 0) {
+ alert(_('订阅到的节点列表') + '\n' + arr.join('\n'));
+ }
+ else {
+ //alert(_('未获取到任何节点'));
+ }
+ }
+ catch (err) {
+ alert(_('更新订阅出现异常' + '\n' + err));
+ }
+
+ return parent.map.save(null, true);
+ }),
+ title: _('Manual Subscribe'),
+ }, _('Manual Subscribe'));
+ };
+}
+
+function render_rule_set_tab(parent) {
+ const tabName = 'rule_set';
+ let s, o;
+
+ parent.tab(tabName, _('Rule Set'));
+
+ const logical_mode_selections = {
+ '0': _('Close'),
+ '1': _('And Mode'),
+ '2': _('Or Mode'),
+ };
+
+ const network_selections = {
+ '0': _('TCP and UDP'),
+ '1': _('TCP'),
+ '2': _('UDP'),
+ };
+
+ const rule_set_type_selections = {
+ 'inline': _('Inline'),
+ 'headless': _('Headless Rule'),
+ 'local': _('Local File'),
+ 'remote': _('Remote File'),
+ };
+
+ const format_selections = {
+ 'binary': _('Binary Format'),
+ 'source': _('Source Format'),
+ };
+
+ o = parent.taboption(tabName, form.DummyValue, '_description_text', '');
+ o.cfgvalue = function (section_id) {
+ return E('div', `${_('The default rule uses the following matching logic:')}
+ ${_('Network')} && ${_('Protocol')} && ${_('Source IP')} && ${_('Source Port')} &&
+ ${_('Dest IP')} && ${_('Dest Port')} && ${_('Domain List')}`);
+ };
+
+ o.cfgvalue = function () {
+ return E('div', `${_('see document here')}`);
+ };
+ o.write = function () { };
+
+ s = parent.taboption(tabName, form.SectionValue, 'rule_set', form.GridSection, 'rule_set').subsection;
+ s.addremove = true;
+ s.anonymous = true;
+ s.sortable = true;
+
+ s.modaltitle = function (section_id) {
+ let name = uci.get(config_name, section_id, 'tag');
+ return _('Rule Set Configuration') + ' » ' + (name ?? _('new ruleset'));
+ };
+
+ s.sectiontitle = section_id => uci.get(config_name, section_id, 'tag');
+
+ s.addModalOptions = function (s, section_id) {
+ if (!uci.get(config_name, section_id, 'tag')) {
+ o = s.option(form.Value, 'tag', _('Rule Set Tag'));
+ o.rmempty = false;
+ o.datatype = 'string';
+ o.validate = (section_id, value) => unique_tag('rule_set', section_id, value);
+ }
+
+ o = s.option(form.ListValue, 'type', _('Rule Set Type'));
+ Object.keys(rule_set_type_selections).forEach(key => o.value(key, rule_set_type_selections[key]));
+
+ const headless_rule_tags = [];
+
+ uci.sections(config_name, 'rule_set', s => {
+ if (s.type === 'headless' && s.sub !== '1' && s['.name'] !== section_id) {
+ headless_rule_tags.push(s.tag);
+ }
+ });
+
+ if (headless_rule_tags.length > 0) {
+ o = s.option(form.Flag, 'advance', _('Advance Mode'));
+ o.rmempty = false;
+ o.depends('type', 'inline');
+ o.modalonly = true;
+
+ o = s.option(form.MultiValue, 'headless', _('Headless rules'));
+ o.depends({ type: 'inline', advance: '1' });
+ o.modalonly = true;
+ headless_rule_tags.forEach(tag => o.value(tag));
+ } else {
+ uci.set(config_name, section_id, 'advance', null);
+ }
+
+ o = s.option(form.ListValue, 'format', _('Rule Set Format'));
+ o.depends('type', 'local');
+ o.depends('type', 'remote');
+ Object.keys(format_selections).forEach(key => o.value(key, format_selections[key]));
+
+ o = s.option(form.Value, 'path', _('File Path'));
+ o.depends('type', 'local');
+
+ o = s.option(form.Value, 'url', _('Download URL'), _('Download URL of rule-set. Will auto update everyday'));
+ o.depends('type', 'remote');
+
+ o = s.option(form.Flag, 'cutom_detour', _('Custom Download Outbound'), _('Use custom outbound to download rule-set.'));
+ o.depends('type', 'remote');
+
+ o = s.option(form.ListValue, 'download_detour', _('Download Outbound'), _('Tag of the outbound to download rule-set.'));
+ o.depends('cutom_detour', '1');
+ uci.sections(config_name, 'outbound', s => o.value(s.tag));
+
+ o = s.option(form.Flag, 'sub', _('Sub Rule Set'), _('Is this part of another rule set.'));
+ o.depends('type', 'headless');
+ o.rmempty = false;
+
+ o = s.option(form.Flag, 'invert', _('Invert'), _('Invert match result.'));
+ o.depends('type', 'headless');
+ o.depends({ type: 'inline', advance: '0' });
+ o.depends({ type: 'inline', advance: undefined });
+ o.rmempty = false;
+
+ const sub_rule_tags = [];
+ uci.sections(config_name, 'rule_set', s => {
+ if (s.type === 'headless' && s.sub === '1' && s['.name'] !== section_id) {
+ sub_rule_tags.push(s.tag);
+ }
+ });
+
+ if (sub_rule_tags.length > 1) {
+ o = s.option(form.ListValue, 'logical_mode', _('Logical Mode'));
+ o.depends({ sub: '0', type: 'headless' });
+ o.depends({ type: 'inline', advance: '0' });
+ o.depends({ type: 'inline', advance: undefined });
+ Object.keys(logical_mode_selections).forEach(key => o.value(key, logical_mode_selections[key]));
+
+ o = s.option(form.MultiValue, 'sub_rule', _('Sub rules'));
+ o.depends('logical_mode', '1');
+ o.depends('logical_mode', '2');
+ o.modalonly = true;
+ sub_rule_tags.forEach(tag => o.value(tag));
+ } else {
+ uci.set(config_name, section_id, 'logical_mode', 0);
+ }
+
+ function headless_config_depends(o) {
+ o.depends({ logical_mode: '0', type: 'headless' });
+ o.depends({ logical_mode: undefined, type: 'headless' });
+ o.depends({ logical_mode: '0', type: 'inline', advance: '0' });
+ o.depends({ logical_mode: undefined, type: 'inline', advance: '0' });
+ o.depends({ logical_mode: '0', type: 'inline', advance: undefined });
+ o.depends({ logical_mode: undefined, type: 'inline', advance: undefined });
+ }
+
+ o = s.option(form.ListValue, 'network', _('Network'));
+ headless_config_depends(o);
+ Object.keys(network_selections).forEach(key => o.value(key, network_selections[key]));
+
+ o = s.option(form.DynamicList, 'source', _('Source IP'), `${_('Example')}:
- ${_('IP')}: 192.168.1.100
- ${_('IP CIDR')}: 192.168.1.0/24`);
+ headless_config_depends(o)
+ o.datatype = 'ipaddr';
+ o = s.option(form.DynamicList, 'source_port', _('Source Port'), `${_('Example')}:
- ${_('Port')}: 80
- ${_('Range')}: 1000-2000`);
+ headless_config_depends(o)
+ o.datatype = 'portrange';
+
+ o = s.option(form.DynamicList, 'dest', _('Dest IP'), `${_('Example')}:
- ${_('IP')}: 192.168.1.100
- ${_('IP CIDR')}: 192.168.1.0/24`);
+ headless_config_depends(o)
+ o.datatype = 'ipaddr';
+ o = s.option(form.DynamicList, 'dest_port', _('Dest Port'), `${_('Example')}:
- ${_('Port')}: 80
- ${_('Range')}: 1000-2000`);
+ headless_config_depends(o)
+ o.datatype = 'portrange';
+
+ o = s.option(form.TextValue, 'domain', _('Domain List'));
+ headless_config_depends(o)
+ o.description = `${_('Each line is parsed as a rule')}:
+ - ${_('Start with #')}: ${_('Comments')}
+ - ${_('Start with domain')}: ${_('Match full domain')}
+ - ${_('Start with suffix')}: ${_('Match domain suffix')}
+ - ${_('Start with keyword')}: ${_('Match domain using keyword')}
+ - ${_('Start with regex')}: ${_('Match domain using regular expression')}`;
+
+ o.rows = 10;
+ o.wrap = true;
+ o.validate = function (section_id, value) {
+ const lines = value.split(/[(\r\n)\r\n]+/);
+
+ for (let line of lines) {
+ line = line.trim();
+
+ if (!line) {
+ continue;
+ }
+
+ if (line.startsWith('#') ||
+ line.startsWith('domain:') ||
+ line.startsWith('suffix:') ||
+ line.startsWith('keyword:') ||
+ line.startsWith('regex:')) {
+ continue;
+ }
+
+ return `${_('Parse Error')}: ${line}`;
+ }
+
+ return true;
+ };
+ };
+ o = s.option(form.DummyValue, '_rule_set_view');
+ o.modalonly = false;
+ o.textvalue = function (section_id) {
+ const description = [];
+
+ const type = uci.get(config_name, section_id, 'type');
+
+ description.push(
+ E('br'),
+ E('b', `${_('Rule Set Type')}: `),
+ `${rule_set_type_selections[type]}`,
+ );
+
+ if (type !== 'inline' && type !== 'headless') {
+ description.push(
+ E('br'),
+ E('b', `${_('Rule Set Format')}: `),
+ `${format_selections[uci.get(config_name, section_id, 'format')]}`,
+ );
+ }
+
+ if (type === 'local') {
+ description.push(
+ E('br'),
+ E('b', `${_('File Path')}: `),
+ uci.get(config_name, section_id, 'path'),
+ );
+ } else if (type === 'remote') {
+ description.push(
+ E('br'),
+ E('b', `${_('Download Url')}: `),
+ uci.get(config_name, section_id, 'url'),
+ );
+ } else if (type === 'headless' || (type === 'inline' && uci.get(config_name, section_id, 'advance') !== '1')) {
+ if (type === 'headless') {
+ description.push(
+ E('br'),
+ E('b', `${_('Sub Rule Set')}: `),
+ `${uci.get(config_name, section_id, 'sub') === '1' ? _('Yes') : _('No')}`,
+ );
+ }
+
+ const logical_mode = uci.get(config_name, section_id, 'logical_mode');
+
+ if (logical_mode && logical_mode !== '0') {
+ description.push(
+ E('br'),
+ E('b', `${_('Logical Mode')}: `),
+ logical_mode_selections[logical_mode],
+ );
+
+ description.push(
+ E('br'),
+ E('b', `${_('Sub rules')}: `),
+ uci.get(config_name, section_id, 'sub_rule').join(', '),
+ );
+ } else {
+ const network = uci.get(config_name, section_id, 'network') ?? '0';
+ const protocols = uci.get(config_name, section_id, 'protocol');
+
+ description.push(
+ E('br'),
+ E('b', `${_('Network')}: `),
+ network_selections[network],
+ );
+
+ if (protocols) {
+ description.push(
+ E('br'),
+ E('b', `${_('Protocol')}: `),
+ protocols.join(', '),
+ );
+ }
+ }
+
+ description.push(
+ E('br'),
+ E('b', `${_('Invert')}: `),
+ `${uci.get(config_name, section_id, 'invert') === '1' ? _('Yes') : _('No')}`,
+ );
+ } else if (type === 'inline') {
+ description.push(
+ E('br'),
+ E('b', `${_('Headless rules')}: `),
+ uci.get(config_name, section_id, 'headless').join(', '),
+ );
+ }
+
+ return E('div', description);
+ };
+}
diff --git a/mihomo/Makefile b/mihomo/Makefile
index 55e76160b..41dfb9ee2 100644
--- a/mihomo/Makefile
+++ b/mihomo/Makefile
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo
PKG_VERSION:=1.18.6
-PKG_RELEASE:=17
+PKG_RELEASE:=18
PKG_BUILD_TIME=$(shell date -u -Iseconds)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
diff --git a/mihomo/files/mihomo.init b/mihomo/files/mihomo.init
index ff7ddc70a..d4a495d77 100644
--- a/mihomo/files/mihomo.init
+++ b/mihomo/files/mihomo.init
@@ -214,7 +214,7 @@ start_service() {
yq -M -i 'del(.tun)' "$run_profile_path"
# test profile
log "Profile testing..."
- if (/usr/bin/mihomo -d "$run_dir" -t > /dev/null 2>&1); then
+ if (/usr/bin/mihomo -d "$run_dir" -t >> "$run_core_log_path" 2>&1); then
log "Profile test passed!"
else
log "Profile test failed! Exiting..."
diff --git a/naiveproxy/Makefile b/naiveproxy/Makefile
index 96a429765..3222fa02c 100644
--- a/naiveproxy/Makefile
+++ b/naiveproxy/Makefile
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=naiveproxy
-PKG_VERSION:=122.0.6261.43-1
-PKG_RELEASE:=100
+PKG_VERSION:=125.0.6422.35-1
+PKG_RELEASE:=101
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/klzgrad/naiveproxy/tar.gz/v$(PKG_VERSION)?
@@ -20,17 +20,6 @@ PKG_BUILD_DEPENDS:=gn/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
-ifeq ($(strip $(NINJA)),)
-ifneq ($(wildcard $(TOPDIR)/feeds/packages/devel/ninja/ninja.mk),)
-PKG_BUILD_DEPENDS+=ninja/host
-NINJA = \
- MAKEFLAGS="$(MAKE_JOBSERVER)" \
- $(STAGING_DIR_HOSTPKG)/bin/ninja \
- $(if $(findstring c,$(OPENWRT_VERBOSE)),-v) \
- $(if $(MAKE_JOBSERVER),,-j1)
-endif
-endif
-
ifneq ($(CONFIG_CPU_TYPE)," ")
CPU_TYPE:=$(word 1, $(subst +," ,$(CONFIG_CPU_TYPE)))
CPU_SUBTYPE:=$(word 2, $(subst +, ",$(CONFIG_CPU_TYPE)))
@@ -66,7 +55,7 @@ ifneq ($(CONFIG_CCACHE),)
export naive_ccache_flags=cc_wrapper="$(CCACHE)"
endif
-CLANG_VER:=18-init-16072-gc4146121e940-5
+CLANG_VER:=19-init-8091-gab037c4f-1
CLANG_FILE:=clang-llvmorg-$(CLANG_VER).tgz
define Download/CLANG
URL:=https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64
@@ -75,7 +64,7 @@ define Download/CLANG
HASH:=skip
endef
-PGO_VER:=6261-1707846690-1391fcc4772c0b31e214f533af5cafa87e4ccf40
+PGO_VER:=6422-1715102072-9bdbfa29f2bb1ff28f0f031b98501a1193b8d03b-13cfbf145656b369f9c23bff70ab2fb07e1e2fdb
PGO_FILE:=chrome-linux-$(PGO_VER).profdata
define Download/PGO_PROF
URL:=https://storage.googleapis.com/chromium-optimization-profiles/pgo_profiles
diff --git a/naiveproxy/src/init_env.sh b/naiveproxy/src/init_env.sh
index e32b7faa9..166bead99 100755
--- a/naiveproxy/src/init_env.sh
+++ b/naiveproxy/src/init_env.sh
@@ -55,6 +55,7 @@ use_gio=false
use_gtk=false
use_platform_icu_alternatives=true
use_glib=false
+enable_js_protobuf=false
disable_file_support=true
enable_websockets=false
diff --git a/sblite/root/etc/hotplug.d/iface/99-sblite b/sblite/root/etc/hotplug.d/iface/99-sblite
new file mode 100644
index 000000000..bca4a589f
--- /dev/null
+++ b/sblite/root/etc/hotplug.d/iface/99-sblite
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+[ "$ACTION" = "ifup" -o "$ACTION" = "ifupdate" ] && [ $(uci get sblite.main.enable) = "1" ] && {
+ # if "$DEVICE" in outbounds restart sblite
+}
\ No newline at end of file
diff --git a/sblite/root/etc/init.d/sblite b/sblite/root/etc/init.d/sblite
new file mode 100644
index 000000000..165d523ce
--- /dev/null
+++ b/sblite/root/etc/init.d/sblite
@@ -0,0 +1,23 @@
+#!/bin/sh /etc/rc.common
+
+USE_PROCD=1
+
+START=99
+STOP=15
+
+CONFIG=sblite
+$APP_FILE=/usr/share/${CONFIG}/app.sh
+
+start_service() {
+ procd_open_instance $CONFIG
+ procd_set_param command $APP_FILE start && sing-box run -c /tmp/sblite/config.json
+ procd_set_param user root
+ procd_set_param limits core="unlimited"
+ procd_set_param limits nofile="1000000 1000000"
+ procd_set_param stdout 1
+ procd_set_param stderr 1
+
+ procd_set_param pidfile /var/run/${CONFIG}.pid
+
+ procd_close_instance
+}
diff --git a/sblite/root/etc/uci-defaults/sblite b/sblite/root/etc/uci-defaults/sblite
new file mode 100644
index 000000000..18d3ebc11
--- /dev/null
+++ b/sblite/root/etc/uci-defaults/sblite
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+config_name="sblite"
+
+touch /etc/config/$config_name
+
+section_type="sing_box"
+uci set $config_name.main=$section_type
+uci set $config_name.access_control=$section_type
+uci set $config_name.route=$section_type
+uci set $config_name.dns=$section_type
+uci set $config_name.subscribe=$section_type
+
+uci commit
+
+/etc/init.d/rpcd reload
+exit 0
diff --git a/sblite/root/usr/share/sblite/app.sh b/sblite/root/usr/share/sblite/app.sh
new file mode 100644
index 000000000..a8b942ea0
--- /dev/null
+++ b/sblite/root/usr/share/sblite/app.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+WORKDIR="$(cd "$(dirname "$0")" && pwd)"
+APP_FILE="$WORKDIR/app.uc"
+
+method=$1
+shift
+case "$method" in
+ start)
+ rm -rf /tmp/sblite
+ mkdir -p /tmp/sblite
+ mkdir -p /tmp/sblite/rule_sets
+ ucode -D action=start $APP_FILE
+ if [ -e '/tmp/sblite/config.json' ]; then
+ sing-box format -w -c /tmp/sblite/config.json
+ return 0
+ fi
+ return -1
+ ;;
+ subscribe) ucode -D action=subscribe -D params="$1" $APP_FILE ;;
+ stop)
+ rm -rf /tmp/sblite
+ ucode -D action=stop $APP_FILE
+ ;;
+ *) ;;
+esac
diff --git a/sblite/root/usr/share/sblite/app.uc b/sblite/root/usr/share/sblite/app.uc
new file mode 100644
index 000000000..5373a4551
--- /dev/null
+++ b/sblite/root/usr/share/sblite/app.uc
@@ -0,0 +1,100 @@
+'use strict';
+
+import { cursor } from 'uci';
+import { CONF_NAME } from './const.uc';
+import { log_t, log_tab } from './utils.uc';
+import { start as start_crontab, stop as stop_crontab } from './cron.uc';
+import { subscribe } from './subscribe.uc';
+import { SingBoxOption } from './singbox.uc';
+import * as LOGLEVEL from './loglevel.uc';
+import { Outbound } from './outbound.uc';
+import { RuleSet } from './rule.uc';
+import { Route } from './route.uc';
+import { DNS } from './dns.uc';
+
+function start() {
+ const uci = cursor();
+
+ // purne node config
+ uci.foreach(CONF_NAME, 'node', section => {
+ const group = section.group;
+ if (group == null || group == '') {
+ uci.set(CONF_NAME, section['.name'], 'hashkey', null);
+ }
+ });
+
+ uci.commit();
+
+ const enable = uci.get(CONF_NAME, 'main', 'enable') == '1';
+
+ if (enable) {
+ log_t('starting...');
+ start_crontab();
+ const outbounds = Outbound(uci);
+ const rule_sets = RuleSet(uci, outbounds);
+ const config = SingBoxOption();
+ if (uci.get(CONF_NAME, 'main', 'loglevel') != '0') {
+ let level;
+ switch (uci.get(CONF_NAME, 'main', 'log')) {
+ case 'trace': level = LOGLEVEL.TRACE; break;
+ case 'debug': level = LOGLEVEL.DEBUG; break;
+ case 'info': level = LOGLEVEL.INFO; break;
+ case 'warn': level = LOGLEVEL.WARN; break;
+ case 'error': level = LOGLEVEL.ERROR; break;
+ case 'fatal': level = LOGLEVEL.FATAL; break;
+ case 'panic': level = LOGLEVEL.PANIC; break;
+ default: level = LOGLEVEL.ERROR; break;
+ }
+ config.logOption(true, level);
+ } else {
+ config.logOption(false);
+ }
+ config.logOption(true, LOGLEVEL.ERROR);
+ config.cacheFileOption(true);
+ // 出站
+ config.outbounds = values(outbounds);
+ const inbounds = {
+ redirect_tcp: {
+ type: 'redirect',
+ tag: 'redirect_tcp',
+ listen: '::',
+ listen_port: 18008,
+ sniff: true,
+ sniff_override_destination: true,
+ },
+ tproxy_udp: {
+ type: 'tproxy',
+ tag: 'tproxy_udp',
+ network: 'udp',
+ listen: '::',
+ listen_port: 18008,
+ sniff: true,
+ sniff_override_destination: true,
+ },
+ };
+ config.dns = DNS(uci, rule_sets, outbounds);
+ // 然后是重头戏,路由
+ config.route = Route(uci, rule_sets, outbounds);
+ config.inbounds = values(inbounds);
+ config.write();
+
+ log_t('Done.');
+ } else {
+ stop_crontab();
+ }
+}
+
+switch (action) {
+ case 'start':
+ start();
+ break;
+ case 'stop':
+ const uci = cursor();
+ uci.set(CONF_NAME, 'main', 'enable', '0');
+ uci.commit();
+ start();
+ break;
+ case 'subscribe':
+ subscribe(params);
+ break;
+}
diff --git a/sblite/root/usr/share/sblite/const.uc b/sblite/root/usr/share/sblite/const.uc
new file mode 100644
index 000000000..460b2387f
--- /dev/null
+++ b/sblite/root/usr/share/sblite/const.uc
@@ -0,0 +1,19 @@
+'use strict';
+
+export const APP_FILE = '/usr/share/sblite/app.sh';
+export const PKG_NAME = 'sing-box lite';
+export const CONF_NAME = 'sblite';
+
+export const LOG_PATH = '/tmp/sblite/singbox.log';
+export const DB_PATH = '/tmp/sblite/singbox.db';
+export const CONFIG_PATH = '/tmp/sblite/config.json';
+
+export const TCP_IN_TAG = 'tcp-in';
+export const UDP_IN_TAG = 'udp-in';
+export const DNS_IN_TAG = 'dns-in';
+
+export const FAKE_IP_TAG = 'fake_ip';
+
+export const REJECT_OUTBOUND_TAG = 'reject';
+export const DNS_OUTBOUND_TAG = 'dns-out';
+export const DNS_BLOCK_TAG = 'block';
diff --git a/sblite/root/usr/share/sblite/cron.uc b/sblite/root/usr/share/sblite/cron.uc
new file mode 100644
index 000000000..643e869b6
--- /dev/null
+++ b/sblite/root/usr/share/sblite/cron.uc
@@ -0,0 +1,37 @@
+'use strict';
+
+import { cursor } from 'uci';
+import { APP_FILE, CONF_NAME } from './const.uc';
+
+function clean() {
+ system('touch /etc/crontabs/root');
+ system(`sed -i "/sh ${replace(APP_FILE, '/', '\\/')} subscribe cfg.* > \\/dev\\/null 2>&1 &/d" /etc/crontabs/root`);
+}
+
+export function start() {
+ clean();
+
+ const uci = cursor();
+
+ uci.foreach(CONF_NAME, 'subscription',
+ function (section) {
+ if (section.auto_subscribe == '1') {
+ let day = section.auto_subscribe_daily;
+ let week = section.auto_subscribe_weekly;
+ if(week == '0') {
+ week = '*';
+ }
+
+ const command =`echo "0 ${day} * * ${week} sh ${APP_FILE} subscribe ${section['.name']} > /dev/null 2>&1 &" >> /etc/crontabs/root`;
+ system(command);
+ }
+ }
+ );
+
+ system('/etc/init.d/cron restart');
+};
+
+export function stop() {
+ clean();
+ system('/etc/init.d/cron restart');
+};
diff --git a/sblite/root/usr/share/sblite/default.uc b/sblite/root/usr/share/sblite/default.uc
new file mode 100644
index 000000000..626b9ca2b
--- /dev/null
+++ b/sblite/root/usr/share/sblite/default.uc
@@ -0,0 +1,4 @@
+'use strict';
+
+export const CLASH_HOST = "127.0.0.1";
+export const CLASH_PORT = 9090;
diff --git a/sblite/root/usr/share/sblite/dns.uc b/sblite/root/usr/share/sblite/dns.uc
new file mode 100644
index 000000000..a72b3343f
--- /dev/null
+++ b/sblite/root/usr/share/sblite/dns.uc
@@ -0,0 +1,155 @@
+'use strict';
+
+import { log_tab, asip, asport } from './utils.uc';
+import { CONF_NAME, DNS_BLOCK_TAG, FAKE_IP_TAG } from './const.uc';
+
+export const PREFER_IPV4 = 'prefer_ipv4';
+export const PREFER_IPV6 = 'prefer_ipv6';
+export const ONLY_IPV4 = 'ipv4_only';
+export const ONLY_IPV6 = 'ipv6_only';
+
+export function DNS(uci, rule_sets, outbounds) {
+ const result = {
+ final: '',
+ strategy: '',
+ disable_cache: false,
+ disable_expire: false,
+ independent_cache: false,
+ reverse_mapping: false,
+ };
+
+ const servers = {};
+
+ uci.foreach(CONF_NAME, 'dns_server', section => {
+ let strategy = section.strategy;
+
+ if (strategy != PREFER_IPV4 && strategy != PREFER_IPV6 && strategy != ONLY_IPV4 && strategy != ONLY_IPV6) {
+ log_tab('[DNS Server %s] Unkown strategy(%s), fallback to %s', section.tag, strategy, PREFER_IPV4);
+ strategy = PREFER_IPV4;
+ }
+
+ const server = {
+ tag: section.tag,
+ address: section.address,
+ strategy: strategy,
+ };
+
+ if (section.custom_detour == '1') {
+ const detour = section.detour;
+
+ if (outbounds[detour]) {
+ server.detour = detour;
+ } else {
+ log_tab('[DNS Server %s] Unkown outbound tag(%s)', detour);
+ }
+ }
+
+ if (section.resolver == '1') {
+ const tag = section.resolver_tag;
+
+ if (outbounds[tag]) {
+ server.address_resolver = tag;
+
+ let strategy = section.resolver_strategy;
+
+ if (strategy != PREFER_IPV4 && strategy != PREFER_IPV6 && strategy != ONLY_IPV4 && strategy != ONLY_IPV6) {
+ log_tab('[DNS Server %s] Unkown resolver strategy(%s), fallback to %s', section.tag, strategy, PREFER_IPV4);
+ strategy = PREFER_IPV4;
+ }
+
+ server.address_strategy = strategy;
+ } else {
+ log_tab('[DNS Server %s] Unkown resolver tag(%s)', tag);
+ }
+ }
+
+ servers[section.tag] = server;
+ });
+
+ servers[DNS_BLOCK_TAG] = {
+ address: 'rcode://success',
+ tag: DNS_BLOCK_TAG,
+ };
+
+ let strategy = uci.get(CONF_NAME, 'dns', 'strategy');
+
+ if (strategy != PREFER_IPV4 && strategy != PREFER_IPV6 && strategy != ONLY_IPV4 && strategy != ONLY_IPV6) {
+ log_tab('[DNS Setting] Unkown strategy(%s), fallback to %s', strategy, PREFER_IPV4);
+ strategy = PREFER_IPV4;
+ }
+
+ result.strategy = strategy;
+
+ if (uci.get(CONF_NAME, 'dns', 'custom_default') == 1) {
+ const final = uci.get(CONF_NAME, 'dns', 'final');
+ if (servers[final]) {
+ result.final = final;
+ } else {
+ log_tab('[DNS Setting] Unkown custom defualt dns tag(%s)', final);
+ }
+ }
+
+ const rules = [];
+ uci.foreach(CONF_NAME, 'dns_rule', section => {
+ const server = section.server;
+
+ if (servers[server]) {
+ if (section.rule_set && length(section.rule_set) > 0) {
+ const current_rule_sets = [];
+ for (let rule_set in section.rule_set) {
+ if (rule_sets[rule_set]) {
+ push(current_rule_sets, rule_set);
+ } else {
+ log_tab('[DNS Rule %s] Unkown rule set tag(%s)', rule_set);
+ }
+ }
+
+ if (length(current_rule_sets) > 0) {
+ push(rules, {
+ rule_set: current_rule_sets,
+ server: server,
+ });
+ }
+ }
+ } else {
+ log_tab('[DNS Rule %s] Unkown dns server tag(%s)', server);
+ }
+ });
+
+ if (uci.get(CONF_NAME, 'dns', 'fake_ip') == '1') {
+ const fake_ip_inet4_range = uci.get(CONF_NAME, 'dns', 'fake_ip_inet4_range');
+ const fake_ip_inet6_range = uci.get(CONF_NAME, 'dns', 'fake_ip_inet4_range');
+ const v4 = asip(fake_ip_inet4_range);
+ const v6 = asip(fake_ip_inet6_range);
+
+ if (v4 && v4.version == 4 && v4.cidr != 32) {
+
+ if (v6 && v6.version == 6 && v6.cidr != 128) {
+ servers[FAKE_IP_TAG] = {
+ tag: FAKE_IP_TAG,
+ adress: 'fakeip',
+ };
+
+ push(rules, {
+ query_type: ['A', 'AAAA'],
+ server: FAKE_IP_TAG,
+ });
+
+ result.fakeip = {
+ enabled: true,
+ inet4_range: `${v4.address}/${v4.cidr}`,
+ inet6_range: `${v6.address}/${v6.cidr}`,
+ };
+ } else {
+ log_tab('[DNS Setting] Invalid fake ip setting(%s)', fake_ip_inet6_range);
+ }
+ } else {
+ log_tab('[DNS Setting] Invalid fake ip setting(%s)', fake_ip_inet4_range);
+ }
+ }
+
+ result.servers = values(servers);
+ result.rules = rules;
+
+ return result;
+};
diff --git a/sblite/root/usr/share/sblite/export.uc b/sblite/root/usr/share/sblite/export.uc
new file mode 100644
index 000000000..b4b85282c
--- /dev/null
+++ b/sblite/root/usr/share/sblite/export.uc
@@ -0,0 +1,9 @@
+'use strict';
+
+import * as CONST from './const.uc';
+import * as Subscribe from './subscribe.uc';
+import * as Utils from './utils.uc';
+
+export const CONF_NAME = CONST.CONF_NAME;
+export const subscribe = Subscribe.subscribe;
+export const clear_log = Utils.clear_log;
diff --git a/sblite/root/usr/share/sblite/loglevel.uc b/sblite/root/usr/share/sblite/loglevel.uc
new file mode 100644
index 000000000..b2d9b29f8
--- /dev/null
+++ b/sblite/root/usr/share/sblite/loglevel.uc
@@ -0,0 +1,9 @@
+'use strict';
+
+export const TRACE = 'trace';
+export const DEBUG = 'debug';
+export const INFO = 'info';
+export const WARN = 'warn';
+export const ERROR = 'error';
+export const FATAL = 'fatal';
+export const PANIC = 'panic';
\ No newline at end of file
diff --git a/sblite/root/usr/share/sblite/outbound.uc b/sblite/root/usr/share/sblite/outbound.uc
new file mode 100644
index 000000000..22b1a2fb8
--- /dev/null
+++ b/sblite/root/usr/share/sblite/outbound.uc
@@ -0,0 +1,111 @@
+'use strict';
+
+import { log_tab } from './utils.uc';
+import { CONF_NAME, REJECT_OUTBOUND_TAG, DNS_OUTBOUND_TAG } from './const.uc';
+import { TYPE as vmess_type, outbound as vmess_outbound } from './protocols/vmess.uc';
+
+export function Outbound(uci) {
+ const result = {};
+
+ // 先找 Direct
+ uci.foreach(CONF_NAME, 'outbound', section => {
+ if (section.type == 'direct') {
+ if (result[section.tag]) {
+ log_tab('Duplicate outbound tag(%s)', section.tag);
+ } else {
+ result[section.tag] = {
+ type: 'direct',
+ tag: section.tag,
+ bind_interface: section.interface,
+ };
+ }
+ }
+ });
+
+ const nodes = {};
+ // 再找 Node
+ uci.foreach(CONF_NAME, 'node', section => {
+ if (section.hashkey) {
+ nodes[section.hashkey] = section;
+ }
+ });
+
+ uci.foreach(CONF_NAME, 'outbound', section => {
+ if (section.type == 'node') {
+ if (result[section.tag]) {
+ log_tab('Duplicate outbound tag(%s)', section.tag);
+ } else {
+ const node = nodes[section.node];
+ const detour = section.outbound;
+ if (!result[detour]) {
+ log_tab('There is no direct outbound(%s)', detour);
+ return;
+ }
+
+ if (result[detour].type != 'direct') {
+ log_tab('The outbound(%s) is not direct', detour);
+ return;
+ }
+
+ if (node) {
+ switch (node.type) {
+ case vmess_type:
+ result[section.tag] = vmess_outbound(node);
+ break;
+ default:
+ log_tab('Unkown protocol(%s) in outbound node(%s)', node.type, section.tag);
+ return;
+ }
+
+ result[section.tag].tag = section.tag;
+ result[section.tag].detour = detour;
+ } else {
+ log_tab('There is no node with hashkey(%s)', section.node);
+ }
+ }
+ }
+ });
+
+ // 然后找 urltest
+ uci.foreach(CONF_NAME, 'outbound', section => {
+ if (section.type == 'urltest') {
+ if (result[section.tag]) {
+ log_tab('Duplicate outbound tag(%s)', section.tag);
+ } else {
+ result[section.tag] = {
+ type: 'urltest',
+ tag: section.tag,
+ outbounds: [],
+ url: section.url,
+ };
+
+ for (let tag in section.include) {
+ const outbound = result[tag];
+
+ if (!outbound) {
+ log_tab('There is no outbound(%s)', tag);
+ return;
+ } else if (outbound.type == 'urltest') {
+ log_tab('The outbound(%s) is urltest, should not be used in another urltest outbound.', tag);
+ return;
+ }
+
+ push(result[section.tag].outbounds, tag);
+ }
+ }
+ }
+ });
+
+ // 最后添加 BLOCK 出站和 DNS 出站
+ result[REJECT_OUTBOUND_TAG] = {
+ type: 'block',
+ tag: REJECT_OUTBOUND_TAG,
+ };
+
+ result[DNS_OUTBOUND_TAG] = {
+ type: 'dns',
+ tag: DNS_OUTBOUND_TAG,
+ };
+
+ return result;
+};
diff --git a/sblite/root/usr/share/sblite/protocols/vmess.uc b/sblite/root/usr/share/sblite/protocols/vmess.uc
new file mode 100644
index 000000000..c81b36bcc
--- /dev/null
+++ b/sblite/root/usr/share/sblite/protocols/vmess.uc
@@ -0,0 +1,62 @@
+'use strict';
+
+import { md5, log_t } from '../utils.uc';
+
+// see: https://github.com/jarryson/singbox-subscribe/blob/main/parsers/vmess.py
+
+export const TYPE = 'vmess';
+
+export function parse(content, result) {
+ const matches = match(content, /vmess:\/\/(.*)/);
+
+ if (matches) {
+ let payload = matches[1];
+
+ const decode = b64dec(payload);
+
+ if (decode != null) {
+ payload = decode;
+ }
+
+ if (payload) {
+ const info = json(payload);
+
+ if (info) {
+ result.type = TYPE;
+ result.server = info['add'];
+ result.server_port = int(info['port']);
+ result.uuid = info['id'];
+ result.alter_id = info['aid'] ?? '0';
+ result.security = info['scy'] ?? 'auto';
+ if(result.security != 'http') {
+ result.security = 'auto';
+ }
+
+ result.alias = sprintf('[%s] %s', result.group, info['ps']);
+ result.hashkey = md5(b64enc(sprintf('[%s] %s://%s:%s?id=%s',
+ result.group,
+ result.type,
+ result.server,
+ result.server_port,
+ result.uuid)));
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+
+export function outbound(section) {
+ return {
+ type: TYPE,
+ tag: section.tag,
+ server: section.server,
+ server_port: int(section.server_port),
+ uuid: section.uuid,
+ security: section.security,
+ alter_id: int(section.alter_id),
+ packet_encoding: 'xudp',
+ };
+};
diff --git a/sblite/root/usr/share/sblite/route.uc b/sblite/root/usr/share/sblite/route.uc
new file mode 100644
index 000000000..50ee967b9
--- /dev/null
+++ b/sblite/root/usr/share/sblite/route.uc
@@ -0,0 +1,67 @@
+'use strict';
+
+import { CONF_NAME } from './const.uc';
+import { log_tab, delete_empty_arr } from './utils.uc';
+
+export function Route(uci, rule_sets, outbounds) {
+ const result = {
+ rules: [],
+ rule_set: [],
+ };
+
+ for (let rule_set in values(rule_sets)) {
+ push(result.rule_set, rule_set);
+ }
+
+ const route_section = uci.get_all(CONF_NAME, 'route');
+
+ if (route_section.custom_default == '1') {
+ const final_tag = route_section.final;
+
+ if (final_tag && rule_sets[final_tag]) {
+ result.final = final_tag;
+ }
+ }
+
+ uci.foreach(CONF_NAME, 'route_rule', section => {
+ const rule = {
+ invert: section.invert == '1',
+ };
+ const outbound = rule.outbound = section.outbound;
+
+ if (!outbound || !outbounds[outbound]) {
+ log_tab('[Route Rule %s] There is no outbound(%s)', section.tag, outbound);
+ return;
+ }
+
+ if (section.protocol && length(section.protocol) > 0) {
+ rule.protocol = [];
+ for (let protocol in section.protocol) {
+ if (index(['HTTP', 'TLS', 'QUIC', 'STUN', 'BitTorrent'], protocol) > 0) {
+ push(rule.protocol, protocol);
+ } else {
+ log_tab('[Route Rule %s] There is no protocol(%s)', section.tag, protocol);
+ }
+ }
+
+ delete_empty_arr(rule, 'protocol');
+ }
+
+ if (section.rule_set && length(section.rule_set) > 0) {
+ rule.rule_set = [];
+ for (let rule_set in section.rule_set) {
+ if (rule_sets[rule_set]) {
+ push(rule.rule_set, rule_set);
+ } else {
+ log_tab('[Route Rule %s] There is no rule_set(%s)', section.tag, rule_set);
+ }
+ }
+
+ delete_empty_arr(rule, 'rule_set');
+ }
+
+ push(result.rules, rule);
+ });
+
+ return result;
+};
diff --git a/sblite/root/usr/share/sblite/rule.uc b/sblite/root/usr/share/sblite/rule.uc
new file mode 100644
index 000000000..b8ed02ab4
--- /dev/null
+++ b/sblite/root/usr/share/sblite/rule.uc
@@ -0,0 +1,278 @@
+'use strict';
+
+import { log_tab, asip, asport, delete_empty_arr, call_system_command } from './utils.uc';
+import { CONF_NAME } from './const.uc';
+import { open as fopen } from 'fs';
+
+function config_ip_and_port(section, section_ip, section_port, config, config_ip, config_port, config_port_range) {
+ if (section[section_ip]) {
+ config[config_ip] = [];
+
+ for (let line in section[section_ip]) {
+ const ip = asip(line);
+ if (ip) {
+ push(config[config_ip], `${ip.address}/${ip.cidr}`);
+ } else {
+ log_tab('Unkown ip (%s) setting in %s', line, section.tag);
+ }
+ }
+
+ if (length(config[config_ip]) == 0) {
+ delete config[config_ip];
+ }
+ }
+
+ if (section[section_port]) {
+ config[config_port] = [];
+ config[config_port_range] = [];
+
+ for (let line in section[section_port]) {
+ const port = asport(line);
+ if (port) {
+ if (port.range) {
+ push(config[config_port_range], `${port.start}:${port.end}`);
+ } else {
+ push(config[config_port], port.value);
+ }
+
+ } else {
+ log_tab('Unkown port (%s) setting in %s', line, section.tag);
+ }
+ }
+
+ delete_empty_arr(config, config_port);
+ delete_empty_arr(config, config_port_range);
+ }
+}
+
+function rule(section) {
+ const result = { invert: section.invert == '1' };
+
+ switch (section.network) {
+ case null:
+ case '0': result.network = ['tcp', 'udp']; break;
+ case '1': result.network = ['tcp']; break;
+ case '2': result.network = ['udp']; break;
+ default:
+ log_tab('Unkown rule_set network (%s) setting in %s', section.network, section.tag);
+ result.network = ['tcp', 'udp']; break;
+ }
+
+ config_ip_and_port(section, 'source', 'source_port', result, 'source_ip_cidr', 'source_port', 'source_port_range');
+ config_ip_and_port(section, 'dest', 'dest_port', result, 'ip_cidr', 'port', 'port_range');
+
+ if (section.domain) {
+ result.domain = [];
+ result.domain_suffix = [];
+ result.domain_keyword = [];
+ result.domain_regex = [];
+
+ const lines = split(section.domain, /[(\r\n)\r\n]+/);
+
+ for (let line in lines) {
+ line = trim(line);
+ let match_result;
+ if (match_result = match(line, /#.*/)) {
+ continue;
+ } else if (match_result = match(line, /domain: *(.+)/)) {
+ push(result.domain, trim(match_result[1]));
+ } else if (match_result = match(line, /suffix: *(.+)/)) {
+ push(result.domain_suffix, trim(match_result[1]));
+ } else if (match_result = match(line, /keyword: *(.+)/)) {
+ push(result.domain_keyword, trim(match_result[1]));
+ } else if (match_result = match(line, /regex: *(.+)/)) {
+ push(result.domain_regex, trim(match_result[1]));
+ } else {
+ log_tab('Unkown rule_set domain (%s) setting in %s', line, section.tag);
+ }
+ }
+
+ delete_empty_arr(result, 'domain');
+ delete_empty_arr(result, 'domain_suffix');
+ delete_empty_arr(result, 'domain_keyword');
+ delete_empty_arr(result, 'domain_regex');
+ }
+
+ return result;
+}
+
+function headless(section, sub_rules) {
+ if (section.logical_mode && section.logical_mode != '0') {
+ const headless = {
+ type: 'logical',
+ invert: section.invert == '1',
+ rules: []
+ };
+ if (section.logical_mode == '1') {
+ headless.mode = 'and';
+ } else if (section.logical_mode == '2') {
+ headless.mode = 'or';
+ } else {
+ log_tab('[Rule Set %s] Unkown rule set logical_mode %s', section.tag, section.logical_mode);
+ return;
+ }
+
+ if (section.sub_rule) {
+ for (let sub_rule_tag in section.sub_rule) {
+ const sub_rule = sub_rules[sub_rule_tag];
+ if (sub_rule) {
+ push(headless.rules, sub_rule);
+ } else {
+ log_tab('[Rule Set %s] Unkown sub rule set tag %s', section.tag, sub_rule_tag);
+ }
+ }
+
+ if (length(headless.rules) > 0) {
+ return headless;
+ }
+ }
+ } else {
+ const headless = rule(section);
+ return headless;
+ }
+}
+
+function rule_set(section, headless_rules, sub_rules, outbounds) {
+ switch (section.type) {
+ case 'headless':
+ return;
+ case 'inline':
+ const inline = {
+ type: section.type,
+ tag: section.tag,
+ rules: [],
+ };
+
+ if (section.advance != '1') {
+ const rule = headless(section, sub_rules);
+ push(inline.rules, rule);
+ } else {
+ for (let tag in section.headless) {
+ const rule = headless_rules[tag];
+ if (rule) {
+ push(inline.rules, rule);
+ } else {
+ log_tab('[Rule Set %s] Unkown headless rule set tag %s', section.tag, tag);
+ }
+ }
+ }
+
+ if (length(inline.rules) > 0) {
+ const raw = `/tmp/sblite/rule_sets/${section['.name']}.json`;
+ const srs = `/tmp/sblite/rule_sets/${section['.name']}.srs`;
+
+ const fp = fopen(raw, 'w');
+
+ if (fp) {
+ fp.write({
+ version: 1,
+ rules: inline.rules,
+ });
+ fp.write('\n');
+ fp.close();
+ } else {
+ log_tab('[Rule Set %s] write headless rule to %s failed', section.tag, raw);
+ return;
+ }
+
+ const compile = call_system_command(`sing-box rule-set compile --output ${srs} ${raw}`);
+
+ if(compile && compile != '') {
+ log_tab('[Rule Set %s] compile headless rule failed\n %s', section.tag, compile);
+ return;
+ }
+
+ //return inline;
+
+ return {
+ type: 'local',
+ tag: section.tag,
+ format: 'binary',
+ path: srs,
+ };
+ }
+ return;
+ case 'local':
+ const local = {
+ type: section.type,
+ tag: section.tag,
+ };
+ if (section.format == 'binary' || section.format == 'source') {
+ local.format = section.format;
+ } else {
+ log_tab('[Rule Set %s] Unkown rule set format %s', section.tag, section.format);
+ return;
+ }
+
+ if (section.path) {
+ local.path = section.path;
+ } else {
+ log_tab('[Rule Set %s] Local rule set path undefined', section.tag);
+ return;
+ }
+
+ return local;
+ case 'remote':
+ const remote = {
+ type: section.type,
+ tag: section.tag,
+ };
+
+ if (section.format == 'binary' || section.format == 'source') {
+ remote.format = section.format;
+ } else {
+ log_tab('[Rule Set %s] Unkown rule set format %s', section.tag, section.format);
+ return;
+ }
+
+ if (section.url) {
+ remote.url = section.url;
+ } else {
+ log_tab('[Rule Set %s] Remote rule set url undefined', section.tag);
+ return;
+ }
+
+ if (section.cutom_detour == '1' && section.download_detour) {
+ if (outbounds[section.download_detour]) {
+ remote.download_detour = section.download_detour;
+ } else {
+ log_tab('[Rule Set %s] There is no outbound(%s)', section.tag, section.download_detour);
+ return;
+ }
+ }
+
+ return remote;
+ default:
+ log_tab('[Rule Set %s] Unkown rule set type %s', section.tag, section.type);
+ return;
+ }
+}
+
+export function RuleSet(uci, outbounds) {
+ const sub_rules = {}, headless_rules = {}, rule_sets = {};
+
+ uci.foreach(CONF_NAME, 'rule_set', section => {
+ if (section.type == 'headless' && section.sub == '1') {
+ sub_rules[section.tag] = rule(section);
+ }
+ });
+
+ uci.foreach(CONF_NAME, 'rule_set', section => {
+ if (section.type == 'headless' && section.sub != '1') {
+ headless_rules[section.tag] = headless(section, sub_rules);
+ }
+ });
+
+ uci.foreach(CONF_NAME, 'rule_set', section => {
+ if (section.type == 'headless') {
+ return;
+ }
+
+ const ruleSet = rule_set(section, headless_rules, sub_rules, outbounds);
+ if (ruleSet) {
+ rule_sets[section.tag] = ruleSet;
+ }
+ });
+
+ return rule_sets;
+};
diff --git a/sblite/root/usr/share/sblite/singbox.uc b/sblite/root/usr/share/sblite/singbox.uc
new file mode 100644
index 000000000..c6b701e74
--- /dev/null
+++ b/sblite/root/usr/share/sblite/singbox.uc
@@ -0,0 +1,122 @@
+'use strict';
+
+import { popen, open as fopen } from 'fs';
+import { LOG_PATH, DB_PATH, CONFIG_PATH } from './const.uc';
+import { CLASH_PORT } from './default.uc';
+import { call_system_command, log_t } from './utils.uc';
+
+function version() {
+ let result = {
+ version: 'unkown',
+ features: {},
+ };
+
+ const handle = popen('/usr/bin/sing-box version');
+
+ if (handle) {
+ for (let line = handle.read('line'); length(line); line = handle.read('line')) {
+ line = trim(line);
+
+ let tags = match(line, /Tags: (.*)/);
+ if (tags) {
+ for (let i in split(tags[1], ',')) {
+ result.features[i] = true;
+ }
+
+ continue;
+ }
+
+ let version = match(line, /sing-box version v(.*)/);
+ if (version) {
+ result.version = split(version[1], ' ')[0];
+ }
+ }
+
+ handle.close();
+ }
+
+ return result;
+}
+
+export function SingBoxOption() {
+ return proto({
+ log: {},
+ dns: {},
+ inbounds: [],
+ outbounds: [],
+ route: {},
+ experimental: {},
+ }, {
+ version: version(),
+
+ logOption: function (enable, level) {
+ if (enable) {
+ this.log.timestamp = true;
+ this.log.level = level;
+ this.log.disabled = false;
+ this.log.output = LOG_PATH;
+ } else {
+ this.log.disabled = true;
+ delete this.log.level;
+ delete this.log.output;
+ delete this.log.timestamp;
+ }
+ },
+ cacheFileOption: function (enable) {
+ if (enable) {
+ this.experimental.cache_file = {
+ enabled: true,
+ path: DB_PATH,
+ store_fakeip: true,
+ };
+ } else {
+ this.experimental.cache_file = {
+ enabled: false,
+ };
+ }
+ },
+
+ clashOption: function (option) {
+ // 必须传入 option 的同时 singbox 编译附带了 clash api 选项才能配置
+ if (!option || !this.version.features.with_clash_api) {
+ delete this.experimental.clash_api;
+
+ return;
+ }
+
+ let port = int(option.port);
+
+ if (port == 'NaN' || port < 0 || port > 65535) {
+ port = CLASH_PORT;
+ }
+
+ let host = option.host;
+ let ui = option.ui;
+ let download_url = option.download_url;
+ let download_detour = option.download_detour;
+ let secret = option.secret;
+ let default_mode = option.default_mode;
+
+ this.experimental.clash_api = {
+ external_controller: host + ':' + port,
+ external_ui: ui,
+ external_ui_download_url: download_url,
+ external_ui_download_detour: download_detour,
+ secret: secret,
+ default_mode: default_mode,
+ };
+
+ return;
+ },
+
+ write: function () {
+ const fp = fopen(CONFIG_PATH, 'w');
+
+ if (fp) {
+ fp.write(this);
+ fp.write('\n');
+ fp.close();
+ }
+ }
+ });
+};
diff --git a/sblite/root/usr/share/sblite/subscribe.uc b/sblite/root/usr/share/sblite/subscribe.uc
new file mode 100644
index 000000000..5ff212d3f
--- /dev/null
+++ b/sblite/root/usr/share/sblite/subscribe.uc
@@ -0,0 +1,211 @@
+'use strict';
+
+import { cursor } from 'uci';
+import { PKG_NAME, CONF_NAME } from './const.uc';
+import { wget, log_t, log_tab } from './utils.uc';
+import { parse as vmess_parse } from './protocols/vmess.uc';
+
+function filter(name, mode) {
+ switch (mode.mode) {
+ case '1': // 按关键字丢弃
+ return !match(name, mode.excludes);
+ case '2': // 按关键字保留
+ return match(name, mode.includes);
+ case '3': // 按关键字丢弃未匹配成功保留列表的项
+ return match(name, mode.includes) || !match(name, mode.excludes);
+ case '4': // 按关键字保留未匹配成功丢弃列表的项
+ return !match(name, mode.excludes) && match(name, mode.includes);
+ default:
+ case '0': // 不过滤
+ return true;
+ }
+}
+
+function get_filter_mode(mode, includes, excludes) {
+ const result = {
+ mode: mode,
+ };
+
+ if (result.mode && result.mode != '0') {
+ if (result.mode == '1' ||
+ result.mode == '3' ||
+ result.mode == '4' &&
+ excludes &&
+ length(excludes) > 0) {
+ excludes = map(excludes, str => replace(str, /[\\.*+?^$|\[(){}]/g, '\\$&'));
+ result.excludes = regexp(join('|', excludes));
+ }
+
+ if (result.mode == '2' ||
+ result.mode == '3' ||
+ result.mode == '4' &&
+ includes &&
+ length(includes) > 0) {
+ includes = map(includes, str => replace(str, /[\\.*+?^$|\[(){}]/g, '\\$&'));
+ result.includes = regexp(join('|', includes));
+ }
+
+ if (result.mode >= '5') {
+ result.mode = '0';
+ }
+ }
+
+ if (result.mode == '3' && !result.includes) {
+ result.mode = '1';
+ }
+
+ if (result.mode == '4' && !result.excludes) {
+ result.mode = '2';
+ }
+
+ if (result.mode == '1' && !result.excludes) {
+ result.mode = '0';
+ }
+
+ if (result.mode == '2' && !result.includes) {
+ result.mode = '0';
+ }
+
+ return result;
+}
+
+function subscribe_one(section, filter_mode, results) {
+ if (!results) {
+ results = {};
+ }
+
+ const group = section.tag;
+ const url = section.subscribe_url;
+ const no_certificate = section.no_certificate;
+ const ua = section.ua;
+
+ log_t('[%s] start...', group);
+ log_tab('Filter mode %J', filter_mode);
+
+ const handle = wget(url, no_certificate, ua);
+
+ if (handle) {
+ let content = handle.read('all');
+
+ const decode = b64dec(content);
+
+ if (decode != null) {
+ content = decode;
+ }
+
+ if (content) {
+ const lines = split(content, /[\r\n]/g);
+
+ for (let line in lines) {
+ line = trim(line);
+
+ if (line != '') {
+ let result = {
+ group: group,
+ };
+
+ if (vmess_parse(line, result)) {
+ //
+ } else {
+ continue;
+ }
+
+ if (filter(result.alias, filter_mode)) {
+ log_tab('Get %s Hash: %s', result.alias, result.hashkey);
+ results[result.hashkey] = result;
+ } else {
+ log_tab('Discard %s', result.alias);
+ }
+ }
+ }
+ }
+
+ log_t('[%s] done.', group);
+ handle.close();
+ }
+ else {
+ log_t('[%s] error.', group);
+ }
+
+ return results;
+}
+
+export function subscribe(section_id) {
+ const uci = cursor();
+ let results = {};
+
+ const subscribe_section = uci.get_all(CONF_NAME, 'subscribe');
+
+
+ const filter_mode = get_filter_mode(
+ subscribe_section.filter_mode,
+ subscribe_section.whitelist,
+ subscribe_section.blacklist
+ );
+
+ log_tab('Global filter mode %J', filter_mode);
+
+ if (section_id) {
+ const section = uci.get_all(CONF_NAME, section_id);
+ if (section) {
+ section_id = section.tag;
+
+ if (section.filter_mode == '5') {
+ results = subscribe_one(section, filter_mode);
+ } else {
+ results = subscribe_one(section, get_filter_mode(
+ section.filter_mode,
+ section.whitelist,
+ section.blacklist
+ ));
+ }
+ } else {
+ log_t('can\'t get subscribe config %s...', section_id);
+ }
+
+ } else {
+ log_t('subscribe starting...');
+ uci.foreach(CONF_NAME, 'subscription',
+ function (section) {
+ if (section.filter_mode == '5') {
+ results = subscribe_one(section, filter_mode, results);
+ } else {
+ results = subscribe_one(section, get_filter_mode(
+ section.filter_mode,
+ section.whitelist,
+ section.blacklist
+ ), results);
+ }
+ }
+ );
+ log_t('All done.');
+ }
+
+ uci.foreach(CONF_NAME, 'node', section => {
+ const group = section.group;
+ if (group != null && group != '') {
+ if (section_id && group != section_id) {
+ return;
+ }
+
+ uci.delete(CONF_NAME, section['.name']);
+ }
+ });
+
+ for (let hashkey in keys(results)) {
+ const result = results[hashkey];
+ const sid = uci.add(CONF_NAME, 'node');
+ for (let option in keys(result)) {
+ uci.set(CONF_NAME, sid, option, result[option]);
+ }
+ }
+
+ print(results);
+
+ if (uci.commit(CONF_NAME)) {
+ return results;
+ }
+ else {
+ die('commit failed!');
+ }
+};
diff --git a/sblite/root/usr/share/sblite/utils.uc b/sblite/root/usr/share/sblite/utils.uc
new file mode 100644
index 000000000..c64124820
--- /dev/null
+++ b/sblite/root/usr/share/sblite/utils.uc
@@ -0,0 +1,155 @@
+'use strict';
+
+import { popen, open as fopen } from 'fs';
+
+const LOG_FILE = '/tmp/log/sblite.log';
+
+export function call_system_command(command) {
+ const handle = popen(command);
+ if (handle) {
+ let content = handle.read('all');
+ handle.close();
+ content = trim(content);
+
+ if (content != null) {
+ return content;
+ }
+ }
+};
+
+// return the content of wget output
+export function wget(url, ua, no_certificate) {
+ let addition_parameters = '';
+
+ if (ua) {
+ addition_parameters += ('--user-agent="' + ua + '" ');
+ }
+
+ if (no_certificate) {
+ addition_parameters += ('--no-check-certificate ');
+ }
+
+ return popen('wget -q ' + addition_parameters + '-t 3 -T 10 -O- ' + url);
+};
+
+// return the md5 value of str
+export function md5(str) {
+ for (let i = 0; i < 3; i++) {
+ const handle = popen('echo -n "' + str + '" | md5sum');
+ if (handle) {
+ let content = handle.read(' ');
+ handle.close();
+ content = trim(content);
+
+ if (content != null) {
+ return content;
+ }
+ }
+ }
+};
+
+export function parse_port(port) {
+ port = int(port);
+
+ if (port <= 0 || port >= 65535) {
+ port = 'NaN';
+ }
+
+ return port;
+};
+
+export function get_loopback(ipv6) {
+ let loopback = '127.0.0.1';
+ if (ipv6) {
+ loopback = '::1';
+ }
+
+ return loopback;
+};
+
+export function delete_empty_arr(obj, arr_name) {
+ if (length(obj[arr_name]) == 0) {
+ delete obj[arr_name];
+ }
+};
+
+/**
+ * 将字符串转化为 ip
+ * @returns {Object} ip
+ * @returns {number} ip.version
+ * @returns {string} ip.address
+ * @returns {number} ip.cidr
+ */
+export function asip(str) {
+ const result = match(str, /^([0-9a-fA-F:.]+)(\/([0-9]+))?$/);
+ if (result) {
+ const ip = iptoarr(result[1]);
+
+ if (ip) {
+ let cidr = (result[3] == null ? null : int(result[3]));
+
+ if (length(ip) == 4) {
+ cidr ??= 32;
+
+ if (cidr <= 32) {
+ return { version: 4, address: arrtoip(ip), cidr: cidr };
+ }
+ } else if (length(ip) == 16) {
+ cidr ??= 128;
+
+ if (cidr <= 128) {
+ return { version: 6, address: arrtoip(ip), cidr: cidr };
+ }
+ }
+ }
+ }
+
+ return null;
+};
+
+export function asport(str) {
+ const result = match(str, /^([0-9]+)(-([0-9]+))?$/);
+ if (result) {
+ const port0 = int(result[1]);
+
+ if (port0 >= 0 && port0 <= 65535) {
+ const port1 = result[3] == null ? null : int(result[3]);
+
+ if (port1) {
+ if (port1 > port0 && port1 <= 65535) {
+ return { range: true, start: port0, end: port1 };
+ }
+
+ } else {
+ return { range: false, value: port0 };
+ }
+ }
+ }
+
+ return null;
+};
+
+function log(fmt, ...args) {
+ const fp = fopen(LOG_FILE, 'a');
+
+ if (fp) {
+ fp.write(sprintf(fmt, ...args));
+ fp.write('\n');
+ fp.close();
+ }
+};
+
+export function log_t(fmt, ...args) {
+ let s = sprintf(fmt, ...args);
+ const now = localtime(time());
+
+ log('[%04d-%02d-%02d %02d:%02d:%02d] %s', now.year, now.mon, now.mday, now.hour, now.min, now.sec, s);
+};
+
+export function log_tab(fmt, ...args) {
+ log(' %s', sprintf(fmt, ...args));
+};
+
+export function clearlog() {
+ system('echo "" > ' + LOG_FILE);
+};
diff --git a/sms-tool/Makefile b/sms-tool/Makefile
index c764e38df..79273aaf6 100644
--- a/sms-tool/Makefile
+++ b/sms-tool/Makefile
@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=sms-tool
-PKG_RELEASE:=22
+PKG_RELEASE:=23
PKG_SOURCE_URL:=https://github.com/obsy/sms_tool
PKG_SOURCE_PROTO:=git
PKG_SOURCE_DATE:=2022-03-21
-PKG_SOURCE_VERSION:=1b6ca03284fd65db8799dbf7c6224210093786b0
+PKG_SOURCE_VERSION:=fce2b931c8d749c28b8281363950e963c98324eb
PKG_MIRROR_HASH:=skip
include $(INCLUDE_DIR)/package.mk