Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10f48f8619 | ||
|
|
694ddac6eb |
No files matched your search
@@ -199,7 +199,7 @@ apiBaseUrls: [
|
||||
- **敏感凭据强制忽略,永远不被远端覆盖**(代码写死,无需配置):
|
||||
- 后端 `access_token`:不随远端覆盖(保持 环境变量 > setting.json),远端里的 `access_token` 会被忽略
|
||||
- 中间件 `apiKeys`:不随远端覆盖,远端里的 `apiKeys` 会被忽略
|
||||
- `remote-ingore-config`(后端键名 `remote-ingore-config` / env `REMOTE_INGORE_CONFIG`):额外指定**不被远端覆盖的配置项列表**,数组中的键即使远端下发也不生效。适用于 access_token / apiKeys 之外的敏感项(如 `ipw-node-key`、`dns-server` 等),也可覆盖"非空才覆盖"规则
|
||||
- `remote-ingore-config`(后端键名 `remote-ingore-config` / env `REMOTE_INGORE_CONFIG`):额外指定**不被远端覆盖的配置项列表**,数组中的键即使远端下发也不生效。适用于 access_token / apiKeys 之外的敏感项(如 `node-key`、`dns-server` 等),也可覆盖"非空才覆盖"规则
|
||||
|
||||
**拉取行为**:
|
||||
|
||||
@@ -256,7 +256,7 @@ systemd 中同样用 `Environment="REMOTE_CONFIG_URL=..."` 配置。
|
||||
|
||||
**安全与排查**:
|
||||
|
||||
- **⚠️ 敏感凭据不得上传远端**:远端配置(Gist / 对象存储 / 静态托管等)**禁止包含任何密钥**——`apiKeys`、`access_token`、节点 key(`ipw-node-key`)等。这些值会被强制忽略,但远端文件本身一旦泄露等于直接暴露凭据;凭据一律放本地 `setting.json` 或环境变量注入
|
||||
- **⚠️ 敏感凭据不得上传远端**:远端配置(Gist / 对象存储 / 静态托管等)**禁止包含任何密钥**——`apiKeys`、`access_token`、节点 key(`node-key`)等。这些值会被强制忽略,但远端文件本身一旦泄露等于直接暴露凭据;凭据一律放本地 `setting.json` 或环境变量注入
|
||||
- 远端 URL 必须能被节点访问;优先使用 **HTTPS** 地址
|
||||
- 远端配置文件如果放在公开可访问的位置(公开 Gist、无鉴权对象存储),即便不含密钥,也等于把节点拓扑、节点 id 等敏感信息公之于众,建议放在私有仓库 / 带鉴权的对象存储 / 内部服务上
|
||||
- 常见失败排查:
|
||||
|
||||
@@ -39,7 +39,7 @@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o middleware-go-linux-amd64 .
|
||||
|
||||
所有配置可用环境变量覆盖(`API_BASE_URLS` / `IP_LOCATION_APIS` / `CORS` / `APIKEYS` / `RATE_LIMIT` 等,数组/对象用 JSON 字符串),优先级:**环境变量 > setting.json > 默认值**(远端配置 `REMOTE_CONFIG_URL` 高于两者,详见 [配置文件 - 远端配置](/guide/config#远端配置remoteconfigurl))。
|
||||
|
||||
需要守护运行时,参考 [快速入门](/guide/getting-started) 中的 systemd 配置(`WorkingDirectory` 指向 `middleware-go/setting.json` 所在目录)。
|
||||
需要守护运行时,参考 [后端节点部署 - 方案五:一键安装](/guide/deploy-node#方案五一键安装installsh) 的 systemd 管理方式(`ExecStart` 指向中间件二进制,`WorkingDirectory` 指向 `middleware-go/setting.json` 所在目录)。
|
||||
|
||||
## WS 通道(拨测数据经 WebSocket 传输)
|
||||
|
||||
@@ -87,7 +87,7 @@ middleware-go 内置 WS 服务端,后端节点可作为 **WS 客户端**连入
|
||||
|
||||
### 节点侧接入(WS 客户端)
|
||||
|
||||
1. 连接 `ws://<中间件IP>:8092/ws`
|
||||
1. 连接 `ws://<中间件IP>:8092/ws`(本仓库后端节点接入方式见 [后端节点部署 - WS 通道接入](/guide/deploy-node#ws-通道接入可选),配置 `WS_URL` / `NODE_ID` / `NODE_KEY` 即可自动连接注册)
|
||||
2. 首条消息发 `register { "nodeId": "<与中间件配置一致的节点 id>", "key": "<注册凭证>" }`(节点在 setting.json `apiKeys` 里配了 key 就必须传对;未配置 key 的开放节点可不传)
|
||||
3. 收到 `probe` → 执行拨测 → 回 `probe_result`(必须携带原 `requestId`,支持乱序返回,同一连接可并发多个拨测)
|
||||
4. 收到 `ping` → 回 `pong`;`status` 可忽略或记录
|
||||
|
||||
@@ -53,8 +53,69 @@ go build -o lemonipw main.go
|
||||
./lemonipw
|
||||
```
|
||||
|
||||
首次启动自动下载 IP 数据库(约 200MB),之后每 24 小时自动更新一次。需要守护运行时,参考 [快速入门](/guide/getting-started) 中的 systemd 配置。
|
||||
首次启动自动下载 IP 数据库(约 200MB),之后每 24 小时自动更新一次。需要守护运行时,可参考下方「方案五」生成的 systemd 服务,或手动创建 service 文件(`ExecStart` 指向二进制,`WorkingDirectory` 指向 `setting.json` 所在目录)。
|
||||
|
||||
## 方案五:一键安装(install.sh)
|
||||
|
||||
仓库根目录提供 `install.sh` 一键安装脚本:自动检测架构 → 下载最新 release 二进制 → **交互式输入配置**(无需准备 setting.json,配置以环境变量注入)→ 生成 systemd 服务并守护进程。
|
||||
|
||||
```bash
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
**交互式配置项**(直接回车使用默认值):
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| 安装目录 | `/opt/lemon-ipw` | 二进制与工作目录 |
|
||||
| 监听端口 | `8080` | `PORTS` |
|
||||
| access_token | 留空 | 留空 = 不启用鉴权 |
|
||||
| DNS 服务器 | `119.28.28.28:53` | 主从逗号分隔(`119.28.28.28:53,223.5.5.5:53`) |
|
||||
| DNSSEC 专用 DNS | 留空 | 留空 = 沿用 dns-server |
|
||||
| IP 数据库 | `Y` | `IPDB`,首次启动下载约 200MB |
|
||||
| CORS | 留空 | 逗号分隔允许来源 |
|
||||
| 远端配置地址 | 留空 | `REMOTE_CONFIG_URL` |
|
||||
| WS 通道接入 | `N` | 选 `y` 后继续输入 WS_URL / NODE_ID / NODE_KEY |
|
||||
| 其他环境变量 | 无 | 每行一个 `K=V`,空行结束 |
|
||||
|
||||
**WS 接入特殊规则**:
|
||||
|
||||
- `WS_URL` 留空时自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top`(可改填自己的中间件地址,逗号分隔多备)
|
||||
- `NODE_ID` **强制自动生成 UUID**(无需输入,如 `4580ea9d-2a16-4b8e-8090-5f2e7f0a2229`),适合作为节点唯一标识
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS(中间件 `apiKeys` 必须包含该节点,否则注册被拒 401);安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 setting.json 的 `apiKeys`
|
||||
|
||||
**安装完成后自动生成并启动 systemd 服务**(`lemon-ipw.service`),配置以 `Environment="K=V"` 注入,无需 setting.json。常用管理命令:
|
||||
|
||||
```bash
|
||||
systemctl status lemon-ipw # 状态
|
||||
journalctl -u lemon-ipw -f # 日志
|
||||
sudo systemctl restart lemon-ipw # 重启(改配置后)
|
||||
```
|
||||
|
||||
修改配置:`sudo systemctl edit --full lemon-ipw` 编辑环境变量,保存后 `sudo systemctl daemon-reload && sudo systemctl restart lemon-ipw` 生效。
|
||||
|
||||
> **版本兼容提醒**:v1.1.0 及更早的 release 二进制读的是旧环境变量名 `IPW_WS_URL` / `IPW_NODE_ID` / `IPW_NODE_KEY`(8-20 及之前构建)。install.sh 生成的 service 用的是新名 `WS_URL` / `NODE_ID` / `NODE_KEY`——**旧二进制 + 新 service 会因读不到配置而完全不启用 WS**(启动日志无任何 ws 输出)。升级到包含改名后的新 release 即可;急用可临时把 service 环境变量改回 `IPW_` 前缀。
|
||||
|
||||
## 配置
|
||||
|
||||
后端配置见 [配置文件](/guide/config):`setting.json` 或环境变量,支持远端配置(`REMOTE_CONFIG_URL`,优先级:远端 > 环境变量 > setting.json)。
|
||||
|
||||
## WS 通道接入(可选)
|
||||
|
||||
后端节点可作为 WS 客户端接入 [独立中间件](/guide/deploy-middleware) 的 WS 通道,拨测请求经 WebSocket 转发,节点本地执行探针后回传。不配置则完全走原 HTTP 接口。
|
||||
|
||||
**配置三个环境变量**(或 setting.json 的 `ws-url` / `node-id` / `node-key`,env 优先;也支持远端配置覆盖,除非列入 `remote-ingore-config`):
|
||||
|
||||
```bash
|
||||
WS_URL=ws://<中间件IP>:8092/ws \
|
||||
NODE_ID=<节点id,与中间件 apiBaseUrls 的节点 id 一致> \
|
||||
NODE_KEY=<注册key,与中间件 apiKeys[节点id] 一致;节点未配置 key 可留空> \
|
||||
./lemonipw
|
||||
```
|
||||
|
||||
- `WS_URL` 支持**逗号分隔多个中间件**(`ws://主:8092/ws,ws://备:8092/ws`),第一个为主,主故障自动切换下一个
|
||||
- **双向心跳**:节点每 10s 发 ping,中间件回 pong;心跳发送连续失败 3 次判定中间件不可用,断开后 3s 重试(注册被拒 30s 重试)
|
||||
- 收到 `probe` 后节点直调探针函数(带缓存)并回 `probe_result`,结果与 HTTP 通道一致
|
||||
- 节点注册带 `NODE_KEY`:中间件 `apiKeys` 里配了 key 就必须传对,否则返回 401 并断开;未配置 key 的节点开放注册
|
||||
|
||||
**注意**:`NODE_ID` 必须与中间件 `apiBaseUrls` / `TCPing` / `SpeedTest` 等配置中的节点 `id` 一致,且该节点需配置 `"ws": true` 才会走 WS 通道。未连接中间件时,`ws:true` 节点的拨测会返回 502。
|
||||
+59
-142
@@ -1,155 +1,72 @@
|
||||
# 快速开始
|
||||
|
||||
> 本章节将指导您以最低成本部署一个功能完整的测试站
|
||||
|
||||
## 尝试一下
|
||||
我们的开发团队已经部署了一个功能完整的预览站:
|
||||
|
||||
我们的开发团队已经部署了一个功能完整的预览站:
|
||||
[预览站点](https://ipw.wsmdn.top/)
|
||||
|
||||
## 前置准备
|
||||
1. Git
|
||||
2. Node.js 24+
|
||||
3. Go1.26
|
||||
4. 一台双栈网络环境的服务器(Linux+Systemd)
|
||||
|
||||
- 一台 Linux 服务器(需 root 权限 + systemd)
|
||||
- 服务器上可访问 GitHub(下载 release 二进制)
|
||||
- 无需本地编译环境,无需准备配置文件
|
||||
|
||||
## 开始部署
|
||||
### 构建后端
|
||||
1. 拉取代码
|
||||
```bash
|
||||
git clone https://github.com/nomdn/ipw-cn && cd ipw-cn
|
||||
```
|
||||
2. 安装依赖
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
3. 构建程序
|
||||
```bash
|
||||
go build
|
||||
```
|
||||
### 构建中间层(可选)
|
||||
> [!TIP]
|
||||
> 如果你的服务器到测速节点的链接延迟不大,可以跳过这一步
|
||||
1. 切换目录
|
||||
```bash
|
||||
cd middleware-go
|
||||
```
|
||||
2. 安装依赖
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
3. 构建程序
|
||||
```bash
|
||||
go build
|
||||
```
|
||||
### 构建前端
|
||||
1. 切换目录
|
||||
```bash
|
||||
cd frontend-ssr
|
||||
```
|
||||
2. 安装依赖
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
3. 本地开发(可选)
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
4. 生产构建
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
> 前端部署方式(Cloudflare Workers / EdgeOne Makers 等)见 [前端部署](/guide/deploy-frontend)。
|
||||
|
||||
### 配置文件
|
||||
|
||||
> 完整字段说明、模板与远端配置用法见 [配置文件](/guide/config)。以下为三个组件的最小示例。
|
||||
|
||||
**后端**(`setting.json`,env 可覆盖 `PORTS` / `DNS_SERVER` / `CORS` / `ACCESS_TOKEN` 等):
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 8080,
|
||||
"dns-server": "119.28.28.28:53",
|
||||
"cors": "",
|
||||
"access_token": ""
|
||||
}
|
||||
```
|
||||
|
||||
**前端**(`frontend-ssr/config/index.ts`,节点 `id` 与中间件 `apiKeys` 键对应):
|
||||
|
||||
```ts
|
||||
const config = {
|
||||
siteUrl: "https://your-domain.com/",
|
||||
siteName: "你的站点名称",
|
||||
EnableInternalMiddleware: true,
|
||||
rateLimitPerMinute: 120,
|
||||
Middleware: <string[]>[],
|
||||
umamiHost: "", umamiScriptUrl: "", umamiWebsiteId: "",
|
||||
ICP: "", GongAn: "",
|
||||
noindex: false,
|
||||
v4OnlyAPI: "https://4.wsmdn.top", v6OnlyAPI: "https://6.wsmdn.top", DualStackAPI: "https://test.wsmdn.top",
|
||||
apiBaseUrls: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
IPLocationAPIs: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
TCPing: { DualStack: [], IPv4: [ { label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" } ], IPv6: [] },
|
||||
SpeedTest: { DualStack: [], IPv4: [ { label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" } ], IPv6: [] },
|
||||
NSLookup: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
]
|
||||
}
|
||||
export { config }
|
||||
```
|
||||
|
||||
**独立中间件**(`middleware-go/setting.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"port": "8091",
|
||||
"cors": "",
|
||||
"rate-limit": 120,
|
||||
"remote-config-url": "",
|
||||
"apiBaseUrls": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ],
|
||||
"apiKeys": { "cn-jiangsu": "" }
|
||||
}
|
||||
```
|
||||
|
||||
> 完整字段(`http-timeout-seconds` / `ws-port` / `remote-ingore-config` 等)见 [配置文件 - 独立中间件](/guide/config#独立中间件middleware-go)。**敏感凭据(`apiKeys` / `access_token`)不得上传远端配置,仅放本地或环境变量。**
|
||||
|
||||
前端内置中间件通过环境变量 **`NUXT_APIKEYS`**(JSON 字符串,如 `'{"cn-jiangsu":"your-key"}'`)注入 key。
|
||||
|
||||
### systemd 守护进程
|
||||
|
||||
以 `/opt/lemon-ipw` 为安装目录(二进制与 `setting.json` 放同一目录),创建服务文件:
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/lemon-ipw.service
|
||||
[Unit]
|
||||
Description=Lemon IPW Backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/lemon-ipw
|
||||
ExecStart=/opt/lemon-ipw/lemonipw
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
> `WorkingDirectory` 必须指向 `setting.json` 所在目录,后端从当前目录读取配置。
|
||||
|
||||
启动与管理:
|
||||
### 交互式一键部署(推荐)
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lemon-ipw
|
||||
sudo systemctl status lemon-ipw
|
||||
sudo journalctl -u lemon-ipw -f
|
||||
# 获取安装脚本(任选其一)
|
||||
git clone https://github.com/nomdn/ipw-cn && cd ipw-cn # 或
|
||||
curl -LO https://raw.githubusercontent.com/nomdn/ipw-cn/main/install.sh
|
||||
|
||||
# 执行安装(root 权限)
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
验证:`curl http://127.0.0.1:8080/` 返回 `{"status":"ok"}` 即正常。中间件也可用同样的方式守护(`ExecStart` 指向中间件二进制,`WorkingDirectory` 指向 `middleware-go/setting.json` 所在目录)。
|
||||
脚本会自动完成:检测架构 → 下载最新 release 二进制 → **交互式输入配置** → 生成并启动 systemd 守护进程。
|
||||
|
||||
**交互式配置项**(直接回车使用默认值):
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| 安装目录 | `/opt/lemon-ipw` | 二进制与工作目录 |
|
||||
| 监听端口 | `8080` | 后端 HTTP 端口 |
|
||||
| access_token | 留空 | 留空 = 不启用鉴权 |
|
||||
| DNS 服务器 | `119.28.28.28:53` | 主从逗号分隔(如 `119.28.28.28:53,223.5.5.5:53`) |
|
||||
| DNSSEC 专用 DNS | 留空 | 留空 = 沿用 dns-server |
|
||||
| IP 数据库 | `Y` | 首次启动下载约 200MB |
|
||||
| CORS | 留空 | 逗号分隔允许来源 |
|
||||
| 远端配置地址 | 留空 | `REMOTE_CONFIG_URL` |
|
||||
| WS 通道接入 | `N` | 选 `y` 后输入 WS_URL / NODE_ID / NODE_KEY |
|
||||
| 其他环境变量 | 无 | 每行一个 `K=V`,空行结束 |
|
||||
|
||||
**WS 通道接入规则**(选 `y` 时):
|
||||
|
||||
- `WS_URL` 留空自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top`,可改填自己的中间件地址
|
||||
- `NODE_ID` **强制自动生成 UUID**(无需输入)
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS;安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 `apiKeys`
|
||||
|
||||
### 验证安装
|
||||
|
||||
```bash
|
||||
systemctl status lemon-ipw # 服务状态(active running)
|
||||
curl http://127.0.0.1:8080/ # 应返回 {"status":"ok"}
|
||||
journalctl -u lemon-ipw -f # 实时日志
|
||||
```
|
||||
|
||||
### 修改配置
|
||||
|
||||
```bash
|
||||
sudo systemctl edit --full lemon-ipw # 编辑环境变量
|
||||
sudo systemctl daemon-reload && sudo systemctl restart lemon-ipw
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 部署**独立中间件**(WS 通道 / 多节点转发):见 [中间件部署](/guide/deploy-middleware)
|
||||
- 部署**前端**(Cloudflare Workers / EdgeOne Makers 等):见 [前端部署](/guide/deploy-frontend)
|
||||
- 完整**配置说明**(后端 / 前端 / 中间件 / 远端配置):见 [配置文件](/guide/config)
|
||||
- 手动**编译部署**(源码构建三个组件):见 [后端节点部署](/guide/deploy-node) / [中间件部署](/guide/deploy-middleware) / [前端部署](/guide/deploy-frontend)
|
||||
+1
-1
@@ -14,7 +14,7 @@ Lemon IPW 采用前后端分离架构,**后端节点、独立中间件、前
|
||||
|
||||
## 快速开始
|
||||
|
||||
想用最低成本部署一套功能完整的测试站,直接看 [快速入门](/guide/getting-started):后端构建 → 前端构建 → 配置文件 → systemd 守护。
|
||||
想用最低成本部署一套功能完整的测试站,直接看 [快速入门](/guide/getting-started):交互式一键部署(install.sh)→ systemd 守护。
|
||||
|
||||
## CI/CD
|
||||
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/bash
|
||||
# ============================================================
|
||||
# 1IPW.CN 后端节点一键安装脚本
|
||||
# - 自动检测架构并下载最新 release 二进制
|
||||
# - 交互式输入配置(环境变量注入,无需 setting.json)
|
||||
# - 生成并启用 systemd 守护进程
|
||||
# 用法:sudo bash install.sh
|
||||
# ============================================================
|
||||
|
||||
text="
|
||||
██╗██████╗ ██╗ ██╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗
|
||||
██║██╔══██╗██║ ██║ ████╗ ██║██╔═══██╗██╔══██╗██╔════╝
|
||||
██║██████╔╝██║ █╗ ██║█████╗██╔██╗ ██║██║ ██║██║ ██║█████╗
|
||||
██║██╔═══╝ ██║███╗██║╚════╝██║╚██╗██║██║ ██║██║ ██║██╔══╝
|
||||
██║██║ ╚███╔███╔╝ ██║ ╚████║╚██████╔╝██████╔╝███████╗
|
||||
╚═╝╚═╝ ╚══╝╚══╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||
|
||||
"
|
||||
echo "$text"
|
||||
|
||||
set -e
|
||||
|
||||
# ---------- 前置检查 ----------
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "错误:需要 root 权限(写入 /etc/systemd/system 与安装目录),请用 sudo 运行" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo "错误:未检测到 systemd(systemctl 不存在),无法创建守护进程" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
|
||||
echo "错误:未找到 wget 或 curl,无法下载" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 架构检测 ----------
|
||||
systemArch=$(uname -m)
|
||||
case "$systemArch" in
|
||||
x86_64) BINARY="lemonipw-linux-amd64" ;;
|
||||
i386|i486|i586|i686) BINARY="lemonipw-linux-386" ;;
|
||||
aarch64|arm64) BINARY="lemonipw-linux-arm64" ;;
|
||||
armv7l|armv7hl) BINARY="lemonipw-linux-armv7" ;;
|
||||
armv6l) BINARY="lemonipw-linux-armv6" ;;
|
||||
loongarch64) BINARY="lemonipw-linux-loong64" ;;
|
||||
*)
|
||||
echo "错误:不支持的架构 '$systemArch'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "检测到架构 $systemArch,将下载 $BINARY"
|
||||
|
||||
# ---------- 交互式配置输入 ----------
|
||||
|
||||
# 生成 UUID(优先 /proc,其次 uuidgen,最后 /dev/urandom)
|
||||
gen_uuid() {
|
||||
if [ -r /proc/sys/kernel/random/uuid ]; then
|
||||
cat /proc/sys/kernel/random/uuid
|
||||
elif command -v uuidgen >/dev/null 2>&1; then
|
||||
uuidgen | tr 'A-Z' 'a-z'
|
||||
else
|
||||
od -An -N16 -tx1 /dev/urandom | tr -d ' \n' | sed 's/\(........\)\(....\)\(....\)\(....\)\(............\)/\1-\2-\3-\4-\5/'
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 配置后端节点(直接回车使用默认值)"
|
||||
echo "========================================"
|
||||
|
||||
read -r -p "安装目录 [/opt/lemon-ipw]: " INSTALL_DIR
|
||||
INSTALL_DIR=${INSTALL_DIR:-/opt/lemon-ipw}
|
||||
|
||||
read -r -p "监听端口 [8080]: " PORTS
|
||||
PORTS=${PORTS:-8080}
|
||||
|
||||
read -r -p "访问令牌 access_token(留空=不启用鉴权): " ACCESS_TOKEN
|
||||
|
||||
read -r -p "DNS 服务器 [119.28.28.28:53](主从逗号分隔,如 119.28.28.28:53,223.5.5.5:53): " DNS_SERVER
|
||||
DNS_SERVER=${DNS_SERVER:-119.28.28.28:53}
|
||||
|
||||
read -r -p "DNSSEC 专用 DNS(留空=沿用上面 dns-server): " DNSSEC_DNS_SERVER
|
||||
|
||||
read -r -p "启用 IP 数据库 ipdb(首次启动下载约 200MB)[Y/n]: " IPDB_CHOICE
|
||||
case "${IPDB_CHOICE,,}" in
|
||||
n|no) IPDB="false" ;;
|
||||
*) IPDB="true" ;;
|
||||
esac
|
||||
|
||||
read -r -p "CORS 允许来源(逗号分隔,留空=不限): " CORS
|
||||
|
||||
read -r -p "远端配置地址 remote-config-url(留空=不启用): " REMOTE_CONFIG_URL
|
||||
|
||||
echo ""
|
||||
echo "--- WS 通道接入(可选,接入独立中间件)---"
|
||||
read -r -p "接入中间件 WS 通道?[y/N]: " WS_CHOICE
|
||||
WS_URL=""
|
||||
NODE_ID=""
|
||||
NODE_KEY=""
|
||||
WS_USED_DEFAULT=""
|
||||
case "${WS_CHOICE,,}" in
|
||||
y|yes)
|
||||
read -r -p " 中间件 WS 地址(留空=默认 wss://middleware-1.api-ipw.wsmdn.top,逗号分隔可多备): " WS_URL
|
||||
if [ -z "$WS_URL" ]; then
|
||||
WS_URL="wss://middleware-1.api-ipw.wsmdn.top"
|
||||
WS_USED_DEFAULT="true"
|
||||
fi
|
||||
NODE_ID=$(gen_uuid)
|
||||
echo " 节点 id(自动生成 UUID): $NODE_ID"
|
||||
while [ -z "$NODE_KEY" ]; do
|
||||
read -r -p " 注册 key(必填,中间件 apiKeys 必须包含此节点,否则注册被拒 401): " NODE_KEY
|
||||
if [ -z "$NODE_KEY" ]; then
|
||||
echo " 错误:不加 key 禁止启用 WS,必须提供注册 key"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "--- 其他环境变量(可选)---"
|
||||
echo "输入额外的 Environment= 项,每行一个(如 GH_PROXY=https://ghproxy.com/),空行结束:"
|
||||
EXTRA_ENVS=""
|
||||
while true; do
|
||||
read -r -p " > " EXTRA
|
||||
[ -z "$EXTRA" ] && break
|
||||
if [ -z "$EXTRA_ENVS" ]; then
|
||||
EXTRA_ENVS="$EXTRA"
|
||||
else
|
||||
EXTRA_ENVS="$EXTRA_ENVS
|
||||
$EXTRA"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- 汇总展示 ----------
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 配置汇总"
|
||||
echo "========================================"
|
||||
echo "安装目录: $INSTALL_DIR"
|
||||
echo "服务名: lemon-ipw"
|
||||
echo "监听端口: $PORTS"
|
||||
echo "access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
|
||||
echo "DNS: $DNS_SERVER"
|
||||
echo "ipdb: $IPDB"
|
||||
echo "WS 接入: ${WS_URL:+$WS_URL (id=$NODE_ID, key=${NODE_KEY:+已设置})}${WS_URL:-未启用}"
|
||||
echo "========================================"
|
||||
read -r -p "确认安装?[Y/n]: " CONFIRM
|
||||
case "${CONFIRM,,}" in
|
||||
n|no) echo "已取消" ; exit 0 ;;
|
||||
*) : ;;
|
||||
esac
|
||||
|
||||
# ---------- 下载二进制 ----------
|
||||
echo ""
|
||||
echo "正在获取最新版本..."
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/nomdn/ipw-cn/releases/latest | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
else
|
||||
LATEST_TAG=$(wget -qO- https://api.github.com/repos/nomdn/ipw-cn/releases/latest | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
fi
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "错误:无法获取最新版本号(检查网络或 GitHub API 速率限制)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "最新版本:$LATEST_TAG"
|
||||
DOWNLOAD_URL="https://github.com/nomdn/ipw-cn/releases/download/$LATEST_TAG/$BINARY"
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
echo "下载 $BINARY ..."
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget -q -O "$INSTALL_DIR/lemonipw" "$DOWNLOAD_URL"
|
||||
else
|
||||
curl -sL -o "$INSTALL_DIR/lemonipw" "$DOWNLOAD_URL"
|
||||
fi
|
||||
chmod +x "$INSTALL_DIR/lemonipw"
|
||||
echo "二进制已安装到 $INSTALL_DIR/lemonipw"
|
||||
|
||||
# ---------- 生成 systemd 服务 ----------
|
||||
SERVICE_FILE="/etc/systemd/system/lemon-ipw.service"
|
||||
|
||||
# 收集环境变量(仅非空的写入,避免空值覆盖默认)
|
||||
ENV_LINES=""
|
||||
[ -n "$PORTS" ] && ENV_LINES="${ENV_LINES}Environment=\"PORTS=$PORTS\"
|
||||
"
|
||||
[ -n "$ACCESS_TOKEN" ] && ENV_LINES="${ENV_LINES}Environment=\"ACCESS_TOKEN=$ACCESS_TOKEN\"
|
||||
"
|
||||
[ -n "$DNS_SERVER" ] && ENV_LINES="${ENV_LINES}Environment=\"DNS_SERVER=$DNS_SERVER\"
|
||||
"
|
||||
[ -n "$DNSSEC_DNS_SERVER" ] && ENV_LINES="${ENV_LINES}Environment=\"DNSSEC_DNS_SERVER=$DNSSEC_DNS_SERVER\"
|
||||
"
|
||||
[ -n "$IPDB" ] && ENV_LINES="${ENV_LINES}Environment=\"IPDB=$IPDB\"
|
||||
"
|
||||
[ -n "$CORS" ] && ENV_LINES="${ENV_LINES}Environment=\"CORS=$CORS\"
|
||||
"
|
||||
[ -n "$REMOTE_CONFIG_URL" ] && ENV_LINES="${ENV_LINES}Environment=\"REMOTE_CONFIG_URL=$REMOTE_CONFIG_URL\"
|
||||
"
|
||||
[ -n "$WS_URL" ] && ENV_LINES="${ENV_LINES}Environment=\"WS_URL=$WS_URL\"
|
||||
"
|
||||
[ -n "$NODE_ID" ] && ENV_LINES="${ENV_LINES}Environment=\"NODE_ID=$NODE_ID\"
|
||||
"
|
||||
[ -n "$NODE_KEY" ] && ENV_LINES="${ENV_LINES}Environment=\"NODE_KEY=$NODE_KEY\"
|
||||
"
|
||||
if [ -n "$EXTRA_ENVS" ]; then
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && ENV_LINES="${ENV_LINES}Environment=\"$line\"
|
||||
"
|
||||
done <<< "$EXTRA_ENVS"
|
||||
fi
|
||||
|
||||
cat > "$SERVICE_FILE" << EOF
|
||||
[Unit]
|
||||
Description=Lemon IPW Backend Node
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$INSTALL_DIR/lemonipw
|
||||
${ENV_LINES}Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "已生成服务文件 $SERVICE_FILE"
|
||||
|
||||
# ---------- 启动服务 ----------
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now lemon-ipw
|
||||
systemctl status lemon-ipw --no-pager | head -15
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 安装完成"
|
||||
echo "========================================"
|
||||
echo "常用命令:"
|
||||
echo " systemctl status lemon-ipw # 查看状态"
|
||||
echo " journalctl -u lemon-ipw -f # 查看日志"
|
||||
echo " systemctl restart lemon-ipw # 重启(改配置后)"
|
||||
echo "验证:curl http://127.0.0.1:$PORTS/ 应返回 {\"status\":\"ok\"}"
|
||||
if [ -n "$WS_URL" ]; then
|
||||
echo ""
|
||||
echo "WS 通道已启用,请把节点注册信息加入中间件 setting.json 的 apiKeys:"
|
||||
echo " \"$NODE_ID\": \"$NODE_KEY\""
|
||||
fi
|
||||
if [ -n "$WS_USED_DEFAULT" ]; then
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "感谢您的贡献,请向 iduhih777@outlook.com 提交您的节点信息,谢谢!"
|
||||
echo " 节点 id: $NODE_ID"
|
||||
echo " 注册 key: $NODE_KEY"
|
||||
echo " WS 地址: $WS_URL"
|
||||
echo " 监听端口: $PORTS"
|
||||
echo " access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
|
||||
echo " CORS: ${CORS:-不限}"
|
||||
echo "========================================"
|
||||
fi
|
||||
echo "========================================"
|
||||
@@ -1026,13 +1026,13 @@ func applyRemoteConfig() {
|
||||
ssrf.SetEnabled(v != "false" && v != "0")
|
||||
}
|
||||
// WS 客户端配置(远端可覆盖,除非在 ignore 列表中)
|
||||
if v := configValue(CONFIG, "ipw-ws-url"); v != "" && !ignored("ipw-ws-url") {
|
||||
if v := configValue(CONFIG, "ws-url"); v != "" && !ignored("ws-url") {
|
||||
WS_URL = v
|
||||
}
|
||||
if v := configValue(CONFIG, "ipw-node-id"); v != "" && !ignored("ipw-node-id") {
|
||||
if v := configValue(CONFIG, "node-id"); v != "" && !ignored("node-id") {
|
||||
WS_NODE_ID = v
|
||||
}
|
||||
if v := configValue(CONFIG, "ipw-node-key"); v != "" && !ignored("ipw-node-key") {
|
||||
if v := configValue(CONFIG, "node-key"); v != "" && !ignored("node-key") {
|
||||
WS_NODE_KEY = v
|
||||
}
|
||||
// access_token 不在此覆盖:保持原有优先级(环境变量 > setting.json)
|
||||
@@ -1099,18 +1099,18 @@ func readConfig() {
|
||||
if REMOTE_CONFIG_URL == "" {
|
||||
REMOTE_CONFIG_URL = viper.GetString("remote-config-url")
|
||||
}
|
||||
// WS 客户端(接入中间件 WS 通道):IPW_WS_URL / IPW_NODE_ID / IPW_NODE_KEY,env 优先
|
||||
WS_URL = os.Getenv("IPW_WS_URL")
|
||||
// WS 客户端(接入中间件 WS 通道):WS_URL / NODE_ID / NODE_KEY,env 优先
|
||||
WS_URL = os.Getenv("WS_URL")
|
||||
if WS_URL == "" {
|
||||
WS_URL = viper.GetString("ipw-ws-url")
|
||||
WS_URL = viper.GetString("ws-url")
|
||||
}
|
||||
WS_NODE_ID = os.Getenv("IPW_NODE_ID")
|
||||
WS_NODE_ID = os.Getenv("NODE_ID")
|
||||
if WS_NODE_ID == "" {
|
||||
WS_NODE_ID = viper.GetString("ipw-node-id")
|
||||
WS_NODE_ID = viper.GetString("node-id")
|
||||
}
|
||||
WS_NODE_KEY = os.Getenv("IPW_NODE_KEY")
|
||||
WS_NODE_KEY = os.Getenv("NODE_KEY")
|
||||
if WS_NODE_KEY == "" {
|
||||
WS_NODE_KEY = viper.GetString("ipw-node-key")
|
||||
WS_NODE_KEY = viper.GetString("node-key")
|
||||
}
|
||||
// REMOTE_INGORE_CONFIG:不被远端覆盖的配置项列表(JSON 数组字符串,env 优先)
|
||||
if raw := os.Getenv("REMOTE_INGORE_CONFIG"); raw != "" {
|
||||
@@ -1150,7 +1150,7 @@ func main() {
|
||||
|
||||
slog.Info("Starting server", "port", PORTS, "gh_proxy", GH_PROXY, "single_stack", SINGLE_STACK, "dns_server", DNS_SERVER, "CORS_ACCEPT", ACCEPT_DOMAINS)
|
||||
|
||||
// WS 客户端:接入中间件 WS 通道(配置 IPW_WS_URL 时启用,HTTP 接口不变)
|
||||
// WS 客户端:接入中间件 WS 通道(配置 WS_URL 时启用,HTTP 接口不变)
|
||||
if WS_URL != "" {
|
||||
go wsClientLoop()
|
||||
slog.Info("WS client enabled", "url", WS_URL, "nodeId", WS_NODE_ID)
|
||||
|
||||
@@ -677,7 +677,7 @@ func main() {
|
||||
// WS 服务端(端口由配置 wsPort / 环境变量 WS_PORT 决定,缺省 8092,0 = 关闭;统一走 readConfig)。
|
||||
// 后端节点作为 WS 客户端连入;节点配置 "ws": true 时拨测请求改走 WS 通道。现有 HTTP 路由不受影响。
|
||||
if WS_PORT > 0 {
|
||||
wsSrv = newWSServer(handleWSCommand)
|
||||
wsSrv = newWSServer()
|
||||
go wsSrv.Start(fmt.Sprintf(":%d", WS_PORT))
|
||||
go wsSrv.maintenanceLoop()
|
||||
log.Printf("[ws] ws channel enabled on port %d (node flag \"ws\": true to use)", WS_PORT)
|
||||
|
||||
+1
-38
@@ -64,9 +64,6 @@ type wsServer struct {
|
||||
probeMu sync.Mutex
|
||||
probeWait map[string]chan wsProbeResult
|
||||
|
||||
// 指令回调(后端节点下发 command 时触发),由 main 注入
|
||||
onCommand func(cmd wsCommand)
|
||||
|
||||
// 统计
|
||||
statMu sync.Mutex
|
||||
totalReqs int64
|
||||
@@ -74,11 +71,10 @@ type wsServer struct {
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func newWSServer(onCommand func(cmd wsCommand)) *wsServer {
|
||||
func newWSServer() *wsServer {
|
||||
return &wsServer{
|
||||
peers: make(map[string]*wsPeer),
|
||||
probeWait: make(map[string]chan wsProbeResult),
|
||||
onCommand: onCommand,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -183,20 +179,6 @@ func (s *wsServer) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
s.sendJSON(c, wsMessage{Type: "pong", NodeID: peer.id, TS: time.Now().Unix()})
|
||||
}
|
||||
|
||||
case "command":
|
||||
if !registered {
|
||||
continue
|
||||
}
|
||||
var cmd wsCommand
|
||||
if err := json.Unmarshal(msg.Data, &cmd); err != nil {
|
||||
log.Printf("[ws] bad command from %s: %v", peer.id, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[ws] command from %s: %s", peer.id, cmd.Command)
|
||||
if s.onCommand != nil {
|
||||
s.onCommand(cmd)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("[ws] unknown message type from %s: %s", peer.id, msg.Type)
|
||||
}
|
||||
@@ -347,22 +329,3 @@ func wsProbeTimeout() time.Duration {
|
||||
}
|
||||
return time.Duration(t) * time.Second
|
||||
}
|
||||
|
||||
// handleWSCommand 处理后端节点经 WS 下发的指令(配置/指令下发)。
|
||||
// 注意:reload_config 会整体重写配置全局变量,运行中重载期间存在与并发请求的竞态,
|
||||
// 生产环境仍建议以重启方式应用配置;此命令主要用于调试与低峰期热更新。
|
||||
func handleWSCommand(cmd wsCommand) {
|
||||
switch cmd.Command {
|
||||
case "reload_config":
|
||||
log.Printf("[ws] command reload_config received, reloading config...")
|
||||
if err := readConfig(); err != nil {
|
||||
log.Printf("[ws] reload_config FAILED: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[ws] config reloaded from %s", CONFIG_SOURCE)
|
||||
case "ping":
|
||||
// 兼容节点发来的 ping(视为心跳)
|
||||
default:
|
||||
log.Printf("[ws] unknown command: %s", cmd.Command)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -10,6 +10,6 @@
|
||||
"remote-config-url": "",
|
||||
"remote-ingore-config": [],
|
||||
"access_token": "",
|
||||
"ipw-ws-url": "",
|
||||
"ipw-node-id": ""
|
||||
"ws-url": "",
|
||||
"node-id": "test"
|
||||
}
|
||||
@@ -18,15 +18,15 @@ import (
|
||||
//
|
||||
// 配置(readConfig 解析,环境变量优先,其次 setting.json,远端配置可覆盖):
|
||||
//
|
||||
// IPW_WS_URL ws://<中间件>:8092/ws,ws://<备中间件>:8092/ws (逗号分隔多个中间件,主失败自动切换下一个;不配置 = 不启用)
|
||||
// IPW_NODE_ID 节点 id(与中间件 apiKeys 键 / 前端配置 id 一致)
|
||||
// IPW_NODE_KEY 注册 key(与中间件 apiKeys[节点id] 一致;中间件未配置该节点 key 时留空)
|
||||
// WS_URL ws://<中间件>:8092/ws,ws://<备中间件>:8092/ws (逗号分隔多个中间件,主失败自动切换下一个;不配置 = 不启用)
|
||||
// NODE_ID 节点 id(与中间件 apiKeys 键 / 前端配置 id 一致)
|
||||
// NODE_KEY 注册 key(与中间件 apiKeys[节点id] 一致;中间件未配置该节点 key 时留空)
|
||||
//
|
||||
// 与 HTTP handler 共用同一批 webtest 探针函数和缓存:收到 probe 后按 apiType 取数(带缓存),
|
||||
// 返回处理在本文件内完成,结果经 probe_result 上报。节点 HTTP 接口完全不变。
|
||||
//
|
||||
// 心跳:NODE 与 MIDDLEWARE 双向心跳。本端每 10s 发一次 ping,发送失败重试,连续失败 3 次
|
||||
// 判定当前中间件不可用,断开并切换下一个 IPW_WS_URL;收到中间件 ping 回 pong。
|
||||
// 判定当前中间件不可用,断开并切换下一个 WS_URL;收到中间件 ping 回 pong。
|
||||
|
||||
// wsProbe 学 HTTP handler:从函数拿数据 + 缓存,返回 (status, body)。
|
||||
// raw 与 query 来自中间件 probe 消息(等价于路由参数)。
|
||||
@@ -360,17 +360,17 @@ const (
|
||||
wsHeartbeatTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// wsClientOnce 连接并服务单个中间件 URL:
|
||||
// - 连接/注册失败 → 返回 true(上层切换下一个中间件)
|
||||
// - 心跳连续失败 wsHeartbeatRetry 次 → 主动断开,返回 true(切换下一个中间件)
|
||||
// - 读循环断开 → 返回 true
|
||||
// - 返回 false = 配置缺失(不再重试)
|
||||
func wsClientOnce(url string) bool {
|
||||
// wsClientOnce 连接并服务单个中间件 URL,返回下次重试前的等待时长:
|
||||
// - 连接/读循环断开 → 3s(切换下一个中间件)
|
||||
// - 注册被拒(register_error)→ 30s(多为 key 配置错误,加大间隔避免空转刷日志)
|
||||
// - 心跳连续失败 wsHeartbeatRetry 次 → 主动断开,3s
|
||||
// - 返回 0 = 配置缺失(不再重试)
|
||||
func wsClientOnce(url string) time.Duration {
|
||||
ctx := context.Background()
|
||||
c, _, err := websocket.Dial(ctx, url, nil)
|
||||
if err != nil {
|
||||
slog.Warn("ws client dial failed", "url", url, "error", err)
|
||||
return true
|
||||
return 3 * time.Second
|
||||
}
|
||||
defer c.CloseNow()
|
||||
|
||||
@@ -383,19 +383,19 @@ func wsClientOnce(url string) bool {
|
||||
_, data, err := c.Read(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("ws client register read failed", "url", url, "error", err)
|
||||
return true
|
||||
return 3 * time.Second
|
||||
}
|
||||
var m wsMsg
|
||||
if json.Unmarshal(data, &m) != nil {
|
||||
return true
|
||||
return 3 * time.Second
|
||||
}
|
||||
if m.Type == "register_error" {
|
||||
slog.Warn("ws client register rejected", "nodeId", WS_NODE_ID, "url", url, "data", string(m.Data))
|
||||
return true
|
||||
return 30 * time.Second
|
||||
}
|
||||
if m.Type != "register_ok" {
|
||||
slog.Warn("ws client unexpected message", "type", m.Type)
|
||||
return true
|
||||
return 3 * time.Second
|
||||
}
|
||||
slog.Info("ws client registered", "nodeId", WS_NODE_ID, "url", url)
|
||||
|
||||
@@ -428,7 +428,7 @@ func wsClientOnce(url string) bool {
|
||||
_, data, err := c.Read(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("ws client disconnected", "url", url, "error", err)
|
||||
return true
|
||||
return 3 * time.Second
|
||||
}
|
||||
var msg wsMsg
|
||||
if json.Unmarshal(data, &msg) != nil {
|
||||
@@ -446,21 +446,21 @@ func wsClientOnce(url string) bool {
|
||||
}
|
||||
|
||||
// wsClientLoop 主循环:依次尝试所有配置的中间件 URL(逗号分隔,第一个为主),
|
||||
// 当前中间件连接失败或心跳超时 → 3s 后切换下一个;全部失败循环重试。
|
||||
// 当前中间件连接失败或心跳超时 → 3s 后切换下一个;注册被拒 → 30s;全部失败循环重试。
|
||||
func wsClientLoop() {
|
||||
urls := splitWSURLs(WS_URL)
|
||||
if len(urls) == 0 {
|
||||
slog.Info("ws client disabled (IPW_WS_URL not set)")
|
||||
slog.Info("ws client disabled (WS_URL not set)")
|
||||
return
|
||||
}
|
||||
for {
|
||||
for _, url := range urls {
|
||||
shouldRetry := wsClientOnce(url)
|
||||
if !shouldRetry {
|
||||
retryDelay := wsClientOnce(url)
|
||||
if retryDelay == 0 {
|
||||
slog.Info("ws client stopped")
|
||||
return
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user