秦绍鹏
秦绍鹏
Published on 2025-06-23 / 2 Visits
0
0

openvpn解决多客户端访问一个目标客户端

基本的openvpn部署配置我就不赘述,可以参考互联网上其他资料,我这边只提供如题所示的解决办法。

一开始想的办法是通过ccd配置,但是被否了,因为ccd配置要求每一个用户都使用不同的配置进行访问vpn主机,这个在我的场景中不适用。

结合gemini2.0 大模型 给出了以下思路

好的,如果不能移除 duplicate-cn,那么标准的 client-config-dir (CCD) 方案就无法可靠地为你的“目标客户端”分配固定IP和iroute,因为CCD是通过客户端证书的Common Name (CN) 来识别客户端的。如果多个客户端共享同一个CN,服务器就不知道哪个是真正的“目标客户端”。

这种情况下,我们需要更动态的方法来识别目标客户端,通常是通过 client-connect 脚本。

核心思路调整:

  1. 保持 duplicate-cn
  2. 移除 client-config-dir (或者至少不依赖它来识别这个共享CN的目标客户端)。
  3. 使用 client-connect 脚本
    • 当一个客户端连接时,OpenVPN会执行这个脚本。
    • 脚本会接收到客户端的各种信息作为环境变量(包括CN,但也包括证书的其他字段,如序列号、OU、O等,以及客户端的真实IP地址)。
    • 脚本需要有一种方法来唯一标识你的“目标客户端”,即使它的CN是共享的。
    • 如果脚本识别出是“目标客户端”,它将动态地生成并传递配置给OpenVPN服务器,以分配固定IP并设置iroute
    • 如果不是“目标客户端”,脚本就不做特殊操作,客户端会从IP池中获取动态IP。
  4. 服务器配置中依然保留 push "route 192.168.255.6 255.255.255.255",以便所有客户端都知道如何访问目标客户端的固定IP。
  5. 依然不使用 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 4verb 5
    • 连接目标客户端。
    • 查看OpenVPN服务器日志 (/var/log/openvpn.log, /tmp/openvpn-status.log 或 systemd journal)。日志中会显示传递给脚本的环境变量。
    • 或者,在脚本中临时加入 echo "tls_serial_0=${tls_serial_0}" >&2 (输出到stderr,可能显示在OpenVPN日志中) 或写入临时文件来查看其确切值和格式。

5. 其他用户的客户端配置

其他用户的客户端配置与之前给出的标准配置完全相同。他们不需要任何特殊设置。 他们会使用共享CN的证书,但由于其证书序列号与脚本中定义的 TARGET_CLIENT_CERT_SERIAL 不匹配,脚本不会为他们推送固定IP或 iroute。他们会从 192.168.255.0/24 池中获取动态IP,并且只会接收到服务器全局 push 的到达 192.168.255.6 的路由。

工作流程:

  1. 一个客户端连接。
  2. OpenVPN执行 /etc/openvpn/scripts/assign_target_ip.sh 脚本。
  3. 脚本检查连接客户端证书的序列号 ($tls_serial_0)。
  4. 如果序列号匹配 TARGET_CLIENT_CERT_SERIAL
    • 脚本向OpenVPN返回 ifconfig-push 192.168.255.6 255.255.255.0iroute 192.168.255.6 255.255.255.255
    • 目标客户端获得固定IP 192.168.255.6,并且服务器知道如何将流量路由到这个IP。
  5. 如果序列号不匹配
    • 脚本不返回特定配置。
    • 客户端从 192.168.255.0/24 IP池中获取动态IP。
  6. 所有客户端(包括目标客户端和普通客户端)都会从 server.conf 接收到 push "route 192.168.255.6 255.255.255.255"
  7. 由于没有 client-to-client,普通客户端A无法直接连接到普通客户端B。但普通客户端A可以连接到 192.168.255.6(目标客户端)。

这种方法稍微复杂一些,因为涉及到脚本和对证书细节的依赖,但它允许你在必须使用 duplicate-cn 的情况下实现你的目标。关键是正确识别目标客户端的证书序列号并正确配置脚本。

客户端配置和原本相同,不需要改动即可,前提是你的客户端原本就是正常运行的!

要注意的地方,你需要生成两个不同的证书,一个是特殊的,只提供给目标客户端的证书,另一个是普通的,提供个其他用户的(客户端)的证书。将目标客户端证书的序列号通过解析工具解析出来,可以使用这个工具

经过配置,其他客户端只需要访问192.168.255.6即可访问到目标客户端。


Comment