也算是用了三年软路由了,从一开始的Smartdns到MosDNS,一直在DNS泄露的问题上弯弯绕绕,浪费了许多时间。尤其是使用MosDNS→AdGuardHome方案后,抖音、哔哩哔哩经常性出现加载卡顿的问题。经过排除,确定了卡顿是由于MosDNS分流所导致的一些CDN地址无法解析,随决定抛弃MosDNS,完全转入AGH分流的怀抱。

个人网络环境

家里的网络环境很简单,运营商光纤接入→光猫→路由→旁路由,路由器拨号,光猫仅负责光电转换;旁路由的意义一方面是方便我用网,另一方面是补全家用网络环境下的一些功能。

分流思路

同MosDNS的分流思路,但是由于AGH在匹配规则方面与MosDNS有所出入,因此是无法直接引用MosDNS的分流文件来进行使用的,另一方面是规则会定期更新的,因此手动组合难度较大,所以是需要本地脚本进行处理的。
分流规则使用的Loyalsoldier发布的规则列表。

配置流程

脚本运行目录是在 /root/dnsrules 下的,在根目录创建名为 rules2agh.sh 的文件,并将下面的文件内容贴入。

#!/usr/bin/env bash
# rules2agh.sh (one-domain-per-line)
# - 源:Loyalsoldier/v2ray-rules-dat(raw + jsDelivr 双源)
# - 输出:
#   1) /root/dnsrules/adguard.upstreams.txt  (AdGuard Home upstream_dns_file)
#   2) /root/dnsrules/adguard.blocklist.txt  (Adblock 语法 ||domain^)
# - 特性:逐域一行,不打包;过滤超长/非法域名;日志走 stderr
# 依赖:bash、curl、sed、awk、sort、uniq

set -Eeuo pipefail

BASE_DIR="/root/dnsrules"
WORK_DIR="$BASE_DIR/work"
UPSTREAM_FILE="$BASE_DIR/adguard.upstreams.txt"
BLOCKLIST_FILE="$BASE_DIR/adguard.blocklist.txt"
LOG_PREFIX="[rules2agh]"
mkdir -p "$WORK_DIR"

# === 上游设置 ===
DIRECT_UPSTREAM="tcp://223.5.5.5"        # 国内
PROXY_UPSTREAM="tls://1.1.1.1"           # 国外
REVERSE_UPSTREAM="$DIRECT_UPSTREAM"      # 反向解析走国内

# --- 源清单 ---
SRC=(
  "direct-list|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/direct-list.txt"
  "proxy-list|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/proxy-list.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/proxy-list.txt"
  "reject-list|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/reject-list.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/reject-list.txt"
  "china-list|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/china-list.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/china-list.txt"
  "apple-cn|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/apple-cn.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/apple-cn.txt"
  "google-cn|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/google-cn.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/google-cn.txt"
  "gfw|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt"
  "win-spy|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-spy.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/win-spy.txt"
  "win-update|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-update.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/win-update.txt"
  "win-extra|https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-extra.txt|https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/win-extra.txt"
)

log() { echo "[$(date +'%F %T')] $LOG_PREFIX $*" >&2; }

fetch_one() {
  local tag="$1"; shift
  local dst="$WORK_DIR/$tag.raw"
  local ok=0
  for url in "$@"; do
    log "GET $url"
    if curl -fsSL -L --connect-timeout 5 --max-time 25 --retry 2 -o "$dst" "$url"; then
      ok=1; break
    fi
  done
  [[ $ok -eq 1 ]] || { log "下载失败:$tag"; return 1; }
  printf '%s\n' "$dst"
}

normalize_domains_file() {
  local in="$1" out="$2"
  awk '
    { gsub(/\r$/, "", $0); line=$0; }
    { sub(/#.*/, "", line); gsub(/^[ \t]+|[ \t]+$/, "", line); }
    line=="" { next }
    /^domain:/ { print substr(line,8); next }
    /^full:/   { print substr(line,6); next }
    /^(regexp:|keyword:|include:|exception:|attr:)/ { next }
    { gsub(/^\|\|/, "", line); gsub(/\^$/, "", line); print line; }
  ' "$in" \
  | sed -E 's/^\*\.//; s/^\.+//' \
  | sed -E 's/[^0-9A-Za-z._-]//g' \
  | sed '/^[._-]*$/d' \
  | sed 's/\.$//' \
  | tr 'A-Z' 'a-z' \
  | sort -u > "$out"
}

merge_sets() { cat "$@" | sort -u; }

filter_too_long_or_invalid() {
  local in="$1" out="$2"
  awk 'length($0)>0 && length($0)<=240 {print}' "$in" | sort -u > "$out"
}

write_one_per_line() {
  local domains_file="$1" upstream="$2"
  while IFS= read -r d || [[ -n "$d" ]]; do
    [[ -z "$d" ]] && continue
    printf "[/%s/]%s\n" "$d" "$upstream"
  done < "$domains_file"
}

make_blocklist() {
  awk '{print "||"$0"^"}' "$1"
}

main() {
  log "下载源列表..."
  declare -A RAW
  for item in "${SRC[@]}"; do
    IFS='|' read -r tag url1 url2 <<< "$item"
    RAW["$tag"]="$(fetch_one "$tag" "$url1" "$url2")"
  done

  log "规范化各列表..."
  declare -A NORM
  for tag in "${!RAW[@]}"; do
    normalize_domains_file "${RAW[$tag]}" "$WORK_DIR/$tag.norm"
    filter_too_long_or_invalid "$WORK_DIR/$tag.norm" "$WORK_DIR/$tag.good"
    NORM["$tag"]="$WORK_DIR/$tag.good"
    log "$tag: $(wc -l < "${NORM[$tag]}" | tr -d ' ') 条"
  done

  # --- 集合定义 ---
  merge_sets "${NORM[direct-list]}" "${NORM[china-list]}" "${NORM[apple-cn]}" "${NORM[google-cn]}" "${NORM[win-update]}" \
    > "$WORK_DIR/DIRECT.all"

  merge_sets "${NORM[proxy-list]}" "${NORM[gfw]}" \
    > "$WORK_DIR/PROXY.all"

  merge_sets "${NORM[reject-list]}" "${NORM[win-spy]}" "${NORM[win-extra]}" \
    > "$WORK_DIR/BLOCK.all"

  log "生成 upstream 文件:$UPSTREAM_FILE"
  {
    echo "# Generated by rules2agh(one-domain-per-line) on $(date -Is)"
    echo "# 默认上游(未匹配域名) -> $PROXY_UPSTREAM"
    echo "$PROXY_UPSTREAM"
    echo
    echo "# 可选:反向解析 -> 直连上游"
    echo "[/in-addr.arpa/]$REVERSE_UPSTREAM"
    echo "[/ip6.arpa/]$REVERSE_UPSTREAM"
    echo
    echo "# === DIRECT -> $DIRECT_UPSTREAM ==="
    write_one_per_line "$WORK_DIR/DIRECT.all" "$DIRECT_UPSTREAM"
    echo
    echo "# === PROXY -> $PROXY_UPSTREAM ==="
    write_one_per_line "$WORK_DIR/PROXY.all" "$PROXY_UPSTREAM"
  } > "$UPSTREAM_FILE"

  log "生成 blocklist:$BLOCKLIST_FILE"
  {
    echo "! Generated by rules2agh on $(date -Is)"
    echo "! 拦截集合:reject + win-spy + win-extra"
    make_blocklist "$WORK_DIR/BLOCK.all"
  } > "$BLOCKLIST_FILE"

  log "完成。统计:"
  echo "  DIRECT: $(wc -l < "$WORK_DIR/DIRECT.all")" >&2
  echo "  PROXY : $(wc -l < "$WORK_DIR/PROXY.all")" >&2
  echo "  BLOCK : $(wc -l < "$WORK_DIR/BLOCK.all")" >&2

  /etc/init.d/AdGuardHome restart
}

main "$@"

该脚本引用了L大的以下列表,并在脚本中一一对应,可以根据自己需求添加或删减对应部分。

直连域名列表 direct-list.txt
——DIRECT直连——direct-list
代理域名列表 proxy-list.txt
——代理PROXY——proxy-list
广告域名列表 reject-list.txt
——BLOCK阻断——reject-list
@felixonmars/dnsmasq-china-list 仓库收集的在中国大陆可直连的域名列表 china-list.txt
——DIRECT直连——china-list
Apple 在中国大陆可直连的域名列表 apple-cn.txt
——DIRECT直连——apple-cn
Google 在中国大陆可直连的域名列表 google-cn.txt
——DIRECT直连——google-cn
GFWList 域名列表 gfw.txt
——代理PROXY——gfw
Windows 操作系统使用的隐私跟踪域名列表 win-spy.txt
——BLOCK阻断——win-spy
Windows 操作系统使用的系统升级域名列表 win-update.txt
——DIRECT直连——win-update
Windows 操作系统使用的附加隐私跟踪域名列表 win-extra.txt
——BLOCK阻断——win-extra

由于 mapfile -t CN_DOMAINS < <(parse_category_recursive "$CN_LIST" | sed 's/\r$//')语法问题, dash 不支持< <(...) 此类进程替换语法,因此运行时需要使用 bash 运行。

#赋权
chmod +x /root/dnsrules/geosite2agh.sh
#运行
bash /root/dnsrules/geosite2agh.sh

不出意外的话会返回以下内容并重启AGH服务

root@iStoreOS:~# bash /root/dnsrules/rules2agh.sh
[2025-08-15 22:51:00] [rules2agh] 下载源列表...
[2025-08-15 22:51:00] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt
[2025-08-15 22:51:02] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/proxy-list.txt
[2025-08-15 22:51:04] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/reject-list.txt
[2025-08-15 22:51:06] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/china-list.txt
[2025-08-15 22:51:08] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/apple-cn.txt
[2025-08-15 22:51:09] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/google-cn.txt
[2025-08-15 22:51:10] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt
[2025-08-15 22:51:11] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-spy.txt
[2025-08-15 22:51:12] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-update.txt
[2025-08-15 22:51:14] [rules2agh] GET https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/win-extra.txt
[2025-08-15 22:51:15] [rules2agh] 规范化各列表...
[2025-08-15 22:51:15] [rules2agh] win-update: 538 条
[2025-08-15 22:51:15] [rules2agh] google-cn: 142 条
[2025-08-15 22:51:15] [rules2agh] gfw: 5882 条
[2025-08-15 22:51:15] [rules2agh] proxy-list: 31163 条
[2025-08-15 22:51:15] [rules2agh] apple-cn: 167 条
[2025-08-15 22:51:17] [rules2agh] china-list: 118747 条
[2025-08-15 22:51:17] [rules2agh] win-extra: 399 条
[2025-08-15 22:51:19] [rules2agh] direct-list: 119387 条
[2025-08-15 22:51:21] [rules2agh] reject-list: 131727 条
[2025-08-15 22:51:21] [rules2agh] win-spy: 347 条
[2025-08-15 22:51:22] [rules2agh] 生成 upstream 文件:/root/dnsrules/adguard.upstreams.txt
[2025-08-15 22:51:25] [rules2agh] 生成 blocklist:/root/dnsrules/adguard.blocklist.txt
[2025-08-15 22:51:25] [rules2agh] 完成。统计:
  DIRECT: 120445
  PROXY : 31348
  BLOCK : 132453
warn ip6tables nat mod is needed
AdGuardHome service disabled
workdir is a tmpfs filesystem
AdGuardHome service enabled
luci enable switch=1

如果出现错误,优先考虑是否是当前系统缺少依赖,如果出现某一条依赖导致的报错,可以安装依赖后再次运行。

成功运行后,可以看到根目录有了 adguard.blocklist.txtadguard.upstreams.txt 两个文件,接下来就需要将 /root/dnsrules/adguard.upstreams.txt 写入 AdGuardHome.yamldns.upstream_dns_file 位置。如果你是使用iStore一键安装的AGH,那么只需要在UI界面的“服务——AdGuard Home——手动设置”中找到 dns.upstream_dns_file ,将文件路径添加在后面,再次运行脚本或者ssh /etc/init.d/AdGuardHome restart 重启AGH即可调用该规则分流。

此外,该脚本会基于L大 “广告域名列表 reject-list.txt” 的规则在根目录生成一个 adguard.blocklist.txt 文件,该文件可以在AGH的DNS黑名单中被加载使用,也可以酌情考虑是否使用该广告拒绝列表。

最后,在Openwrt的Crontab中加入自动执行该脚本的计划任务。 0 12 * * * /root/dnsrules/rules2agh.sh >> /root/dnsrules/work/update.log 2>&1

/etc/init.d/cron enable
/etc/init.d/cron restart

保存,并重启cron服务。