
OpenVPN高级用法
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 下载稳定版
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
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
启动 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
使用 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/.....
要检索实时日志事件,请运行以下 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 用户证书是维护服务器安全的关键步骤,适用于员工离职、设备丢失或证书泄露等情况。以下是详细的操作流程:
📌 核心步骤概览
定位文件: 找到 OpenVPN 的 PKI 文件(主要是 CA 相关文件)。
获取序列号: 确定要吊销证书的序列号或名称。
执行吊销: 使用
openssl ca
或easy-rsa
吊销证书。生成/更新 CRL: 创建或更新证书吊销列表 (CRL)。
配置服务器: 确保 OpenVPN 服务器加载 CRL。
重启服务: 重启 OpenVPN 服务器使更改生效。
分发 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
bashcat 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
(推荐,更简单):进入你的
easy-rsa
目录 (例如/etc/openvpn/server/easy-rsa/
)。加载环境变量 (如果
easy-rsa
版本需要):
bashsource vars # 或 source ./vars (某些版本)
执行吊销命令:
./easyrsa revoke "client_user1"
将
"client_user1"
替换为你要吊销证书的 Common Name (用户名) 。
确认吊销操作 (输入
yes
)。该命令会自动更新index.txt
,将状态标记为R
。
4. 生成/更新证书吊销列表 (CRL)
使用
easy-rsa
:
bash./easyrsa gen-crl
使用原生
openssl ca
:
bashopenssl 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.conf
或server.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
) 中添加:
confcrl-verify crl.pem
并确保
crl.pem
文件与客户端的.ovpn
配置文件放在同一目录下。
⚠ 重要注意事项
备份: 操作前务必备份整个 PKI 目录 (包括
index.txt
,serial
,ca.key
,ca.crt
, 证书文件等)。吊销操作不可逆!CRL 有效期: CRL 文件本身有过期时间。
easy-rsa
生成的默认有效期通常较长,但需定期检查并重新生成 (gen-crl
) 以保持其有效性。服务器加载过期 CRL 会导致所有连接被拒绝。立即生效: 服务器重启后,新的 CRL 立即生效,任何使用被吊销证书的连接尝试都会被拒绝。
安全存放
ca.key
:ca.key
是你的根证书私钥,一旦泄露整个 PKI 体系崩溃。只在生成新 CA、签发证书或吊销证书时需要,操作后务必安全存放 (离线、加密存储)。序列号格式: 使用
openssl
命令时注意序列号的格式 (去除冒号、大写)。吊销后不可恢复: 吊销操作记录在
index.txt
中并最终体现在 CRL 里,无法直接撤销吊销操作。如果误吊销,唯一办法是签发一个全新的证书给该用户,但默认被吊销的证书不会被删除,需要手动删除才能创建同名证书。客户端 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>
- 感谢你赐予我前进的力量