本指南详细介绍如何在 WSL2(Windows Subsystem for Linux 2)中,通过安装 dnsmasq 替代默认 DNS Resolver,搭配 gfwlist.conf 进行域名分流,并配合 https_dns_proxy 进行安全 DNS 查询(DoH),结合 iptables 以及运行在特定端口的本地代理服务(HTTP 代理与 TCP REDIR 代理)实现本地网络层透明代理。
这能彻底解决国内开发环境下没有软路由时,由于境外 DNS 污染和网络封锁导致 antigravity-cli 无法运行的问题。
- DNS 污染:国内网络对境外域名(如 Google DeepMind、GitHub、HuggingFace 等)存在严重的 DNS 污染,导致域名被解析到错误的 IP 地址,造成连接超时或拒绝。
- WSL2 网络隔离:WSL2 拥有独立的 Linux 内核和虚拟网络栈。默认情况下,它自动生成
/etc/resolv.conf指向宿主机的虚拟网卡,无法直接享受宿主机上普通代理软件的全局代理效果。 - antigravity-cli 的强网络依赖:
antigravity-cli内部需要频繁进行境外 API 的高频通信。在没有透明代理的情况下,即使在终端设置了export HTTPS_PROXY,部分底层的非标准 TCP/UDP 流量或特定三方库仍然会绕过环境变量直连,从而运行报错。 - 解决方案:
在 WSL2 内部署透明代理。通过 dnsmasq 接管本机的 DNS 请求:国内域名直连国内公共 DNS,解析 IP 自动记录至
chnroute(国内 IP 段)中;境外域名则分流给通过加密通道运作的 https_dns_proxy (DoH),解析 IP 自动记录至gfwip集合。随后,利用 iptables 将所有目标为gfwip集合的 TCP 流量重定向到本地运行的 TCP REDIR 代理服务,从而实现无感的底层透明代理。
graph TD
UserRequest[antigravity-cli / 命令行网络请求] --> DNS_Query{发起 DNS 查询}
DNS_Query -->|目标域名| Dnsmasq[dnsmasq :53]
Dnsmasq -->|国内域名| ChinaDNS[国内公共 DNS: 223.5.5.5 / 119.29.29.29]
ChinaDNS -->|解析 IP| AddToChnroute[加入 ipset chnroute]
Dnsmasq -->|境外域名 gfwlist / custom_gfwlist| HttpsDnsProxy[https_dns_proxy :15353 / :15454]
HttpsDnsProxy -->|加密 DoH 请求| Local_HTTP_Proxy[本地 HTTP 代理 :10800]
Local_HTTP_Proxy -->|海外节点| SafeDNS[安全 DoH 解析: Cloudflare / Google]
SafeDNS -->|未污染 IP| AddToGfwip[加入 ipset gfwip]
UserRequest -->|发起 TCP 连接| IptablesRules{iptables 规则分流}
IptablesRules -->|目标 IP 属于 chnroute / 局域网| Direct[直连网络]
IptablesRules -->|目标 IP 属于 gfwip| Local_REDIR_Proxy[本地 TCP REDIR 代理 :10801]
Local_REDIR_Proxy -->|加密隧道| OverseasServer[海外代理服务器] -->|访问| TargetServer[境外 API 目标服务器]
我们提供了一个自动化的部署脚本 deploy.sh,用以一键安装依赖、拉取并编译 https_dns_proxy、部署配置文件并设置相关权限。
- 请确保你在当前项目根目录下。
- 运行以下命令执行部署脚本:
sudo ./deploy.sh
该脚本会自动执行以下流程:
- 使用
apt安装所有需要的系统工具和编译依赖(ipset,iptables,jq,cmake,g++ 等)。 - 克隆 https_dns_proxy 项目源码并在临时目录中将其完成编译、安装到
/usr/local/bin/中。 - 自动部署
dnsmasq.conf至系统的/etc/dnsmasq.conf目录(附带原文件备份)。 - 自动创建并将
/var/log/dnsmasq.log日志文件权限修改为 777,解决启动时权限报错且方便非 root 用户查看日志。 - 下载最新的国内 IP 白名单
chnroute.ipset部署到指定目录。 - 拷贝项目下的规则与脚本文件(
gfwlist.conf、start-redir、manage_rules.py)到系统对应的配置及运行目录,并设置好可执行权限。
如果你选择手动部署,或需要检查相关配置,请务必关注以下核心文件的具体路径:
在我们的方案中,配置与规则文件存放路径如下:
- 境外域名分流配置文件:
/etc/dnsmasq.d/gfwlist.conf - 国内 IP 过滤段配置文件:
/etc/chinadns-ng/chnroute.ipset
在系统的 /etc/dnsmasq.conf 配置文件中,包含了以下几处对分流和安全至关重要的修改:
-
① 开启模块化配置子目录(核心):
conf-dir=/etc/dnsmasq.d/,*.conf这行配置表明 dnsmasq 在启动时会加载
/etc/dnsmasq.d/目录下所有以.conf结尾的配置文件。这使得我们可以把庞大的gfwlist.conf和用户自定义的custom_gfwlist.conf直接丢进该目录下,无需修改主配置文件,从而实现优雅的配置分离。 -
② 启用 IPv6 解析过滤:
filter-AAAA$(nproc)参数指定 dnsmasq 丢弃所有的 IPv6 域名解析(AAAA 记录)请求。这对于国内 IPv6 环境不佳或为了防止境外 IPv6 地址泄露和干扰分流规则非常有效,确保所有的解析都走受控的 IPv4 链路。
-
③ 开启 DNS 查询日志(供调试排查):
log-queries log-facility=/var/log/dnsmasq.log将所有 DNS 解析过程和解析结果记录在指定的日志文件
/var/log/dnsmasq.log中,方便在透明代理出现断网或分流异常时进行追踪排查。[!IMPORTANT] 日志文件权限注意事项:由于
/var/log目录默认只有root权限可写,而dnsmasq服务通常以非 root 的dnsmasq用户身份运行。为了防止其因无权限创建或打开该日志文件而导致服务启动失败,必须手动创建该文件,将其所有权赋予dnsmasq用户,并将文件权限设置为777(以方便非 root 用户免 sudo 直接查询解析日志)(使用一键部署脚本时已自动处理):sudo touch /var/log/dnsmasq.log sudo chown dnsmasq: /var/log/dnsmasq.log sudo chmod 777 /var/log/dnsmasq.log
在运行透明代理前,你需要在本地(WSL2 内部或宿主机可通过 WSL2 访问的地址)运行代理客户端,并确保提供以下两个代理端口:
- HTTP 代理(默认端口
10800):供 https_dns_proxy 建立安全 DoH 加密查询通道。 - TCP REDIR 透明代理(默认端口
10801):供 iptables 规则重定向 TCP 流量。
Tip
常见的客户端如 Clash、v2ray、shadowsocks-rust 等均可配置并同时提供上述两种端口的代理服务。
为了防止被代理的流量再次进入代理链中形成死循环,控制脚本需要将代理节点(海外服务器)的公网 IP 段在 iptables 分流链中进行直连绕过(RETURN)。
在控制脚本中,这部分 IP 解析与防火墙绕过规则的自动清空/添加逻辑示例如下:
while iptables -t nat -L WSL_SPROXY --line-numbers -n | grep -q "SS_SERVER_BYPASS"; do
LINE=$(iptables -t nat -L WSL_SPROXY --line-numbers -n | grep "SS_SERVER_BYPASS" | head -n 1 | awk '{print $1}')
iptables -t nat -D WSL_SPROXY "$LINE"
done
server=$(jq -r ".server" /etc/shadowsocks-rust/config.json)
DNS_SERVER="223.5.5.5"
# 优先尝试匹配 IP,若匹配失败(||)则通过指定 DNS 解析域名
ips=$(echo "$server" | grep -E '^([0-9]{1,3}.){3}[0-9]{1,3}$' || dig @"$DNS_SERVER" +short A "$server" | grep -E '^([0-9]{1,3}.){3}[0-9]{1,3}$')
if [ -n "$ips" ]; then
for ip in $ips; do
echo "Adding bypass rule for IP: $ip"
iptables -t nat -I WSL_SPROXY -d "$ip" -m comment --comment "SS_SERVER_BYPASS" -j RETURN
done
fi如果使用其他代理客户端,可根据自身客户端配置文件格式,提取 server 配置,或直接将 server 变量赋值为你的节点 IP/域名,即可自动实现节点流量的直连绕过。
使用 /etc/init.d/start-redir 脚本,可以一键开启/关闭透明代理。
确保你的本地 HTTP 与 REDIR 代理服务已就绪,运行:
sudo /etc/init.d/start-redir startsudo /etc/init.d/start-redir status如果成功,您应该看到类似下面的输出:
https_dns_proxy (15353): Running (PID 12345)
https_dns_proxy (15354): Running (PID 12346)
iptables redirect: Active
需要回退网络状态或关闭代理时,运行:
sudo /etc/init.d/start-redir stop为了方便批量增加/删减 dnsmasq 配置中的境外域名代理规则,我们提供了一个工具脚本 manage_rules.py。该脚本操作单独的自定义规则文件 /etc/dnsmasq.d/custom_gfwlist.conf,既不会破坏体积庞大的 gfwlist.conf,又能灵活覆盖自定义的被墙域名。
管理工具已被一键脚本自动部署并命名为 manage-rules 存入 /usr/local/bin。你可以在任意目录下直接运行:
sudo manage-rules add google.com openai.com huggingface.co -rsudo manage-rules remove google.com -rmanage-rules list如果你需要手动下载或更新 chnroute.ipset 以保持分流规则准确,可以运行:
# 下载最新版 chnroute.ipset (以 misakaio/chnroutes 开源源为例)
curl -sSL -o chnroute.ipset https://raw.githubusercontent.com/misakaio/chnroutes/master/chnroute.ipset
sudo mkdir -p /etc/chinadns-ng
sudo cp chnroute.ipset /etc/chinadns-ng/chnroute.ipset以下是部署在 /etc/init.d/start-redir 中的完整控制脚本:
#!/bin/bash
### BEGIN INIT INFO
# Provides: start-redir
# Required-Start: $network $local_fs
# Required-Stop: $network $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start redirect proxy socks
# Description: Manage https_dns_proxy, chinadns-ng and iptables redirect rules
### END INIT INFO
# PID 文件路径
PID_DIR="/var/run"
DNS_PROXY_15353_PID="$PID_DIR/https_dns_proxy_15353.pid"
DNS_PROXY_15354_PID="$PID_DIR/https_dns_proxy_15354.pid"
CHINADNS_PID="$PID_DIR/chinadns-ng.pid"
RESOLV_BAK="/etc/resolv.conf.redir.bak"
# 检查权限
if [ "$EUID" -ne 0 ]; then
echo "Error: This script must be run as root." >&2
exit 1
fi
function start_dns() {
https_dns_proxy \
-a 127.0.0.1 \
-p 15353 \
-b 1.1.1.1,1.0.0.1 \
-r "https://cloudflare-dns.com/dns-query" \
-t "http://127.0.0.1:10800" \
-u nobody \
-4 &
echo $! > "$DNS_PROXY_15353_PID"
https_dns_proxy \
-a 127.0.0.1 \
-p 15354 \
-b 8.8.8.8,8.8.4.4 \
-r "https://dns.google/dns-query" \
-t "http://127.0.0.1:10800" \
-u nobody \
-4 &
echo $! > "$DNS_PROXY_15354_PID"
}
function init_ipset() {
ipset create chnroute hash:net family inet -exist
ipset create gfwip hash:net family inet -exist
ipset -F chnroute
ipset -R -exist < /etc/chinadns-ng/chnroute.ipset
iptables -t nat -N WSL_SPROXY 2>/dev/null || true
iptables -t nat -F WSL_SPROXY
iptables -t nat -D OUTPUT -p tcp -j WSL_SPROXY 2>/dev/null || true
iptables -t nat -A OUTPUT -p tcp -j WSL_SPROXY
# 启动脚本会自动从 /etc/shadowsocks-rust/config.json 读取 server
# 并把解析出的所有 IPv4 逐条加入 RETURN 白名单
server=$(jq -r ".server" /etc/shadowsocks-rust/config.json)
DNS_SERVER="223.5.5.5"
# 优先尝试匹配 IP,若匹配失败(||)则通过指定 DNS 解析域名
ips=$(echo "$server" | grep -E '^([0-9]{1,3}.){3}[0-9]{1,3}$' || dig @"$DNS_SERVER" +short A "$server" | grep -E '^([0-9]{1,3}.){3}[0-9]{1,3}$')
if [ -n "$ips" ]; then
for ip in $ips; do
echo "Adding bypass rule for IP: $ip"
iptables -t nat -I WSL_SPROXY -d "$ip" -m comment --comment "SS_SERVER_BYPASS" -j RETURN
done
fi
# iptables -t nat -A WSL_SPROXY -d "$ip" -m comment --comment "SS_SERVER_BYPASS" -j RETURN
iptables -t nat -A WSL_SPROXY -d 0.0.0.0/8 -j RETURN
iptables -t nat -A WSL_SPROXY -d 10.0.0.0/8 -j RETURN
iptables -t nat -A WSL_SPROXY -d 100.64.0.0/10 -j RETURN
iptables -t nat -A WSL_SPROXY -d 127.0.0.0/8 -j RETURN
iptables -t nat -A WSL_SPROXY -d 169.254.0.0/16 -j RETURN
iptables -t nat -A WSL_SPROXY -d 172.16.0.0/12 -j RETURN
iptables -t nat -A WSL_SPROXY -d 192.168.0.0/16 -j RETURN
iptables -t nat -A WSL_SPROXY -d 224.0.0.0/4 -j RETURN
iptables -t nat -A WSL_SPROXY -d 240.0.0.0/4 -j RETURN
iptables -t nat -I WSL_SPROXY -d 119.29.29.29 -j RETURN
iptables -t nat -I WSL_SPROXY -d 223.5.5.5 -j RETURN
iptables -t nat -I WSL_SPROXY -d 180.76.76.76 -j RETURN
iptables -t nat -I WSL_SPROXY -d 114.114.114.114 -j RETURN
iptables -t nat -A WSL_SPROXY -m set --match-set chnroute dst -j RETURN
iptables -t nat -A WSL_SPROXY -p tcp -m set --match-set gfwip dst -j REDIRECT --to-ports 10801
iptables -t nat -A WSL_SPROXY -p udp -m set --match-set gfwip dst -j REDIRECT --to-ports 10801
}
function start_dnsmasq() {
# chinadns-ng -C /etc/chinadns-ng/chinadns.conf &
# echo $! > "$CHINADNS_PID"
/etc/init.d/dnsmasq restart
}
function set_resolv_conf() {
# 备份现有的 resolv.conf(如果它不是 127.0.0.1 且没有备份过)
if [ ! -f "$RESOLV_BAK" ]; then
if [ -f /etc/resolv.conf ] && ! grep -q "127.0.0.1" /etc/resolv.conf; then
if [ -L /etc/resolv.conf ]; then
readlink /etc/resolv.conf > "$RESOLV_BAK"
else
cp /etc/resolv.conf "$RESOLV_BAK"
fi
fi
fi
# 写入指向本地 dnsmasq
rm -f /etc/resolv.conf
echo "nameserver 127.0.0.1" > /etc/resolv.conf
}
function restore_resolv_conf() {
if [ -f "$RESOLV_BAK" ]; then
local content=$(cat "$RESOLV_BAK")
rm -f /etc/resolv.conf
if [[ "$content" == /* ]]; then
ln -s "$content" /etc/resolv.conf
else
echo "$content" > /etc/resolv.conf
fi
rm -f "$RESOLV_BAK"
else
# 如果没有备份,还原为公共 DNS,避免断网
rm -f /etc/resolv.conf
echo -e "nameserver 223.5.5.5\nnameserver 119.29.29.29" > /etc/resolv.conf
fi
}
kill_pid() {
local pid_file=$1
if [ -f "$pid_file" ]; then
local pid=$(cat "$pid_file")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null
for i in {1..10}; do
sleep 0.2
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null
fi
fi
rm -f "$pid_file"
fi
}
function clean_ipset_iptables() {
iptables -t nat -D OUTPUT -p tcp -j WSL_SPROXY 2>/dev/null || true
iptables -t nat -F WSL_SPROXY 2>/dev/null || true
iptables -t nat -X WSL_SPROXY 2>/dev/null || true
ipset destroy chnroute 2>/dev/null || true
ipset destroy gfwip 2>/dev/null || true
}
function start() {
echo "Starting redirection proxy services..."
set_resolv_conf
init_ipset
if [ $? -ne 0 ]; then
echo "Failed to initialize ipset/iptables."
stop
exit 1
fi
start_dns
start_dnsmasq
echo "Started successfully."
}
function stop() {
echo "Stopping redirection proxy services..."
restore_resolv_conf
/etc/init.d/dnsmasq stop 2>/dev/null || true
kill_pid "$CHINADNS_PID"
kill_pid "$DNS_PROXY_15353_PID"
kill_pid "$DNS_PROXY_15354_PID"
clean_ipset_iptables
echo "Stopped."
}
function status() {
local running=0
if [ -f "$DNS_PROXY_15353_PID" ] && kill -0 $(cat "$DNS_PROXY_15353_PID") 2>/dev/null; then
echo "https_dns_proxy (15353): Running (PID $(cat "$DNS_PROXY_15353_PID"))"
else
echo "https_dns_proxy (15353): Stopped"
running=1
fi
if [ -f "$DNS_PROXY_15354_PID" ] && kill -0 $(cat "$DNS_PROXY_15354_PID") 2>/dev/null; then
echo "https_dns_proxy (15354): Running (PID $(cat "$DNS_PROXY_15354_PID"))"
else
echo "https_dns_proxy (15354): Stopped"
running=1
fi
# if [ -f "$CHINADNS_PID" ] && kill -0 $(cat "$CHINADNS_PID") 2>/dev/null; then
# echo "chinadns-ng: Running (PID $(cat "$CHINADNS_PID"))"
# else
# echo "chinadns-ng: Stopped"
# running=1
# fi
if iptables -t nat -C OUTPUT -p tcp -j WSL_SPROXY 2>/dev/null; then
echo "iptables redirect: Active"
else
echo "iptables redirect: Inactive"
running=1
fi
return $running
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 1
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac