Skip to content

Repository files navigation

WSL2 环境透明代理与 DNS 分流部署文档

本指南详细介绍如何在 WSL2(Windows Subsystem for Linux 2)中,通过安装 dnsmasq 替代默认 DNS Resolver,搭配 gfwlist.conf 进行域名分流,并配合 https_dns_proxy 进行安全 DNS 查询(DoH),结合 iptables 以及运行在特定端口的本地代理服务(HTTP 代理与 TCP REDIR 代理)实现本地网络层透明代理。

这能彻底解决国内开发环境下没有软路由时,由于境外 DNS 污染和网络封锁导致 antigravity-cli 无法运行的问题。


1. 背景与根因分析

  • 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 代理服务,从而实现无感的底层透明代理。

2. 方案网络架构图

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 目标服务器]
Loading

3. 一键自动化部署

我们提供了一个自动化的部署脚本 deploy.sh,用以一键安装依赖、拉取并编译 https_dns_proxy、部署配置文件并设置相关权限。

执行步骤

  1. 请确保你在当前项目根目录下。
  2. 运行以下命令执行部署脚本:
    sudo ./deploy.sh

该脚本会自动执行以下流程:

  • 使用 apt 安装所有需要的系统工具和编译依赖(ipsetiptablesjq,cmake,g++ 等)。
  • 克隆 https_dns_proxy 项目源码并在临时目录中将其完成编译、安装到 /usr/local/bin/ 中。
  • 自动部署 dnsmasq.conf 至系统的 /etc/dnsmasq.conf 目录(附带原文件备份)。
  • 自动创建并将 /var/log/dnsmasq.log 日志文件权限修改为 777,解决启动时权限报错且方便非 root 用户查看日志
  • 下载最新的国内 IP 白名单 chnroute.ipset 部署到指定目录。
  • 拷贝项目下的规则与脚本文件(gfwlist.confstart-redirmanage_rules.py)到系统对应的配置及运行目录,并设置好可执行权限。

4. 关键配置文件与存放路径

如果你选择手动部署,或需要检查相关配置,请务必关注以下核心文件的具体路径:

4.1 gfwlist.conf 与 chnroute.ipset 的存放路径

在我们的方案中,配置与规则文件存放路径如下:

  1. 境外域名分流配置文件/etc/dnsmasq.d/gfwlist.conf
  2. 国内 IP 过滤段配置文件/etc/chinadns-ng/chnroute.ipset

4.2 dnsmasq.conf 中的关键修改说明

在系统的 /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

5. 前置条件与配置

在运行透明代理前,你需要在本地(WSL2 内部或宿主机可通过 WSL2 访问的地址)运行代理客户端,并确保提供以下两个代理端口:

  1. HTTP 代理(默认端口 10800:供 https_dns_proxy 建立安全 DoH 加密查询通道。
  2. TCP REDIR 透明代理(默认端口 10801:供 iptables 规则重定向 TCP 流量。

Tip

常见的客户端如 Clash、v2ray、shadowsocks-rust 等均可配置并同时提供上述两种端口的代理服务。

5.1 配置代理服务端 IP 绕过(避免循环代理)

为了防止被代理的流量再次进入代理链中形成死循环,控制脚本需要将代理节点(海外服务器)的公网 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/域名,即可自动实现节点流量的直连绕过。


6. 启动与服务管理

使用 /etc/init.d/start-redir 脚本,可以一键开启/关闭透明代理。

6.1 启动透明代理与 DNS 分流

确保你的本地 HTTP 与 REDIR 代理服务已就绪,运行:

sudo /etc/init.d/start-redir start

6.2 检查运行状态

sudo /etc/init.d/start-redir status

如果成功,您应该看到类似下面的输出:

https_dns_proxy (15353): Running (PID 12345)
https_dns_proxy (15354): Running (PID 12346)
iptables redirect: Active

6.3 停止透明代理

需要回退网络状态或关闭代理时,运行:

sudo /etc/init.d/start-redir stop

7. 自定义规则管理脚本

为了方便批量增加/删减 dnsmasq 配置中的境外域名代理规则,我们提供了一个工具脚本 manage_rules.py。该脚本操作单独的自定义规则文件 /etc/dnsmasq.d/custom_gfwlist.conf,既不会破坏体积庞大的 gfwlist.conf,又能灵活覆盖自定义的被墙域名。

7.1 使用说明

管理工具已被一键脚本自动部署并命名为 manage-rules 存入 /usr/local/bin。你可以在任意目录下直接运行:

① 批量添加被代理域名 (添加后可使用 -r 参数自动重启 dnsmasq 刷新)

sudo manage-rules add google.com openai.com huggingface.co -r

② 批量删除已代理的域名并重启服务

sudo manage-rules remove google.com -r

③ 查看当前所有的自定义代理域名列表

manage-rules list

8. 获取与更新 chnroute.ipset

如果你需要手动下载或更新 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

9. 附录:完整的 start-redir 脚本

以下是部署在 /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

About

在WSL2 中运行透明代理,解决部分AI CLI (Antigravity CLI)无法运行的问题

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages