用 OpenVPN 访问 ChatGPT 后,国内网站怎么反而慢了?

本机使用 Tunnelblick / OpenVPN 访问 ChatGPT、Claude,同时保留国内 IP 直连。DNS 也需要分流,否则国内网站可能解析到海外节点,后面的连接跟着绕路。

以前按域名手动指定 DNS,这次改用 KixDNS 和 GeoSite 自动分流,再加上本地缓存。环境是 Apple Silicon Mac,记录下配置和维护过程。

国内网站为什么会绕到国外

应用先解析域名,再按目标 IP 路由。OpenVPN 按 IP 分流,DNS 返回的地址会影响后面走哪条路。

比如国内网站的查询跟着 VPN 发到海外 DNS。部分使用 CDN 的网站会根据解析器位置或查询携带的客户端子网信息选择节点,可能返回靠近海外解析器或代理出口的国外节点 IP。本机拿到这个 IP 后,又按海外路由走 VPN,访问国内网站也绕到了国外,速度就慢了。

国内域名使用国内 DNS,可以减少这种绕路。CDN 调度仍由网站决定,配置后需要检查实际目标路由。VPN 出口拥塞和带宽不足等原因,要另外排查。



flowchart TD
    A[访问国内网站] --> B[查询经 VPN 发往海外 DNS]
    B --> C[CDN 可能返回海外节点 IP]
    C --> D[目标 IP 命中海外路由]
    D --> E[网站连接也经过 VPN]
    E --> F[国内访问绕路变慢]

以前通过域名手动指定 DNS

以前 DNS 分流是在 macOS 的 /etc/resolver/ 里手动配的,按域名指定 nameserver。比如阿里云、腾讯云使用国内 DNS,需要海外解析的域名单独指定海外 DNS。

几个固定网站这样配就够用。但新增网站要逐个补规则,登录、接口和静态资源还可能使用不同域名,漏掉的查询仍走默认 DNS。

这次保留原来的上游选择,把规则迁入 KixDNS。GeoSite 负责覆盖更多域名,少量例外继续手动指定。

改用 KixDNS 后自动处理什么

系统统一查询本地 KixDNS。规则按自定义、GeoSite cn、默认海外的顺序匹配。旧的域名专用配置也已调整为查询本地服务,避免绕过它。

国内使用阿里 DoH,海外按 Cloudflare 1.1.1.1、Quad9 9.9.9.9、Google 8.8.8.8 的顺序回退。规则库已收录的域名自动分类,有遗漏或特殊需求时再加例外。

项目 以前手动配置 本地 KixDNS
域名分流 每个域名单独指定 DNS GeoSite 自动分类,自定义规则覆盖
规则维护 新网站逐个补配置 更新分类库,少量例外单独维护
重复查询 主要依赖系统和上游缓存 增加统一的本地缓存和后台刷新
检查问题 检查系统 resolver 和目标路由 日志直接显示管道、上游和缓存命中

缓存和后台刷新减少重复查询的等待,上游短暂失败时可在限定时间内返回旧缓存。解析速度的实测放在后面。

安全方面,应用到 KixDNS 的查询留在本机回环地址,KixDNS 到上游使用 DoH,开启 TLS 加密和证书校验,降低这段传输中查询被读取或响应被篡改的风险。转发时清除 ECS,不主动附带客户端子网信息。上游仍能看到收到的域名查询,所以要选择信任的解析服务。

服务只监听 127.0.0.1。浏览器自带 DNS、应用自行解析或 VPN 接管 DNS,都可能绕过它,需要单独检查。

整体访问流程

DNS 和访问流量分别处理。



flowchart TD
    A[应用发起 DNS 查询] --> B["本地 KixDNS<br/>127.0.0.1:53"]
    B --> C{缓存是否命中}
    C -->|命中| G[返回解析结果]
    C -->|未命中| D{域名分流}
    D -->|国内域名| E[阿里 DoH]
    D -->|海外及其他域名| F[Cloudflare DoH 1.1.1.1]
    E --> G
    F -->|有效响应| G
    F -->|失败| Q[Quad9 DoH 9.9.9.9]
    Q -->|有效响应| G
    Q -->|失败| R[Google DoH 8.8.8.8]
    R -->|有效响应| G
    R -->|仍失败| S[按缓存策略兜底或返回失败]
    G --> H[应用向目标 IP 发起 HTTPS 连接]
    H --> I{macOS 系统 IP 路由}
    I -->|国内目标 IP| J[Wi-Fi 网关直连]
    I -->|海外目标 IP| K[OpenVPN 隧道]
    J --> L[国内网站]
    K --> M[海外网站]

这里沿用已有 OpenVPN 路由,下面只部署 DNS。网站是否走 VPN,最后通过目标路由和实际 HTTPS 连接验证。

编译 KixDNS

源码在 olicesx/kixdns。本次使用 Rust 1.98.1,编译的源码 commit 为 42981abe52ce53271be50b430594e02601e8b615。

1
2
3
4
5
6
7
8
mkdir -p "$HOME/localdns"
cd "$HOME/localdns"
git clone --depth 1 https://github.com/olicesx/kixdns.git
cd kixdns
# 固定到本次验证过的版本
git fetch --depth 1 origin 42981abe52ce53271be50b430594e02601e8b615
git checkout --detach 42981abe52ce53271be50b430594e02601e8b615
cargo +1.98.1 build --release --locked

需要先安装 Rust 1.98.1。本机 rsproxy 连接异常,实际从 /tmp 使用独立 CARGO_HOME 和官方 crates.io 构建,没有修改全局配置。

1
2
3
4
5
mkdir -p "$HOME/localdns/cargo-home"
cd /tmp
CARGO_HOME="$HOME/localdns/cargo-home" cargo +1.98.1 build \
--release --locked \
--manifest-path "$HOME/localdns/kixdns/Cargo.toml"

这里切到 /tmp 也有作用。从用户目录下构建时,Cargo 会沿父目录读取 .cargo/config.toml,仅换 CARGO_HOME 仍可能读到原来的镜像配置。

准备国内域名规则

GeoSite 使用 Loyalsoldier/v2ray-rules-dat 的数据。下载后检查 SHA-256,再安装到系统目录。

1
2
3
4
5
6
7
8
9
10
11
12
set -eu
mkdir -p "$HOME/localdns/data"
cd "$HOME/localdns/data"
RULES_URL=https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download
curl -fL "$RULES_URL/geosite.dat" -o geosite.dat
curl -fL "$RULES_URL/geosite.dat.sha256sum" -o geosite.dat.sha256sum
shasum -a 256 -c geosite.dat.sha256sum

sudo mkdir -p /usr/local/libexec /usr/local/etc/kixdns /usr/local/var/log/kixdns
sudo install -o root -g wheel -m 755 \
"$HOME/localdns/kixdns/target/release/kixdns" /usr/local/libexec/kixdns
sudo install -o root -g wheel -m 644 geosite.dat /usr/local/etc/kixdns/geosite.dat

后续更新方法放在维护部分。

配置分流和缓存



flowchart TD
    A[收到域名查询] --> B{命中自定义规则}
    B -->|是| C[采用指定管道]
    B -->|否| D{命中 GeoSite cn}
    D -->|是| E[国内管道 cn]
    D -->|否| F["海外入口 global<br/>Cloudflare → Quad9 → Google"]

配置保存在 /usr/local/etc/kixdns/pipeline.json。

下面是按本次部署整理的完整配置示例。其中 aliyun.com、cloud.tencent.com 固定走国内,manus.im 固定走海外。自定义规则之后匹配 GeoSite 的 cn,其余域名默认走海外。

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
216
217
218
219
220
221
222
223
224
{
"version": "1.0",
"settings": {
"bind_udp": "127.0.0.1:53",
"bind_tcp": "127.0.0.1:53",
"min_ttl": 0,
"default_upstream": "https://1.1.1.1/dns-query",
"upstream_timeout_ms": 9000,
"request_timeout_ms": 30000,
"cache_capacity": 10000,
"cache_max_ttl": 3600,
"cache_background_refresh": true,
"cache_refresh_threshold_percent": 15,
"cache_refresh_min_ttl": 5,
"serve_stale": true,
"serve_stale_ttl": 30,
"serve_stale_expire_ttl": 300,
"serve_stale_ttl_reset": false,
"serve_stale_client_timeout_ms": 300,
"doh_pool_size": 8,
"geosite_data_paths": [
"/usr/local/etc/kixdns/geosite.dat"
]
},
"pipeline_select": [
{
"pipeline": "cn",
"matchers": [
{
"type": "domain_suffix",
"value": "aliyun.com"
}
]
},
{
"pipeline": "cn",
"matchers": [
{
"type": "domain_suffix",
"value": "cloud.tencent.com"
}
]
},
{
"pipeline": "global",
"matchers": [
{
"type": "domain_suffix",
"value": "manus.im"
}
]
},
{
"pipeline": "cn",
"matchers": [
{
"type": "geo_site",
"value": "cn"
}
]
},
{
"pipeline": "global",
"matchers": [
{
"type": "any"
}
]
}
],
"pipelines": [
{
"id": "global",
"ecs": {
"mode": "clear"
},
"rules": [
{
"name": "cloudflare-primary",
"matchers": [
{
"type": "any"
}
],
"actions": [
{
"type": "forward",
"upstream": "https://1.1.1.1/dns-query",
"transport": "doh",
"ecs": {
"mode": "clear"
}
}
],
"response_matchers": [
{
"type": "response_rcode",
"value": "NOERROR"
},
{
"type": "response_rcode",
"value": "NXDOMAIN"
}
],
"response_matcher_operator": "or",
"response_actions_on_match": [
{
"type": "allow"
}
],
"response_actions_on_miss": [
{
"type": "jump_to_pipeline",
"pipeline": "global-quad9"
}
]
}
]
},
{
"id": "cn",
"rules": [
{
"name": "cn-doh",
"matchers": [
{
"type": "any"
}
],
"actions": [
{
"type": "forward",
"upstream": [
"https://223.5.5.5/dns-query",
"https://223.6.6.6/dns-query"
],
"transport": "doh",
"ecs": {
"mode": "clear"
}
}
]
}
],
"ecs": {
"mode": "clear"
}
},
{
"id": "global-quad9",
"ecs": {
"mode": "clear"
},
"rules": [
{
"name": "quad9-backup",
"matchers": [
{
"type": "any"
}
],
"actions": [
{
"type": "forward",
"upstream": "https://9.9.9.9/dns-query",
"transport": "doh",
"ecs": {
"mode": "clear"
}
}
],
"response_matchers": [
{
"type": "response_rcode",
"value": "NOERROR"
},
{
"type": "response_rcode",
"value": "NXDOMAIN"
}
],
"response_matcher_operator": "or",
"response_actions_on_match": [
{
"type": "allow"
}
],
"response_actions_on_miss": [
{
"type": "jump_to_pipeline",
"pipeline": "global-google"
}
]
}
]
},
{
"id": "global-google",
"ecs": {
"mode": "clear"
},
"rules": [
{
"name": "google-last-resort",
"matchers": [
{
"type": "any"
}
],
"actions": [
{
"type": "forward",
"upstream": "https://8.8.8.8/dns-query",
"transport": "doh",
"ecs": {
"mode": "clear"
}
}
]
}
]
}
]
}

国内两条阿里 DoH 并发查询。海外采用三个独立管道串联,先查 global 中的 Cloudflare,失败后跳到 global-quad9,最后才是 global-google。不要把三个地址写成同一个 upstream 数组,那会变成并发取最快响应。

NOERROR 和 NXDOMAIN 都作为有效结果停止回退;超时、SERVFAIL、REFUSED 等错误才进入下一家。这是 DNS 查询失败后的回退,网站连接到某个 IP 超时,不会自动换 DNS。上游使用 IP 地址,避免启动时依赖自身解析。



flowchart TD
    A[海外域名未命中缓存] --> B["global<br/>Cloudflare 1.1.1.1"]
    B -->|有效响应| E[返回结果]
    B -->|查询失败| C["global-quad9<br/>Quad9 9.9.9.9"]
    C -->|有效响应| E
    C -->|查询失败| D["global-google<br/>Google 8.8.8.8"]
    D -->|有效响应| E
    D -->|仍失败| F[按缓存策略兜底或返回失败]

原始测速使用 3 秒上游超时。复核时 VPN 下冷查询约需 4 秒,3 秒会误报 SERVFAIL,所以文中示例放宽到 9 秒,整体请求上限改为 30 秒,覆盖三家顺序尝试。加长超时只避免过早失败,不会让正常查询变快,仍需检查 VPN 和上游连通性。

缓存容量是 10000 条,尊重上游原始 TTL,最长保留 1 小时。TTL 剩余 15% 时触发后台刷新。上游暂时失败时允许返回旧缓存,最多过期 5 分钟,返回的 TTL 为 30 秒,并且关闭过期窗口重置,避免旧记录一直续下去。服务重启后内存缓存清空。

国内上游选阿里的两个已验证地址。DNSPod 的 IP HTTPS 地址在当时网络下连接超时,未加入配置。

这里还踩了一个配置字段的问题。文档里出现过 geosite,这次编译出的程序要求写成 geo_site,否则启动时会报 unknown variant。配置文件要用实际运行的版本验证一下。

用 launchd 管理服务

创建 /Library/LaunchDaemons/org.localdns.kixdns.plist。

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
<?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>org.localdns.kixdns</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/libexec/kixdns</string>
<string>run</string>
<string>-c</string><string>/usr/local/etc/kixdns/pipeline.json</string>
<string>--udp-workers</string><string>2</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
<key>WorkingDirectory</key><string>/usr/local/etc/kixdns</string>
<key>EnvironmentVariables</key>
<dict><key>RUST_LOG</key><string>warn,kixdns::engine::phases=info</string></dict>
<key>StandardOutPath</key><string>/usr/local/var/log/kixdns/service.log</string>
<key>StandardErrorPath</key><string>/usr/local/var/log/kixdns/error.log</string>
<key>SoftResourceLimits</key>
<dict><key>NumberOfFiles</key><integer>4096</integer></dict>
<key>HardResourceLimits</key>
<dict><key>NumberOfFiles</key><integer>65536</integer></dict>
</dict>
</plist>

修正权限,检查语法,再启动。已安装过的服务先执行 sudo launchctl bootout system/org.localdns.kixdns,再执行 bootstrap。

1
2
3
4
5
6
7
8
sudo chown root:wheel /Library/LaunchDaemons/org.localdns.kixdns.plist
sudo chmod 644 /Library/LaunchDaemons/org.localdns.kixdns.plist
plutil -lint /Library/LaunchDaemons/org.localdns.kixdns.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/org.localdns.kixdns.plist
sudo launchctl print system/org.localdns.kixdns

dig @127.0.0.1 www.baidu.com
dig @127.0.0.1 www.google.com +tcp

正式部署前先用 1053 端口验证,UDP、TCP、IPv6 记录和并发查询均正常。模拟上游故障时,旧缓存兜底和过期限制也通过了测试。

服务重启和日志维护命令放在后面。

切换 macOS 系统 DNS

先确认服务能解析,再保存原来的设置。

1
2
3
networksetup -listnetworkserviceorder
networksetup -getdnsservers "Wi-Fi"
scutil --dns

本机使用 Wi-Fi,原来没有手动指定 DNS,使用的是网络下发的地址。切换后只指定本地 DNS。

1
sudo networksetup -setdnsservers "Wi-Fi" 127.0.0.1

切换后刷新系统 DNS 缓存,再检查默认 nameserver。

1
2
3
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
scutil --dns

默认 resolver 应指向 127.0.0.1,.local 继续使用 mDNS。

验证 DNS 分流

直接查询可以确认 KixDNS 有回应,再用系统解析确认 macOS 真正在使用它。

1
2
3
4
5
6
7
8
9
10
11
12
dig @127.0.0.1 api.anthropic.com
dig @127.0.0.1 api.openai.com
python3 - <<'PYCODE'
import socket
for domain in ["www.baidu.com", "aliyun.com", "cloud.tencent.com",
"claude.ai", "api.anthropic.com", "chatgpt.com", "api.openai.com"]:
addresses = {item[4][0] for item in socket.getaddrinfo(
domain, 443, socket.AF_INET, socket.SOCK_STREAM)}
print(domain, sorted(addresses))
PYCODE

tail -n 30 /usr/local/var/log/kixdns/service.log

对照日志,本次百度、阿里云和腾讯云使用 cn,Claude、Anthropic API、ChatGPT 和 OpenAI API 使用 global。

系统自身有缓存,重复解析可能不产生新的 KixDNS 日志。

验证访问流量

解析出来的 IP 还要检查路由。选择查询结果中的实际目标地址,执行下面的命令。

1
2
3
4
5
6
7
route -n get 160.79.104.10
# 检查 gateway 和 interface,本机 Claude 目标走 utun4

curl --noproxy '*' -4 -I -sS --connect-timeout 8 --max-time 15 \
-o /dev/null \
-w 'remote=%{remote_ip} local=%{local_ip} http=%{http_code}\n' \
https://claude.ai/

对其它网站同样检查。下面是当时全部解析地址的路由检查和实际 HTTPS 连接结果。

测试网站 DNS 管道 目标 IP 路由 HTTPS 连接源地址
百度 cn Wi-Fi en0 Wi-Fi 地址
阿里云及官网 cn Wi-Fi en0 Wi-Fi 地址
腾讯云及控制台 cn Wi-Fi en0 Wi-Fi 地址
Claude、Anthropic API global VPN utun4 VPN 地址
ChatGPT、OpenAI API global VPN utun4 VPN 地址

本机系统 HTTP/SOCKS 代理没有开启,海外连接由 Tunnelblick / OpenVPN 的系统路由接管。同一隧道访问 Cloudflare trace,出口检测为 SG。

部分 HEAD 请求返回 403、404 等状态,TCP/TLS 已建立。这只验证连接路径,不代表登录或认证 API 可用。VPN 重连或 CDN 地址变化后需要重新检查。

解析速度和缓存效果

下面保留 2026 年 10 月 13 日的原始测速。当时海外上游为 Cloudflare 和 Google 并发,尚未切换为本文现在的三家顺序回退,数据不代表新顺序的首次查询耗时。测试选了 12 个国内外域名。每轮启动一个临时 KixDNS 实例,做 3 轮冷缓存查询。随后在正式服务上预热,再对每个域名做 10 次缓存查询,不清空正在使用的服务缓存。

查询 A 记录,使用 UDP,超时 4 秒,不重试。计时直接从 socket 发出查询到收到响应,不包含进程启动,也不经过系统 DNS 缓存。

下面是耗时中位数,单位毫秒。

查询方式 国内域名 海外域名 整体
本地 DNS,未命中缓存 27.70 168.80 109.53
本地 DNS,命中缓存 0.143 0.142 0.142
直接查询 1.1.1.1 165.39 136.19 151.60
直接查询 8.8.8.8 173.85 165.97 166.36

120 次重复查询全部命中缓存,整体 P95 为 0.256ms。这次国内首次解析比两个海外公共 DNS 快约 6 倍。海外首次解析和直接查询 Google 接近,没测出明显加速,缓存命中后的收益更大。

本地上游走 DoH,对比公共 DNS 走 UDP,公共 DNS 自身的缓存无法清空。这是当前网络下的小样本结果,首次查询仍出现过约 1.2 秒的延迟尖峰,不能拿它当所有网络环境下的性能保证。

Wi-Fi 和有线 DNS 自动恢复

2026 年 11 月补充。设置好本地 DNS 后,还要处理网络重连、唤醒后 DNS 被改写的情况。先检查 VPN 或网络管理软件是否接管 DNS,自动恢复作为兜底。

本机加了一个独立的 launchd 守护,管理当前网络位置中启用、已连接且已有 IP 的 Wi-Fi 和物理以太网。iPhone USB、VPN、网桥和未连接的网卡会跳过。

守护怎么判断和恢复

守护通过 SystemConfiguration 监听网络变化,通过 IOKit 接收唤醒通知,启动时也检查一次。事件合并处理,正常状态不轮询。



flowchart TD
    A[启动、网络变化或唤醒] --> B[筛选已连接的 Wi-Fi 和有线服务]
    B --> C{本地 KixDNS 健康检查通过}
    C -->|否| D[保持 DNS 不变,延迟重试]
    D --> C
    C -->|是| E{已保存 DNS 是否仅为 127.0.0.1}
    E -->|是| F[不改写,必要时记录生效配置差异]
    E -->|否| G[记录原配置,恢复本地 DNS]
    G --> H[回读验证并记录修改前后值]

健康检查向 127.0.0.1:53 查询 _kixdns-health.invalid 的 TXT,必须收到 localdns-guardian-ok。KixDNS 静态应答,不依赖外网。

保存值已为 127.0.0.1 时,守护不与 VPN 争抢解析器。日志保留 macOS 报告的 DHCP DNS,判断服务配置时优先使用手动值,避免误报。

编译和安装

源码包包含代码、测试和安装验证脚本,附 SHA-256 校验文件,不含本机网络配置或账号信息。

需要前文的 KixDNS 服务和带 Swift 的 Xcode Command Line Tools。检查源码和校验结果后编译。

1
2
3
4
5
6
7
8
9
10
11
12
set -eu
mkdir -p "$HOME/localdns-guardian-download"
cd "$HOME/localdns-guardian-download"
curl -fLO https://uublog.com/downloads/localdns-guardian-source.zip
curl -fLO https://uublog.com/downloads/localdns-guardian-source.zip.sha256
shasum -a 256 -c localdns-guardian-source.zip.sha256
unzip localdns-guardian-source.zip
cd localdns-guardian/guardian
swift test
swift build -c release
cd ..
sudo bash scripts/install-dns-guardian.sh

入口脚本先备份 KixDNS 配置,再添加本地健康检查记录,确认应答正确后安装守护。原有国内外解析上游不变。程序安装到 /Library/PrivilegedHelperTools/org.localdns.dns-guardian,以 root 运行,开机启动、异常退出后由 launchd 重启。

plist 安装到 /Library/LaunchDaemons/org.localdns.dns-guardian.plist。RunAtLoad 和 KeepAlive 均启用,异常重启间隔由 ThrottleInterval 限制为 10 秒。完整文件在源码包中,不需要手动重复创建。

验证恢复是否生效

确认 Wi-Fi 已使用本地 DNS 后,在源码包根目录复测恢复和健康检查门禁。

1
2
cd "$HOME/localdns-guardian-download/localdns-guardian"
sudo /usr/bin/python3 scripts/verify-dns-guardian.py

脚本保留 127.0.0.1 为首选,临时增加备用回环地址来制造漂移,再更换健康应答验证拒绝写入和恢复重试。结束后恢复配置并保存记录。

本机的验证结果如下。

检查项 结果
自动测试 52 项通过,含 Wi-Fi、有线恢复、错误健康应答、重试及日志轮转
独立定向复查 健康门禁、接口筛选、DNS 写入和唤醒处理通过
Wi-Fi 真实配置漂移 自动恢复为仅 127.0.0.1
健康检查不通过 DNS 保持不变
健康恢复后的重试 自动完成修复
修改前后日志 值与实际配置一致,没有重复写入循环
有线实网 当时未接线,接口识别和隔离测试通过,未做实网恢复测试
实际睡眠唤醒 通知注册测试通过,未主动休眠电脑验证

后续维护

更新 GeoSite 域名规则

GeoSite 规则可以每周检查一次。遇到分流异常时,先更新分类库,再判断是否需要加自定义规则。



flowchart TD
    A[下载到临时目录] --> B{下载及 SHA-256 校验成功}
    B -->|否| C[结束并保留原规则]
    B -->|是| D[备份现有规则]
    D --> E[安装新文件并替换]
    E --> F[重启服务并清空缓存]
    F --> G{解析与路由是否正常}
    G -->|是| H[完成更新]
    G -->|否| I[恢复备份并重启]

下面的脚本保存为 $HOME/localdns/update-geosite.sh。它先在临时目录下载并校验,成功后备份现有规则,再替换生效文件。下载或校验失败时,原规则不变。

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
#!/bin/sh
set -eu

if [ "$(id -u)" != 0 ]; then
echo "请通过 sudo 执行"
exit 1
fi

RULE_DIR=/usr/local/etc/kixdns
RULE_URL=https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT HUP INT TERM

curl -fLsS --connect-timeout 10 --max-time 180 \
"$RULE_URL/geosite.dat" -o "$WORK_DIR/geosite.dat"
curl -fLsS --connect-timeout 10 --max-time 30 \
"$RULE_URL/geosite.dat.sha256sum" -o "$WORK_DIR/geosite.dat.sha256sum"
(cd "$WORK_DIR" && shasum -a 256 -c geosite.dat.sha256sum)

cp -p "$RULE_DIR/geosite.dat" "$RULE_DIR/geosite.dat.previous"
install -o root -g wheel -m 644 \
"$WORK_DIR/geosite.dat" "$RULE_DIR/geosite.dat.new"
mv -f "$RULE_DIR/geosite.dat.new" "$RULE_DIR/geosite.dat"
launchctl kickstart -k system/org.localdns.kixdns

echo "规则已更新,服务已重启"

执行更新,然后检查服务和解析。

1
2
3
4
chmod +x "$HOME/localdns/update-geosite.sh"
sudo "$HOME/localdns/update-geosite.sh"
dig @127.0.0.1 www.baidu.com
dig @127.0.0.1 api.openai.com

GeoSite 支持热加载。脚本主动重启,清掉旧缓存后重新解析。它只更新 DNS 分类,OpenVPN 的 IP 路由仍需独立维护。

如果更新后有异常,先恢复上一份规则。

1
2
3
sudo cp -p /usr/local/etc/kixdns/geosite.dat.previous \
/usr/local/etc/kixdns/geosite.dat
sudo launchctl kickstart -k system/org.localdns.kixdns

添加自定义域名规则

日志确认分类不合适后,在 pipeline_select 里添加例外,放到 geo_site 和最后的 any 之前。第一个命中的规则生效。

比如要让某个域名及其子域名走国内上游,可以加入下面这个条目,example.com 替换成自己的域名。指定海外上游时把 pipeline 改成 global。

1
2
3
4
5
6
7
8
9
{
"pipeline": "cn",
"matchers": [
{
"type": "domain_suffix",
"value": "example.com"
}
]
}

修改生效配置之前先备份,并检查 JSON 语法。

1
2
3
4
sudo cp -p /usr/local/etc/kixdns/pipeline.json \
/usr/local/etc/kixdns/pipeline.json.previous
sudo vi /usr/local/etc/kixdns/pipeline.json
python3 -m json.tool /usr/local/etc/kixdns/pipeline.json >/dev/null

管道规则支持热加载,无效配置会保留上一份有效配置。JSON 语法检查只能确认文件格式,最终还要检查服务日志和目标域名的实际解析。监听端口、缓存容量等初始化参数修改后需要重启服务。

检查服务与日志

先看系统当前使用的 DNS,再检查两个服务。

1
2
3
4
5
6
networksetup -getdnsservers "Wi-Fi"
scutil --dns
sudo launchctl print system/org.localdns.kixdns
sudo launchctl print system/org.localdns.dns-guardian
/Library/PrivilegedHelperTools/org.localdns.dns-guardian --check-health
sudo /Library/PrivilegedHelperTools/org.localdns.dns-guardian --status

健康检查应显示 PASS。--status 列出各网络服务的识别结果、保存值和报告值,未连接的有线网卡应为 disconnected。保存值由 networksetup 检查,最终解析器再看 scutil --dns,VPN 重连后也这样核对。

两个服务的日志分别查看。

1
2
3
tail -n 30 /usr/local/var/log/kixdns/service.log
cat /usr/local/var/log/kixdns/error.log
sudo tail -n 30 /Library/Logs/LocalDNSGuardian/events.jsonl

KixDNS 日志看 pipeline、upstream 和 cache_hit;守护日志看触发原因、健康状态和修改前后值。

守护日志事件 含义
dns_applied 已恢复 DNS,savedDNS 为修改前的值,savedDNSAfter 应为 127.0.0.1
health_skip 本地健康检查失败,没有写入 DNS
drift_deferred 接口未连接等原因,暂不处理
effective_conflict 保存值已正确,但报告的生效配置存在差异,只记录、不争抢

守护日志达到 1 MiB 时轮转,保留 3 份备份;日志和原始配置快照仅 root 可读。KixDNS 查询日志需要另行轮转,长期使用也可以把它的 plist 中 RUST_LOG 改为 warn,排查时再打开。

重启和更新

只重启 KixDNS 进程使用下面的命令,内存缓存会清空。

1
sudo launchctl kickstart -k system/org.localdns.kixdns

修改 plist 后要用 bootout 和 bootstrap 重新加载,单独 kickstart 不会读取新 plist。下面以守护为例,也可用于临时暂停守护、手动排查 DNS。

1
2
3
sudo launchctl bootout system/org.localdns.dns-guardian
# 在这里修改 plist 或临时排查网络配置
sudo launchctl bootstrap system /Library/LaunchDaemons/org.localdns.dns-guardian.plist

重新加载 KixDNS 时,将上述命令中的服务名同时替换为 org.localdns.kixdns。

更新守护源码后,在 guardian/ 目录重新执行 swift test、swift build -c release,再回到源码包根目录运行 sudo bash scripts/install-dns-guardian.sh。GeoSite 独立更新,不需要重装守护。

卸载守护或停用本地 DNS

只移除守护、继续使用 KixDNS 时,从源码包根目录执行下面的命令。脚本会先停守护,默认保留当前 DNS、日志和快照,不会卸载 KixDNS。

1
2
cd "$HOME/localdns-guardian-download/localdns-guardian"
sudo bash guardian/uninstall.sh

需要卸载时一并恢复原始 DNS,改用 sudo bash guardian/uninstall.sh --restore-original。它只恢复仍为守护目标值的接口,避免覆盖后来的手动修改。原始记录在 /Library/Application Support/LocalDNSGuardian/state.json。

完全停用本地 DNS 时,先停守护,再恢复网络 DNS。本机 Wi-Fi 原先使用网络下发的地址,可以这样恢复。

1
2
3
4
sudo launchctl bootout system/org.localdns.dns-guardian
sudo networksetup -setdnsservers "Wi-Fi" Empty
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

有线网卡按实际服务名恢复,原先手动配置过 DNS 的机器要填回旧地址。域名专用规则也要恢复或移除。确认解析不再依赖本地服务后,最后停 KixDNS,日志和快照继续保留。

1
sudo launchctl bootout system/org.localdns.kixdns

参考

关注公众号 尹安灿