1、OpenVPN-DCO

服务器端

安装 openvpn-dco

# Debian/Ubuntu (需启用Backports)
apt install openvpn-dco-dkms -y

# CentOS/Rocky Linux
yum install kmod-openvpn-dco openvpn

#对于使用 deb 包的系统来说,如果软件仓库找不到 dco,也可以到 ubuntu 的对应官网使用 weget 下载稳定版

Ubuntu 关于 OpenVPN-DCO 的界面

wget https://xxxxxxxxx .
dpkg -i xxxxxxxx

启用

modprobe ovpn_dco_v2
lsmod | grep ovpn
# modprobe -r ovpn_dco_v2                       # 卸载ovpn_dco_v2
# ip -details link show | grep ovpn

服务器端启用模块后,在服务启动时会自动启用,Debain 系统可能需要重启设备才能使用命令 modprobe ovpn_dco_v2

image

windows 客户端在官网下载对应软件(openvpn-connect),需要在设置中打开 DCO。

Linux 客户端需要和服务端一样安装 openvpn-dco-dkms,使用 openvpn@.service 的话是 2.x 版本,只能单会话,且必须 root 用户才能开启,可以安装 openvpn3,使得普通用户也可开启,可进行多会话等操作,以下为 Linux 客户端安装 OpenVPN3

Ubuntu24.04 安装方法

确保您已经安装了所需的支撑软件包:

apt install apt-transport-https curl

获取 OpenVPN Inc 的软件包签名密钥:

mkdir -p /etc/apt/keyrings
curl -sSfL https://packages.openvpn.net/packages-repo.gpg >/etc/apt/keyrings/openvpn.asc

替换下方命令中的 DISTRIBUTION 部分,使用上表中提供的发行版名称来设置 apt 源列表:

echo "deb [signed-by=/etc/apt/keyrings/openvpn.asc] https://packages.openvpn.net/openvpn3/debian noble main" >>/etc/apt/sources.list.d/openvpn3.list

要安装 OpenVPN 3 Linux,请运行以下命令:

apt update && apt install openvpn3 -y

将配置文件导入 OpenVPN3(–config 可缩写为-c,–config-path 可缩写为-p)

openvpn3 config-import --persistent --name CONFIG_NAME --config /path/to/profile.ovpn
openvpn3 config-manage --dco true --config CONFIG_NAME
openvpn3 config-manage --show --config CONFIG_NAME

微信图片_20250620211934_4

启动 OpenVPN3

openvpn3 session-start -c CONFIG_NAME

每次启动 VPN 配置时,无论是通过 openvpn3 session-start 还是 systemd openvpn3-sessions@.service 单元文件,DCO 都将被启用。请确认日志输出确实表明 DCO 已被启用,因为如果您的配置文件不符合 DCO 标准,它可能会被动态禁用。符合 DCO 标准的配置文件不能使用压缩功能,并且必须使用基于 AEAD 的加密算法(如 AES-GCM 或 ChaCha20-Poly1305)。

一旦 VPN 会话开始,它应该可以在 openvpn3 sessions-list 中看到:

openvpn3 sessions-list

image

使用 openvpn3 session-manage 可以进行一些操作,但最典型的是使用 --disconnect 或 --restart 选项。

openvpn3 session-manage -c ${CONFIGURATION_PROFILE_NAME} --restart

这会断开连接并重新连接到服务器,重新建立连接。${CONFIGURATION_PROFILE_NAME} 是 openvpn3 sessions-list 中显示的配置名称。也可以使用会话的 D-Bus 路径:

openvpn3 session-manage --session-path /net/openvpn/v3/sessions/..... --disconnect

上述命令将断开正在运行的会话。一旦此操作完成,它将从 openvpn3 sessions-list 概览中移除。

也可以从正在运行的会话中检索实时隧道统计信息:

openvpn3 session-stats -c ${CONFIGURATION_PROFILE_NAME}
openvpn3 session-stats --session-path /net/openvpn/v3/sessions/.....

image

要检索实时日志事件,请运行以下 openvpn3 log 命令行:

openvpn3 log --c ${CONFIGURATION_PROFILE_NAME}

这可能非常安静,因为它不提供过去的任何日志事件。从不同的终端发出一个 openvpn3 session-manage --restart ,就会发生日志事件。您可能想要通过 --log-level 6 提高日志级别。有效的日志级别范围是 0 到 6,其中 6 是最详细的。

请注意,最大日志级别是集中配置的。如果你在使用更高的日志级别时没有得到更多的输出,请首先使用 openvpn3-admin 增加最大日志级别(注意:这个命令需要以 root 身份执行):

openvpn3-admin log-service --log-level 6

VPN 会话也由启动它的用户拥有。但 会话管理器 也通过 openvpn3 session-acl 提供了自己的访问控制列表功能。

如果不使用 DCO,那么可以从 NetworkManager 快速导入和启动 OpenVPN

nmcli connection import type openvpn file client.ovpn

2、使用高强度 AES 加密

服务端和客户端配置文件都需要

cipher AES-256-GCM                            # 在openvpn2.5之前的会不识别data-ciphers,如果不识别也没有配置cipher,默认会使用一个安全性非常低的加密算法
data-ciphers AES-256-GCM:AES-128-GCM

3、加密手握(TLSv2)

tls-crypt-v2 的工作机制要求:

① 服务器主密钥用于**封装**客户端密钥

② 每个客户端密钥都是唯一的(增强安全性)

③ 客户端启动时,服务器用主密钥**解封**客户端密钥

tls-crypt-v2 的核心目的是用来加密 OpenVPN 连接建立初期那些原本是明文的协商报文,极大地提升了初始握手阶段的安全性和隐私性;默认未配置 tls-auth 或 tls-crypt-v2 的情况下,连接建立时的协商报文是明文状态,容易被拦截和获取协商内容,这在某些较封闭的环境下很容易被阻塞(可以翻墙);

在服务端生成 tlskey

openvpn --genkey tls-crypt-v2-server server/server.tlskey

在服务器的配置文件中加入

tls-crypt-v2 server/server.tlskey

使用服务端生成的 TLSKEY 文件生成客户端文件并将其加入到客户端配置文件中,注意:最好一个客户端生成一个,但也可以共用一个

openvpn --tls-crypt-v2 server/server.tlskey --genkey tls-crypt-v2-client client/client.tlskey
<tls-crypt-v2>
tls-crypt-v2文件内容
</tls-crypt-v2>

4、为客户端分配指定的 IP 地址

在服务器的配置中修改

client-config-dir /etc/openvpn/user-dir

在服务器中创建目录

mkdir /etc/openvpn/user-dir

创建远程用户的文件名,比如 opennw,建议在服务器端添加 username-as-common-name,这样证书的 cn 名等于用户名;

echo "ifconfig-push 10.8.0.4 255.255.255.0" > /etc/openvpn/user-dir/opennw

此命令表示会把 10.8.0.4 推送给客户端

5、强制 TLS 1.3

客户端与服务端的配置文件都需要加入以下内容

tls-version-min 1.3

6、调整 MTU

对于 openvpn,无论 TCP 还是 UDP,Tun 网卡建议设置为 1420

tun-mtu 1420

7、防止 DNS 泄露

服务端配置

# 阻止非VPN接口的DNS请求 (Windows专用)
push "block-outside-dns"

# 自动更新DNS服务器为VPN推送的地址(Linux/MacOS)
update-resolv-conf

以上为客户端所有流量都经过服务器端的情况,如果只指定了某些网段,那么,在向客户端推送路由的时候最好将 DNS 的路由也推送下去,防止 DNS 泄露被检测到

push "dhcp-option DNS 1.1.1.1"              # Cloudflare
push "route 1.1.1.1 255.255.255.255"        # 推送DNS路由到服务器,防止DNS泄露

8、IPv6 流量

如果客户端有 ipv6 地址,但服务器端没有,最好阻断 ipv6 流量,防止流量跳过 VPN 服务器,被检测到

服务端和客户端配置文件都需要做

block-ipv6

9、强制 OpenVPN 一天时间后重连认证

服务器端配置

reneg-sec 86400

10、构建 openvpn 的 docker 镜像

由于很多系统版本低,或者各种问题导致的配置文件无法统一,不安全,可构建自己的 openvpn-docker 镜像

创建一个 Dockerfile

# 使用Ubuntu LTS基础镜像确保稳定性
FROM ubuntu:latest

ADD ["init.sh", "/init.sh"]

RUN apt update && apt install -y \
    openvpn easy-rsa \
    && apt clean all

WORKDIR /etc/openvpn

CMD ["/init.sh"]
touch init.sh && chmod +x init.sh
vim init.sh
#!/bin/sh

mkdir -p /dev/net
# 仅当设备不存在时创建(--device 挂载时不会执行)
if [ ! -c /dev/net/tun ]; then
    mknod /dev/net/tun c 10 200
    chmod 600 /dev/net/tun
fi
cd /etc/openvpn
exec openvpn --config *.conf

构建镜像

docker build -t opennw/openvpn-docker .

构建完成后运行容器

docker run -id --name openvpn-docker \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--device=/dev/net/tun \
--network=host \
--restart always \
-v /etc/openvpn:/etc/openvpn \
opennw/openvpn-docker

此容器会在运行时自动加载已存在的配置,因此运行前需要保证有正确的配置!

将镜像推送到 dockerhub

docker login -u xxx 
docker push opennw/openvpn-docker

11、增大缓冲区大小,提升队列长度

在高并发,高延场景下对于连接的稳定性和速度有一定提升

sndbuf 393216                     # 发送缓冲区大小
rcvbuf 393216                     # 接收缓冲区大小
tcp-queue-limit 256               # TCP队列限制256,仅在TCP模式下有效

12、创建多个 OpenVPN 进程

可创建多个 OpenVPN 进程,每个进程可定向不同的流量,也可基于用户、IP 等分配给不同的进程,提高 CPU 使用率和转发效率。

需要注意的是,不同进程的虚拟网卡,虚拟 IP,监听端口等不能相同。

可直接创建多个 openvpn-docker 的容器,默认情况下,使用 dev tun ​配置,Tun 网卡会从 tun0 开始叠加,也可以一个容器内运行多个 openvpn 进程。但有时可能存在 openvpn 进程重启后原网卡未被删除,同时存在两个 tun 网卡,有一样 ip 的情况,同时为了控制的更精细,也可以手动指定 tun 网卡名称

dev tun1

13、使用 TAP 网卡模式

TAP 模式与 TUN 模式最主要的区别是:TAP 可传递二层数据,而 TUN 网卡不行;TAP 网卡可传递 ARP,Boradcast 等数据,属于以太网链路;而 TUN 网卡只能传递单播数据,属于 P2P 链路,无需 MAC 地址,直接根据出接口转发数据,所谓的“添加路由”其实是为了查找出接口。

TAP 模式在某些需要广播连接的游戏,或是某些广播电视中很有效。可以说是 OpenVPN 的 TAP 是 windows 上唯一原生支持转发二层数据帧的 VPN 客户端了,其他方法 windows 大多不使用(比如 VXLAN,GRE 隧道等)。但 TAP 作为不常用的模式,只有 OpenVPN 的 server 端,和客户端的 OpenVPN GUI(Windows 的社区版客户端)支持,而 OpenVPN Connect 则不支持,同样,手机端只能下载 OpenVPN Connect,也就不支持 TAP 模式。

服务端和客户端的配置与 Tun 模式相差无几,只需要将模式由 TUN 改为 TAP 即可

dev tap

如果客户端需要和服务器端外网网卡使用相同 IP,需要手动创建一个 bridge ​网卡,并把物理网卡和 TAP 网卡绑定在 bridge ​网卡上。这里的 bridge ​网卡相当于软件交换机。

14、吊销客户端证书

吊销 OpenVPN 用户证书是维护服务器安全的关键步骤,适用于员工离职、设备丢失或证书泄露等情况。以下是详细的操作流程:

📌 核心步骤概览

  1. 定位文件: 找到 OpenVPN 的 PKI 文件(主要是 CA 相关文件)。

  2. 获取序列号: 确定要吊销证书的序列号或名称。

  3. 执行吊销: 使用 openssl caeasy-rsa 吊销证书。

  4. 生成/更新 CRL: 创建或更新证书吊销列表 (CRL)。

  5. 配置服务器: 确保 OpenVPN 服务器加载 CRL。

  6. 重启服务: 重启 OpenVPN 服务器使更改生效。

  7. 分发 CRL: 将 CRL 分发给所有客户端(可选但推荐)。


🔍 详细操作步骤

1. 准备环境 (定位 PKI 文件)

  • 进入存储 OpenVPN PKI (公钥基础设施) 文件的目录。通常在使用 easy-rsa 管理证书时,位于 /etc/openvpn/server/easy-rsa//etc/openvpn/easy-rsa/。也可能是你初始化 CA 时自定义的目录。

  • 确保你拥有以下关键文件:

    • ca.crt:你的根 CA 证书。

    • ca.key:你的根 CA 私钥 (高度敏感!操作后务必妥善保管)。

    • index.txt:数据库文件,记录所有颁发/吊销证书的状态。

    • serial:包含下一个证书序列号的文件。

2. 确定要吊销证书的序列号或名称

  • 查看 index.txt
    bash

    cat easy-rsa/pki/index.txt
    • 输出类似:V 310414125702Z 01 unknown /CN=client_user1

    • V 表示有效 (Valid),R 表示已吊销 (Revoked)。

    • 01 就是该证书的序列号 (十六进制)。

    • /CN=client_user1 是证书的通用名称 (Common Name),即用户名。

3. 执行吊销命令

  • 使用 easy-rsa (推荐,更简单):

    1. 进入你的 easy-rsa 目录 (例如 /etc/openvpn/server/easy-rsa/)。

    2. 加载环境变量 (如果 easy-rsa 版本需要):
      bash

      source vars # 或 source ./vars (某些版本)
    3. 执行吊销命令:

      ./easyrsa revoke "client_user1"
      • "client_user1" 替换为你要吊销证书的 Common Name (用户名)

    4. 确认吊销操作 (输入 yes)。该命令会自动更新 index.txt,将状态标记为 R

4. 生成/更新证书吊销列表 (CRL)

  • 使用 easy-rsa:
    bash

    ./easyrsa gen-crl
  • 使用原生 openssl ca:
    bash

    openssl ca -config openssl.cnf -gencrl -out crl.pem
    • -out crl.pem 指定输出的 CRL 文件名,通常命名为 crl.pem

5. 配置 OpenVPN 服务器加载 CRL

  • 将生成的 crl.pem 文件复制到 OpenVPN 服务器配置目录 (如 /etc/openvpn/server/)。

  • 编辑你的 OpenVPN 服务器配置文件 (如 server.confserver.ovpn)。

  • 添加或确认以下指令:

    crl-verify /etc/openvpn/server/crl.pem
    • 确保路径 (/etc/openvpn/server/crl.pem) 指向你放置 crl.pem 的实际位置。

6. 重启 OpenVPN 服务器

sudo systemctl restart openvpn@server 	# 或 openvpn-server@server.service,具体取决于你的系统和服务名

7. (可选但推荐) 分发更新的 CRL 给客户端

  • 为了让客户端也能验证服务器证书是否被吊销 (双向 TLS 验证),将 crl.pem 文件分发给所有客户端。

  • 在客户端的配置文件 (.ovpn) 中添加:
    conf

    crl-verify crl.pem
    • 并确保 crl.pem 文件与客户端的 .ovpn 配置文件放在同一目录下。


⚠ 重要注意事项

  1. 备份: 操作前务必备份整个 PKI 目录 (包括 index.txt, serial, ca.key, ca.crt, 证书文件等)。吊销操作不可逆!

  2. CRL 有效期: CRL 文件本身有过期时间。easy-rsa 生成的默认有效期通常较长,但需定期检查并重新生成 (gen-crl) 以保持其有效性。服务器加载过期 CRL 会导致所有连接被拒绝。

  3. 立即生效: 服务器重启后,新的 CRL 立即生效,任何使用被吊销证书的连接尝试都会被拒绝。

  4. 安全存放 ca.key ca.key 是你的根证书私钥,一旦泄露整个 PKI 体系崩溃。只在生成新 CA、签发证书或吊销证书时需要,操作后务必安全存放 (离线、加密存储)。

  5. 序列号格式: 使用 openssl 命令时注意序列号的格式 (去除冒号、大写)。

  6. 吊销后不可恢复: 吊销操作记录在 index.txt 中并最终体现在 CRL 里,无法直接撤销吊销操作。如果误吊销,唯一办法是签发一个全新的证书给该用户,但默认被吊销的证书不会被删除,需要手动删除才能创建同名证书。

  7. 客户端 CRL 更新: 如果更改了客户端的 CRL 文件 (crl.pem),客户端下次连接时就会使用新的列表进行验证。

15、实现 OpenVPN 分流并自动更新路由

OpenVPN 在服务端和客户端中允许在启动和结束 OpenVPN 进程时运行自定义脚本的功能,在某些场景中会很实用,例如:服务器处于没有 GFW 的网络环境,客户端连接上后可正常访问国际网络,但同时需要进行分流,在访问国际的时候走 OpenVPN 隧道,而访问国内的时候不走 VPN 隧道。

👋 实现 OpenVPN 分流的步骤

1、路由流量的分流;
获取一份国内CIDR集合,经过脚本转化为系统路由表,网关指向物理网关,这样可确保目的IP为国内地址的时候不走VPN隧道,而走物理网关。

2、DNS的分流;
仅仅考路由流量分流是不够的,中大型公司的站点都会部署DNS分流,当你使用国外的DNS解析时,解析的IP是他们在海外的节点,而国内DNS解析的话就是国内的机房节点。这就要求服务器端必须可以处理DNS流量,且要有一份国内域名列表,在列表内的域名使用国内DNS解析,不在列表内的域名使用默认(海外)DNS进行解析

以上路由+DNS的分流基本上可以称作是99%程度的分流转发了,具体精准度要看CIDR文件和域名列表文件内的精准度了。

路由分流实现

(1)下载 China_ip 集合

cd /etc/openvpn
wget -O china_ip.txt  https://metowolf.github.io/iplist/data/special/china.txt

默认CIDR文件内不包含国内常见DNS,可以将国内常用DNS服务器地址写入进去,防止某些场景下访问国内DNS的流量走了“远路”(隧道)

echo "
223.5.5.5/32
223.6.6.6/32
114.114.114.114/32
114.114.115.115/32
180.76.76.76/32
119.29.29.29/32
211.138.24.66/32
123.123.123.123/32
218.85.152.99/32
1.2.4.8/32
210.2.4.8/32" >> china_ip.txt

(2)编写启动脚本,在启动时读取 china_ip.txt 内的 CIDR 批量添加到内核路由表中,以下脚本是一个高性能脚本,默认使用单进程运行,在单进程下批量添加 6000 条路由只需耗时 300 毫秒左右,如果文件内 CIDR 数量过大,例如大于 3 万条时,添加速度过慢可选择多进程运行,但进程数不宜大于 cpu 核心数

touch add_route.sh
chmod +x add_route.sh
vim add_route.sh
#!/bin/bash

# 预定义配置
CONFIG_FILE="china_ip.txt"               # 请修改为实际的文本文件路径
GATEWAY="192.168.10.2"                   # 请修改为实际的网关地址
PROCESSES=1                              # 默认进程数,进程数不宜大于cpu核心数

# 记录开始时间
START_TIME=$(date +%s%3N)

# 解析命令行参数
while getopts ":p:" opt; do
  case $opt in
    p)
      PROCESSES="$OPTARG"
      ;;
    \?)
      echo "无效选项: -$OPTARG" >&2
      exit 1
      ;;
  esac
done

# 检查文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
    echo "错误:文件 $CONFIG_FILE 不存在"
    echo "请修改脚本中的 CONFIG_FILE 变量为正确的文件路径"
    exit 1
fi

# 检查root权限
if [ "$(id -u)" != "0" ]; then
    echo "错误:此脚本需要root权限执行"
    exit 1
fi

# 验证网关格式
if [[ ! "$GATEWAY" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "错误:网关地址格式无效: $GATEWAY"
    echo "请修改脚本中的 GATEWAY 变量为有效的IP地址"
    exit 1
fi

# 验证进程数
if ! [[ "$PROCESSES" =~ ^[1-9][0-9]*$ ]] || [ "$PROCESSES" -lt 1 ]; then
    echo "错误:进程数必须是正整数"
    exit 1
fi

echo "正在处理路由文件: $CONFIG_FILE"
echo "使用网关: $GATEWAY"
echo "使用进程数: $PROCESSES"
echo "正在添加路由,请稍候..."

# 创建临时目录
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT

# 分割文件
LINES=$(wc -l < "$CONFIG_FILE")
SPLIT_LINES=$(( (LINES + PROCESSES - 1) / PROCESSES ))
split -l "$SPLIT_LINES" "$CONFIG_FILE" "$TMP_DIR/part-"

# 函数:处理单个部分
process_part() {
    local part_file="$1"
    local gateway="$2"
    local counts_file=$(mktemp)
    local errors_file=$(mktemp)

    awk -v gateway="$gateway" -v counts="$counts_file" '
    BEGIN {
        total = 0
        valid = 0
        invalid = 0
    }

    # 跳过空行和注释行
    /^[[:space:]]*#/ || /^[[:space:]]*$/ { next }

    {
        total++
        # 验证CIDR格式
        if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+$/) {
            valid++
            # 输出batch命令
            print "route add " $0 " via " gateway
        } else {
            invalid++
        }

        # 每处理1000行输出一个点,显示进度但不影响性能
        if (total % 1000 == 0) {
            printf "." > "/dev/stderr"
            fflush("/dev/stderr")
        }
    }

    END {
        print total > counts
        print valid >> counts
        print invalid >> counts
    }
    ' "$part_file" | ip -force -batch - 2> "$errors_file"

    # 输出临时文件路径,用于主进程收集
    echo "$counts_file $errors_file"
}

# 并行处理
PIDS=()
RESULT_FILES=()

for part in "$TMP_DIR"/part-*; do
    result_file=$(mktemp)
    (
        result=$(process_part "$part" "$GATEWAY")
        echo "$result" > "$result_file"
    ) &
    PIDS+=($!)
    RESULT_FILES+=("$result_file")
done

# 等待所有进程并收集临时文件
COUNTS_FILES=()
ERRORS_FILES=()

for i in "${!PIDS[@]}"; do
    wait "${PIDS[$i]}"
    result=$(cat "${RESULT_FILES[$i]}")
    counts_file=$(echo "$result" | cut -d' ' -f1)
    errors_file=$(echo "$result" | cut -d' ' -f2)
    COUNTS_FILES+=("$counts_file")
    ERRORS_FILES+=("$errors_file")
    rm -f "${RESULT_FILES[$i]}"
done

# 聚合计数
total=0
valid=0
invalid=0
failures=0

for counts_file in "${COUNTS_FILES[@]}"; do
    if [ -f "$counts_file" ]; then
        total_part=$(head -n1 "$counts_file")
        valid_part=$(sed -n '2p' "$counts_file")
        invalid_part=$(tail -n1 "$counts_file")
        total=$((total + total_part))
        valid=$((valid + valid_part))
        invalid=$((invalid + invalid_part))
        rm -f "$counts_file"
    fi
done

for errors_file in "${ERRORS_FILES[@]}"; do
    if [ -f "$errors_file" ]; then
        failures_part=$(grep -c "RTNETLINK answers" "$errors_file")
        failures=$((failures + failures_part))
        rm -f "$errors_file"
    fi
done

added=$((valid - failures))

echo "============================================"
echo "处理完成:"
echo "总行数: $total"
echo "有效CIDR: $valid"
echo "无效CIDR: $invalid"
echo "成功添加: $added"
echo "失败: $((valid - added)) (可能已存在)"

echo "============================================"
echo "路由添加完毕,VPN进程退出后会自动删除!"
echo "============================================"

# 记录结束时间并计算
END_TIME=$(date +%s%3N)
ELAPSED=$((END_TIME - START_TIME))
echo "脚本执行时间: $ELAPSED 毫秒"

(3)编写结束脚本,在结束时读取 china_ip.txt 内的 CIDR 批量从内核路由表中删除,以下脚本是一个高性能脚本,默认使用单进程运行,在单进程下批量添加 6000 条路由只需耗时 300 毫秒左右,如果文件内 CIDR 数量过大,例如大于 3 万条时,添加速度过慢可选择多进程运行,但进程数不宜大于 cpu 核心数;

touch del_route.sh
chmod +x del_route.sh
vim del_route.sh
#!/bin/bash

# 预定义配置
CONFIG_FILE="china_ip.txt"               # 请修改为实际的文本文件路径
GATEWAY="192.168.10.2"                   # 请修改为实际的网关地址
PROCESSES=1                              # 默认进程数,进程数不宜大于cpu核心数

# 记录开始时间
START_TIME=$(date +%s%3N)

# 解析命令行参数
while getopts ":p:" opt; do
  case $opt in
    p)
      PROCESSES="$OPTARG"
      ;;
    \?)
      echo "无效选项: -$OPTARG" >&2
      exit 1
      ;;
  esac
done

# 检查文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
    echo "错误:文件 $CONFIG_FILE 不存在"
    echo "请修改脚本中的 CONFIG_FILE 变量为正确的文件路径"
    exit 1
fi

# 检查root权限
if [ "$(id -u)" != "0" ]; then
    echo "错误:此脚本需要root权限执行"
    exit 1
fi

# 验证网关格式
if [[ ! "$GATEWAY" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "错误:网关地址格式无效: $GATEWAY"
    echo "请修改脚本中的 GATEWAY 变量为有效的IP地址"
    exit 1
fi

# 验证进程数
if ! [[ "$PROCESSES" =~ ^[1-9][0-9]*$ ]] || [ "$PROCESSES" -lt 1 ]; then
    echo "错误:进程数必须是正整数"
    exit 1
fi

echo "正在处理路由文件: $CONFIG_FILE"
echo "使用网关: $GATEWAY"
echo "使用进程数: $PROCESSES"
echo "正在删除路由,请稍候..."

# 创建临时目录
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT

# 分割文件
LINES=$(wc -l < "$CONFIG_FILE")
SPLIT_LINES=$(( (LINES + PROCESSES - 1) / PROCESSES ))
split -l "$SPLIT_LINES" "$CONFIG_FILE" "$TMP_DIR/part-"

# 函数:处理单个部分
process_part() {
    local part_file="$1"
    local gateway="$2"
    local counts_file=$(mktemp)
    local errors_file=$(mktemp)

    awk -v gateway="$gateway" -v counts="$counts_file" '
    BEGIN {
        total = 0
        valid = 0
        invalid = 0
    }

    # 跳过空行和注释行
    /^[[:space:]]*#/ || /^[[:space:]]*$/ { next }

    {
        total++
        # 验证CIDR格式
        if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+$/) {
            valid++
            # 输出batch命令
            print "route del " $0 " via " gateway
        } else {
            invalid++
        }

        # 每处理1000行输出一个点,显示进度但不影响性能
        if (total % 1000 == 0) {
            printf "." > "/dev/stderr"
            fflush("/dev/stderr")
        }
    }

    END {
        print total > counts
        print valid >> counts
        print invalid >> counts
    }
    ' "$part_file" | ip -force -batch - 2> "$errors_file"

    # 输出临时文件路径,用于主进程收集
    echo "$counts_file $errors_file"
}

# 并行处理
PIDS=()
RESULT_FILES=()

for part in "$TMP_DIR"/part-*; do
    result_file=$(mktemp)
    (
        result=$(process_part "$part" "$GATEWAY")
        echo "$result" > "$result_file"
    ) &
    PIDS+=($!)
    RESULT_FILES+=("$result_file")
done

# 等待所有进程并收集临时文件
COUNTS_FILES=()
ERRORS_FILES=()

for i in "${!PIDS[@]}"; do
    wait "${PIDS[$i]}"
    result=$(cat "${RESULT_FILES[$i]}")
    counts_file=$(echo "$result" | cut -d' ' -f1)
    errors_file=$(echo "$result" | cut -d' ' -f2)
    COUNTS_FILES+=("$counts_file")
    ERRORS_FILES+=("$errors_file")
    rm -f "${RESULT_FILES[$i]}"
done

# 聚合计数
total=0
valid=0
invalid=0
failures=0

for counts_file in "${COUNTS_FILES[@]}"; do
    if [ -f "$counts_file" ]; then
        total_part=$(head -n1 "$counts_file")
        valid_part=$(sed -n '2p' "$counts_file")
        invalid_part=$(tail -n1 "$counts_file")
        total=$((total + total_part))
        valid=$((valid + valid_part))
        invalid=$((invalid + invalid_part))
        rm -f "$counts_file"
    fi
done

for errors_file in "${ERRORS_FILES[@]}"; do
    if [ -f "$errors_file" ]; then
        failures_part=$(grep -c "RTNETLINK answers" "$errors_file")
        failures=$((failures + failures_part))
        rm -f "$errors_file"
    fi
done

deleted=$((valid - failures))

echo "============================================"
echo "处理完成:"
echo "总行数: $total"
echo "有效CIDR: $valid"
echo "无效CIDR: $invalid"
echo "成功删除: $deleted"
echo "失败: $((valid - deleted)) (可能不存在)"

echo "============================================"
echo "路由删除完毕!"
echo "============================================"

# 记录结束时间并计算
END_TIME=$(date +%s%3N)
ELAPSED=$((END_TIME - START_TIME))
echo "脚本执行时间: $ELAPSED 毫秒"

(3)在客户端配置中添加配置(只有 Linux 和 Windows 的 OpenVPN GUI 支持)

up /etc/openvpn/add_route.sh
down /etc/openvpn/del_route.sh

正常运行和启动就可以在日志中看到脚本输出

正在处理路由文件: china_ip.txt
使用网关: 192.168.10.2
使用进程数: 1
正在添加路由,请稍候...
============================================
处理完成:
总行数: 5516
有效CIDR: 5516
无效CIDR: 0
成功添加: 5516
失败: 0 (可能已存在)
============================================
路由添加完毕,VPN进程退出后会自动删除!
============================================
脚本执行时间: 293 毫秒

---------------------------------------------------------------------------------


正在处理路由文件: china_ip.txt
使用网关: 192.168.10.2
使用进程数: 1
正在删除路由,请稍候...
============================================
处理完成:
总行数: 5516
有效CIDR: 5516
无效CIDR: 0
成功删除: 5516
失败: 0 (可能不存在)
============================================
路由删除完毕!
============================================
脚本执行时间: 191 毫秒

(4)创建定时任务,使得可以定时更新 CIDR

创建更新脚本(本案例使用 docker 环境,如果是 systemed 运行,更换相应 docker 命令即可)

mkdir /root/shell/
cd /root/shell/
touch auto_china_ip.sh
chmod +x auto_china_ip.sh
vim auto_china_ip.sh
#!/bin/bash

# 下载China_ip到本地
echo "正在下载China_ip,请稍等..."
echo ""
echo ""
wget -O /etc/openvpn/china_ip.txt https://metowolf.github.io/iplist/data/special/china.txt
echo "下载完毕!"

# 为china_ip添加国内DNS地址
echo "正在添加国内常见DNS地址..."
echo "
223.5.5.5/32
223.6.6.6/32
114.114.114.114/32
114.114.115.115/32
180.76.76.76/32
119.29.29.29/32
211.138.24.66/32
123.123.123.123/32
218.85.152.99/32
1.2.4.8/32
210.2.4.8/32" >> /etc/openvpn/china_ip.txt
echo "下载完毕!"

# 重启Docker已加载新的IP列表
echo "正在停止docker..."
docker stop openvpn-docker
echo "正在启动docker..."
docker start openvpn-docker
echo "执行完毕!"

可以运行测试下

bash auto_china_ip.sh
---------------------------------------------------------------------------------------------------
正在下载China_ip,请稍等...


--2025-08-31 20:38:15--  https://metowolf.github.io/iplist/data/special/china.txt
Resolving metowolf.github.io... 185.199.108.153, 185.199.109.153, 185.199.110.153, ...
Connecting to metowolf.github.io|185.199.108.153|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 85028 (83K) [text/plain]
Saving to: '/Docker_Data/openvpn/china_ip.txt'

/Docker_Data/openvpn/ 100%[========================>]  83.04K   158KB/s    in 0.5s

2025-08-31 20:38:18 (158 KB/s) - '/etc/openvpn/china_ip.txt' saved [85028/85028]

下载完毕!
正在添加国内常见DNS地址...
下载完毕!
正在停止docker...
openvpn-docker
正在启动docker...
openvpn-docker
执行完毕!

创建定时任务,以下配置使得系统每天凌晨 2 点自动运行 /root/shell/auto_china_ip.sh ​脚本

echo "* 2 * * * bash /root/shell/auto_china_ip.sh" >> /etc/crontab
crontab /etc/crontab
crontab -l					# 查看已经创建的任务

以上是Linux客户端可实现的路由分流脚本,下面是Windows实现的脚本

新建文本文档输入以下内容

# 预定义配置
$CONFIG_FILE = "C:\Users\OpenNW\china_ip.txt"  # 请修改为实际的文本文件路径
$GATEWAY = "192.168.71.1"      # 请修改为实际的网关地址
$PROCESSES = 4                 # 设置进程数

# 记录开始时间
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# 检查文件是否存在
if (-not (Test-Path $CONFIG_FILE)) {
    Write-Host "错误:文件 $CONFIG_FILE 不存在"
    Write-Host "请修改脚本中的 CONFIG_FILE 变量为正确的文件路径"
    exit 1
}

# 验证网关格式 (保持原样,已经很简洁)
if ($GATEWAY -notmatch '^(\d{1,3}\.){3}\d{1,3}$') {
    Write-Host "错误:网关地址格式无效: $GATEWAY"
    Write-Host "请修改脚本中的 GATEWAY 变量为有效的IP地址"
    exit 1
}

# 验证进程数
if ($PROCESSES -lt 1) {
    Write-Host "错误:进程数必须大于等于1"
    exit 1
}

Write-Host "正在处理路由文件: $CONFIG_FILE"
Write-Host "使用网关: $GATEWAY"
Write-Host "使用进程数: $PROCESSES"
Write-Host "正在添加路由,请稍候..."

# 读取所有行
$lines = Get-Content $CONFIG_FILE

# 过滤有效CIDR并跳过空行和注释
# 简化正则表达式:使用 \d+ 并放宽对IP各段的严格检查(假设输入相对规范)
$validCIDRs = $lines | Where-Object {
    $_ -notmatch '^\s*(#|$)' -and $_ -match '^\d+\.\d+\.\d+\.\d+/\d+$'
}

$total = $lines.Count
$valid = $validCIDRs.Count
$invalid = $total - $valid

# --- 优化的分块逻辑 ---
$chunks = @()
if ($validCIDRs.Count -gt 0) {
    # 计算每个块的理想大小
    $chunkSize = [Math]::Ceiling($validCIDRs.Count / $PROCESSES)
    # 如果块大小为0(例如进程数大于CIDR数),则至少处理一个
    if ($chunkSize -eq 0) { $chunkSize = 1 }

    for ($i = 0; $i -lt $validCIDRs.Count; $i += $chunkSize) {
        $endIndex = [Math]::Min($i + $chunkSize - 1, $validCIDRs.Count - 1)
        if ($i -le $endIndex) {
            $chunks += , @($validCIDRs[$i..$endIndex])
        }
        # 如果块数已经达到进程数,则停止创建新块
        if ($chunks.Count -ge $PROCESSES) {
            break
        }
    }
}
# --- 分块逻辑结束 ---


# 使用 Start-Job 进行多进程处理
$jobs = @()
foreach ($chunk in $chunks) {
    # 将函数定义和处理逻辑直接嵌入到 ScriptBlock 中
    $job = Start-Job -ScriptBlock {
        param($cidrsChunk, $gw)

        # --- 嵌入函数定义 ---
        function CidrToMask {
            param([int]$maskLength)
            if ($maskLength -eq 0) { return '0.0.0.0' }
            # 使用位移和掩码计算子网掩码
            $mask = ([math]::pow(2, $maskLength) - 1) -shl (32 - $maskLength)
            $octets = @(
                ($mask -shr 24) -band 255
                ($mask -shr 16) -band 255
                ($mask -shr 8) -band 255
                $mask -band 255
            )
            return ($octets -join '.')
        }

        # 处理部分的逻辑直接内联,减少函数调用开销
        $added = 0
        $failures = 0

        foreach ($cidr in $cidrsChunk) {
            # 使用 -split 一次分割
            $parts = $cidr -split '/', 2
            $network = $parts[0]
            $maskLength = [int]$parts[1]
            $subnetMask = CidrToMask -maskLength $maskLength

            try {
                # 注意:移除了 -p 参数,添加的是临时路由
                # 将输出重定向到 $null 以提高性能
                $output = route ADD $network MASK $subnetMask $gw 2>&1 > $null
                if ($LASTEXITCODE -eq 0) {
                    $added++
                } else {
                    $failures++
                }
            } catch {
                $failures++
            }
        }

        # 返回结果对象
        return [PSCustomObject]@{
            Added = $added
            Failures = $failures
        }
        # --- 函数定义和处理逻辑结束 ---
    } -ArgumentList @($chunk), $GATEWAY
    $jobs += $job
}

# 等待所有 Job 完成并收集结果
$results = @()
foreach ($job in $jobs) {
    $result = Wait-Job $job | Receive-Job
    $results += $result
    Remove-Job $job # 清理已完成的 Job
}

# 聚合结果
$addedTotal = ($results | Measure-Object -Property Added -Sum).Sum
$failuresTotal = ($results | Measure-Object -Property Failures -Sum).Sum

Write-Host ""
Write-Host "============================================"
Write-Host "处理完成:"
Write-Host "总行数: $total"
Write-Host "有效CIDR: $valid"
Write-Host "无效CIDR: $invalid"
Write-Host "成功添加: $addedTotal"
Write-Host "失败/已存在: $failuresTotal"
Write-Host "============================================"
Write-Host "路由添加完毕!"
Write-Host "============================================"

# 记录结束时间并计算
$stopwatch.Stop()
$elapsed = $stopwatch.ElapsedMilliseconds
Write-Host "脚本执行时间: $elapsed 毫秒"

另存为xxxx.ps1文件,注意保存时要选择UTF-8 BOM​编码格式,否则保存的脚本powershell无法识别,随后打开powershell(管理员),切换到脚本所在目录使用命令.\xxxxx.ps1​即可开始添加路由

以下是windows客户端删除路由的脚本,和添加路由一样的过程。不过windows的openvpn客户端只有OpenVPN GUI可以自动执行脚本,如果使用的是OpenVPN Connetct的话,下面脚本需要在运行VPN前手动执行(为什么是之前?因为之后运行的话,VPN因为路由表变化可能会断开重连)

# 预定义配置
$CONFIG_FILE = "C:\Users\OpenNW\china_ip.txt"  # 请修改为实际的文本文件路径
$PROCESSES = 4                 # 设置进程数
$ERROR_LOG = "route_delete_errors.log"

# 检查管理员权限
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Host "错误:需要管理员权限运行此脚本"
    exit 1
}

# 记录开始时间
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# 检查文件是否存在
if (-not (Test-Path $CONFIG_FILE)) {
    Write-Host "错误:文件 $CONFIG_FILE 不存在"
    Write-Host "请修改脚本中的 CONFIG_FILE 变量为正确的文件路径"
    exit 1
}

# 验证进程数
if ($PROCESSES -lt 1) {
    Write-Host "错误:进程数必须大于等于1"
    exit 1
}

# 清空错误日志
if (Test-Path $ERROR_LOG) {
    Remove-Item $ERROR_LOG -Force
}

Write-Host "正在处理路由文件: $CONFIG_FILE"
Write-Host "使用进程数: $PROCESSES"
Write-Host "正在删除路由,请稍候..."

# 读取所有行
$lines = Get-Content $CONFIG_FILE

# 过滤有效CIDR并跳过空行和注释
$validCIDRs = $lines | Where-Object {
    $_ -notmatch '^\s*(#|$)' -and $_ -match '^\d+\.\d+\.\d+\.\d+/\d+$'
}

$total = $lines.Count
$valid = $validCIDRs.Count
$invalid = $total - $valid

# 分块处理
$chunks = @()
if ($validCIDRs.Count -gt 0) {
    $chunkSize = [Math]::Ceiling($validCIDRs.Count / $PROCESSES)
    if ($chunkSize -eq 0) { $chunkSize = 1 }

    for ($i = 0; $i -lt $validCIDRs.Count; $i += $chunkSize) {
        $endIndex = [Math]::Min($i + $chunkSize - 1, $validCIDRs.Count - 1)
        if ($i -le $endIndex) {
            $chunks += , @($validCIDRs[$i..$endIndex])
        }
        if ($chunks.Count -ge $PROCESSES) {
            break
        }
    }
}

# 使用 Start-Job 进行多进程处理
$jobs = @()
$completed = 0
foreach ($chunk in $chunks) {
    $completed += $chunk.Count
    $percentComplete = ($completed / $validCIDRs.Count) * 100
    Write-Progress -Activity "删除路由" -Status "启动处理进程" -PercentComplete $percentComplete

    $job = Start-Job -ScriptBlock {
        param($cidrsChunk, $errorLog)

        $deleted = 0
        $failures = 0

        foreach ($cidr in $cidrsChunk) {
            $maxRetries = 2
            $retryCount = 0
            $success = $false

            do {
                try {
                    $output = route DELETE $cidr 2>&1
                    if ($LASTEXITCODE -eq 0) {
                        $deleted++
                        $success = $true
                        break
                    } else {
                        $retryCount++
                        Start-Sleep -Milliseconds 100
                    }
                } catch {
                    $retryCount++
                    Start-Sleep -Milliseconds 100
                }
            } while ($retryCount -lt $maxRetries)

            if (-not $success) {
                $failures++
                Add-Content -Path $errorLog -Value "删除失败: $cidr"
            }
        }

        return [PSCustomObject]@{
            Deleted = $deleted
            Failures = $failures
        }
    } -ArgumentList @($chunk, $ERROR_LOG)
    $jobs += $job
}

# 等待所有 Job 完成并收集结果
$results = @()
foreach ($job in $jobs) {
    $result = Wait-Job $job | Receive-Job
    $results += $result
    Remove-Job $job
}

# 聚合结果
$deletedTotal = ($results | Measure-Object -Property Deleted -Sum).Sum
$failuresTotal = ($results | Measure-Object -Property Failures -Sum).Sum

Write-Host ""
Write-Host "============================================"
Write-Host "处理完成:"
Write-Host "总行数: $total"
Write-Host "有效CIDR: $valid"
Write-Host "无效CIDR: $invalid"
Write-Host "成功删除: $deletedTotal"
Write-Host "失败/未找到: $failuresTotal"
Write-Host "============================================"
Write-Host "路由删除完毕!"
Write-Host "============================================"

# 记录结束时间并计算
$stopwatch.Stop()
$elapsed = $stopwatch.ElapsedMilliseconds
Write-Host "脚本执行时间: $elapsed 毫秒"

DNS分流的实现

正如开头所说,仅仅只有路由分流是无法完全使得VPN流量分流的

安装dnsmasq

apt install dnsmasq -y

获取国内域名列表

cd /etc/dnsmasq.d
wget -O cn.conf https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/refs/heads/master/accelerated-domains.china.conf

将.cn加入到列表的最上方,这样如果访问的是.cn域名的站点服务器直接从文件最上方就能匹配到,不需要往下寻找,消耗CPU资源了

server=/cn/114.114.114.114

创建配置文件并指定上游DNS服务器

touch server.conf
vim server.conf
server=1.1.1.1
servers-file=/etc/dnsmasq.d/cn.conf
cache-size=1000							# 启用并设置缓存大小(例如 1000 条记录,根据内存调整,太大可能占用过多 RAM						

修改OpenVPN配置文件,为客户端推送的DNS配置为本机隧道地址

push "dhcp-option DNS 10.10.30.1"

在Ubuntu上systemd-resolved默认会监听53端口,导致dnsmasq无法正常启动,可以配置其继续处理上游 DNS,但释放端口 53 供 dnsmasq 使用

vim /etc/systemd/resolved.conf
# 写入下面内容
[Resolve]
DNSStubListener=no

重启各服务

systemctl restart systemd-resolved
systemctl restart dnsmasq
systemctl enable dnsmasq
docker restart openvpn-docker				# 我这里用的docker,如果你是物理机安装,使用systemctl restart openvpn@servre.conf重启

随后客户端可连接OpenVPN测试(确保已经运行了添加路由的脚本),打开抖音或CSDN(这俩绝B是做了DNS分流解析,我也是因为访问这俩站点发现走隧道了才想到了DNS分流,原本只写了路由流量分流)随后查看OpenVPN流量图,发现没有明显变化证明配置成功

16、高安全的配置示例

IPv4

服务端

port 64371
proto tcp
dev tun0
ca ca.crt
cert server/server.crt
key server/server.key
crl-verify server/crl.pem
dh dh.pem
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM
topology subnet
server 10.10.30.0 255.255.255.0
ifconfig-pool-persist log/ipp.txt
push "route 1.1.1.1 255.255.255.255"
push "route 8.8.8.8 255.255.255.255"
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
client-to-client
duplicate-cn
keepalive 10 120
max-clients 10
persist-key
persist-tun
status log/openvpn-status.log
log-append  log/openvpn.log
verb 3
tls-version-min 1.3
tls-crypt-v2 server/server.tlskey
tun-mtu 1420
client-config-dir user-dir
push "block-outside-dns"
block-ipv6
reneg-sec 86400
sndbuf 393216
rcvbuf 393216
tcp-queue-limit 256

客户端

client
dev tun
remote xxxxx 64371 tcp
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM
tls-version-min 1.3
tun-mtu 1420
block-ipv6
remote-cert-tls server
verb 3
<ca>
# ca文件内容
</ca>

<cert>
# crt文件内容
</cert>

<key>
# key文件内容
</key>

<tls-crypt-v2>
# tlsv2文件内容
</tls-crypt-v2>

IPv6

服务端

port 64372
proto tcp
proto tcp6
dev tun0
ca ca.crt
cert server/server.crt
key server/server.key
dh dh.pem
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM
topology subnet
server 10.10.30.0 255.255.255.0
server-ipv6 fd0a:1234:5678::/64
ifconfig-pool-persist /var/log/openvpn/ipp.txt

push "route 1.1.1.1 0.0.0.0"
push "route 8.8.8.8 0.0.0.0"
push "route-ipv6 2000::/3"
push "route-ipv6 3000::/3"
push "route-ipv6 fc00::/7"
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS6 2400:3200::1" 
push "dhcp-option DNS6 2400:3200:baba::1"

client-to-client
duplicate-cn
keepalive 10 120
max-clients 10
persist-key
persist-tun
log-append  /var/log/openvpn/openvpn.log
verb 3
tls-version-min 1.3
tls-crypt-v2 server/server.tlskey
tun-mtu 1420
push "block-outside-dns"
reneg-sec 86400
sndbuf 393216
rcvbuf 393216
tcp-queue-limit 256

客户端

client
dev tun
remote xxxxxxxx 64372 tcp6
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM
tls-version-min 1.3
tun-mtu 1420
remote-cert-tls server
verb 3
<ca>
# ca文件内容
</ca>

<cert>
# crt文件内容
</cert>

<key>
# key文件内容
</key>

<tls-crypt-v2>
# tlsv2文件内容
</tls-crypt-v2>