基本的openvpn部署配置我就不赘述,可以参考互联网上其他资料,我这边只提供如题所示的解决办法。
一开始想的办法是通过ccd配置,但是被否了,因为ccd配置要求每一个用户都使用不同的配置进行访问vpn主机,这个在我的场景中不适用。
结合gemini2.0 大模型 给出了以下思路
好的,如果不能移除 duplicate-cn
,那么标准的 client-config-dir
(CCD) 方案就无法可靠地为你的“目标客户端”分配固定IP和iroute
,因为CCD是通过客户端证书的Common Name (CN) 来识别客户端的。如果多个客户端共享同一个CN,服务器就不知道哪个是真正的“目标客户端”。
这种情况下,我们需要更动态的方法来识别目标客户端,通常是通过 client-connect
脚本。
核心思路调整:
- 保持
duplicate-cn
。 - 移除
client-config-dir
(或者至少不依赖它来识别这个共享CN的目标客户端)。 - 使用
client-connect
脚本:- 当一个客户端连接时,OpenVPN会执行这个脚本。
- 脚本会接收到客户端的各种信息作为环境变量(包括CN,但也包括证书的其他字段,如序列号、OU、O等,以及客户端的真实IP地址)。
- 脚本需要有一种方法来唯一标识你的“目标客户端”,即使它的CN是共享的。
- 如果脚本识别出是“目标客户端”,它将动态地生成并传递配置给OpenVPN服务器,以分配固定IP并设置
iroute
。 - 如果不是“目标客户端”,脚本就不做特殊操作,客户端会从IP池中获取动态IP。
- 服务器配置中依然保留
push "route 192.168.255.6 255.255.255.255"
,以便所有客户端都知道如何访问目标客户端的固定IP。 - 依然不使用
client-to-client
。
如何唯一标识“目标客户端”?
既然CN是共享的,你需要其他标识符:
- 证书序列号 (Serial Number):每个证书都有唯一的序列号。这是最可靠的方法之一。你需要知道目标客户端所使用的那个特定证书的序列号。
- 证书中的其他字段:如果目标客户端的证书在其他字段(如Organizational Unit -
OU
,Organization -O
)有唯一的值,即使CN相同,也可以用这些字段组合来识别。 - (不太推荐,但可能) 客户端的真实公网IP:如果目标客户端总是在一个固定的公网IP后,可以用
trusted_ip
环境变量。但这很脆弱,因为IP可能会变。
我们将以“证书序列号”为例进行配置。
步骤:
1. 获取目标客户端证书的序列号
假设你的目标客户端使用的证书文件是 target_device_shared_cn.crt
。 在可以访问这个证书文件的机器上运行:
openssl x509 -in target_device_shared_cn.crt -noout -serial
它会输出类似 serial=01A2B3C4D5E6F7
的内容。记下这个序列号 (例如 01A2B3C4D5E6F7
)。 注意: OpenVPN client-connect
脚本中的环境变量 tls_serial_0
(或 tls_serial_N
对于证书链) 通常是不带冒号的十六进制字符串,并且可能需要去除前导的 0
(如果OpenSSL输出的是 00AB
而脚本中是 AB
)。你需要测试一下。通常直接使用OpenSSL输出的十六进制值(不含serial=
)即可。
2. 修改服务器配置 (server.conf
)
# VPN IP地址池
server 192.168.255.0 255.255.255.0
topology subnet # 推荐
verb 3
key /etc/openvpn/pki/private/xxx.key
ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/160.202.234.233.crt
dh /etc/openvpn/pki/dh.pem
tls-auth /etc/openvpn/pki/ta.key
key-direction 0
keepalive 10 60
persist-key
persist-tun
proto udp
port 1194
dev tun0
status /tmp/openvpn-status.log
user nobody
group nogroup
comp-lzo no
# --- 关键修改 ---
duplicate-cn # 你需要保留这个
# client-to-client # 确保已移除或注释掉
# 移除或注释掉 client-config-dir,因为我们用 client-connect 脚本
# client-config-dir /etc/openvpn/ccd
# 启用 client-connect 脚本
script-security 2 # 允许OpenVPN调用用户定义的脚本
client-connect /etc/openvpn/scripts/assign_target_ip.sh # 脚本路径
# --- 结束关键修改 ---
ifconfig-pool-persist /etc/openvpn/ipp.txt
route-noexec
### Route Configurations Below
push "route 192.168.255.6 255.255.255.255" # 所有客户端都需要知道如何访问目标IP
### Push Configurations Below
push "comp-lzo no"
3. 创建 client-connect
脚本
创建目录(如果不存在):
sudo mkdir -p /etc/openvpn/scripts
创建脚本文件 /etc/openvpn/scripts/assign_target_ip.sh
:
#!/bin/bash
# This script is called by OpenVPN when a client connects.
# It receives client information via environment variables.
# It can dynamically generate client-specific configurations.
# The first argument ($1) to this script is a temporary file
# where client-specific configurations should be written.
CONFIG_FILE="$1"
# --- Configuration for your target client ---
TARGET_CLIENT_CERT_SERIAL="YOUR_TARGET_CLIENT_CERT_SERIAL_HERE" # 替换为实际序列号,例如 "01A2B3C4D5E6F7"
TARGET_CLIENT_VPN_IP="192.168.255.6"
VPN_SUBNET_MASK="255.255.255.0" # Assuming topology subnet
# Log script execution for debugging (optional, useful during setup)
# Ensure the OpenVPN user (e.g., nobody) has write permission to this log file's directory
# Or comment out these lines in production
# echo "$(date): client-connect script called for CN: ${common_name}, Serial0: ${tls_serial_0}, Serial1: ${tls_serial_1}, Source IP: ${trusted_ip}" >> /var/log/openvpn/client_connect.log
# The serial number is usually in tls_serial_0 for the client cert itself.
# If using a chain, it might be tls_serial_1, etc. Check OpenVPN logs (verb 4+) to confirm.
# Remove leading '0's if necessary for matching, or adjust the comparison.
# OpenSSL often shows serials like "01:AB:CD...", OpenVPN env var might be "1ABCD..."
# Let's assume tls_serial_0 provides the hex string directly.
# Convert serial from env (might have colons or be different case) to a comparable format
# For example, if openssl output is "01:A2:B3" and env var is "01:a2:b3" or "01A2B3"
# For simplicity, we assume TARGET_CLIENT_CERT_SERIAL is set to match EXACTLY what tls_serial_0 will provide.
# You might need to echo $tls_serial_0 to logs to see its exact format first.
# Example transformation if needed:
# CURRENT_CERT_SERIAL_FROM_ENV=$(echo "${tls_serial_0}" | tr '[:lower:]' '[:upper:]' | sed 's/://g')
CURRENT_CERT_SERIAL_FROM_ENV="${tls_serial_0}" # Assuming tls_serial_0 is directly comparable
if [ "${CURRENT_CERT_SERIAL_FROM_ENV}" == "${TARGET_CLIENT_CERT_SERIAL}" ]; then
# This is our target client
echo "ifconfig-push ${TARGET_CLIENT_VPN_IP} ${VPN_SUBNET_MASK}" > "${CONFIG_FILE}"
echo "iroute ${TARGET_CLIENT_VPN_IP} 255.255.255.255" >> "${CONFIG_FILE}"
# (Optional) If this target client also needs to reach all other VPN clients (full client-to-client for *this* client only):
# echo "push \"route 192.168.255.0 255.255.255.0\"" >> "${CONFIG_FILE}"
# echo "$(date): Target client ${common_name} (Serial: ${CURRENT_CERT_SERIAL_FROM_ENV}) assigned IP ${TARGET_CLIENT_VPN_IP}" >> /var/log/openvpn/client_connect.log
exit 0 # Success, apply these configs
else
# This is not the target client, let it get a dynamic IP from the pool.
# No specific configuration written to CONFIG_FILE, or write an empty file.
> "${CONFIG_FILE}" # Ensure an empty file if no specific config
# echo "$(date): Non-target client ${common_name} (Serial: ${CURRENT_CERT_SERIAL_FROM_ENV}) connecting." >> /var/log/openvpn/client_connect.log
exit 0 # Success, proceed with default IP assignment
fi
# If script exits with non-zero, client connection will be rejected.
使脚本可执行并设置权限:
sudo chmod +x /etc/openvpn/scripts/assign_target_ip.sh
# 确保OpenVPN运行的用户 (e.g., nobody) 可以执行此脚本。
# 如果需要日志,确保OpenVPN用户可以写入日志文件/目录。
# sudo mkdir -p /var/log/openvpn
# sudo chown nobody:nogroup /var/log/openvpn # Adjust user/group if different
# sudo touch /var/log/openvpn/client_connect.log
# sudo chown nobody:nogroup /var/log/openvpn/client_connect.log
4. 调试和测试
- 非常重要: 你需要确认
tls_serial_0
(或tls_serial_1
等,取决于你的证书链) 环境变量在脚本中提供的序列号格式,并相应地调整TARGET_CLIENT_CERT_SERIAL
的值或脚本中的比较逻辑。- 在OpenVPN服务器配置中临时增加
verb 4
或verb 5
。 - 连接目标客户端。
- 查看OpenVPN服务器日志 (
/var/log/openvpn.log
,/tmp/openvpn-status.log
或 systemd journal)。日志中会显示传递给脚本的环境变量。 - 或者,在脚本中临时加入
echo "tls_serial_0=${tls_serial_0}" >&2
(输出到stderr,可能显示在OpenVPN日志中) 或写入临时文件来查看其确切值和格式。
- 在OpenVPN服务器配置中临时增加
5. 其他用户的客户端配置
其他用户的客户端配置与之前给出的标准配置完全相同。他们不需要任何特殊设置。 他们会使用共享CN的证书,但由于其证书序列号与脚本中定义的 TARGET_CLIENT_CERT_SERIAL
不匹配,脚本不会为他们推送固定IP或 iroute
。他们会从 192.168.255.0/24
池中获取动态IP,并且只会接收到服务器全局 push
的到达 192.168.255.6
的路由。
工作流程:
- 一个客户端连接。
- OpenVPN执行
/etc/openvpn/scripts/assign_target_ip.sh
脚本。 - 脚本检查连接客户端证书的序列号 (
$tls_serial_0
)。 - 如果序列号匹配
TARGET_CLIENT_CERT_SERIAL
:- 脚本向OpenVPN返回
ifconfig-push 192.168.255.6 255.255.255.0
和iroute 192.168.255.6 255.255.255.255
。 - 目标客户端获得固定IP
192.168.255.6
,并且服务器知道如何将流量路由到这个IP。
- 脚本向OpenVPN返回
- 如果序列号不匹配:
- 脚本不返回特定配置。
- 客户端从
192.168.255.0/24
IP池中获取动态IP。
- 所有客户端(包括目标客户端和普通客户端)都会从
server.conf
接收到push "route 192.168.255.6 255.255.255.255"
。 - 由于没有
client-to-client
,普通客户端A无法直接连接到普通客户端B。但普通客户端A可以连接到192.168.255.6
(目标客户端)。
这种方法稍微复杂一些,因为涉及到脚本和对证书细节的依赖,但它允许你在必须使用 duplicate-cn
的情况下实现你的目标。关键是正确识别目标客户端的证书序列号并正确配置脚本。
客户端配置和原本相同,不需要改动即可,前提是你的客户端原本就是正常运行的!
要注意的地方,你需要生成两个不同的证书,一个是特殊的,只提供给目标客户端的证书,另一个是普通的,提供个其他用户的(客户端)的证书。将目标客户端证书的序列号通过解析工具解析出来,可以使用这个工具。
经过配置,其他客户端只需要访问192.168.255.6即可访问到目标客户端。