From 05306ffdc7f76b617cbeaf4e3a538ff90cf02f2e Mon Sep 17 00:00:00 2001 From: yorkyang2333 Date: Sat, 8 Aug 2026 07:07:13 +0800 Subject: [PATCH 1/3] fix(dns): preserve subscription DNS policy during config generation ShellCrash previously rebuilt the DNS section from its own defaults and dropped subscription-specific nameserver-policy entries. Merge subscription DNS with managed fields and user overrides into one YAML section so provider-specific node resolution survives config generation. Also preserve DNS sections in provider templates, fix the encrypted DNS resolver preset, and add regression coverage for block and flow YAML plus override precedence. --- scripts/libs/yaml_dns.sh | 279 +++++++++++++++++++++++++++++++ scripts/menus/dns.sh | 4 +- scripts/menus/providers_clash.sh | 6 +- scripts/starts/clash_modify.sh | 32 ++-- tests/test_yaml_dns.sh | 80 +++++++++ 5 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 scripts/libs/yaml_dns.sh create mode 100644 tests/test_yaml_dns.sh diff --git a/scripts/libs/yaml_dns.sh b/scripts/libs/yaml_dns.sh new file mode 100644 index 000000000..11705c616 --- /dev/null +++ b/scripts/libs/yaml_dns.sh @@ -0,0 +1,279 @@ +#!/bin/sh +# Copyright (C) Juewuy + +# Extract the top-level dns section without requiring a YAML parser. The +# merger below normalizes block and one-line flow mappings before overlaying +# fields, so the final configuration contains only one dns key. + +yaml_dns_extract() { + awk ' + function top_level(line) { + return line !~ /^[[:space:]#]/ && line ~ /^[^:]+:/ + } + function brace_delta(line, i, c, quote, escaped, delta) { + for (i = 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + } else if (c == "\047" || c == "\042") quote = c + else if (c == "{") delta++ + else if (c == "}") delta-- + } + return delta + } + { + if (!found) { + if ($0 ~ /^dns:[[:space:]]*\{/) { + print + flow = 1 + balance = brace_delta($0) + if (balance <= 0) exit + found = 1 + next + } + if ($0 ~ /^dns:[[:space:]]*($|#)/) { + print + found = 1 + } + next + } + if (flow) { + print + balance += brace_delta($0) + if (balance <= 0) exit + next + } + if (top_level($0)) exit + print + }' "$1" +} + +yaml_dns_without() { + awk ' + function top_level(line) { + return line !~ /^[[:space:]#]/ && line ~ /^[^:]+:/ + } + function brace_delta(line, i, c, quote, escaped, delta) { + for (i = 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + } else if (c == "\047" || c == "\042") quote = c + else if (c == "{") delta++ + else if (c == "}") delta-- + } + return delta + } + { + if (!in_dns) { + if ($0 ~ /^dns:[[:space:]]*\{/) { + in_dns = 1 + flow = 1 + balance = brace_delta($0) + if (balance <= 0) in_dns = 0 + next + } + if ($0 ~ /^dns:[[:space:]]*($|#)/) { + in_dns = 1 + next + } + print + next + } + if (flow) { + balance += brace_delta($0) + if (balance <= 0) in_dns = 0 + next + } + if (top_level($0)) { + in_dns = 0 + print + next + } + }' "$1" +} + +yaml_dns_merge() { + awk -v original="${1:-/dev/null}" \ + -v managed="${2:-/dev/null}" \ + -v user="${3:-/dev/null}" ' + function trim(value) { + sub(/^[[:space:]]+/, "", value) + sub(/[[:space:]]+$/, "", value) + return value + } + function brace_delta(line, i, c, quote, escaped, delta) { + for (i = 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + } else if (c == "\047" || c == "\042") quote = c + else if (c == "{") delta++ + else if (c == "}") delta-- + } + return delta + } + function add_entry(source, key, value) { + if (!(source SUBSEP key in entry)) { + order[source, ++count[source]] = key + } + entry[source, key] = value + } + function append_entry(source, key, line) { + if (entry[source, key] == "") entry[source, key] = line + else entry[source, key] = entry[source, key] "\n" line + } + function flow_pair(source, pair, i, c, depth_brace, depth_square, quote, escaped, colon, key, value) { + pair = trim(pair) + colon = 0 + depth_brace = depth_square = 0 + quote = escaped = 0 + for (i = 1; i <= length(pair); i++) { + c = substr(pair, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + continue + } + if (c == "\047") quote = "\047" + else if (c == "\042") quote = "\042" + else if (c == "{") depth_brace++ + else if (c == "}") depth_brace-- + else if (c == "[") depth_square++ + else if (c == "]") depth_square-- + else if (c == ":" && depth_brace == 0 && depth_square == 0) { + colon = i + break + } + } + if (!colon) return + key = trim(substr(pair, 1, colon - 1)) + value = trim(substr(pair, colon + 1)) + if ((substr(key, 1, 1) == "\047" && substr(key, length(key), 1) == "\047") || + (substr(key, 1, 1) == "\042" && substr(key, length(key), 1) == "\042")) + key = substr(key, 2, length(key) - 2) + add_entry(source, key, " " key ": " value) + } + function parse_flow(source, text, i, c, depth_brace, depth_square, quote, escaped, start) { + text = trim(text) + if (substr(text, 1, 1) == "{") text = substr(text, 2) + if (substr(text, length(text), 1) == "}") text = substr(text, 1, length(text) - 1) + start = 1 + depth_brace = depth_square = 0 + quote = escaped = 0 + for (i = 1; i <= length(text); i++) { + c = substr(text, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + continue + } + if (c == "\047") quote = "\047" + else if (c == "\042") quote = "\042" + else if (c == "{") depth_brace++ + else if (c == "}") depth_brace-- + else if (c == "[") depth_square++ + else if (c == "]") depth_square-- + else if (c == "," && depth_brace == 0 && depth_square == 0) { + flow_pair(source, substr(text, start, i - start)) + start = i + 1 + } + } + flow_pair(source, substr(text, start)) + } + function parse_file(file, source, line, rest, key, value) { + started = flow = balance = 0 + current = "" + flow_text = "" + while ((getline line < file) > 0) { + if (!started) { + if (line ~ /^dns:[[:space:]]*\{/) { + rest = line + sub(/^dns:[[:space:]]*/, "", rest) + flow_text = rest + flow = 1 + balance = brace_delta(line) + started = 1 + if (balance <= 0) { + parse_flow(source, flow_text) + started = flow = 0 + } + } else if (line ~ /^dns:[[:space:]]*($|#)/) { + started = 1 + } + continue + } + if (flow) { + flow_text = flow_text " " trim(line) + balance += brace_delta(line) + if (balance <= 0) { + parse_flow(source, flow_text) + started = flow = 0 + } + continue + } + if (line ~ /^[[:space:]][[:space:]][A-Za-z0-9_-]+[[:space:]]*:/) { + if (current != "") add_entry(source, key, value) + key = line + sub(/^[[:space:]]+/, "", key) + sub(/[[:space:]]*:.*/, "", key) + value = line + current = key + } else if (current != "") { + value = value "\n" line + } + } + if (current != "") add_entry(source, key, value) + close(file) + } + function managed_key(key) { + return key == "enable" || key == "listen" || key == "use-hosts" || + key == "ipv6" || key == "default-nameserver" || key == "direct-nameserver" || + key == "enhanced-mode" || key == "fake-ip-range" || key == "fake-ip-range6" || + key == "fake-ip-filter" || key == "respect-rules" || + key == "proxy-server-nameserver" || key == "nameserver" + } + BEGIN { + parse_file(original, "original") + parse_file(managed, "managed") + parse_file(user, "user") + print "dns:" + for (i = 1; i <= count["original"]; i++) { + key = order["original", i] + # nameserver-policy is intentionally passthrough: ShellCrash + # generated policy must not replace subscription-specific routes. + if (!managed_key(key) && !("user" SUBSEP key in entry)) + print entry["original", key] + } + for (i = 1; i <= count["managed"]; i++) { + key = order["managed", i] + if (!("user" SUBSEP key in entry) && + !(key == "nameserver-policy" && ("original" SUBSEP key in entry))) + print entry["managed", key] + } + for (i = 1; i <= count["user"]; i++) { + key = order["user", i] + print entry["user", key] + } + }' +} + +case "${1:-}" in +extract) + yaml_dns_extract "$2" + ;; +without) + yaml_dns_without "$2" + ;; +merge) + yaml_dns_merge "$2" "$3" "$4" + ;; +esac diff --git a/scripts/menus/dns.sh b/scripts/menus/dns.sh index 72b0491ae..9cb031c0b 100644 --- a/scripts/menus/dns.sh +++ b/scripts/menus/dns.sh @@ -316,7 +316,9 @@ set_dns_adv() { if echo "$crashcore" | grep -qE 'meta|singbox'; then dns_nameserver='https://dns.alidns.com/dns-query, https://doh.pub/dns-query' dns_fallback='https://cloudflare-dns.com/dns-query, https://dns.google/dns-query, https://doh.opendns.com/dns-query' - dns_resolver='https://223.5.5.5/dns-query, 2400:3200::1' + # Mihomo's default-nameserver resolver must be plain IPs; + # encrypted resolvers belong in dns_nameserver/dns_fallback. + dns_resolver='223.5.5.5, 2400:3200::1' setconfig dns_nameserver "'$dns_nameserver'" setconfig dns_fallback "'$dns_fallback'" setconfig dns_resolver "'$dns_resolver'" diff --git a/scripts/menus/providers_clash.sh b/scripts/menus/providers_clash.sh index f8bc313a2..7135f472b 100644 --- a/scripts/menus/providers_clash.sh +++ b/scripts/menus/providers_clash.sh @@ -7,6 +7,7 @@ __IS_PROVIDERS_CLASH=1 load_lang providers . "$CRASHDIR"/libs/web_get_bin.sh +. "$CRASHDIR"/libs/yaml_dns.sh # 生成clash的providers配置文件 gen_providers() { @@ -29,6 +30,9 @@ gen_providers() { fi # 生成proxy_providers模块 mkdir -p "$TMPDIR"/providers + # Keep an optional DNS section from the provider template. It is otherwise + # lost when the template is reduced to proxy providers, groups and rules. + yaml_dns_extract "$TMPDIR"/provider_temp_file >"$TMPDIR"/providers/dns.yaml # 预创建文件并写入对应文件头 echo 'proxy-providers:' >"$TMPDIR"/providers/providers.yaml # 切割模版文件 @@ -58,7 +62,7 @@ gen_providers() { fi # 修饰模版文件并合并 sed -i "s/{providers_tags}/$providers_tags/g" "$TMPDIR"/providers/proxy-groups.yaml - cut -c 1- "$TMPDIR"/providers/providers.yaml "$TMPDIR"/providers/proxy-groups.yaml "$TMPDIR"/providers/rules.yaml >"$TMPDIR"/config.yaml + cut -c 1- "$TMPDIR"/providers/providers.yaml "$TMPDIR"/providers/proxy-groups.yaml "$TMPDIR"/providers/rules.yaml "$TMPDIR"/providers/dns.yaml >"$TMPDIR"/config.yaml rm -rf "$TMPDIR"/providers # 调用内核测试 . "$CRASHDIR"/starts/check_core.sh && check_core && "$TMPDIR"/CrashCore -t -d "$BINDIR" -f "$TMPDIR"/config.yaml diff --git a/scripts/starts/clash_modify.sh b/scripts/starts/clash_modify.sh index 493c0e200..8706bb6d9 100644 --- a/scripts/starts/clash_modify.sh +++ b/scripts/starts/clash_modify.sh @@ -25,10 +25,16 @@ prepare_clash_base_config() { fi } } - #dns配置 - [ -z "$(cat "$CRASHDIR"/yamls/user.yaml 2>/dev/null | grep '^dns:')" ] && { - [ "$crashcore" != meta ] && dns_resolver='223.5.5.5' - cat >"$TMPDIR"/dns.yaml <"$TMPDIR"/original-dns.yaml + if [ -s "$CRASHDIR"/yamls/user.yaml ]; then + yaml_dns_extract "$CRASHDIR"/yamls/user.yaml >"$TMPDIR"/user-dns.yaml + yaml_dns_without "$CRASHDIR"/yamls/user.yaml >"$TMPDIR"/user.yaml + fi + [ "$crashcore" != meta ] && dns_resolver='223.5.5.5' + cat >"$TMPDIR"/managed-dns.yaml </dev/null | grep -v '#' | sed "s/^/ - '/" | sed "s/$/'/" >>"$TMPDIR"/dns.yaml + cat "$CRASHDIR"/configs/fake_ip_filter "$CRASHDIR"/configs/fake_ip_filter.list 2>/dev/null | grep -v '#' | sed "s/^/ - '/" | sed "s/$/'/" >>"$TMPDIR"/managed-dns.yaml else - echo " - '+.*'" >>"$TMPDIR"/dns.yaml #使用fake-ip模拟redir_host + echo " - '+.*'" >>"$TMPDIR"/managed-dns.yaml #使用fake-ip模拟redir_host fi #mix模式fakeip绕过cn - [ "$dns_mod" = "mix" ] && echo ' - "rule-set:cn"' >>"$TMPDIR"/dns.yaml + [ "$dns_mod" = "mix" ] && echo ' - "rule-set:cn"' >>"$TMPDIR"/managed-dns.yaml #mix模式和route模式插入分流设置 if [ "$dns_mod" = "mix" ] || [ "$dns_mod" = "route" ]; then [ "$dns_protect" != "OFF" ] && dns_final="$dns_fallback" || dns_final="$dns_nameserver" - cat >>"$TMPDIR"/dns.yaml <>"$TMPDIR"/managed-dns.yaml <>"$TMPDIR"/dns.yaml <>"$TMPDIR"/managed-dns.yaml <"$TMPDIR"/dns.yaml #域名嗅探配置 [ "$sniffer" = "ON" ] && [ "$crashcore" = "meta" ] && sniffer_set="sniffer: {enable: true, parse-pure-ip: true, skip-domain: ['+.push.apple.com', 'Mijia Cloud'], sniff: {http: {ports: [80, 8080-8880], override-destination: true}, tls: {ports: [443, 8443]}, quic: {ports: [443, 8443]}}}" [ "$crashcore" = "clashpre" ] && [ "$dns_mod" = "redir_host" -o "$sniffer" = "ON" ] && exper="experimental: {ignore-resolve-fail: true, interface-name: en0,sniff-tls-sni: true}" @@ -212,7 +220,7 @@ merger_yaml() { sed -i 's/^ *-/ -/g' "$TMPDIR"/rules.yaml #合并文件 [ -s "$CRASHDIR"/yamls/user.yaml ] && { - yaml_user="$CRASHDIR"/yamls/user.yaml + yaml_user="$TMPDIR"/user.yaml #set和user去重,且优先使用user.yaml cp -f "$TMPDIR"/set.yaml "$TMPDIR"/set_bak.yaml for char in mode allow-lan log-level tun experimental external-ui-url interface-name dns store-selected unified-delay; do @@ -253,7 +261,7 @@ finalize_clash_yaml() { #建立软连接 [ ""$TMPDIR"" = ""$BINDIR"" ] || ln -sf "$TMPDIR"/config.yaml "$BINDIR"/config.yaml 2>/dev/null || cp -f "$TMPDIR"/config.yaml "$BINDIR"/config.yaml #清理缓存 - for char in $yaml_char set set_bak dns hosts; do + for char in $yaml_char set set_bak dns managed-dns original-dns user-dns user hosts; do rm -f "$TMPDIR"/${char}.yaml done } diff --git a/tests/test_yaml_dns.sh b/tests/test_yaml_dns.sh new file mode 100644 index 000000000..baef31e6d --- /dev/null +++ b/tests/test_yaml_dns.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +set -eu + +test_tmp=$(mktemp -d "${TMPDIR:-/tmp}/shellcrash-dns-test.XXXXXX") +trap 'rm -rf "$test_tmp"' EXIT + +. "$(dirname "$0")/../scripts/libs/yaml_dns.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_contains() { + grep -Fq -- "$1" "$2" || fail "$1 not found in $2" +} + +assert_not_contains() { + grep -Fq -- "$1" "$2" && fail "$1 unexpectedly found in $2" || : +} + +# A subscription block keeps provider-specific DNS policy and unknown fields, +# while ShellCrash-managed values still replace the subscription nameserver. +printf '%s\n' \ + 'dns:' \ + ' nameserver-policy:' \ + ' "+.v51124-4.qpon": [1.1.1.1]' \ + ' fallback-filter: {geoip: true}' \ + ' fallback-filter: {geosite: true}' \ + ' nameserver: [9.9.9.9]' \ + 'proxies: []' >"$test_tmp/original.yaml" +printf '%s\n' \ + 'dns:' \ + ' nameserver: [223.5.5.5]' \ + ' enable: true' >"$test_tmp/managed.yaml" +yaml_dns_merge "$test_tmp/original.yaml" "$test_tmp/managed.yaml" /dev/null >"$test_tmp/block.out" +assert_contains '"+.v51124-4.qpon": [1.1.1.1]' "$test_tmp/block.out" +assert_contains 'fallback-filter: {geosite: true}' "$test_tmp/block.out" +assert_not_contains 'fallback-filter: {geoip: true}' "$test_tmp/block.out" +[ "$(grep -c '^ fallback-filter:' "$test_tmp/block.out")" = 1 ] || fail 'repeated DNS key was not reduced' +assert_contains 'nameserver: [223.5.5.5]' "$test_tmp/block.out" +assert_not_contains 'nameserver: [9.9.9.9]' "$test_tmp/block.out" +[ "$(grep -c '^dns:' "$test_tmp/block.out")" = 1 ] || fail 'block output has duplicate dns keys' + +# Common one-line flow mappings are normalized before merging. +printf '%s\n' \ + 'dns: {nameserver-policy: {"+.flow": [1.1.1.1]}, fallback-filter: {geoip: true}, nameserver: [9.9.9.9]}' \ + 'proxies: []' >"$test_tmp/flow.yaml" +yaml_dns_merge "$test_tmp/flow.yaml" "$test_tmp/managed.yaml" /dev/null >"$test_tmp/flow.out" +assert_contains '"+.flow": [1.1.1.1]' "$test_tmp/flow.out" +assert_contains 'fallback-filter: {geoip: true}' "$test_tmp/flow.out" +[ "$(grep -c '^dns:' "$test_tmp/flow.out")" = 1 ] || fail 'flow output has duplicate dns keys' + +# No subscription DNS still produces the managed DNS section. +: >"$test_tmp/empty.yaml" +yaml_dns_merge "$test_tmp/empty.yaml" "$test_tmp/managed.yaml" /dev/null >"$test_tmp/empty.out" +assert_contains 'enable: true' "$test_tmp/empty.out" + +# User DNS wins field-by-field and is removed from the ordinary user overlay. +printf '%s\n' \ + 'dns:' \ + ' nameserver: [8.8.8.8]' \ + ' custom-user: yes' \ + 'mode: Rule' >"$test_tmp/user.yaml" +yaml_dns_merge "$test_tmp/original.yaml" "$test_tmp/managed.yaml" "$test_tmp/user.yaml" >"$test_tmp/user.out" +assert_contains 'nameserver: [8.8.8.8]' "$test_tmp/user.out" +assert_contains 'custom-user: yes' "$test_tmp/user.out" +assert_not_contains 'nameserver: [223.5.5.5]' "$test_tmp/user.out" +yaml_dns_without "$test_tmp/user.yaml" >"$test_tmp/user-without.out" +assert_contains 'mode: Rule' "$test_tmp/user-without.out" +if grep -q '^dns:' "$test_tmp/user-without.out"; then + fail 'user overlay still contains dns' +fi + +# The one-click encrypted DNS preset must keep the resolver as IP literals. +assert_not_contains "dns_resolver='https://" "$(dirname "$0")/../scripts/menus/dns.sh" +assert_contains "dns_resolver='223.5.5.5, 2400:3200::1'" "$(dirname "$0")/../scripts/menus/dns.sh" + +printf '%s\n' 'yaml dns tests passed' From d3c6e9f01ba564ab90f5ddccd6707e81b20e8183 Mon Sep 17 00:00:00 2001 From: yorkyang2333 Date: Sat, 8 Aug 2026 07:44:39 +0800 Subject: [PATCH 2/3] fix(dns): stop provider rule extraction at next section --- scripts/libs/yaml_dns.sh | 12 ++++++++++++ scripts/menus/providers_clash.sh | 2 +- tests/test_yaml_dns.sh | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scripts/libs/yaml_dns.sh b/scripts/libs/yaml_dns.sh index 11705c616..6e9e2a7de 100644 --- a/scripts/libs/yaml_dns.sh +++ b/scripts/libs/yaml_dns.sh @@ -97,6 +97,18 @@ yaml_dns_without() { }' "$1" } +yaml_top_level_section() { + awk -v section="$2" ' + function top_level(line) { + return line !~ /^[[:space:]#]/ && line ~ /^[^:]+:/ + } + { + if (!started && $0 ~ section) started = 1 + if (started && top_level($0) && $0 !~ section) exit + if (started) print + }' "$1" +} + yaml_dns_merge() { awk -v original="${1:-/dev/null}" \ -v managed="${2:-/dev/null}" \ diff --git a/scripts/menus/providers_clash.sh b/scripts/menus/providers_clash.sh index 7135f472b..a9054c23c 100644 --- a/scripts/menus/providers_clash.sh +++ b/scripts/menus/providers_clash.sh @@ -37,7 +37,7 @@ gen_providers() { echo 'proxy-providers:' >"$TMPDIR"/providers/providers.yaml # 切割模版文件 sed -n '/^proxy-groups:/,/^[a-z]/ { /^rule/d; p; }' "$TMPDIR"/provider_temp_file >"$TMPDIR"/providers/proxy-groups.yaml - sed -n '/^rule/,$p' "$TMPDIR"/provider_temp_file >"$TMPDIR"/providers/rules.yaml + yaml_top_level_section "$TMPDIR"/provider_temp_file '^rule' >"$TMPDIR"/providers/rules.yaml rm -rf "$TMPDIR"/provider_temp_file # 基于单订阅生成providers模块 if [ -n "$1" ]; then diff --git a/tests/test_yaml_dns.sh b/tests/test_yaml_dns.sh index baef31e6d..08af2fb30 100644 --- a/tests/test_yaml_dns.sh +++ b/tests/test_yaml_dns.sh @@ -73,6 +73,24 @@ if grep -q '^dns:' "$test_tmp/user-without.out"; then fail 'user overlay still contains dns' fi +# A provider template may place dns after rules. Section extraction must stop +# at that next top-level key before dns.yaml is appended. +printf '%s\n' \ + 'proxy-groups: []' \ + 'rule-providers:' \ + ' sample: {type: http, behavior: classical}' \ + 'rules:' \ + ' - MATCH,DIRECT' \ + 'dns:' \ + ' nameserver-policy: {"+.provider": [1.1.1.1]}' >"$test_tmp/provider.yaml" +yaml_top_level_section "$test_tmp/provider.yaml" '^rule' >"$test_tmp/provider-rules.out" +assert_contains 'rules:' "$test_tmp/provider-rules.out" +assert_not_contains 'dns:' "$test_tmp/provider-rules.out" +yaml_dns_extract "$test_tmp/provider.yaml" >"$test_tmp/provider-dns.out" +cat "$test_tmp/provider-rules.out" "$test_tmp/provider-dns.out" >"$test_tmp/provider-config.out" +[ "$(grep -c '^dns:' "$test_tmp/provider-config.out")" = 1 ] || + fail 'provider output has duplicate dns keys' + # The one-click encrypted DNS preset must keep the resolver as IP literals. assert_not_contains "dns_resolver='https://" "$(dirname "$0")/../scripts/menus/dns.sh" assert_contains "dns_resolver='223.5.5.5, 2400:3200::1'" "$(dirname "$0")/../scripts/menus/dns.sh" From 47f724d3c418c651eae82dcf2e3dc60cfb53d3ce Mon Sep 17 00:00:00 2001 From: yorkyang2333 Date: Sat, 8 Aug 2026 07:48:35 +0800 Subject: [PATCH 3/3] fix(dns): handle comments after flow mappings --- scripts/libs/yaml_dns.sh | 22 +++++++++++++++++++--- tests/test_yaml_dns.sh | 8 ++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/libs/yaml_dns.sh b/scripts/libs/yaml_dns.sh index 6e9e2a7de..6cb2e1bbd 100644 --- a/scripts/libs/yaml_dns.sh +++ b/scripts/libs/yaml_dns.sh @@ -18,6 +18,7 @@ yaml_dns_extract() { else if (quote == "\042" && c == "\\") escaped = 1 else if (c == quote) quote = 0 } else if (c == "\047" || c == "\042") quote = c + else if (c == "#" && (i == 1 || substr(line, i - 1, 1) ~ /[[:space:]]/)) break else if (c == "{") delta++ else if (c == "}") delta-- } @@ -63,6 +64,7 @@ yaml_dns_without() { else if (quote == "\042" && c == "\\") escaped = 1 else if (c == quote) quote = 0 } else if (c == "\047" || c == "\042") quote = c + else if (c == "#" && (i == 1 || substr(line, i - 1, 1) ~ /[[:space:]]/)) break else if (c == "{") delta++ else if (c == "}") delta-- } @@ -118,6 +120,19 @@ yaml_dns_merge() { sub(/[[:space:]]+$/, "", value) return value } + function strip_comment(line, i, c, quote, escaped) { + for (i = 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (quote) { + if (quote == "\042" && escaped) escaped = 0 + else if (quote == "\042" && c == "\\") escaped = 1 + else if (c == quote) quote = 0 + } else if (c == "\047" || c == "\042") quote = c + else if (c == "#" && (i == 1 || substr(line, i - 1, 1) ~ /[[:space:]]/)) + return substr(line, 1, i - 1) + } + return line + } function brace_delta(line, i, c, quote, escaped, delta) { for (i = 1; i <= length(line); i++) { c = substr(line, i, 1) @@ -126,6 +141,7 @@ yaml_dns_merge() { else if (quote == "\042" && c == "\\") escaped = 1 else if (c == quote) quote = 0 } else if (c == "\047" || c == "\042") quote = c + else if (c == "#" && (i == 1 || substr(line, i - 1, 1) ~ /[[:space:]]/)) break else if (c == "{") delta++ else if (c == "}") delta-- } @@ -174,7 +190,7 @@ yaml_dns_merge() { add_entry(source, key, " " key ": " value) } function parse_flow(source, text, i, c, depth_brace, depth_square, quote, escaped, start) { - text = trim(text) + text = trim(strip_comment(text)) if (substr(text, 1, 1) == "{") text = substr(text, 2) if (substr(text, length(text), 1) == "}") text = substr(text, 1, length(text) - 1) start = 1 @@ -210,7 +226,7 @@ yaml_dns_merge() { if (line ~ /^dns:[[:space:]]*\{/) { rest = line sub(/^dns:[[:space:]]*/, "", rest) - flow_text = rest + flow_text = strip_comment(rest) flow = 1 balance = brace_delta(line) started = 1 @@ -224,7 +240,7 @@ yaml_dns_merge() { continue } if (flow) { - flow_text = flow_text " " trim(line) + flow_text = flow_text " " trim(strip_comment(line)) balance += brace_delta(line) if (balance <= 0) { parse_flow(source, flow_text) diff --git a/tests/test_yaml_dns.sh b/tests/test_yaml_dns.sh index 08af2fb30..d01464424 100644 --- a/tests/test_yaml_dns.sh +++ b/tests/test_yaml_dns.sh @@ -52,6 +52,14 @@ assert_contains '"+.flow": [1.1.1.1]' "$test_tmp/flow.out" assert_contains 'fallback-filter: {geoip: true}' "$test_tmp/flow.out" [ "$(grep -c '^dns:' "$test_tmp/flow.out")" = 1 ] || fail 'flow output has duplicate dns keys' +printf '%s\n' \ + 'dns: {nameserver-policy: {"+.comment": [1.1.1.1]}, fallback-filter: {geoip: true}} # trailing comment' \ + 'proxies: []' >"$test_tmp/flow-comment.yaml" +yaml_dns_merge "$test_tmp/flow-comment.yaml" "$test_tmp/managed.yaml" /dev/null >"$test_tmp/flow-comment.out" +assert_contains '"+.comment": [1.1.1.1]' "$test_tmp/flow-comment.out" +assert_contains 'fallback-filter: {geoip: true}' "$test_tmp/flow-comment.out" +assert_not_contains 'fallback-filter: {geoip: true}}' "$test_tmp/flow-comment.out" + # No subscription DNS still produces the managed DNS section. : >"$test_tmp/empty.yaml" yaml_dns_merge "$test_tmp/empty.yaml" "$test_tmp/managed.yaml" /dev/null >"$test_tmp/empty.out"