From 1eefe41bac31322371ca2198a5675783f8e34c75 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 23 Jul 2026 15:13:49 +0800 Subject: [PATCH] test --- .gitignore | 6 +- config.yaml | 2 + configs/env.development.yaml | 2 + configs/env.production.yaml | 2 + deployments/openvpn/Dockerfile | 25 ++ deployments/openvpn/README.md | 13 + .../docker-compose.rongxing-vpn.snippet.yml | 31 +++ deployments/openvpn/entrypoint.sh | 69 +++++ deployments/openvpn/env.example | 6 + deployments/openvpn/tiany0721.ovpn.example | 23 ++ deployments/openvpn/upload-checklist.txt | 17 ++ docker-compose.prod.yml | 43 +++ docs/戎行OpenVPN接入方案.md | 262 ++++++++++++++++++ internal/config/config.go | 1 + .../external/rongxing/http_client_test.go | 47 ++++ .../external/rongxing/rongxing_factory.go | 3 +- .../external/rongxing/rongxing_service.go | 52 +++- secrets/openvpn/.gitkeep | 3 + 18 files changed, 601 insertions(+), 6 deletions(-) create mode 100644 deployments/openvpn/Dockerfile create mode 100644 deployments/openvpn/README.md create mode 100644 deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml create mode 100644 deployments/openvpn/entrypoint.sh create mode 100644 deployments/openvpn/env.example create mode 100644 deployments/openvpn/tiany0721.ovpn.example create mode 100644 deployments/openvpn/upload-checklist.txt create mode 100644 docs/戎行OpenVPN接入方案.md create mode 100644 internal/infrastructure/external/rongxing/http_client_test.go create mode 100644 secrets/openvpn/.gitkeep diff --git a/.gitignore b/.gitignore index db5acfd..d3773b8 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,8 @@ resources/Pure_Component/ *.dll *.so *.dylib -cmd/api/__debug_bin* \ No newline at end of file +cmd/api/__debug_bin* + +# 戎行 OpenVPN 机密(仅保留 secrets/openvpn/.gitkeep) +secrets/openvpn/* +!secrets/openvpn/.gitkeep \ No newline at end of file diff --git a/config.yaml b/config.yaml index a8855de..59106fe 100644 --- a/config.yaml +++ b/config.yaml @@ -860,6 +860,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 仅戎行请求走 VPN SOCKS;本机已连 OpenVPN GUI 时可留空。生产 Docker:socks5://rongxing-vpn:1080 + proxy: "" logging: enabled: true log_dir: "logs/external_services" diff --git a/configs/env.development.yaml b/configs/env.development.yaml index e3e9812..68e935b 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -279,6 +279,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 开发机用 OpenVPN GUI 直连时可留空;若走本地 Docker VPN 可填 socks5://rongxing-vpn:1080 + proxy: "" logging: enabled: true log_dir: "logs/external_services" diff --git a/configs/env.production.yaml b/configs/env.production.yaml index d3c7815..1c8306e 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -245,6 +245,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 生产:戎行请求经 Docker 内 rongxing-vpn 的 SOCKS5 进企业内网 + proxy: "socks5://rongxing-vpn:1080" logging: enabled: true log_dir: "logs/external_services" diff --git a/deployments/openvpn/Dockerfile b/deployments/openvpn/Dockerfile new file mode 100644 index 0000000..ae3c004 --- /dev/null +++ b/deployments/openvpn/Dockerfile @@ -0,0 +1,25 @@ +# 戎行专用:OpenVPN 客户端 + SOCKS5(仅内网代理,不接管默认路由) +FROM alpine:3.21 + +RUN apk add --no-cache \ + openvpn \ + bash \ + iproute2 \ + microsocks \ + curl + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENV VPN_DIR=/vpn \ + SOCKS_HOST=0.0.0.0 \ + SOCKS_PORT=1080 \ + RONGXING_HOST=192.168.3.43 + +VOLUME ["/vpn"] +EXPOSE 1080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=5 \ + CMD ip link show tun0 >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deployments/openvpn/README.md b/deployments/openvpn/README.md new file mode 100644 index 0000000..e253e96 --- /dev/null +++ b/deployments/openvpn/README.md @@ -0,0 +1,13 @@ +# Rongxing OpenVPN(部署附件) + +完整方案见:[docs/戎行OpenVPN接入方案.md](../../docs/戎行OpenVPN接入方案.md) + +| 文件 | 用途 | +|------|------| +| `Dockerfile` | OpenVPN + microsocks 自建镜像 | +| `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` | +| `tiany0721.ovpn.example` | ovpn 模板 | +| `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) | +| `upload-checklist.txt` | 上传清单 | + +**真实证书放到服务器 secrets 目录或 `./secrets/openvpn/`,不要提交 Git。** diff --git a/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml b/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml new file mode 100644 index 0000000..a704e63 --- /dev/null +++ b/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml @@ -0,0 +1,31 @@ +# 已并入 docker-compose.prod.yml 的 rongxing-vpn 服务。 +# 保留本文件便于 1Panel 单独编排时参考。 +# +# 1Panel 机密目录示例: +# RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn +# +# 独立栈时需 external 网络(名称以 docker network ls 为准): +# networks: +# hyapi-network: +# external: true +# name: hyapi-server_hyapi-network + +services: + rongxing-vpn: + build: + context: ./deployments/openvpn + dockerfile: Dockerfile + image: hyapi-rongxing-vpn:local + container_name: hyapi-rongxing-vpn + environment: + TZ: Asia/Shanghai + RONGXING_HOST: "192.168.3.43" + volumes: + - ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + networks: + - hyapi-network + restart: unless-stopped diff --git a/deployments/openvpn/entrypoint.sh b/deployments/openvpn/entrypoint.sh new file mode 100644 index 0000000..4190986 --- /dev/null +++ b/deployments/openvpn/entrypoint.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# OpenVPN 连通后拉起 SOCKS5;仅服务戎行内网代理。 +set -euo pipefail + +VPN_DIR="${VPN_DIR:-/vpn}" +SOCKS_HOST="${SOCKS_HOST:-0.0.0.0}" +SOCKS_PORT="${SOCKS_PORT:-1080}" +RONGXING_HOST="${RONGXING_HOST:-192.168.3.43}" + +cd "$VPN_DIR" + +CONF="" +for f in *.conf *.ovpn; do + if [ -f "$f" ]; then + CONF="$f" + break + fi +done + +if [ -z "$CONF" ]; then + echo "[rongxing-vpn] 未在 ${VPN_DIR} 找到 .ovpn/.conf,请挂载证书目录" + exit 1 +fi + +echo "[rongxing-vpn] 使用配置: ${CONF}" +echo "[rongxing-vpn] 仅路由 ${RONGXING_HOST}/32,SOCKS5 ${SOCKS_HOST}:${SOCKS_PORT}" + +openvpn \ + --config "$CONF" \ + --cd "$VPN_DIR" \ + --route-nopull \ + --route "$RONGXING_HOST" 255.255.255.255 \ + --daemon \ + --writepid /var/run/openvpn.pid \ + --log /var/log/openvpn.log \ + --verb 3 + +for i in $(seq 1 90); do + if ip link show tun0 >/dev/null 2>&1; then + echo "[rongxing-vpn] tun0 已就绪 (${i}s)" + break + fi + if [ "$i" -eq 90 ]; then + echo "[rongxing-vpn] 等待 tun0 超时,OpenVPN 日志:" + cat /var/log/openvpn.log || true + exit 1 + fi + sleep 1 +done + +# 前台跑 microsocks,进程退出则容器退出;同时监控 openvpn +microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" & +SOCKS_PID=$! + +cleanup() { + kill "$SOCKS_PID" 2>/dev/null || true + if [ -f /var/run/openvpn.pid ]; then + kill "$(cat /var/run/openvpn.pid)" 2>/dev/null || true + fi +} +trap cleanup EXIT INT TERM + +while kill -0 "$(cat /var/run/openvpn.pid)" 2>/dev/null && kill -0 "$SOCKS_PID" 2>/dev/null; do + sleep 5 +done + +echo "[rongxing-vpn] OpenVPN 或 SOCKS 已退出" +cat /var/log/openvpn.log || true +exit 1 diff --git a/deployments/openvpn/env.example b/deployments/openvpn/env.example new file mode 100644 index 0000000..baab3e7 --- /dev/null +++ b/deployments/openvpn/env.example @@ -0,0 +1,6 @@ +# 可选:复制为 .env 供 docker compose 读取 +# 1Panel 推荐: +RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn + +# 本地默认(不设则用 compose 内 ./secrets/openvpn): +# RONGXING_VPN_CONFIG=./secrets/openvpn diff --git a/deployments/openvpn/tiany0721.ovpn.example b/deployments/openvpn/tiany0721.ovpn.example new file mode 100644 index 0000000..c18e024 --- /dev/null +++ b/deployments/openvpn/tiany0721.ovpn.example @@ -0,0 +1,23 @@ +# 复制为服务器上的 tiany0721.ovpn,并与 ca.crt / 证书 / 密钥同目录放置。 +# 勿将真实证书提交到 Git。 + +client +dev tun +proto tcp +remote 111.31.14.226 32199 +resolv-retry infinite +nobind +persist-key +persist-tun +ca ca.crt +cert tiany0721.crt +key tiany0721.key +remote-cert-tls server +tls-auth ta.key 1 +cipher AES-256-CBC +compress lz4-v2 +verb 3 + +# 仅路由戎行主机,不接管默认网关(其它流量仍走容器原出口) +route-nopull +route 192.168.3.43 255.255.255.255 diff --git a/deployments/openvpn/upload-checklist.txt b/deployments/openvpn/upload-checklist.txt new file mode 100644 index 0000000..e671b8c --- /dev/null +++ b/deployments/openvpn/upload-checklist.txt @@ -0,0 +1,17 @@ +# 从本机 C:\Users\a1726\Desktop\11\OpenVPN\config 上传到服务器: +# /opt/1panel/apps/hyapi-secrets/openvpn/ +# +# 需要: +# [ ] tiany0721.ovpn (上传后按 example 加上 route-nopull + route) +# [ ] ca.crt +# [ ] tiany0721.crt +# [ ] tiany0721.key +# [ ] ta.key +# +# 不需要: +# [x] bin/、doc/、Uninstall.exe、openvpn-gui.exe 等 Windows 安装文件 +# +# 权限: +# chmod 700 /opt/1panel/apps/hyapi-secrets /opt/1panel/apps/hyapi-secrets/openvpn +# chmod 600 *.key +# chmod 644 *.crt *.ovpn diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8162020..2c5e8d2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -38,6 +38,45 @@ services: ports: - "25010:5432" + # 戎行 OpenVPN 客户端 + SOCKS5(仅 192.168.3.43 走隧道) + # 证书目录:默认 ./secrets/openvpn;1Panel 建议: + # export RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn + rongxing-vpn: + build: + context: ./deployments/openvpn + dockerfile: Dockerfile + image: hyapi-rongxing-vpn:local + container_name: hyapi-rongxing-vpn + environment: + TZ: Asia/Shanghai + RONGXING_HOST: "192.168.3.43" + SOCKS_HOST: "0.0.0.0" + SOCKS_PORT: "1080" + volumes: + - ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + networks: + - hyapi-network + # 禁止把 1080 暴露到宿主机公网 + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "ip link show tun0 >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 60s + deploy: + resources: + limits: + memory: 256M + cpus: "0.3" + reservations: + memory: 64M + cpus: "0.1" + # Redis 缓存 (生产环境) redis: image: redis:8.0.2 @@ -101,6 +140,8 @@ services: condition: service_healthy redis: condition: service_healthy + rongxing-vpn: + condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s @@ -134,6 +175,8 @@ services: condition: service_healthy redis: condition: service_healthy + rongxing-vpn: + condition: service_started healthcheck: test: ["CMD", "ps", "aux", "|", "grep", "worker"] interval: 30s diff --git a/docs/戎行OpenVPN接入方案.md b/docs/戎行OpenVPN接入方案.md new file mode 100644 index 0000000..109b110 --- /dev/null +++ b/docs/戎行OpenVPN接入方案.md @@ -0,0 +1,262 @@ +# 戎行数据源 OpenVPN 接入方案(1Panel) + +> 目标:**仅** `rongxing`(`http://192.168.3.43:7007`)走企业 VPN;Postgres / Redis / 其它外部 API 仍走原出口。 +> 方案:**OpenVPN 容器 + SOCKS5 代理 + 戎行 HTTP 客户端单独走代理**。 + +--- + +## 1. 架构 + +``` + hyapi-network(现有 bridge) +┌─────────────────────────────────────────────────────────────┐ +│ hyapi-app / hyapi-worker │ +│ ├─ DB / Redis / 其它 API ──► 默认出口(不经 VPN) │ +│ └─ 戎行 Client ──► socks5://rongxing-vpn:1080 │ +│ │ │ +│ rongxing-vpn(openvpn + microsocks)│ │ +│ TUN ──► OpenVPN Server 111.31.14.226:32199 │ +│ ──► 企业内网 192.168.3.43:7007 │ +└─────────────────────────────────────────────────────────────┘ +``` + +要点: + +- 业务容器网络身份不变(端口、健康检查、服务发现保持现状)。 +- VPN 只负责进企业网;对外暴露的是 Docker 内网 SOCKS5,不映射到公网。 +- OpenVPN 使用 `route-nopull`,只加 `192.168.3.43`,避免抢走默认路由。 + +--- + +## 2. VPN 材料要不要上传? + +**要上传,但只上传「客户端配置与证书」,不要上传整个 Windows OpenVPN 安装目录。** + +| 本地路径(你机器) | 是否上传 | 说明 | +|--------------------|----------|------| +| `OpenVPN\config\tiany0721.ovpn` | ✅ | 连接配置(服务器上会再加路由控制指令) | +| `OpenVPN\config\ca.crt` | ✅ | CA | +| `OpenVPN\config\tiany0721.crt` | ✅ | 客户端证书 | +| `OpenVPN\config\tiany0721.key` | ✅ | 客户端私钥(敏感) | +| `OpenVPN\config\ta.key` | ✅ | tls-auth | +| `OpenVPN\bin\`、`doc\`、`Uninstall.exe` 等 | ❌ | Windows 安装包,容器不用 | + +服务器上使用 **Linux 版 OpenVPN 镜像**,复用上述 5 个文件即可。 + +--- + +## 3. 1Panel 推荐目录(最优落点) + +假设 1Panel 数据盘常用根为 `/opt/1panel`(若你改过安装目录,把前缀换成实际路径)。 + +**推荐:密钥与业务代码分离。** + +```text +/opt/1panel/apps/hyapi/ # 业务 Compose / 代码(可随发布更新) +├── docker-compose.prod.yml +├── config.yaml / configs/... +├── ... +└── (不要把 .key 放进 git 仓库目录) + +/opt/1panel/apps/hyapi-secrets/openvpn/ # ★ VPN 机密目录(推荐) +├── tiany0721.ovpn # 服务器侧改过的版本(见下文) +├── ca.crt +├── tiany0721.crt +├── tiany0721.key # 权限 600 +└── ta.key # 权限 600 +``` + +备选(同样可以): + +```text +/data/hyapi/secrets/openvpn/ # 若 1Panel 数据盘挂在 /data +``` + +**不要放:** + +- 网站静态目录 `/opt/1panel/www/...`(易被误暴露) +- 仓库内 `hyapi-server/config/` 或会随 CI 同步的路径(防泄漏) + +**权限建议(SSH 上执行):** + +```bash +sudo mkdir -p /opt/1panel/apps/hyapi-secrets/openvpn +sudo chmod 700 /opt/1panel/apps/hyapi-secrets +sudo chmod 700 /opt/1panel/apps/hyapi-secrets/openvpn +# 上传文件后: +sudo chmod 600 /opt/1panel/apps/hyapi-secrets/openvpn/*.key +sudo chmod 644 /opt/1panel/apps/hyapi-secrets/openvpn/*.{crt,ovpn} +``` + +--- + +## 4. 服务器侧 OpenVPN 配置(准备好再挂载) + +把本地 `tiany0721.ovpn` 拷到服务器后,**追加**以下行(保留原有 `remote` / 证书引用): + +```conf +# --- 仅路由戎行内网,不接管默认网关 --- +route-nopull +route 192.168.3.43 255.255.255.255 +``` + +完整示例见仓库:`deployments/openvpn/tiany0721.ovpn.example`。 + +证书相对路径保持与 `.ovpn` 同目录即可(`ca ca.crt` 等),挂载整个目录到容器 `/vpn`。 + +--- + +## 5. Compose 片段(接入现有 prod) + +参考文件:`deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml` + +核心服务示意: + +```yaml + rongxing-vpn: + image: ghcr.io/linuxserver/openvpn-as:latest # 不推荐 AS;见下方推荐镜像 +``` + +**推荐用轻量客户端镜像**(文档配套 snippet 已按此写): + +- 镜像:`dperson/openvpn-client` **或** 自建 `alpine + openvpn + microsocks` +- 能力:`cap_add: [NET_ADMIN]`,`devices: [/dev/net/tun]` +- 挂载:`/opt/1panel/apps/hyapi-secrets/openvpn:/vpn:ro` +- 网络:加入现有 `hyapi-network` +- **不要**把 `1080` 映射到宿主机公网端口 + +业务侧: + +- `hyapi-app` /(若异步也会调戎行)`hyapi-worker` 保持原 `networks: [hyapi-network]` +- 配置增加代理地址:`socks5://rongxing-vpn:1080` + +--- + +## 6. 应用配置(仅戎行) + +在 `rongxing` 段增加代理字段(实现代码时再接线;配置先预留): + +```yaml +rongxing: + url: "http://192.168.3.43:7007" + account: "..." + password: "..." + app_id: "..." + private_key: "..." + timeout: 10s + # 仅戎行走 VPN 旁路代理;留空则直连(开发机已连 VPN 时可留空) + proxy: "socks5://rongxing-vpn:1080" + logging: + # ... +``` + +开发机若已用 GUI 连上 VPN,`proxy` 留空即可,无需起 Docker VPN。 + +--- + +## 7. 代码改动清单(后续开发) + +当前 `RongxingService` 使用裸 `http.Client`,需: + +1. `RongxingConfig` / `serviceConfig` 增加 `Proxy string` +2. `NewRongxingServiceWithConfig` 传入 `cfg.Rongxing.Proxy` +3. 创建 `http.Client` 时:若 `Proxy` 非空,用 `http.ProxyURL` 或 SOCKS5 dialer(`golang.org/x/net/proxy`) +4. **只改戎行包**,其它外部服务不动 + +验证点:未配 `proxy` 时行为与现在一致。 + +--- + +## 8. 1Panel 上操作清单 + +### 8.1 一次性准备 + +1. SSH 登录服务器,创建 `/opt/1panel/apps/hyapi-secrets/openvpn/` +2. 用 SFTP / `scp` 上传 5 个文件(见第 2 节) +3. 编辑服务器上的 `tiany0721.ovpn`,加上 `route-nopull` + `route 192.168.3.43 ...` +4. 设好目录/文件权限(第 3 节) +5. 确认内核允许 TUN:多数 1Panel/Linux 默认有 `/dev/net/tun`;若容器报错再装 `tun` 模块 + +### 8.2 接入 Compose + +任选其一: + +- **A. 1Panel「编排」**:打开现有 hyapi 的 Compose,粘贴 `rongxing-vpn` 服务片段,挂载 secrets 目录,重新部署 +- **B. 独立编排**:单独建一个 `rongxing-vpn` 栈,但必须加入与 hyapi **同一个** Docker network(`hyapi-network` 的外部网络名以 `docker network ls` 为准) + +查现有网络名: + +```bash +docker network ls | grep hyapi +docker inspect hyapi-app-prod --format '{{json .NetworkSettings.Networks}}' +``` + +若网络是 compose 项目前缀(如 `hyapi-server_hyapi-network`),独立栈里要: + +```yaml +networks: + hyapi-network: + external: true + name: hyapi-server_hyapi-network # 以实际名为准 +``` + +### 8.3 发布后验证 + +```bash +# 1) VPN 容器是否拿到 tun / 路由 +docker exec -it ip route +# 应能看到 192.168.3.43 经 tun0 + +# 2) 在 VPN 容器内测内网(镜像若有 curl) +docker exec -it wget -qO- --timeout=5 http://192.168.3.43:7007/ || true + +# 3) 从业务容器经 SOCKS5 测(需业务镜像有 curl,或临时 alpine) +docker run --rm --network curlimages/curl:8.5.0 \ + -x socks5h://rongxing-vpn:1080 -m 10 -v http://192.168.3.43:7007/ + +# 4) 确认「不走代理」时业务仍能访问公网/库(对照现有 health) +curl -f http://127.0.0.1:25000/health +``` + +业务侧再打一笔戎行真实查询,看 `logs/external_services` 下戎行日志是否成功。 + +--- + +## 9. 安全与运维 + +- `.key` / `ta.key` / 账号密码:**不进 Git**;secrets 目录不挂到可下载的 Web 根。 +- SOCKS5 **仅**监听容器网络,不 `-p 1080:1080` 到宿主机。 +- VPN 证书到期前找对方续期,替换 secrets 目录文件后 `docker compose restart rongxing-vpn`。 +- 断线策略:容器 `restart: unless-stopped`;OpenVPN 侧已有 `resolv-retry infinite` / `persist-tun`。 +- 监控:可对 `rongxing-vpn` 做 1Panel 容器存活告警;业务侧戎行错误率告警。 + +--- + +## 10. 本地文件 → 服务器对照表 + +| 本地 | 服务器推荐路径 | +|------|----------------| +| `...\OpenVPN\config\tiany0721.ovpn` | `/opt/1panel/apps/hyapi-secrets/openvpn/tiany0721.ovpn`(改过路由) | +| `...\OpenVPN\config\ca.crt` | 同目录 | +| `...\OpenVPN\config\tiany0721.crt` | 同目录 | +| `...\OpenVPN\config\tiany0721.key` | 同目录(600) | +| `...\OpenVPN\config\ta.key` | 同目录(600) | +| Windows `bin/` 整包 | **不上传** | + +--- + +## 11. 实施状态与顺序 + +**代码与 Compose 已落地**(见仓库当前变更): + +- `rongxing` 支持 `proxy`(SOCKS5/HTTP) +- `docker-compose.prod.yml` 已加入 `rongxing-vpn` 服务 +- `deployments/openvpn/` 自建镜像(OpenVPN + microsocks) +- 生产配置 `configs/env.production.yaml`:`proxy: "socks5://rongxing-vpn:1080"` + +**1Panel 服务器仍需人工完成:** + +1. 创建 `/opt/1panel/apps/hyapi-secrets/openvpn/`,上传 5 个证书/配置文件(可用本机已备好的 `secrets/openvpn/` 内容) +2. 设置环境变量:`RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn` +3. 拉取/构建并重新部署 Compose(确保服务器能访问 Docker Hub / 镜像源以下载 `alpine`) +4. 验证:`docker exec hyapi-rongxing-vpn ip route` 含 `192.168.3.43`;业务侧打一笔戎行查询 diff --git a/internal/config/config.go b/internal/config/config.go index b263956..251d959 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -846,6 +846,7 @@ type RongxingConfig struct { AppID string `mapstructure:"app_id"` // 应用 ID PrivateKey string `mapstructure:"private_key"` // RSA 私钥(PEM 或 PKCS#8 Base64) Timeout time.Duration `mapstructure:"timeout"` + Proxy string `mapstructure:"proxy"` // 可选,仅戎行请求走此代理,如 socks5://rongxing-vpn:1080 Logging RongxingLoggingConfig `mapstructure:"logging"` } diff --git a/internal/infrastructure/external/rongxing/http_client_test.go b/internal/infrastructure/external/rongxing/http_client_test.go new file mode 100644 index 0000000..0068c22 --- /dev/null +++ b/internal/infrastructure/external/rongxing/http_client_test.go @@ -0,0 +1,47 @@ +package rongxing + +import ( + "net/http" + "testing" + "time" +) + +func TestNewHTTPClient_Direct(t *testing.T) { + client, err := newHTTPClient(5*time.Second, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client.Timeout != 5*time.Second { + t.Fatalf("timeout = %v", client.Timeout) + } + if client.Transport != nil && client.Transport != http.DefaultTransport { + // 直连允许 Transport 为 nil(使用 DefaultTransport) + } +} + +func TestNewHTTPClient_Socks5(t *testing.T) { + client, err := newHTTPClient(3*time.Second, "socks5://rongxing-vpn:1080") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client.Transport == nil { + t.Fatal("expected custom transport for socks5") + } +} + +func TestNewHTTPClient_HTTPProxy(t *testing.T) { + client, err := newHTTPClient(3*time.Second, "http://127.0.0.1:8888") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client.Transport == nil { + t.Fatal("expected custom transport for http proxy") + } +} + +func TestNewHTTPClient_UnsupportedScheme(t *testing.T) { + _, err := newHTTPClient(time.Second, "ftp://x") + if err == nil { + t.Fatal("expected error for unsupported scheme") + } +} diff --git a/internal/infrastructure/external/rongxing/rongxing_factory.go b/internal/infrastructure/external/rongxing/rongxing_factory.go index 795777a..fdd7dab 100644 --- a/internal/infrastructure/external/rongxing/rongxing_factory.go +++ b/internal/infrastructure/external/rongxing/rongxing_factory.go @@ -46,5 +46,6 @@ func NewRongxingServiceWithConfig(cfg *config.Config) (*RongxingService, error) AppID: cfg.Rongxing.AppID, PrivateKey: cfg.Rongxing.PrivateKey, Timeout: timeout, - }, logger), nil + Proxy: cfg.Rongxing.Proxy, + }, logger) } diff --git a/internal/infrastructure/external/rongxing/rongxing_service.go b/internal/infrastructure/external/rongxing/rongxing_service.go index 2713ebb..229681d 100644 --- a/internal/infrastructure/external/rongxing/rongxing_service.go +++ b/internal/infrastructure/external/rongxing/rongxing_service.go @@ -7,7 +7,9 @@ import ( "errors" "fmt" "io" + "net" "net/http" + "net/url" "strings" "sync" "time" @@ -15,6 +17,7 @@ import ( "hyapi-server/internal/shared/external_logger" "go.uber.org/zap" + "golang.org/x/net/proxy" ) const ( @@ -32,12 +35,14 @@ type serviceConfig struct { AppID string PrivateKey string Timeout time.Duration + Proxy string } // RongxingService 戎行数据源服务 type RongxingService struct { config serviceConfig logger *external_logger.ExternalServiceLogger + client *http.Client tokenMu sync.RWMutex cachedToken string @@ -65,12 +70,52 @@ func (r apiResponse) code() string { } // NewRongxingService 创建戎行服务实例 -func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) *RongxingService { +func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) (*RongxingService, error) { if cfg.Timeout <= 0 { cfg.Timeout = defaultRequestTimeout } cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/") - return &RongxingService{config: cfg, logger: logger} + client, err := newHTTPClient(cfg.Timeout, cfg.Proxy) + if err != nil { + return nil, err + } + return &RongxingService{config: cfg, logger: logger, client: client}, nil +} + +// newHTTPClient 构建 HTTP 客户端;proxyURL 支持 socks5/socks5h/http/https,空则直连。 +func newHTTPClient(timeout time.Duration, proxyURL string) (*http.Client, error) { + client := &http.Client{Timeout: timeout} + proxyURL = strings.TrimSpace(proxyURL) + if proxyURL == "" { + return client, nil + } + + u, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("解析 proxy 失败: %w", err) + } + + switch strings.ToLower(u.Scheme) { + case "socks5", "socks5h": + dialer, err := proxy.FromURL(u, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("创建 SOCKS 代理失败: %w", err) + } + transport := &http.Transport{} + if cd, ok := dialer.(proxy.ContextDialer); ok { + transport.DialContext = cd.DialContext + } else { + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } + } + client.Transport = transport + case "http", "https": + client.Transport = &http.Transport{Proxy: http.ProxyURL(u)} + default: + return nil, fmt.Errorf("不支持的 proxy scheme: %s(支持 socks5/socks5h/http/https)", u.Scheme) + } + return client, nil } // GetConfig 获取运行时配置 @@ -309,8 +354,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri } func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) { - client := &http.Client{Timeout: s.config.Timeout} - resp, err := client.Do(req) + resp, err := s.client.Do(req) if err != nil { return nil, 0, err } diff --git a/secrets/openvpn/.gitkeep b/secrets/openvpn/.gitkeep new file mode 100644 index 0000000..7ab5b85 --- /dev/null +++ b/secrets/openvpn/.gitkeep @@ -0,0 +1,3 @@ +# 本地/仓库内占位:真实证书请放到此目录(勿提交 *.key / *.crt / *.ovpn) +# 1Panel 推荐改用绝对路径:/opt/1panel/apps/hyapi-secrets/openvpn +# Compose 通过环境变量 RONGXING_VPN_CONFIG 覆盖挂载源目录。