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. 目标¶
- OpenWrt 上 frpc 与香港 frps 版本对齐(0.70.1)并稳定在线(日志出现
login to server success); - OpenWrt 与香港建立 OpenVPN 隧道,设备为
tun0,地址10.8.0.9; - 香港主机可达内网
192.168.31.0/24(实测192.168.31.249、192.168.31.1均 0% 丢包); - 香港既有能力不受损:VPN 客户端经香港出口上网的 SNAT 仍生效,仅对目标内网的流量豁免;
- 幂等:连续两次执行,第二次零变更、全跳过、无报错;
- 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. 后续步骤(本次之外)¶
- frp 端口映射:在
config/deploy.yaml的openwrt.frpc.proxies追加条目(name/type/local_ip/local_port/remote_port),重跑工程即可生效;注意云安全组需放行对应remote_port。 - 拨入客户端:香港 easy-rsa 为用户设备签客户端证书;服务端已
routepush 内网网段,拨入后可直接访问192.168.31.0/24。 - 香港 frps 自启:
frps当前无 systemd 单元(systemctl is-enabled返回 unknown),主机重启后隧道会整体失效,建议补一个 unit。 - 证书续期: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同步,请勿在站点侧修改)