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