跳转至

SOP:FRP + OpenVPN 内网穿透(香港 frps/OpenVPN server ⇄ 内网 OpenWrt 旁路由)

变更类型:C 类(生产变更) 实现体:本工程 Python 部署程序(入口 main.py,业务模块 deployer.py) 目标主机:rainchen.top(香港,129.226.138.204)、192.168.31.2(内网 OpenWrt 24.10) 本次范围:复用香港既有 frps 与 OpenVPN server,把内网 OpenWrt 接入隧道;frp 只打通控制连接底座,暂不映射具体服务


1. 背景

需求是"借香港主机 + 内网 OpenWrt 做内网穿透",形态为:

  • frp 底座:OpenWrt 作为 frpc 连香港既有 frps,为后续按需暴露内网服务留通道;
  • OpenVPN 隧道:让香港主机(以及经它拨入的 VPN 客户端)能直接访问内网 192.168.31.0/24。

改造前实测到的四个障碍,本 SOP 逐条固化处置:

# 现象 根因
1 OpenWrt 上 frpc 装了却没跑,配置指向失效地址 旧配置 server_addr=43.129.206.40 已不可达;frpc 0.51.3 与 frps 0.70.1 跨 19 个版本
2 隧道建不起来,日志 Cannot ioctl TUNSETIFF tun0: Resource busy OpenWrt 的 /etc/init.d/openvpn 会扫描 /etc/openvpn/*.conf 自动拉起实例,既有 server.conf 占用 tun0 且用同一网段 10.8.0.0/24
3 隧道通了但香港访问内网 100% 丢包 香港 openvpn-iptables.service 的 SNAT 规则把所有出隧道流量改写源为 10.0.4.4,内网回包目标变成 10.0.4.4,隧道侧无该网段路由,回包进默认路由后被丢弃
4 「排除内网网段」的修法一度让 SNAT 规则彻底消失 CentOS 7 的 iptables 1.4.21 不支持同一规则出现多个 -d(multiple -d flags not allowed),规则非法导致 unit 启动失败

2. 目标

  1. OpenWrt 上 frpc 与香港 frps 版本对齐(0.70.1)并稳定在线(日志出现 login to server success);
  2. OpenWrt 与香港建立 OpenVPN 隧道,设备为 tun0,地址 10.8.0.9;
  3. 香港主机可达内网 192.168.31.0/24(实测 192.168.31.249、192.168.31.1 均 0% 丢包);
  4. 香港既有能力不受损:VPN 客户端经香港出口上网的 SNAT 仍生效,仅对目标内网的流量豁免;
  5. 幂等:连续两次执行,第二次零变更、全跳过、无报错;
  6. frp 端口映射本期留空(proxies: []),后续按需在配置里追加。

3. 前置条件

项 要求
权限 两台主机均可 root 登录(本项目走 SSH 密钥免密,ssh_password 留空)
香港侧 frps 已运行;openvpn-server@server 已运行;easy-rsa 可用;ip_forward=1
香港安全组 放行 UDP 1194(已实测:抓包确认公网包可达 10.0.4.4:1194)
OpenWrt 侧 24.10 + kmod-tun + openvpn-openssl + openclash;x86_64(frpc 用 linux_amd64)
网络 OpenWrt 可出公网;本机可直连 GitHub Release(用于取 frpc 二进制)
敏感值 frp token 走环境变量 FRP_TOKEN,不落盘;config/deploy.yaml 已在 .gitignore 中

准备命令:

cd frp_openvpn_tunnel
python3 -m venv .venv                                     # 独立 venv
.venv/bin/python -c "import sysconfig, pathlib; p = pathlib.Path(sysconfig.get_paths()['purelib']) / 'py_public_modules.pth'; p.write_text('/home/rain/py_public_modules', encoding='utf-8')"
.venv/bin/python -m pip install -r requirements.txt       # paramiko / PyYAML
cp config/deploy.yaml.example config/deploy.yaml          # 按环境核对

4. 影响范围

维度 说明
主机 香港 rainchen.top、内网 192.168.31.2,共 2 台
是否停服 香港 OpenVPN 重启一次(当前无客户端连接,影响可忽略);OpenClash 重启一次(代理中断约 20s,Windows 不走代理不受影响)
既有实例 OpenWrt 上的 server.conf 实例被改名为 .disabled 停用(零客户端连接,改回即恢复)
网络 内网段与 VPN 段不变;香港既有 SNAT 语义收窄(仅新增内网豁免)
窗口期 建议 15 分钟;期间 VPN 与代理短暂抖动
兜底 所有改动前自动带时间戳备份;--rollback 可整体回退

5. 分步操作

以下命令与实现体 deployer.py 中各模块一一对应,供人工复核、手工复现与断线接管使用;以实现体为准。 删除、改名、重启服务均属高危动作,已在注释中标注「高危」,并给出对应备份方式。

5.1 自动执行(推荐)

cd frp_openvpn_tunnel
export FRP_TOKEN=$(ssh root@rainchen.top "grep -E '^auth.token' /usr/local/frps/frps.toml | head -1 | cut -d'\"' -f2")   # token 只进环境变量,不落盘

sudo .venv/bin/python main.py --dry-run --config config/deploy.yaml   # 预演:只列变更,不改状态
sudo .venv/bin/python main.py --config config/deploy.yaml             # 正式执行(会要求输入 YES)
sudo .venv/bin/python main.py --config config/deploy.yaml             # 复跑一次,验证幂等(应全跳过)

5.2 等价的手工序列

# ---------- 步骤 1:香港签发 OpenWrt 客户端证书(实现:HkCertModule)----------
cd /etc/openvpn/server/easy-rsa && ./easyrsa --batch build-client-full openwrt nopass   # 只新增,不动既有 8 张证书
openssl crl -in /etc/openvpn/server/crl.pem -noout -nextupdate                          # 先确认 CRL 未过期(过期会拒绝所有客户端)

# ---------- 步骤 2:香港配置 ccd 与内网路由(实现:HkServerCcdModule)----------
cp -a /etc/openvpn/server/server.conf /etc/openvpn/server/server.conf.bak.$(date +%Y%m%d-%H%M%S)   # 高危动作前置备份
mkdir -p /etc/openvpn/server/ccd && chmod 755 /etc/openvpn/server/ccd                    # openvpn 以 nobody 运行,须可读可进入
printf 'iroute 192.168.31.0 255.255.255.0\n' > /etc/openvpn/server/ccd/openwrt           # 去内网的包交给哪个客户端
chmod 644 /etc/openvpn/server/ccd/openwrt
grep -qF 'client-config-dir /etc/openvpn/server/ccd' /etc/openvpn/server/server.conf || \
  printf 'client-config-dir /etc/openvpn/server/ccd\nroute 192.168.31.0 255.255.255.0\n' >> /etc/openvpn/server/server.conf
systemctl restart openvpn-server@server                                                  # 高危:重启会断现有 VPN 连接

# ---------- 步骤 3:香港修正出口 NAT(实现:HkOvpnNatModule)----------
# 关键:iptables 1.4.21 不支持多个 -d,故用 RETURN 豁免内网,而非 "! -d A ! -d B"
cp -a /etc/systemd/system/openvpn-iptables.service{,.bak.$(date +%Y%m%d-%H%M%S)}          # 高危动作前置备份
# 在 SNAT 规则之前插入(-A):
#   iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -d 192.168.31.0/24 -j RETURN
# 对应的 -D 行也要同步加,保持增删对称
systemctl daemon-reload
systemctl stop openvpn-iptables 2>/dev/null || true                                      # 高危:停服务
iptables -t nat -S POSTROUTING | grep -E '10\.8\.0\.0/24|192\.168\.31\.0/24' \
  | sed 's/^-A POSTROUTING //' | while read -r a; do iptables -t nat -D POSTROUTING $a; done   # 清掉旧/非法规则
systemctl reset-failed openvpn-iptables 2>/dev/null || true
systemctl start openvpn-iptables

# ---------- 步骤 4:OpenClash 白名单(实现:OpenwrtClashModule)----------
# 追加式写入,与既有双机场白名单共存;不改订阅原件
echo '+.rainchen.top' >> /etc/openclash/custom/openclash_custom_fake_filter.list
echo "'+.rainchen.top': ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query', '223.5.5.5', '119.29.29.29']" \
     >> /etc/openclash/custom/openclash_custom_domain_dns_policy.list
uci set openclash.config.custom_fakeip_filter='1'; uci set openclash.config.custom_name_policy='1'; uci commit openclash
touch /etc/config/openclash                                                              # 否则「快速启动」会跳过配置生成
/etc/init.d/openclash stop; sleep 3                                                      # 高危:代理短暂中断
uci set openclash.config.enable='1'; uci commit openclash                                # stop 会把 enable 置 0,必须显式设回
/etc/init.d/openclash start

# ---------- 步骤 5:OpenWrt 升级 frpc 并托管配置(实现:OpenwrtFrpcModule)----------
curl -fsSL -o /tmp/frp.tar.gz https://github.com/fatedier/frp/releases/download/v0.70.1/frp_0.70.1_linux_amd64.tar.gz   # 本机下载后上传
tar -xzf /tmp/frp.tar.gz -C /tmp frp_0.70.1_linux_amd64/frpc
install -m 755 /tmp/frp_0.70.1_linux_amd64/frpc /usr/local/bin/frpc                     # 放 /usr/local/bin,不覆盖 opkg 的 /usr/bin/frpc
# /etc/frp/frpc.toml(600):serverAddr=129.226.138.204 serverPort=5443 auth.method/token transport.tls.enable=false
# /etc/init.d/frpc-tunnel(755,procd,独立实例)
/etc/init.d/frpc stop 2>/dev/null; /etc/init.d/frpc disable 2>/dev/null                    # 高危:停用旧 frpc 服务,让位新实例
/etc/init.d/frpc-tunnel enable && /etc/init.d/frpc-tunnel start

# ---------- 步骤 6:停用冲突的既有 OpenVPN 实例(实现:OpenwrtLegacyOvpnModule)----------
mv /etc/openvpn/server.conf /etc/openvpn/server.conf.disabled                            # 高危:停用既有实例(原文件保留,改回即恢复)
uci set openvpn.hk.enabled='1'; uci commit openvpn
/etc/init.d/openvpn restart                                                              # 让隧道实例接管 tun0

# ---------- 步骤 7:部署 OpenVPN 客户端(实现:OpenwrtOvpnClientModule)----------
mkdir -p /etc/openvpn/hk                                                                 # 上传 ca.crt / openwrt.crt / tc.key / openwrt.key
chmod 600 /etc/openvpn/hk/openwrt.key /etc/openvpn/hk/tc.key
# /etc/openvpn/openwrt-hk.ovpn(600)关键项:
#   client / dev tun0 / proto udp / remote 129.226.138.204 1194
#   data-ciphers AES-256-GCM:AES-256-CBC / data-ciphers-fallback AES-256-CBC   # OpenVPN 2.6 连 2.4 服务端的必需项
uci set openvpn.hk=openvpn; uci set openvpn.hk.enabled='1'
uci set openvpn.hk.config='/etc/openvpn/openwrt-hk.ovpn'; uci commit openvpn
/etc/init.d/openvpn enable && /etc/init.d/openvpn start

# ---------- 步骤 8:隧道接口并入 lan zone(实现:OpenwrtFirewallModule)----------
# 旁路由下内网设备默认网关是上游小米路由,必须靠 lan zone 的 masq 做源地址伪装,回包才回得来
uci add_list firewall.@zone[0].device='tun0'; uci commit firewall
/etc/init.d/firewall reload

6. 验证

# 1) frpc 在线(预期:login to server success)
ssh root@192.168.31.2 'tail -5 /var/log/frpc.log; /usr/local/bin/frpc -v'          # 预期 0.70.1
ssh root@rainchen.top 'ss -tn | grep :5443'                                        # 预期 ESTAB 连接

# 2) 隧道与地址
ssh root@192.168.31.2 'ip -4 addr show tun0'                                       # 预期 inet 10.8.0.9/24
ssh root@rainchen.top 'sed -n "4,9p" /run/openvpn-server/status-server.log'        # 预期 CLIENT_LIST 含 openwrt

# 3) 香港可达内网(本次实测:均 0% 丢包,约 62–68ms)
ssh root@rainchen.top 'ping -c2 192.168.31.249; ping -c2 192.168.31.1'

# 4) NAT 规则形态(预期:先 RETURN 豁免内网,再 SNAT)
ssh root@rainchen.top 'iptables -t nat -S POSTROUTING; systemctl is-active openvpn-iptables'

# 5) 幂等验收(本次实测:变更类模块 8 项全 skipped,变更动作 0 条)
sudo .venv/bin/python main.py --config config/deploy.yaml

工程内 verify 模块会一次性核对上述 1–3 项(共 8 个检查点),任一项不达标都在汇总里以 ⚠️ 列出。


7. 回滚

7.1 备份方式

对象 位置
香港 server.conf /etc/openvpn/server/server.conf.bak.<时间戳>
香港 NAT 单元 /etc/systemd/system/openvpn-iptables.service.bak.<时间戳>
OpenWrt frpc 配置/init /etc/frp/frpc.toml.bak.<ts>、/etc/init.d/frpc-tunnel.bak.<ts>(若原本存在)
OpenWrt 既有 OpenVPN 实例 /etc/openvpn/server.conf.disabled(改名保留,非删除)
OpenClash 白名单 openclash_custom_fake_filter.list.bak.<ts> 等
连接材料 本机 artifacts/certs/(含私钥,勿外传)

7.2 回滚触发判据(量化,任一命中即回滚)

  • verify 出现任一 ❌,或香港 ping 内网出现 100% 丢包;
  • frpc 日志持续无 login to server success(>2 分钟);
  • tun0 未建立或香港 status-server.log 无 openwrt 客户端;
  • OpenClash 重启后 status 非 running。

7.3 回滚步骤

# ① 工程内整体回滚(按时间戳备份还原,并停用新增服务)
sudo .venv/bin/python main.py --rollback --config config/deploy.yaml

# ② 或手工分步回滚
# 香港:还原 server.conf 与 NAT 单元
ssh root@rainchen.top 'cp -a /etc/openvpn/server/server.conf.bak.<ts> /etc/openvpn/server/server.conf; \
  rm -f /etc/openvpn/server/ccd/openwrt; systemctl restart openvpn-server@server'
ssh root@rainchen.top 'cp -a /etc/systemd/system/openvpn-iptables.service.bak.<ts> /etc/systemd/system/openvpn-iptables.service; \
  systemctl daemon-reload; systemctl reset-failed openvpn-iptables; systemctl start openvpn-iptables'

# OpenWrt:停隧道、恢复既有实例、还原 frpc
ssh root@192.168.31.2 'uci -q delete openvpn.hk; uci commit openvpn; /etc/init.d/openvpn stop; \
  mv /etc/openvpn/server.conf.disabled /etc/openvpn/server.conf; \
  /etc/init.d/frpc-tunnel disable; /etc/init.d/frpc-tunnel stop; /etc/init.d/frpc enable; \
  /etc/init.d/openvpn restart; /etc/init.d/firewall reload'
# 如需吊销证书:ssh root@rainchen.top 'cd /etc/openvpn/server/easy-rsa && ./easyrsa revoke openwrt && ./easyrsa gen-crl'

7.4 回滚后验证

ssh root@192.168.31.2 'ip -br addr | grep -c tun; /etc/init.d/frpc-tunnel status'   # 隧道应消失,旧 frpc 服务恢复
ssh root@192.168.31.2 'ping -c2 192.168.31.1'                                       # 内网正常
ssh root@rainchen.top 'systemctl is-active openvpn-server@server openvpn-iptables'  # 香港服务 normal

8. 风险说明

风险点 说明 缓解
🔴 香港 OpenVPN 重启 重启会断现有 VPN 连接 执行前确认无人使用(本次 CLIENT_LIST 为空);窗口期通知
🔴 改香港 NAT 规则 改错会同时影响"VPN 客户端上网"与"访问内网"两个功能 前置备份 unit;用 RETURN 豁免而非删除 SNAT;执行后核对 iptables -t nat -S
🔴 iptables 1.4.x 不支持多个 -d 用 ! -d A ! -d B 表达排除会直接让 unit 启动失败(multiple -d flags not allowed) 改用 -j RETURN 豁免行;工程在 validate 里同时校验单元文件与运行态规则
🟠 模板实例不在 list-unit-files openvpn-server@server 只以模板 openvpn-server@.service 出现在该命令输出里,用 grep 判断存在性会失败,进而回退到不存在的 openvpn 单元 一律用 systemctl cat <unit> 判定存在性;配置中显式写 service: openvpn-server@server
🟠 /etc/init.d/openvpn 会扫描 /etc/openvpn/*.conf 任何放在该目录的 .conf 都会被自动拉起并抢占 tun0/网段 冲突实例改名为 .disabled;新增隧道用 uci 实例而非丢 .conf
🟠 旁路由回包不对称 内网设备默认网关是上游小米路由,不经 OpenWrt 时回包会被丢弃 把 tun0 并入 lan zone,借其 masq=1 做源地址伪装
🟠 fake-ip 污染 OpenWrt 上解析 rainchen.top 得到 198.18.x.x,按域名连香港必失败 配置全程用 IP;同时把 +.rainchen.top 追加进 fake-ip 白名单与 DNS 策略
🟠 frp 版本需对齐 frpc 0.51.3 与 frps 0.70.1 跨 19 个版本,ini/uci 模板参数可能失配 二进制升级到 0.70.1,并用 toml 配置 + 独立 procd 服务,不依赖 luci-app-frpc 模板
🟡 远端读取裁剪尾部换行 内容比对的"看起来不一致"其实只差一个 \n,会导致每次都被判为漂移、反复重写 比对统一用 strip();幂等验收以"变更动作 0 条"为准
🟡 订阅更新覆盖配置 OpenClash 订阅更新会覆盖 config/*.yaml 白名单走 custom/*.list 追加式写入,不碰订阅原件
🟡 凭据分散 frp token、VPN 私钥、Clash API 密钥均属敏感 token 只走 FRP_TOKEN 环境变量;artifacts/certs/ 含私钥,勿外传;config/deploy.yaml 已 gitignore

9. 后续步骤(本次之外)

  1. frp 端口映射:在 config/deploy.yaml 的 openwrt.frpc.proxies 追加条目(name/type/local_ip/local_port/remote_port),重跑工程即可生效;注意云安全组需放行对应 remote_port。
  2. 拨入客户端:香港 easy-rsa 为用户设备签客户端证书;服务端已 route push 内网网段,拨入后可直接访问 192.168.31.0/24。
  3. 香港 frps 自启:frps 当前无 systemd 单元(systemctl is-enabled 返回 unknown),主机重启后隧道会整体失效,建议补一个 unit。
  4. 证书续期:CA 与 CRL 有效期至 2031-09-22,届时需整体重建 PKI。

来源:/home/rain/Documents/kvm/openwrt_bypass/frp_openvpn_tunnel/docs/SOP.md | 最后更新:2026-09-16 16:34(由 build/sync.py 同步,请勿在站点侧修改)