本文介绍了一种轻量级智能 DNS 分流解决方案,通过在本地搭建 Python DNS 服务器,同时查询国内外上游 DNS 并智能判断结果,有效避免 DNS 污染问题,同时保证国内网站获得最佳的本地解析结果。这种方案无需维护复杂的域名列表,能自动适应网络环境变化,为用户提供无缝的上网体验。

整体架构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                   +------------------------+
| 应用程序 DNS 请求 |
+------------------------+
|
V
+----------------+ +------------------------------------------------+
| | | 轻量级 Python DNS 服务器 (53 端口) |
| 中国 IP 地址库 |--->| 同时查询国内外DNS,智能判断最佳响应 |
| | +------------------------------------------------+
+----------------+ / \
/ \
+------------------+ +------------------+
| 国内 DNS 服务器 | | 国外 DNS 服务器 |
| (114.114.114.114)| | (1.1.1.1) |
+------------------+ +------------------+

系统工作流程:

  1. 接收 DNS 请求 - Python DNS 服务器监听 53 端口,接收应用程序的 DNS 请求
  2. 双重查询 - 同时向国内 DNS (114.114.114.114) 和国外 DNS (1.1.1.1) 发送查询请求
  3. 智能判断 - 分析两者返回结果:
    • 检查国内 DNS 返回的 IP 是否位于中国(使用中国 IP 地址库)
    • 如果国内 DNS 返回的都是国外 IP 而国外 DNS 有结果,可能是污染结果
  4. 选择最佳结果 - 根据判断结果选择最合适的 DNS 响应:
    • 如果国内 DNS 返回的是国内 IP,优先使用国内结果(获得更好的 CDN 分配)
    • 如果检测到可能的污染,或者返回的 IP 在海外,使用国外 DNS 结果
  5. 返回结果 - 将最佳结果返回给应用程序

这种架构的优势在于:

  • 自动化决策 - 无需维护繁琐的域名列表,系统自动判断最佳 DNS 解析路径
  • 动态适应 - 能适应网络环境变化和 DNS 污染策略的变化
  • 最佳体验 - 国内网站获得最佳的本地 CDN,国外网站避免 DNS 污染,并获得距离海外隧道出口较近的 CDN
  • 轻量级实现 - 纯 Python 实现,资源占用少,启动快速,无需额外安装 unbound、dnsmasq 等复杂软件,并且容易根据需求修改业务逻辑,实现更复杂的解析规则

为什么需要国内外区分解析

大型网站通常会根据用户的网络环境提供不同的服务器地址。例如,从电信网络查询可能得到电信 CDN 的 IP,从教育网查询可能得到教育网的 IP,从国外查询则可能得到国外服务器的 IP。这种机制称为 “分区域解析” 或 “智能 DNS”。

如果我们简单地使用国外 DNS 服务器(如 Google 的 8.8.8.8)来解析所有域名,会带来两个问题:

  1. 对于国内网站,可能会解析到离你较远的 IP 地址(甚至是海外 IP 地址),导致访问速度变慢
  2. 对于使用了 CDN 的网站,可能无法获得最适合你所在网络环境的服务器 IP

我 11 年前的文章《一个抗污染 DNS 的搭建》介绍了一种基于权威 DNS 服务器的方案,对权威 DNS 服务器在国内的域名,使用国内出口解析;对权威 DNS 服务器在海外的域名,采用海外隧道出口解析。但这种方法目前已经不再实用,因为很多权威 DNS 服务器具有国内和国外多个 IP 地址,它们根据用户的公网 IP 分区域解析,因此简单通过权威 DNS 服务器的 IP 地址来区分国内和国外网站已经不再实用了。

本文提出的新方案不再依赖于权威 DNS 服务器的位置,而是基于 DNS 响应内容进行智能判断。它是基于一个简单的观察,DNS 污染返回的 IP 地址一般不在中国境内。那么我们只要通过国内和国外的 DNS 分别解析,只要国内 DNS 返回的结果在中国境内,就认为是合法的 DNS 查询结果。否则,就以国外 DNS 的查询结果为准。之所以要通过隧道获取国外 DNS 查询结果,是为了获取距离隧道的海外出口最近的 CDN 节点。

轻量级 Python DNS 服务器实现

1. 准备工作

首先,确保已经按照《搭建全程美国 IP、无需手动设置代理的三层隧道》设置好隧道并配置了分区域路由。确认:

  • WireGuard 隧道已连接
  • 国内外分流规则已配置(国内 IP 直连,国外 IP 走隧道)

这是为了保证本地既可以快速访问国内公共 DNS 服务器(如 114.114.114.114),又可以通过隧道访问海外公共 DNS 服务器(如 1.1.1.1 和 8.8.8.8)。

2. 创建 Python DNS 服务器脚本

创建一个目录来存放 DNS 服务器脚本:

1
mkdir -p ~/smart_dns

然后创建脚本文件:

1
nano ~/smart_dns/standalone_smart_dns.py

写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/usr/bin/env python3
# standalone_smart_dns.py - 轻量级 DNS 转发服务器,具有防污染能力

import socket
import threading
import ipaddress
import os
import time
import requests
import json
from dnslib import DNSRecord, DNSHeader, RR, QTYPE, A, AAAA

# 配置项
LISTEN_IP = "127.0.0.1"
LISTEN_PORT = 53
CHINA_DNS = "114.114.114.114"
FOREIGN_DNS = "1.1.1.1"
CHINA_IP_LIST_URL = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute.txt"
CHINA_IPV6_LIST_URL = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute_v6.txt"
CHINA_IP_CACHE_FILE = "/tmp/.china_ip_list.txt"
CHINA_IPV6_CACHE_FILE = "/tmp/.china_ipv6_list.txt"
CACHE_DURATION = 7 * 24 * 60 * 60 # 7 天秒数
MAX_PACKET_SIZE = 1024
TIMEOUT = 5

# 全局变量,存储 IP 网络
cn_networks = []
cn_networks_v6 = []

def get_china_ip_list(is_ipv6=False):
"""获取或更新中国 IP 列表"""
url = CHINA_IPV6_LIST_URL if is_ipv6 else CHINA_IP_LIST_URL
cache_file = CHINA_IPV6_CACHE_FILE if is_ipv6 else CHINA_IP_CACHE_FILE

# 检查缓存是否存在且未过期
if os.path.exists(cache_file):
file_age = time.time() - os.path.getmtime(cache_file)
if file_age < CACHE_DURATION:
with open(cache_file, 'r') as f:
return [line.strip() for line in f if line.strip() and not line.startswith('#')]

# 下载新列表
try:
response = requests.get(url)
if response.status_code == 200:
ip_list = [line.strip() for line in response.text.split('\n')
if line.strip() and not line.startswith('#')]

# 保存到缓存
with open(cache_file, 'w') as f:
f.write('\n'.join(ip_list))

return ip_list
else:
# 如果下载失败但缓存存在,使用缓存
if os.path.exists(cache_file):
with open(cache_file, 'r') as f:
return [line.strip() for line in f if line.strip() and not line.startswith('#')]
else:
return []
except Exception as e:
print(f"获取中国 IP 列表出错: {e}")
if os.path.exists(cache_file):
with open(cache_file, 'r') as f:
return [line.strip() for line in f if line.strip() and not line.startswith('#')]
return []

def is_cn_ip(ip):
"""检查 IP 是否在中国网络列表中"""
global cn_networks, cn_networks_v6

try:
ip_obj = ipaddress.ip_address(ip)

if ip_obj.version == 4:
# 首次运行时加载中国 IPv4 列表
if not cn_networks:
china_ip_list = get_china_ip_list(is_ipv6=False)
cn_networks = [ipaddress.ip_network(net) for net in china_ip_list]

# 检查 IP 是否在任何网络中
for net in cn_networks:
if ip_obj in net:
return True
elif ip_obj.version == 6:
# 首次运行时加载中国 IPv6 列表
if not cn_networks_v6:
china_ipv6_list = get_china_ip_list(is_ipv6=True)
cn_networks_v6 = [ipaddress.ip_network(net) for net in china_ipv6_list]

# 检查 IP 是否在任何网络中
for net in cn_networks_v6:
if ip_obj in net:
return True

return False
except:
return False

def forward_dns_request(domain, dns_server, record_type):
"""将 DNS 请求转发到指定的 DNS 服务器"""
try:
# 正确使用 dnslib 创建 DNS 请求
from dnslib import DNSQuestion
record = DNSRecord()
record.add_question(DNSQuestion(domain, getattr(QTYPE, record_type)))
packet = record.pack()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(TIMEOUT)

s.sendto(packet, (dns_server, 53))
reply_packet, _ = s.recvfrom(MAX_PACKET_SIZE)

reply = DNSRecord.parse(reply_packet)
return reply
except Exception as e:
print(f"查询 {dns_server} 关于 {domain} ({record_type}) 出错: {e}")
return None

def handle_dns_request(data, client_addr, sock):
"""处理 DNS 请求并返回智能选择的响应"""
try:
request = DNSRecord.parse(data)
qname = str(request.q.qname)
qtype = QTYPE[request.q.qtype]

print(f"查询: {qname} ({qtype}) 来自 {client_addr[0]}")

# 仅对 A 和 AAAA 记录使用智能逻辑
if qtype in ('A', 'AAAA'):
# 查询两个 DNS 服务器
cn_reply = forward_dns_request(qname, CHINA_DNS, qtype)
foreign_reply = forward_dns_request(qname, FOREIGN_DNS, qtype)

# 提取回复中的 IP
cn_ips = []
if cn_reply and cn_reply.rr:
cn_ips = [str(rr.rdata) for rr in cn_reply.rr if rr.rtype == getattr(QTYPE, qtype)]

foreign_ips = []
if foreign_reply and foreign_reply.rr:
foreign_ips = [str(rr.rdata) for rr in foreign_reply.rr if rr.rtype == getattr(QTYPE, qtype)]

# 检查任何 CN IP 是否实际在中国
cn_ips_in_china = [ip for ip in cn_ips if is_cn_ip(ip)]

# 智能选择逻辑
if cn_ips_in_china:
# 中国 DNS 返回了中国 IP,使用 CN 结果
print(f"为 {qname} ({qtype}) 使用中国 DNS")
response = cn_reply
elif foreign_ips:
# 中国 DNS 没有返回中国 IP,但国外 DNS 有结果
# 可能是 DNS 污染,使用国外结果
print(f"为 {qname} ({qtype}) 使用国外 DNS")
response = foreign_reply
elif cn_ips:
# 默认使用中国 DNS 结果(如果有)
print(f"默认为 {qname} ({qtype}) 使用中国 DNS")
response = cn_reply
else:
# 如果以上都失败,使用国外 DNS 或空响应
print(f"默认为 {qname} ({qtype}) 使用国外 DNS")
response = foreign_reply if foreign_reply else cn_reply
else:
# 对于非 A/AAAA 记录,直接转发到国外 DNS
response = forward_dns_request(qname, FOREIGN_DNS, qtype)
if not response:
response = forward_dns_request(qname, CHINA_DNS, qtype)

# 如果有响应,发送它
if response:
# 保持原始请求 ID
response.header.id = request.header.id
sock.sendto(response.pack(), client_addr)
else:
# 创建空响应
response = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=0, ra=1), q=request.q)
sock.sendto(response.pack(), client_addr)

except Exception as e:
print(f"处理请求出错: {e}")

def run_dns_server():
"""运行 DNS 服务器"""
# 尝试 UDP 和 TCP 套接字
try:
# 用于 DNS 的 UDP 套接字
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.bind((LISTEN_IP, LISTEN_PORT))
print(f"DNS 服务器监听 {LISTEN_IP}:{LISTEN_PORT} (UDP)")

while True:
data, addr = udp_sock.recvfrom(MAX_PACKET_SIZE)
threading.Thread(target=handle_dns_request, args=(data, addr, udp_sock)).start()

except Exception as e:
print(f"服务器错误: {e}")
finally:
if 'udp_sock' in locals():
udp_sock.close()

if __name__ == "__main__":
# 加载库
try:
import dnslib
except ImportError:
print("安装必要的包...")
import pip
pip.main(['install', 'dnslib', 'requests'])
import dnslib

print("启动智能 DNS 服务器...")
run_dns_server()

设置脚本权限:

1
chmod +x ~/smart_dns/standalone_smart_dns.py

3. 创建 Conda 环境并安装依赖

为了避免与系统 Python 包发生冲突,我们将使用 miniconda 创建一个独立的 Python 环境。如果你还没有安装 miniconda,请先安装。

1
2
3
4
5
6
7
8
# 创建专用的 Conda 环境
conda create -n smartdns python=3.11 -y

# 激活环境
conda activate smartdns

# 安装必要的 Python 包
conda install -c conda-forge dnslib requests -y

注意:使用 Conda 环境可以避免使用 --break-system-packages,这是一种更安全的安装第三方 Python 包的方式。

4. 创建 LaunchDaemon

由于 DNS 服务需要在特权端口 (53) 上运行,我们将创建一个 LaunchDaemon 以确保有足够的权限:

1
sudo nano /Library/LaunchDaemons/com.user.smartdns.plist

写入以下内容:

(注意:将 yourusername 替换为你的用户名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.smartdns</string>
<key>ProgramArguments</key>
<array>
<string>/Users/yourusername/miniconda3/envs/smartdns/bin/python3</string>
<string>/Users/yourusername/smart_dns/standalone_smart_dns.py</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/smartdns.err</string>
<key>StandardOutPath</key>
<string>/tmp/smartdns.out</string>
</dict>
</plist>

提示:如果不确定你的用户名,可以在终端中运行 whoami 命令查看。同时,确保 Python 路径正确,可以运行 which python3 查看 Python 安装路径。

5. 加载 LaunchDaemon

1
sudo launchctl load /Library/LaunchDaemons/com.user.smartdns.plist

6. 配置系统使用本地 DNS

1
sudo networksetup -setdnsservers Wi-Fi 127.0.0.1

如果 Wi-Fi 接口不存在:

1
2
3
4
5
# 首先查看当前活跃的网络接口
networksetup -listallnetworkservices

# 根据上面的输出,使用正确的网络接口名称(可能是 Wi-Fi、Ethernet 等)
sudo networksetup -setdnsservers <你的网络接口名称> 127.0.0.1

测试 DNS 解析

1
2
3
4
5
6
7
8
# 测试国内域名
dig @127.0.0.1 baidu.com

# 测试被污染域名
dig @127.0.0.1 google.com

# 测试有分区域解析的域名
dig @127.0.0.1 www.zhihu.com

正常情况下,应该能够得到正确的 IP 地址。对于 google.com,应该返回真实的 Google IP。对于知乎等国内网站,应该返回适合你网络环境的国内 IP。

优化与排错

查看日志

如果遇到问题,可以查看 DNS 服务器日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看 Python DNS 服务器日志
cat /tmp/smartdns.out

# 显示的日志内容应该类似:
# 查询: ssl.gstatic.com. (A) 来自 127.0.0.1
# 为 ssl.gstatic.com. (A) 使用中国 DNS
# 查询: api2.cursor.sh. (A) 来自 127.0.0.1
# 查询: api2.cursor.sh. (AAAA) 来自 127.0.0.1
# 为 api2.cursor.sh. (A) 使用国外 DNS
# 为 api2.cursor.sh. (AAAA) 使用国外 DNS

cat /tmp/smartdns.err
cat /tmp/smartdns.log

处理特殊域名

如果某些特定域名解析有问题,可以直接编辑 hosts 文件:

1
sudo nano /etc/hosts

添加:

1
2
# 特殊域名手动指定 IP
123.45.67.89 problem-domain.com

使用负载均衡增强可靠性

如果单一 DNS 服务偶尔出现失败,可以在脚本中添加多个国内/国外 DNS 服务器并随机选择,提高可用性:

1
2
3
4
5
6
7
8
9
# 国内 DNS 列表
CHINA_DNS_SERVERS = ["114.114.114.114", "223.5.5.5", "119.29.29.29"]
# 国外 DNS 列表
FOREIGN_DNS_SERVERS = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]

# 随机选择一个 DNS 服务器
import random
CHINA_DNS = random.choice(CHINA_DNS_SERVERS)
FOREIGN_DNS = random.choice(FOREIGN_DNS_SERVERS)

恢复默认 DNS 设置

如果您需要停止使用本地防污染 DNS 并恢复系统默认 DNS 设置,可以按照以下步骤操作:

  1. 停止 DNS 服务:

    1
    sudo launchctl unload /Library/LaunchDaemons/com.user.smartdns.plist
  2. 恢复默认 DNS 设置:

    1
    sudo networksetup -setdnsservers Wi-Fi "Empty"

    如果 Wi-Fi 接口不存在:

    1
    2
    3
    4
    5
    # 首先查看当前活跃的网络接口
    networksetup -listallnetworkservices

    # 根据上面的输出,使用正确的网络接口名称(可能是 Wi-Fi、Ethernet 等)
    sudo networksetup -setdnsservers <你的网络接口名称> "Empty"

Windows 系统上的实现

在 Windows 系统上,轻量级 Python DNS 服务器的实现步骤如下:

1. 安装 Python 环境

  1. Python 官网下载并安装 Python 3.x
  2. 安装必要的 Python 包:
    1
    pip install dnslib requests

2. 创建智能 DNS 服务器脚本

  1. 创建目录:

    1
    mkdir C:\SmartDNS
  2. 创建脚本文件 C:\SmartDNS\standalone_smart_dns.py,内容与 macOS 版本基本相同,只需将路径相关配置做适当调整:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 配置项
    LISTEN_IP = "127.0.0.1"
    LISTEN_PORT = 53
    CHINA_DNS = "114.114.114.114"
    FOREIGN_DNS = "1.1.1.1"
    CHINA_IP_LIST_URL = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute.txt"
    CHINA_IPV6_LIST_URL = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute_v6.txt"
    CHINA_IP_CACHE_FILE = os.path.join(os.path.expanduser("~"), "china_ip_list.txt")
    CHINA_IPV6_CACHE_FILE = os.path.join(os.path.expanduser("~"), "china_ipv6_list.txt")

3. 创建 Windows 服务

为了让 Python 脚本能作为 Windows 服务运行,我们需要使用 NSSM(Non-Sucking Service Manager)工具:

  1. 下载 NSSM

  2. 解压到一个合适的目录,比如 C:\SmartDNS\nssm

  3. 以管理员身份运行命令提示符,然后运行:

    1
    2
    cd C:\SmartDNS\nssm\win64
    nssm.exe install SmartDNS
  4. 在弹出的配置窗口中设置:

    • Path: C:\Windows\System32\python.exe
    • Startup directory: C:\SmartDNS
    • Arguments: C:\SmartDNS\standalone_smart_dns.py
    • 在 “Details” 选项卡设置服务显示名称和描述
    • 在 “Log on” 选项卡选择 “Local System account”
    • 点击 “Install service”

注意:Windows 中,端口 53 是特权端口,Python 脚本需要以管理员权限运行才能绑定此端口。上面的服务设置使用 “Local System account” 可以解决这个问题。如果仍然遇到权限问题,可以考虑将脚本中的监听端口改为非特权端口(如 5353),然后通过防火墙规则将 UDP 53 端口的请求转发到 5353。

  1. 启动服务:
    1
    sc start SmartDNS

4. 配置系统使用本地 DNS

  1. 打开控制面板 > 网络和 Internet > 网络和共享中心
  2. 点击当前活动网络连接
  3. 点击”属性”
  4. 选择”Internet 协议版本 4(TCP/IPv4)”,点击”属性”
  5. 选择”使用下面的 DNS 服务器地址”
  6. 将首选 DNS 服务器设置为”127.0.0.1”
  7. 点击”确定”保存设置

5. 验证测试

在命令提示符中执行:

1
2
nslookup baidu.com 127.0.0.1
nslookup google.com 127.0.0.1

如果设置正确,应该能够正确解析域名。

6. 恢复默认设置

如果需要恢复默认设置:

  1. 停止 DNS 服务:

    1
    2
    sc stop SmartDNS
    sc delete SmartDNS
  2. 恢复默认 DNS 设置:

    • 打开控制面板 > 网络和 Internet > 网络和共享中心
    • 点击当前活动网络连接
    • 点击 “属性”
    • 选择 “Internet 协议版本 4(TCP/IPv4)”,点击 “属性”
    • 选择 “自动获取 DNS 服务器地址”
    • 点击 “确定”保存设置

结语

本地防污染 DNS 与三层隧道的结合,为我们提供了一种优雅的解决方案,既避免了 DNS 污染问题,又保证了访问国内外网站的最佳速度。这种方案特别适合需要同时访问国内外资源的用户。

当你同时使用本地防污染 DNS 和三层隧道(配置了分区域路由)时,将获得以下优势:

  1. 解析无污染:所有域名都能获得正确的 IP 地址
  2. 访问高效
    • 国内网站的 DNS 查询直接走本地网络,获得最适合你网络环境的 IP
    • 国外网站的 DNS 查询通过隧道进行,避免污染,并能获取到接近隧道出口位置的 CDN IP
    • 访问国内网站时走直连,速度快
    • 访问国外网站时走隧道,稳定可靠
  3. 全自动分流:系统会自动判断走哪条线路,无需手动切换 DNS 或代理

Comments