Skip to content

Commit 5567051

Browse files
committed
feat(overwrite): isolate DNS from subscription configs
1 parent cac4108 commit 5567051

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

overwrite/README.md

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
| 模块 | 主要作用 | 影响范围 | 是否需要参数 |
3535
| --- | --- | --- | :---: |
3636
| [`Prevent_DNS_Leak.conf`](./Prevent_DNS_Leak.conf) | 综合降低 DNS 泄漏风险 | DNS、规则、最终策略及专用策略组 | 可选 |
37+
| [`Use_LuCI_DNS_Only.conf`](./Use_LuCI_DNS_Only.conf) | 隔离机场 YAML 的 DNS 设置,以 OpenClash LuCI 为 DNS 来源 | `dns` 与顶层 `hosts` ||
3738
| [`Block_Encrypted_DNS.conf`](./Block_Encrypted_DNS.conf) | 阻断常见 DoH、DoT、DoQ 绕过 | Rule Provider 与前置阻断规则 ||
3839
| [`Add_No_Resolve.conf`](./Add_No_Resolve.conf) | 为目标 IP 类规则补充 `no-resolve` | `rules``sub-rules` ||
3940
| [`Add_Custom_Direct_Rules.conf`](./Add_Custom_Direct_Rules.conf) | 为其他规则方案附加本项目的域名与 IP 直连规则 | Rule Provider 与前置直连规则 ||
@@ -46,6 +47,7 @@
4647
### 按需求选择
4748

4849
- 想系统降低 DNS 泄漏风险:使用 `Prevent_DNS_Leak.conf`
50+
- 直接引用机场 YAML,希望机场 DNS 不进入最终运行配置:使用 `Use_LuCI_DNS_Only.conf`
4951
- 只想阻止终端使用常见加密 DNS 绕过本地 DNS:使用 `Block_Encrypted_DNS.conf`
5052
- 只需要给 IP 类规则补充 `no-resolve`:使用 `Add_No_Resolve.conf`
5153
- 正在使用其他规则方案,只想附加本项目的域名与 IP 直连规则:使用 `Add_Custom_Direct_Rules.conf`
@@ -61,7 +63,7 @@
6163
1. 进入「服务」→「OpenClash」运行状态页。
6264
2. 点击页面顶部的「覆写模块」按钮,打开覆写编辑器。
6365
3. 点击模块卡片栏中的「+」,选择「Subscribe」新建远程模块。
64-
4. 填写便于识别的模块名称,并从本文复制 testingcf 或 GitHub Raw 订阅地址。
66+
4. 填写便于识别的模块名称,并从本文复制 jsDelivr CDN 或 GitHub Raw 订阅地址。
6567
5. 将匹配配置文件设置为 `all` 或当前配置文件。不要留空,否则模块不会生效。
6668
6. 仅在模块说明要求时填写 `EN_KEY` 参数。
6769
7. 添加并启用模块,然后保存设置。
@@ -71,7 +73,7 @@
7173
不同 OpenClash 版本的菜单名称可能略有差异。
7274

7375
> [!TIP]
74-
> testingcf 与 GitHub Raw 指向同一个仓库文件,只需要选择其中一个。testingcf 通常更适合 GitHub Raw 访问质量不佳的网络环境。
76+
> jsDelivr CDN 与 GitHub Raw 指向同一个仓库文件,只需要选择其中一个。jsDelivr CDN 通常更适合 GitHub Raw 访问质量不佳的网络环境。
7577
7678
<!-- -->
7779

@@ -80,6 +82,64 @@
8082
8183
## 📦 模块说明
8284

85+
### 🧱 Use LuCI DNS Only
86+
87+
[`Use_LuCI_DNS_Only.conf`](./Use_LuCI_DNS_Only.conf) 面向直接引用机场或服务商 YAML 的用户。模块在 OpenClash 完成 `yml_change.sh``yml_rules_change.sh` 后运行,从同一次启动生成的 LuCI DNS 临时片段、UCI 开关和 OpenClash 自定义 DNS 文件原子重建 `dns` 与顶层 `hosts`
88+
89+
模块不会先删除整个 `dns` 再让 Mihomo 使用默认值,也不会内置或选择任何 DNS 上游。模块只保留 OpenClash 已根据 LuCI 生成的基础字段,并重新读取以下来源:
90+
91+
- `/tmp/yaml_config.namedns.yaml``falldns``defaultdns``proxynamedns``directnamedns` 片段;
92+
- LuCI 中的 DNS、运行模式、Fake-IP 与大陆绕过开关;
93+
- 已启用的 Nameserver Policy、Proxy Server Nameserver Policy、Fallback Filter、Fake-IP Filter 和 Hosts 自定义文件;
94+
- OpenClash 根据路由器主机名生成的内置 Hosts。
95+
96+
机场 YAML 中未被 LuCI 明确配置的 `fallback``default-nameserver``proxy-server-nameserver``direct-nameserver``nameserver-policy``proxy-server-nameserver-policy``fallback-filter`、Fake-IP DNS 字段及顶层 `hosts` 不会进入重建结果。
97+
98+
前置条件:
99+
100+
1. 进入「服务」→「OpenClash」→「覆写设置」→「DNS 设置」。
101+
2. 启用「自定义上游 DNS 服务器」。
102+
3. 至少启用一个分组为 `nameserver` 的 DNS 服务器。
103+
4. Fake-IP 模式下,在 LuCI 明确设置 Fake-IP IPv4 范围。不要保留 `0``0` 会让 OpenClash 从机场 YAML 读取 `dns.fake-ip-range`
104+
5. Fake-IP 模式同时启用大陆 IPv4 或 IPv6 绕过时,启用「自定义 Fake-IP-Filter」并明确选择模式。
105+
6. 启用 `respect-rules`、代理节点 DNS 策略,或为全部 `nameserver` 指定代理组时,在 LuCI 至少配置一个不经代理组转发的 `proxy-server-nameserver`
106+
107+
不满足前置条件时,模块在 OpenClash 日志中写入 `Use LuCI DNS Only refused` 并拒绝重建。此时机场 DNS 仍可能保留,不能继续使用该运行配置作为验收结果。
108+
109+
jsDelivr CDN:
110+
111+
```text
112+
https://cdn.jsdelivr.net/gh/Aethersailor/Custom_OpenClash_Rules@main/overwrite/Use_LuCI_DNS_Only.conf
113+
```
114+
115+
GitHub Raw:
116+
117+
```text
118+
https://raw.githubusercontent.com/Aethersailor/Custom_OpenClash_Rules/main/overwrite/Use_LuCI_DNS_Only.conf
119+
```
120+
121+
最终运行配置验收:
122+
123+
1. 保存 LuCI DNS 设置并重新应用配置。
124+
2. 在 OpenClash 配置管理中查看「运行时」配置,或检查 `/etc/openclash/<当前配置文件名>`。不要只检查 `/etc/openclash/config/` 下的原始订阅文件。
125+
3. 确认日志存在独立的 `[Tip] Use LuCI DNS Only rebuilt DNS from OpenClash settings` 条目,且不存在 `[Error] Use LuCI DNS Only refused`、Ruby 或 YAML 错误。不要把「加载覆写脚本」日志中回显的模块源码误判为执行结果。
126+
4. 对照 LuCI 检查 `dns`:LuCI 未配置 `fallback` 时,最终配置不得存在机场 `fallback`;LuCI 已配置时,值必须与 LuCI 生成片段一致。
127+
5. 同样检查 `default-nameserver``proxy-server-nameserver``direct-nameserver`、两类 Policy、`fallback-filter`、Fake-IP 字段和顶层 `hosts`,确认没有机场标记值。
128+
6. 使用设备当前的 Mihomo 核心加载最终文件。核心加载成功后,再检查 OpenClash 运行状态与 DNS 解析。
129+
130+
限制:
131+
132+
- 模块依赖 OpenClash 当前 `yml_change.sh` 的临时文件名与 UCI 结构。升级 OpenClash 后,必须重新核对源码、日志和最终运行配置。
133+
- 使用域名形式的 DoH 或 DoT 上游时,仍需在 LuCI 配置可用的 `default-nameserver`;模块不会补充通用 DNS。
134+
- 模块只处理 Mihomo 配置内的 DNS 与 Hosts,不替代 Dnsmasq、OpenWrt DHCP、终端私有 DNS 或防火墙配置。
135+
- 模块不会修改节点、代理集合、策略组、规则或现有 Rule Provider。OpenClash 因大陆绕过自动创建的 `oc-cn-domain` Provider 仍由插件管理。
136+
- 更晚执行的 DNS 覆写仍可修改重建结果;`openclash_custom_overwrite.sh` 在远程模块之后执行,也能再次改写 `dns``hosts`
137+
138+
> [!CAUTION]
139+
> 本模块必须是最后执行的 DNS 覆写模块。覆写模块的 `order` 值越大越先执行,因此应确保没有更晚执行的模块修改 `dns``hosts`。不要与第三方完整 DNS 覆写方案组合。
140+
141+
---
142+
83143
### 🛡️ Prevent DNS Leak
84144

85145
[`Prevent_DNS_Leak.conf`](./Prevent_DNS_Leak.conf) 是本项目自行维护且影响范围最大的模块,用于通过 DNS 劫持、DNS 上游规则跟随、`no-resolve` 以及将最终规则指向代理目标等方式降低 DNS 泄漏风险。
@@ -455,6 +515,9 @@ https://raw.githubusercontent.com/Aethersailor/Custom_OpenClash_Rules/main/overw
455515
| 组合 | 建议 |
456516
| --- | --- |
457517
| `Prevent_DNS_Leak.conf` 与 `Block_Encrypted_DNS.conf` | ✅ 可组合,分别处理 OpenClash 内部 DNS 路由和终端常见加密 DNS |
518+
| `Use_LuCI_DNS_Only.conf` 与 `Block_Encrypted_DNS.conf` | ✅ 可以组合;后者只修改规则与 Rule Provider |
519+
| `Use_LuCI_DNS_Only.conf` 与 `Prevent_DNS_Leak.conf` | ❌ 不建议组合;两者都会修改最终 DNS,且后者还会改写规则和代理策略 |
520+
| `Use_LuCI_DNS_Only.conf` 与其他 DNS/Hosts 覆写 | ❌ 不要组合;更晚执行的模块会破坏 LuCI 唯一来源保证 |
458521
| `Prevent_DNS_Leak.conf` 与 `Add_No_Resolve.conf` | ❌ 不需要组合,前者已包含 `no-resolve` 处理 |
459522
| `Add_Custom_Direct_Rules.conf` 与 `Add_No_Resolve.conf` | ✅ 可以组合;本模块的 IP 规则已带 `no-resolve`,后者可继续处理其他 IP 类规则 |
460523
| `Replace_China_MRS_With_GeoSite.conf` 与 `Set_China_IP_Route_URL.conf` | ✅ 可以组合,分别修改 Fake-IP Filter 和 Chnroute 数据源 |
@@ -485,7 +548,7 @@ https://raw.githubusercontent.com/Aethersailor/Custom_OpenClash_Rules/main/overw
485548
模块没有生效时,依次检查:
486549

487550
1. 模块是否已经启用;
488-
2. testingcf 或 GitHub Raw 地址是否能够正常下载;
551+
2. jsDelivr CDN 或 GitHub Raw 地址是否能够正常下载;
489552
3. 是否已保存设置并重新应用配置;
490553
4. `EN_KEY` 参数格式是否正确;
491554
5. OpenClash 是否支持模块使用的 `[General]`、`[YAML]`、`[Overwrite]` 或 `ruby_edit`;

overwrite/Use_LuCI_DNS_Only.conf

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[Overwrite]
2+
# Name: Use LuCI DNS Only
3+
# Author: Aethersailor
4+
# Project: Custom_OpenClash_Rules
5+
# Description: 原子重建最终 DNS 配置,移除机场 YAML 对 DNS、Hosts 与 Fake-IP DNS 字段的影响
6+
# Compatibility: 依赖 OpenClash 当前 yml_change.sh 生成的 /tmp/yaml_config.* 片段及 ruby_edit 执行环境
7+
# Action: 从 LuCI/UCI 生成片段和 OpenClash 自定义文件重建 dns 与顶层 hosts,不修改其他配置
8+
# Conflicts: 必须作为最后执行的 DNS 覆写;不得与其他修改 dns 或 hosts 的覆写模块同时使用
9+
# Verification: 必须检查最终运行 YAML,并使用当前 Mihomo 核心加载该文件;订阅下载成功不能证明生效
10+
#
11+
# 硬性前置条件:
12+
# 1. 已在「覆写设置」→「DNS 设置」启用「自定义上游 DNS 服务器」
13+
# 2. LuCI 中至少有一个已启用且分组为 nameserver 的 DNS 服务器
14+
# 3. Fake-IP 模式下,必须在 LuCI 明确设置 Fake-IP IPv4 范围,不能保留 0(从机场 YAML 读取)
15+
# 4. Fake-IP 模式同时启用大陆 IPv4/IPv6 绕过时,必须启用自定义 Fake-IP-Filter 并明确选择模式
16+
# 5. 启用 respect-rules、代理节点 DNS 策略或为全部 nameserver 指定代理组时,必须在 LuCI 明确配置至少一个不经代理组转发的 proxy-server-nameserver
17+
#
18+
# 功能:
19+
# 1. nameserver、fallback、default-nameserver、proxy-server-nameserver 与 direct-nameserver 只读取本次启动由 LuCI 生成的临时片段
20+
# 2. 未在 LuCI 配置 fallback、默认 DNS、代理节点 DNS 或直连 DNS 时,从最终 dns 中删除对应机场键
21+
# 3. nameserver-policy、proxy-server-nameserver-policy、fallback-filter、Fake-IP-Filter 与 Hosts 只读取已启用的 OpenClash 自定义文件
22+
# 4. 保留 yml_change.sh 已根据 LuCI 生成的 enable、ipv6、enhanced-mode、listen 与 respect-rules
23+
# 5. 保留 OpenClash 根据路由器主机名生成的内置 Hosts;删除机场 YAML 的顶层 hosts
24+
# 6. 只替换 dns 与顶层 hosts,保留节点、代理集合、策略组、规则、Rule Provider 和其他非 DNS 配置
25+
#
26+
# 限制:
27+
# - 模块依赖 OpenClash 内部临时文件名与 UCI 结构;升级 OpenClash 后必须重新核对源码和最终运行配置
28+
# - 前置条件不满足时,模块记录错误并拒绝重建;此时原运行配置保持不变,不能视为机场 DNS 已被隔离
29+
# - 本模块不设置任何 DNS 上游,也不提供通用默认 DNS
30+
# - 使用域名形式的加密 DNS 时,仍需在 LuCI 配置可用的 default-nameserver,最终以 Mihomo 核心加载结果为准
31+
# - 其他更晚执行的模块或 /etc/openclash/custom/openclash_custom_overwrite.sh 仍可再次修改 dns 或 hosts
32+
33+
ruby_edit "$CONFIG_FILE" "['dns']" "begin; uci_get = lambda { |key, default_value| value = %x{uci -q get openclash.@overwrite[0].#{key} 2>/dev/null || uci -q get openclash.config.#{key} 2>/dev/null}.strip; value.empty? ? default_value : value }; load_yaml = lambda { |path| begin; File.exist?(path) ? YAML.load_file(path) : nil; rescue Exception; nil; end }; list_from = lambda { |path, key| data = load_yaml.call(path); data.is_a?(Hash) && data[key].is_a?(Array) ? data[key].map { |item| item.to_s }.reject(&:empty?).uniq : [] }; redirect_dns = uci_get.call('enable_redirect_dns', '1') == '1'; clean_list = lambda { |items| values = items.to_a.map { |item| item.to_s }.reject(&:empty?); redirect_dns ? values.reject { |item| item.match?(/^system($|:\/\/)/) }.uniq : values.uniq }; clean_policy = lambda { |policy| result = policy.is_a?(Hash) ? Marshal.load(Marshal.dump(policy)) : {}; if redirect_dns; result.keys.each { |key| value = result[key]; if value.is_a?(Array); value = value.map { |item| item.to_s }.reject { |item| item.match?(/^system($|:\/\/)/) }.uniq; value.empty? ? result.delete(key) : result[key] = value; elsif value.to_s.match?(/^system($|:\/\/)/); result.delete(key); end }; end; result }; current_dns = Value['dns'].is_a?(Hash) ? Value['dns'] : {}; nameservers = clean_list.call(list_from.call('/tmp/yaml_config.namedns.yaml', 'nameserver')); fallback = clean_list.call(list_from.call('/tmp/yaml_config.falldns.yaml', 'fallback')); default_nameservers = clean_list.call(list_from.call('/tmp/yaml_config.defaultdns.yaml', 'default-nameserver')); proxy_nameservers = clean_list.call(list_from.call('/tmp/yaml_config.proxynamedns.yaml', 'proxy-server-nameserver')); direct_nameservers = clean_list.call(list_from.call('/tmp/yaml_config.directnamedns.yaml', 'direct-nameserver')); append_default = uci_get.call('append_default_dns', '0') == '1'; if append_default; domain_server = /^dhcp:\/\/|^system($|:\/\/)|([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})/; derived_defaults = (nameservers | fallback).reject { |server| server.match?(domain_server) }; default_nameservers = (default_nameservers + derived_defaults).uniq; end; name_policy_data = load_yaml.call('/etc/openclash/custom/openclash_custom_domain_dns_policy.list'); name_policy = uci_get.call('custom_name_policy', '0') == '1' ? clean_policy.call(name_policy_data) : {}; proxy_policy_data = load_yaml.call('/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list'); proxy_policy = uci_get.call('custom_proxy_server_policy', '0') == '1' ? clean_policy.call(proxy_policy_data) : {}; local_exclude = ((Dir.children('/sys/class/net') rescue []) + ['h3=', 'skip-cert-verify=', 'ecs=', 'ecs-override=', 'disable-ipv6=', 'disable-ipv4=', 'disable-qtype-', 'disable-reuse=', 'utun', 'tailscale0', 'docker0', 'tun163', 'br-lan', 'mihomo']).uniq.map { |item| Regexp.escape(item) }.join('|'); proxied_server = /^[^#&]+#(?:(?:#{local_exclude})[^&]*&)*(?:(?!(?:#{local_exclude}))[^&]+)/; errors = []; errors << 'Custom DNS Setting is disabled' unless uci_get.call('enable_custom_dns', '0') == '1'; errors << 'LuCI nameserver is empty' if nameservers.empty?; fake_mode = current_dns['enhanced-mode'].to_s == 'fake-ip'; fake_range = uci_get.call('fakeip_range', '0'); errors << 'Fake-IP Range must be set explicitly in LuCI' if fake_mode && (fake_range.empty? || fake_range == '0'); china_bypass = uci_get.call('china_ip_route', '0') != '0' || uci_get.call('china_ip6_route', '0') != '0'; custom_fake_filter = uci_get.call('custom_fakeip_filter', '0') == '1'; errors << 'China bypass in Fake-IP mode requires Custom Fake-IP-Filter' if fake_mode && china_bypass && !custom_fake_filter; routed_nameservers = !nameservers.empty? && nameservers.all? { |server| server.match?(proxied_server) }; proxy_required = current_dns['respect-rules'] == true || routed_nameservers || !proxy_policy.empty?; errors << 'proxy-server-nameserver must be set explicitly in LuCI' if proxy_required && proxy_nameservers.empty?; errors << 'proxy-server-nameserver needs at least one ungrouped server' if !proxy_nameservers.empty? && proxy_nameservers.all? { |server| server.match?(proxied_server) }; if errors.any?; YAML.LOG_ERROR('Use LuCI DNS Only refused: %s' % [errors.join('; ')]); current_dns; else; rebuilt = {}; ['enable', 'ipv6', 'enhanced-mode', 'listen', 'respect-rules'].each { |key| rebuilt[key] = current_dns[key] if current_dns.key?(key) }; rebuilt['nameserver'] = nameservers; rebuilt['fallback'] = fallback unless fallback.empty?; rebuilt['default-nameserver'] = default_nameservers unless default_nameservers.empty?; rebuilt['proxy-server-nameserver'] = proxy_nameservers unless proxy_nameservers.empty?; rebuilt['direct-nameserver'] = direct_nameservers unless direct_nameservers.empty?; rebuilt['nameserver-policy'] = name_policy unless name_policy.empty?; rebuilt['proxy-server-nameserver-policy'] = proxy_policy unless proxy_policy.empty?; if uci_get.call('custom_fallback_filter', '0') == '1' && !fallback.empty?; fallback_filter_data = load_yaml.call('/etc/openclash/custom/openclash_custom_fallback_filter.yaml'); fallback_filter = fallback_filter_data.is_a?(Hash) ? fallback_filter_data['fallback-filter'] : nil; rebuilt['fallback-filter'] = fallback_filter if fallback_filter.is_a?(Hash); end; if fake_mode; rebuilt['fake-ip-range'] = fake_range; fake_range6 = uci_get.call('fakeip_range6', '0'); rebuilt['fake-ip-range6'] = fake_range6 if rebuilt['ipv6'] == true && !fake_range6.empty? && fake_range6 != '0'; if custom_fake_filter; filter_mode = uci_get.call('custom_fakeip_filter_mode', 'blacklist'); rebuilt['fake-ip-filter-mode'] = filter_mode; filter_files = ['/etc/openclash/custom/openclash_custom_fake_filter.list', '/tmp/yaml_openclash_fake_filter_include']; filters = filter_files.flat_map { |path| File.exist?(path) ? File.readlines(path).map { |line| line.gsub(/#.*$/, '').strip }.reject(&:empty?) : [] }; rebuilt['fake-ip-filter'] = filters.uniq unless filters.empty?; end; if china_bypass; filter_mode = rebuilt['fake-ip-filter-mode']; filters = rebuilt['fake-ip-filter'].to_a; if filter_mode == 'blacklist' || filter_mode.nil?; filters << 'rule-set:oc-cn-domain'; elsif filter_mode == 'whitelist'; filters.reject! { |filter| filter =~ /(geosite:?|rule-set:?).*(@cn|:cn|,cn|:china)/i }; elsif filter_mode == 'rule'; filters.unshift('RULE-SET,oc-cn-domain,real-ip'); end; rebuilt['fake-ip-filter'] = filters.uniq unless filters.empty?; end; end; hosts = {}; if uci_get.call('custom_host', '0') == '1'; hosts_data = load_yaml.call('/etc/openclash/custom/openclash_custom_hosts.list'); custom_hosts = hosts_data.is_a?(Hash) && hosts_data['hosts'].is_a?(Hash) ? hosts_data['hosts'] : hosts_data; hosts.merge!(custom_hosts) if custom_hosts.is_a?(Hash); end; {'openwrt.lan' => 'lan', 'immortalwrt.lan' => 'lan', 'lede.lan' => 'lan', 'router.lan' => 'lan'}.each { |key, value| hosts[key] ||= value }; system_hostname = %x{uci -q get system.@system[0].hostname 2>/dev/null}.strip; dnsmasq_domain = %x{uci -q get dhcp.@dnsmasq[0].domain 2>/dev/null}.strip; hosts[system_hostname + '.lan'] ||= 'lan' unless system_hostname.empty?; hosts[system_hostname + '.' + dnsmasq_domain] ||= 'lan' unless system_hostname.empty? || dnsmasq_domain.empty?; Value['hosts'] = hosts; rebuilt['use-hosts'] = true; YAML.LOG_TIP('Use LuCI DNS Only rebuilt DNS from OpenClash settings: nameserver=%d, fallback=%d' % [nameservers.length, fallback.length]); rebuilt; end; end"

0 commit comments

Comments
 (0)