Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e3601f20 | ||
|
|
6a4a3821de | ||
|
|
069847e11f | ||
|
|
30ba61bc11 | ||
|
|
94f03ffa85 | ||
|
|
3a3eeab9f9 | ||
|
|
fa5c2329fe | ||
|
|
957ac5a4c8 | ||
|
|
63c42627f6 | ||
|
|
6512a46596 | ||
|
|
e261666efc | ||
|
|
0d1a9e5bbd | ||
|
|
279f663694 | ||
|
|
3af1076492 |
No files matched your search
@@ -83,7 +83,6 @@ jobs:
|
||||
- name: Prepare config files for release
|
||||
run: |
|
||||
# 后端:发布真实 setting.json(重命名为 setting.json.example);中间件:只发布 setting.json.example
|
||||
cp setting.json setting.json.example
|
||||
cp middleware-go/setting.json.example middleware.setting.json.example
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
|
||||
+3
-2
@@ -15,9 +15,10 @@ lemon-ipw
|
||||
*.tmp
|
||||
*.log
|
||||
node_modules/
|
||||
setting.json
|
||||
.workbuddy/
|
||||
# VitePress 构建产物
|
||||
docs/.vitepress/.temp/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
docs/.vitepress/dist/
|
||||
tmp/
|
||||
setting.json
|
||||
@@ -1,278 +1,48 @@
|
||||
# Lemon IPW
|
||||
|
||||
ipw.cn 替代品,提供 IP 查询、网站检测、SSL 检查、DNS 解析、TCPing 测速、Whois、DNSSEC、ASN 查询、网站截图等功能。
|
||||
文档编写中....
|
||||
ipw.cn 替代品,提供 IP 查询、网站检测、SSL 检查、DNS 解析、TCPing 测速、Whois、DNSSEC、ASN 查询、网站截图等功能。
|
||||
|
||||
## 功能
|
||||
**[预览站点](https://ipw.wsmdn.top/)** · **[文档](https://github.com/nomdn/ipw-cn/tree/main/docs)**
|
||||
|
||||
- **IP 地址查询** — 支持 IPv4/IPv6,集成 ip2region、qqwry、MaxMind GeoIP、GeoCN、DbIP、Bilibili 等多数据源
|
||||
- **ASN 自治系统查询** — 查询 IP 所属自治系统号,支持 MaxMind GeoLite2、DbIP 双数据源,集成 WHOIS 解析
|
||||
- **网站检测** — HTTP 状态码、响应时间、Host 记录
|
||||
- **SSL 证书检查** — 证书有效期、颁发机构、剩余天数
|
||||
- **DNS 解析** — 多节点并发查询,支持 A/AAAA/CNAME/MX/TXT/NS/SRV/PTR/CAA 等记录类型
|
||||
- **DNSSEC 验证** — 检测域名 DNSSEC 签名状态,验证 DNSKEY、RRSIG、DS 记录链式信任关系
|
||||
- **Whois 查询** — 域名注册信息、注册商、到期时间等
|
||||
- **TCPing** — TCP 连接延迟测试,支持 IPv4/IPv6 双栈
|
||||
- **网站测速** — 下载速度、响应头、Host 记录
|
||||
- **网站截图** — 在线网站页面截图
|
||||
- **SSRF 防护** — 自动拦截对内网/私有 IP 的出站请求
|
||||
- **暗色模式** — 支持明暗主题切换
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/nomdn/ipw-cn/main/install.sh -o install.sh && sudo bash install.sh
|
||||
```
|
||||
|
||||
完整部署(后端节点 / 独立中间件 / 前端)与配置说明见 [文档](https://github.com/nomdn/ipw-cn/tree/main/docs):
|
||||
|
||||
- [快速开始](docs/guide/getting-started.md)
|
||||
- [后端节点部署](docs/guide/deploy-node.md)
|
||||
- [中间件部署](docs/guide/deploy-middleware.md)
|
||||
- [前端部署](docs/guide/deploy-frontend.md)
|
||||
- [配置文件](docs/guide/config.md)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
ipw-cn/
|
||||
├── main.go # Go 后端入口(Gin 框架,自托管)
|
||||
├── go.mod # Go 模块定义
|
||||
├── Dockerfile # 后端 Docker 镜像
|
||||
├── setting.json # 后端运行配置
|
||||
│
|
||||
├── ipdb/ # IP 数据库查询模块
|
||||
│ ├── searchip.go # 多数据源 IP 查询(ip2region/qqwry/MaxMind/GeoCN/DbIP/Bilibili)
|
||||
│ └── ipdb.go # 数据库下载与更新
|
||||
│
|
||||
├── webtest/ # 网络测试工具
|
||||
│ ├── dns.go # DNS 查询(基于 miekg/dns)
|
||||
│ ├── dnssec.go # DNSSEC 验证
|
||||
│ ├── tcping.go # TCP 连接测试
|
||||
│ └── whois.go # WHOIS 域名查询
|
||||
│
|
||||
├── ssrf/ # SSRF 防护模块
|
||||
│ └── ssrf.go # 私有 IP 拦截、安全跳转校验
|
||||
│
|
||||
├── middleware-go/ # 独立转发中间件(Go Fiber v3,可单独部署)
|
||||
│ ├── main.go # 中间件入口(/v1/* 与 /middleware/* 路由)
|
||||
│ ├── setting.json # 中间件配置(节点列表、apiKeys、cors 等)
|
||||
│ ├── setting.json.example # 中间件配置示例
|
||||
│ ├── Dockerfile # 中间件 Docker 镜像
|
||||
│ └── middleware-go-linux-arm64 # Linux ARM64 预编译二进制
|
||||
│
|
||||
├── frontend-ssr/ # Nuxt 4 SSR 前端(主版本,部署至 Cloudflare Workers)
|
||||
│ ├── app/ # Nuxt 应用源码
|
||||
│ │ └── pages/ # 页面组件(location/dns/tcping/ssl/speedtest/whois/dnssec/asn/screenshot)
|
||||
│ ├── config/ # 前端配置(API 地址、节点列表)
|
||||
│ ├── nuxt.config.ts # Nuxt 配置
|
||||
│ ├── package.json
|
||||
│ ├── pnpm-workspace.yaml
|
||||
│ └── wrangler.jsonc # Cloudflare Workers 部署配置
|
||||
│
|
||||
├── edgeone/ # 腾讯 EdgeOne 边缘函数版本后端
|
||||
│ ├── cloud-functions/ # Go 边缘函数源码
|
||||
│ │ ├── index.go # 后端入口(Gin 框架)
|
||||
│ │ ├── webtest/ # 网络测试工具
|
||||
│ │ ├── ssrf/ # SSRF 防护
|
||||
│ │ └── go.mod
|
||||
│ └── .edgeone/ # EdgeOne 部署配置
|
||||
│
|
||||
├── lemon-getip/ # Cloudflare Workers IP 查询服务(Hono + TypeScript)
|
||||
│ ├── src/index.ts
|
||||
│ ├── wrangler.jsonc
|
||||
│ ├── package.json
|
||||
│ └── test/
|
||||
│
|
||||
├── edgeone-getip/ # EdgeOne Pages IP 查询服务(TypeScript 边缘函数)
|
||||
│ ├── edge-functions/index.ts
|
||||
│ └── .edgeone/
|
||||
│
|
||||
├── .github/workflows/ # CI/CD 流水线
|
||||
│ ├── frontend-ssr.yml # 部署至 Cloudflare Workers
|
||||
│ ├── workers.yml # 部署 lemon-getip 至 Cloudflare Workers
|
||||
│ ├── edgeone-backend.yml # 部署 edgeone 至 EdgeOne Pages
|
||||
│ └── edgeone-getip.yml # 部署 edgeone-getip 至 EdgeOne Pages
|
||||
│
|
||||
├── tmp/ # IP 数据库下载临时目录
|
||||
├── short.txt # GeoCN 行政区划短表(省市级)
|
||||
├── full.txt # GeoCN 行政区划全表(区县级)
|
||||
├── ipw-backend.exe # Windows 预编译后端二进制
|
||||
├── .node-version # Node.js 版本(v22)
|
||||
└── LICENSE # GPL-3.0
|
||||
├── main.go # Go 后端入口(Gin,自托管)
|
||||
├── webtest/ # NODE模块 网络测试工具库
|
||||
├── ipdb/ # NODE模块 IP 数据库模块
|
||||
├── ssrf/ # NODE模块 SSRF 防护
|
||||
├── middleware-go/ # 独立组件 转发中间件(Fiber v3,多节点候选)
|
||||
├── edgeone/ # 独立组件 腾讯 EdgeOne 边缘函数后端
|
||||
├── frontend-ssr/ # Nuxt 4 SSR 前端
|
||||
├── docs/ # 文档站
|
||||
└── install.sh # 一键安装脚本
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| 后端(自托管) | Go 1.26 + Gin + Resty |
|
||||
| 独立中间件 | Go 1.26 + Fiber v3(转发中间件,多节点候选) |
|
||||
| 边缘后端(EdgeOne) | Go(边缘函数) |
|
||||
| SSR 前端 | Nuxt 4 + Vue 3 + Element Plus + VueUse |
|
||||
| Cloudflare Workers | Hono + TypeScript + Wrangler |
|
||||
| EdgeOne Pages | TypeScript 边缘函数 |
|
||||
| IP 数据库 | ip2region、qqwry.ipdb、MaxMind GeoLite2、GeoCN、DbIP、Bilibili API |
|
||||
| DNS 查询 | miekg/dns(Go)/ 原生 DNS-over-HTTPS(EdgeOne) |
|
||||
| 其他 | SSRF 防护、Zstandard 压缩、WordPress mshots 截图 |
|
||||
| 部署 | Docker、Cloudflare Workers、EdgeOne Pages |
|
||||
|
||||
## 一键部署到 EdgeOne(Nuxt 原生支持)
|
||||
|
||||
前端 `frontend-ssr/` 基于 Nuxt,腾讯云 EdgeOne 原生支持 Nuxt 构建,点击下方按钮即可通过 EdgeOne Makers 一键部署前端:
|
||||
|
||||
[](https://console.cloud.tencent.com/edgeone/makers/new?repository-url=https%3A%2F%2Fgithub.com%2Fnomdn%2Fipw-cn&root-directory=frontend-ssr&install-command=pnpm%20install&build-command=pnpm%20run%20build&output-directory=.output)
|
||||
|
||||
| 按钮参数 | 值 | 说明 |
|
||||
|----------|-----|------|
|
||||
| `repository-url` | `https://github.com/nomdn/ipw-cn` | GitHub 仓库地址 |
|
||||
| `root-directory` | `frontend-ssr` | 构建根目录(前端源码所在目录) |
|
||||
| `install-command` | `pnpm install` | 依赖安装命令 |
|
||||
| `build-command` | `pnpm run build` | 构建命令(Nuxt 默认 SSR 构建) |
|
||||
| `output-directory` | `.output` | 构建产物输出目录(Nitro SSR 产物) |
|
||||
|
||||
> 说明:EdgeOne Makers 原生支持 Nuxt 部署,SSR / SSG / ISR(SWR) / 中间件 / 流式传输均支持(Nuxt 3.16+,推荐 Nuxt 4),按钮默认以 **SSR** 方式构建。如需纯静态部署,可将 `build-command` 改为 `pnpm run generate`、`output-directory` 改为 `.output/public`。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 后端(自托管)
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
go mod download
|
||||
|
||||
# 配置 setting.json(可选)
|
||||
# {
|
||||
# "port": 8080,
|
||||
# "gh-proxy": "https://fastgit.cc/",
|
||||
# "single-stack": "", # "ipv4" 或 "ipv6" 仅启用单栈
|
||||
# "dns-server": "", # 自定义 DNS 服务器
|
||||
# "block-private-ips": true,# SSRF 防护开关
|
||||
# "ipdb": true, # IP 数据库开关
|
||||
# "cors": "", # 允许的请求来源,详见 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/CORS
|
||||
# "access_token": "" # API 访问令牌(可选)
|
||||
# }
|
||||
|
||||
# 运行
|
||||
go run main.go
|
||||
```
|
||||
|
||||
首次启动时会自动下载 IP 数据库文件(约 200MB),之后每 24 小时自动更新。
|
||||
|
||||
### 独立中间件(middleware-go)
|
||||
|
||||
转发中间件,供 SSR 前端在多个上游节点之间做候选重试,可独立部署。
|
||||
|
||||
```bash
|
||||
cd middleware-go
|
||||
|
||||
# 构建(Linux ARM64 示例)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o middleware-go-linux-arm64 .
|
||||
|
||||
# 运行(需配置文件,可用 SETTING_FILE 指定路径)
|
||||
./middleware-go-linux-arm64
|
||||
|
||||
# 查看版本
|
||||
./middleware-go-linux-arm64 --version
|
||||
```
|
||||
|
||||
所有配置均可通过环境变量覆盖,无需修改配置文件(详见下方「中间件配置」)。
|
||||
|
||||
### SSR 前端(部署至 Cloudflare Workers)
|
||||
|
||||
```bash
|
||||
cd frontend-ssr
|
||||
|
||||
# 安装依赖(需要 pnpm)
|
||||
pnpm install
|
||||
|
||||
# 开发
|
||||
pnpm dev
|
||||
|
||||
# 构建并部署
|
||||
pnpm deploy
|
||||
```
|
||||
|
||||
### SPA 前端(旧版本,部署至 Cloudflare Pages)
|
||||
|
||||
> [!WARNING]
|
||||
> 原生 SPA 已停止支持。要部署 SPA 版本前端请使用 Nuxt SPA 版本。
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
docker build -t lemon-ipw .
|
||||
docker run -p 8080:8080 -v $(pwd)/setting.json:/app/setting.json lemon-ipw
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/v1/location/:ip` | 查询指定 IP 地理位置 |
|
||||
| GET | `/v1/location` | 查询请求者 IP 地理位置 |
|
||||
| GET | `/v1/asn/:ip` | 查询 IP 所属自治系统号(ASN) |
|
||||
| GET | `/v1/detail/*url` | 检测网站可达性(HTTP 状态码、响应时间) |
|
||||
| GET | `/v1/ssl/*url` | 检查 SSL 证书信息 |
|
||||
| GET | `/v1/speed/:version/*url` | 网站测速(version: v4/v6/dual) |
|
||||
| GET | `/v1/tcping/:ip?port=80&count=4` | TCP 连接测试 |
|
||||
| GET | `/v1/dns/:type/*domain` | DNS 解析(type: A/AAAA/CNAME/MX/TXT/NS/SRV/PTR/CAA) |
|
||||
| GET | `/v1/dnssec/:domain` | DNSSEC 验证 |
|
||||
| GET | `/v1/whois/:domain` | Whois 域名查询 |
|
||||
| GET | `/` | 健康检查 |
|
||||
|
||||
## 部署架构
|
||||
|
||||
本项目支持多平台部署,前端和后端可独立部署到不同平台:
|
||||
|
||||
- **自托管后端**:使用 Docker 或直接运行 Go 二进制,配合 `setting.json` 配置
|
||||
- **独立转发中间件**:`middleware-go/` 使用 Go Fiber v3,提供 `/v1/*` 与 `/middleware/*` 转发路由,供前端做多节点候选重试
|
||||
- **Cloudflare Workers 后端**:`lemon-getip/` 使用 Hono 框架部署到 Cloudflare Workers
|
||||
- **EdgeOne 边缘后端**:`edgeone/` 和 `edgeone-getip/` 部署到腾讯 EdgeOne Pages
|
||||
- **SSR 前端**:`frontend-ssr/` 使用 Nuxt 4 + Wrangler 部署到 Cloudflare Workers
|
||||
|
||||
## 配置
|
||||
|
||||
### 后端环境变量
|
||||
|
||||
| 配置项 | 环境变量 | 默认值 | 说明 |
|
||||
|--------|----------|--------|------|
|
||||
| `port` | `PORT` | `8080` | 监听端口 |
|
||||
| `gh-proxy` | `GH_PROXY` | `""` | GitHub 文件代理(用于下载数据库) |
|
||||
| `single-stack` | `SINGLE_STACK` | `""` | 单栈模式:`ipv4` 或 `ipv6` |
|
||||
| `dns-server` | `DNS_SERVER` | `119.28.28.28:53` | DNS 服务器地址 |
|
||||
| `block-private-ips` | `BLOCK_PRIVATE_IPS` | `true` | SSRF 防护开关 |
|
||||
| `ipdb` | `IPDB` | `true` | IP 数据库开关 |
|
||||
| `access_token` | `ACCESS_TOKEN` | `""` | API 访问令牌 |
|
||||
| `cors` | `CORS` | `""` | 允许的请求来源 |
|
||||
| 远端配置 | `REMOTE_CONFIG_URL` | `""` | 远端配置文件 URL(遵守 `setting.json` 格式),优先级最高 |
|
||||
|
||||
> 配置优先级:**远端配置(`REMOTE_CONFIG_URL`)> 环境变量 > setting.json**。远端配置 URL 可通过环境变量 `REMOTE_CONFIG_URL` 或 setting.json 的 `remote-config-url` 提供(环境变量优先);远端配置本身需遵守 `setting.json` 的 JSON 格式(`port` / `gh-proxy` / `single-stack` / `dns-server` / `block-private-ips` / `ipdb` / `cors` 等),拉取或解析失败时自动回退到本地配置并打印警告。`access_token` 例外:**不随远端配置覆盖**,保持原有优先级(环境变量 > setting.json)。EdgeOne 版本(`edgeone/`)配置来源为 环境变量 + 远端配置。
|
||||
|
||||
### 前端配置
|
||||
|
||||
前端 API 地址和多节点配置位于各前端项目的 `config/` 目录中。
|
||||
|
||||
### 中间件配置(middleware-go)
|
||||
|
||||
配置来源优先级:**环境变量 > `setting.json` > 默认值**。`setting.json` 采用平铺结构(键名与前端 `config/index.ts` 一致),也可用环境变量覆盖任意配置项;数组/对象类配置项从环境变量读取 **JSON 字符串**解析。
|
||||
|
||||
```json
|
||||
{
|
||||
"port": "8091",
|
||||
"httpTimeoutSeconds": 30,
|
||||
"cors": "https://ipw.wsmdn.top",
|
||||
"apiBaseUrls": [{ "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" }],
|
||||
"IPLocationAPIs": [],
|
||||
"TCPing": { "DualStack": [], "IPv4": [], "IPv6": [] },
|
||||
"SpeedTest": { "DualStack": [], "IPv4": [], "IPv6": [] },
|
||||
"NSLookup": [],
|
||||
"apiKeys": { "cn-jiangsu": "token" }
|
||||
}
|
||||
```
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 说明 |
|
||||
|--------|----------|------|------|
|
||||
| `port` | `PORT` | 数字/字符串 | 监听端口 |
|
||||
| `httpTimeoutSeconds` | `HTTP_TIMEOUT` | 数字 | 上游请求超时(秒) |
|
||||
| `cors` | `CORS` | 字符串 | 逗号分隔的允许域名;为空则允许所有(`*`) |
|
||||
| `apiBaseUrls` | `API_BASE_URLS` | JSON 数组 | whois/ssl/detail 上游节点 |
|
||||
| `IPLocationAPIs` | `IP_LOCATION_APIS` | JSON 数组 | location/asn 上游节点 |
|
||||
| `TCPing` | `TCPING` | JSON 对象 | tcping 节点(DualStack/IPv4/IPv6) |
|
||||
| `SpeedTest` | `SPEED_TEST` | JSON 对象 | speed 节点(DualStack/IPv4/IPv6) |
|
||||
| `NSLookup` | `NS_LOOKUP` | JSON 数组 | dns/dnssec 上游节点 |
|
||||
| `apiKeys` | `APIKEYS` | JSON 对象 | `{"backendID": "token"}`,转发时加 `Authorization: Bearer <token>` |
|
||||
| 配置文件路径 | `SETTING_FILE` | 字符串 | 指定 setting.json 路径(默认 `./setting.json` 或 `../setting.json`) |
|
||||
|
||||
**路由格式**:`/{prefix}/{backendID}/{apiType}/{raw...}`,其中 `prefix` 为 `/v1` 或 `/middleware`,`apiType` 支持 `whois/dns/location/ssl/asn/dnssec/detail/tcping/speed`。响应透传上游状态码与 body;上游网络不可达返回 502。
|
||||
|
||||
**版本信息**:通过 ldflags 注入 `main.VERSION / main.COMMIT / main.BUILD_TIME`,运行 `--version` 查看。
|
||||
| 后端(自托管) | Go + Gin + Resty |
|
||||
| 独立中间件 | Go + Fiber v3(WS 通道:coder/websocket) |
|
||||
| 边缘后端(EdgeOne) | Go 边缘函数 |
|
||||
| SSR 前端 | Nuxt 4 + Vue 3 + Element Plus |
|
||||
| DNS 查询 | miekg/dns(DoH / UDP 双通道 + 主从 failover) |
|
||||
| 部署 | Docker、Cloudflare Workers、EdgeOne Pages、systemd |
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+57
-42
@@ -13,7 +13,7 @@
|
||||
"block-private-ips": true,
|
||||
"ipdb": true,
|
||||
"cors": "",
|
||||
"access_token": ""
|
||||
"access-token": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
- `block-private-ips`:SSRF 防护开关
|
||||
- `ipdb`:IP 数据库开关(首次启动自动下载约 200MB,之后每 24h 更新)
|
||||
- `cors`:允许的请求来源(逗号分隔)
|
||||
- `access_token`:API 访问令牌,留空则不启用鉴权
|
||||
- `access-token`:API 访问令牌,留空则不启用鉴权
|
||||
- `ws-url`:WS 通道地址——接入独立中间件的 WebSocket 地址(如 `"wss://middleware-1.api-ipw.wsmdn.top/ws"`,须含 `wss://` 前缀与 `/ws` 路径);支持**逗号分隔多备**(第一个为主,主故障自动切换下一个);留空 = 不启用 WS 客户端,走原 HTTP 接口
|
||||
- `node-id`:WS 节点 id(与中间件 `apiKeys`/`wsKeys` 键、前端配置的节点 `id` 一致;建议用 UUID 唯一标识)
|
||||
- `node-key`:WS 注册 key(与中间件 `wsKeys[节点id]` 一致;**必填**——节点未配置该 key 时中间件拒绝注册并返回 401)
|
||||
|
||||
以上字段均可用环境变量覆盖(`PORTS` / `DNS_SERVER` / `DNSSEC_DNS_SERVER` / `BLOCK_PRIVATE_IPS` / `IPDB` / `CORS` / `ACCESS_TOKEN`)。需要从远端拉取配置时,设置 `remote-config-url` 或环境变量 `REMOTE_CONFIG_URL`(优先级:远端 > 环境变量 > setting.json)。
|
||||
以上字段均可用环境变量覆盖(`PORTS` / `DNS_SERVER` / `DNSSEC_DNS_SERVER` / `BLOCK_PRIVATE_IPS` / `IPDB` / `CORS` / `ACCESS_TOKEN` / `WS_URL` / `NODE_ID` / `NODE_KEY`)。需要从远端拉取配置时,设置 `remote-config-url` 或环境变量 `REMOTE_CONFIG_URL`(优先级:远端 > 环境变量 > setting.json)。
|
||||
|
||||
---
|
||||
|
||||
@@ -44,11 +47,8 @@
|
||||
| `ICP` / `GongAn` | string | 网站备案号(页脚展示) |
|
||||
| `noindex` | boolean | 全站是否禁止搜索引擎索引 |
|
||||
| `v4OnlyAPI` / `v6OnlyAPI` / `DualStackAPI` | string | IPv4 / IPv6 / 双栈 IP 查询接口 |
|
||||
| `apiBaseUrls` | 节点数组 | 基础 API 节点列表(detail / ssl / whois 等) |
|
||||
| `IPLocationAPIs` | 节点数组 | IP 归属地 / ASN 查询节点列表 |
|
||||
| `TCPing` | 节点分组 | TCPing 节点,按 `DualStack` / `IPv4` / `IPv6` 分组 |
|
||||
| `SpeedTest` | 节点分组 | 测速节点,按 `DualStack` / `IPv4` / `IPv6` 分组 |
|
||||
| `NSLookup` | 节点数组 | DNS 解析节点列表 |
|
||||
| `APIBaseURL` | 节点池 | 拨测上游节点池(whois / ssl / detail / dns / dnssec / tcping / speed),含 `DualStack` / `IPv4` / `IPv6` 三栈 |
|
||||
| `IPLocationAPI` | 节点数组 | IP 归属地 / ASN 查询节点池(纯数组,无栈区分) |
|
||||
|
||||
完整模板(复制到 `frontend-ssr/config/index.ts`,替换占位值即可):
|
||||
|
||||
@@ -75,49 +75,38 @@ const config = {
|
||||
v4OnlyAPI: "https://4.wsmdn.top",
|
||||
v6OnlyAPI: "https://6.wsmdn.top",
|
||||
DualStackAPI: "https://test.wsmdn.top",
|
||||
// 基础 API 节点(detail / ssl / whois 等)
|
||||
apiBaseUrls: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
// IP 归属地 / ASN 查询节点
|
||||
IPLocationAPIs: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
// TCPing 节点:IPv4-only 节点只能放 IPv4 分组,不能放 DualStack / IPv6
|
||||
TCPing: {
|
||||
// 拨测节点池(whois / ssl / detail / dns / dnssec / tcping / speed)
|
||||
// IPv4-only 节点只能放 IPv4 栈,IPv6-only 放 IPv6 栈,双栈放 DualStack
|
||||
APIBaseURL: {
|
||||
DualStack: [],
|
||||
IPv4: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
IPv6: []
|
||||
},
|
||||
// 测速节点:同上,IPv4-only 节点只能放 IPv4 分组
|
||||
SpeedTest: {
|
||||
DualStack: [],
|
||||
IPv4: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
],
|
||||
IPv6: []
|
||||
},
|
||||
// DNS 解析节点
|
||||
NSLookup: [
|
||||
// IP 归属地 / ASN 查询节点池(纯数组,无栈区分)
|
||||
IPLocationAPI: [
|
||||
{ label: "节点显示名", id: "node-1", url: "https://node-1.example.com/" }
|
||||
]
|
||||
}
|
||||
export { config }
|
||||
```
|
||||
|
||||
> 节点 `id` 需与后端节点 / 中间件 `apiKeys` 的键保持一致,中间件按 `id` 注入 `Authorization: Bearer <key>`。
|
||||
> 节点 `id` 需与后端节点 / 中间件 `apiKeys` 的键保持一致,中间件按 `id` 注入 `Authorization: Bearer <key>`;WS 通道的节点注册校验另见中间件 `wsKeys`(与后端 `node-key` 对应)。
|
||||
|
||||
节点数组项均为 `{ label, id, url }` 结构,例如:
|
||||
|
||||
```ts
|
||||
apiBaseUrls: [
|
||||
{ label: "中国 江苏 移动", id: "cn-jiangsu", url: "https://cn-jiangsu.api-ipw.wsmdn.top/" }
|
||||
]
|
||||
APIBaseURL: {
|
||||
DualStack: [
|
||||
{ label: "中国 江苏 移动", id: "cn-jiangsu", url: "https://cn-jiangsu.api-ipw.wsmdn.top/" }
|
||||
],
|
||||
IPv4: [],
|
||||
IPv6: []
|
||||
}
|
||||
```
|
||||
|
||||
`label` 为节点展示名,`id` 为节点唯一标识(中间件 `apiKeys` 也按此 id 注入 key),`url` 为节点 base URL。`TCPing` / `SpeedTest` 节点再多一层 `DualStack` / `IPv4` / `IPv6` 分组,分别对应双栈 / 单栈测试时使用的节点列表。
|
||||
`label` 为节点展示名,`id` 为节点唯一标识(中间件 `apiKeys` 按此 id 注入 HTTP 转发 key,WS 注册校验另见 `wsKeys`),`url` 为节点 base URL。节点池结构如下:`APIBaseURL` 为拨测节点池,含 `DualStack` / `IPv4` / `IPv6` 三栈(分别对应双栈 / 单栈测试时使用的节点列表,IPv4-only 节点只能放 `IPv4` 栈,IPv6-only 只能放 `IPv6` 栈,不能放 `DualStack`);`IPLocationAPI` 为 IP 归属地 / ASN 查询节点池,为纯数组(无栈区分)。
|
||||
|
||||
### 出站 IP 检测接口(v4OnlyAPI / v6OnlyAPI / DualStackAPI)
|
||||
|
||||
@@ -165,8 +154,14 @@ apiBaseUrls: [
|
||||
"ws-port": 8092,
|
||||
"remote-config-url": "",
|
||||
"remote-ingore-config": [],
|
||||
"apiBaseUrls": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ],
|
||||
"apiKeys": { "cn-jiangsu": "" }
|
||||
"APIBaseURL": {
|
||||
"DualStack": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ],
|
||||
"IPv4": [],
|
||||
"IPv6": []
|
||||
},
|
||||
"IPLocationAPI": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ],
|
||||
"apiKeys": { "cn-jiangsu": "" },
|
||||
"wsKeys": { "cn-jiangsu": "" }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -175,7 +170,22 @@ apiBaseUrls: [
|
||||
- `remote-config-url`:远端配置地址,也可用环境变量 `REMOTE_CONFIG_URL`(环境变量优先)
|
||||
- `remote-ingore-config`:**不被远端配置覆盖的配置项列表**(数组),如 `["port", "rate-limit"]`;也可用环境变量 `REMOTE_INGORE_CONFIG`(JSON 数组字符串,优先于 setting.json)
|
||||
- `apiKeys`:用于向后端注入 `Authorization: Bearer <key>`,优先级:setting.json `apiKeys` > 环境变量 `APIKEYS`(JSON 字符串)。**敏感凭据,强制忽略,不随远端配置覆盖**
|
||||
- 节点数组项(`apiBaseUrls` / `IPLocationAPIs` / `TCPing` / `SpeedTest` / `NSLookup`)与前端 `config/index.ts` 结构一致;节点加 `"ws": true` 可让该节点走 WS 通道通信,详见 [中间件部署 - WS 通道](/guide/deploy-middleware#ws-通道拨测数据经-websocket-传输)
|
||||
- `wsKeys`:WS 注册校验表(`节点id → key`):节点经 WS 注册时,`register` 消息的 `key` 必须与 `wsKeys[节点id]` 一致,否则返回 401 并断开;优先级:setting.json `wsKeys` > 环境变量 `WSKEYS`(JSON 字符串)。**敏感凭据,强制忽略,不随远端配置覆盖**。与 `apiKeys` **相互独立**——`apiKeys` 管 HTTP 转发鉴权,`wsKeys` 管 WS 注册校验,可分别配置不同 key(例如 WS 节点单独换 key 不影响 HTTP 转发)
|
||||
- 节点池与前端 `config/index.ts` 结构一致:`APIBaseURL` 为拨测节点池(含 `DualStack` / `IPv4` / `IPv6` 三栈),`IPLocationAPI` 为 IP 归属地 / ASN 节点池(纯数组,无栈);节点加 `"ws": true` 可让该节点走 WS 通道通信,详见 [中间件部署 - WS 通道](/guide/deploy-middleware#ws-通道拨测数据经-websocket-传输)
|
||||
|
||||
### WS 通道配置一览
|
||||
|
||||
WS 通道涉及**中间件(服务端)**与**后端节点(客户端)**两侧配置:
|
||||
|
||||
| 角色 | 配置项 | 环境变量 | 说明 |
|
||||
|------|--------|----------|------|
|
||||
| 中间件(服务端) | `ws-port` | `WS_PORT` | WS 监听端口,默认 `8092`,`0`=关闭;节点连 `ws(s)://<中间件>:<ws-port>/ws` |
|
||||
| 中间件(服务端) | `wsKeys` | `WSKEYS` | WS 注册校验表:`{"<node-id>": "<key>"}`,节点 `register` 的 key 必须匹配 |
|
||||
| 节点(客户端) | `ws-url` | `WS_URL` | 中间件 WS 完整地址(含 `/ws` 路径),逗号分隔多备主备切换 |
|
||||
| 节点(客户端) | `node-id` | `NODE_ID` | 节点 id,与中间件 `apiKeys`/`wsKeys` 键一致(建议 UUID) |
|
||||
| 节点(客户端) | `node-key` | `NODE_KEY` | 注册 key,与中间件 `wsKeys[节点id]` 一致,**必填** |
|
||||
|
||||
**链路**:中间件收到 `ws:true` 节点的拨测请求 → 经 WS 发 `probe` → 节点执行探针(直调 webtest 函数 + 缓存)→ `probe_result` 回传 → 中间件返回 HTTP 响应(`Content-Type: application/json`)。节点侧接入步骤见 [后端节点部署 - WS 通道接入](/guide/deploy-node#ws-通道接入可选)。
|
||||
|
||||
---
|
||||
|
||||
@@ -197,9 +207,9 @@ apiBaseUrls: [
|
||||
- 远端 JSON 中**非空的字段才覆盖**本地值:字符串非空、数组非空、数字非 0(`rate-limit` 为指针语义,缺省=不覆盖,显式 0=不限流)
|
||||
- 部分字段缺省不会冲掉本地配置——远端只需包含要覆盖的字段
|
||||
- **敏感凭据强制忽略,永远不被远端覆盖**(代码写死,无需配置):
|
||||
- 后端 `access_token`:不随远端覆盖(保持 环境变量 > setting.json),远端里的 `access_token` 会被忽略
|
||||
- 中间件 `apiKeys`:不随远端覆盖,远端里的 `apiKeys` 会被忽略
|
||||
- `remote-ingore-config`(后端键名 `remote-ingore-config` / env `REMOTE_INGORE_CONFIG`):额外指定**不被远端覆盖的配置项列表**,数组中的键即使远端下发也不生效。适用于 access_token / apiKeys 之外的敏感项(如 `node-key`、`dns-server` 等),也可覆盖"非空才覆盖"规则
|
||||
- 后端 `access-token`:不随远端覆盖(保持 环境变量 > setting.json),远端里的 `access-token` 会被忽略
|
||||
- 中间件 `apiKeys` / `wsKeys`:不随远端覆盖,远端里的 `apiKeys`/`wsKeys` 会被忽略
|
||||
- `remote-ingore-config`(后端键名 `remote-ingore-config` / env `REMOTE_INGORE_CONFIG`):额外指定**不被远端覆盖的配置项列表**,数组中的键即使远端下发也不生效。适用于 access-token / apiKeys / wsKeys 之外的敏感项(如 `node-key`、`dns-server` 等),也可覆盖"非空才覆盖"规则
|
||||
|
||||
**拉取行为**:
|
||||
|
||||
@@ -234,11 +244,16 @@ REMOTE_CONFIG_URL=https://example.com/ipw-config.json ./lemonipw
|
||||
"port": "8091",
|
||||
"cors": "",
|
||||
"rate-limit": 120,
|
||||
"apiBaseUrls": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ]
|
||||
"APIBaseURL": {
|
||||
"DualStack": [ { "label": "中国 江苏 移动", "id": "cn-jiangsu", "url": "https://cn-jiangsu.api-ipw.wsmdn.top/" } ],
|
||||
"IPv4": [],
|
||||
"IPv6": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠️ 敏感凭据不得上传远端**:`apiKeys`(及后端 `access_token`、节点 key 等密钥)**禁止**写入远端配置——即使写了也会被强制忽略。凭据只放本地 setting.json 或环境变量。
|
||||
> [!WARNING]
|
||||
> 敏感凭据不得上传远端:`apiKeys`/`wsKeys`(及后端 `access-token`、节点 key 等密钥)**禁止**写入远端配置——即使写了也会被强制忽略。凭据只放本地 setting.json 或环境变量。
|
||||
|
||||
部署时:
|
||||
|
||||
@@ -256,7 +271,7 @@ systemd 中同样用 `Environment="REMOTE_CONFIG_URL=..."` 配置。
|
||||
|
||||
**安全与排查**:
|
||||
|
||||
- **⚠️ 敏感凭据不得上传远端**:远端配置(Gist / 对象存储 / 静态托管等)**禁止包含任何密钥**——`apiKeys`、`access_token`、节点 key(`node-key`)等。这些值会被强制忽略,但远端文件本身一旦泄露等于直接暴露凭据;凭据一律放本地 `setting.json` 或环境变量注入
|
||||
- **敏感凭据不得上传远端**:远端配置(Gist / 对象存储 / 静态托管等)**禁止包含任何密钥**——`apiKeys`/`wsKeys`、`access-token`、节点 key(`node-key`)等。这些值会被强制忽略,但远端文件本身一旦泄露等于直接暴露凭据;凭据一律放本地 `setting.json` 或环境变量注入
|
||||
- 远端 URL 必须能被节点访问;优先使用 **HTTPS** 地址
|
||||
- 远端配置文件如果放在公开可访问的位置(公开 Gist、无鉴权对象存储),即便不含密钥,也等于把节点拓扑、节点 id 等敏感信息公之于众,建议放在私有仓库 / 带鉴权的对象存储 / 内部服务上
|
||||
- 常见失败排查:
|
||||
|
||||
@@ -32,9 +32,8 @@ pnpm dev
|
||||
|--------|------|
|
||||
| `siteUrl` | 站点对外地址 |
|
||||
| `siteName` | 站点名称(页面标题 / 描述 / 页脚品牌) |
|
||||
| `apiBaseUrls` | 后端上游节点列表(whois / ssl / detail) |
|
||||
| `IPLocationAPIs` | IP 归属地 / ASN 上游节点列表 |
|
||||
| `TCPing` / `SpeedTest` / `NSLookup` | 对应功能的多节点候选(DualStack / IPv4 / IPv6) |
|
||||
| `APIBaseURL` | 拨测上游节点池(whois / ssl / detail / dns / dnssec / tcping / speed),含 `DualStack` / `IPv4` / `IPv6` 三栈 |
|
||||
| `IPLocationAPI` | IP 归属地 / ASN 上游节点池(纯数组,无栈区分) |
|
||||
| `Middleware` | 外部独立中间件 base URL 列表,`/middleware/*` 请求依次尝试、失败重试下一个 |
|
||||
| `EnableInternalMiddleware` | 是否启用前端内置中间件(本地转发,作为候选列表最后一位兜底),默认 `true` |
|
||||
| `rateLimitPerMinute` | 内置中间件单 IP 限流次数(次/分钟),默认 `120`,`0` 表示不限流 |
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
> cp middleware.setting.json.example setting.json
|
||||
> ```
|
||||
>
|
||||
> 复制后按需修改 `apiBaseUrls` / `apiKeys` / `cors` 等配置项。
|
||||
> 复制后按需修改 `APIBaseURL` / `IPLocationAPI` / `apiKeys` / `wsKeys` / `cors` 等配置项。
|
||||
|
||||
## 方案一:Docker
|
||||
|
||||
@@ -27,7 +27,7 @@ docker run -d -p 8091:8091 \
|
||||
middleware-go
|
||||
```
|
||||
|
||||
配置通过挂载 `setting.json` 提供(`apiBaseUrls` / `apiKeys` / `cors` / `rate-limit` / `remote-config-url` 等;`rate-limit` 为单 IP 每分钟限流次数,默认 120,0 表示不限流,可用环境变量 `RATE_LIMIT` 覆盖;`remote-config-url` 为远端配置地址,可用环境变量 `REMOTE_CONFIG_URL` 覆盖;**`apiKeys` 为敏感凭据,不随远端配置覆盖**)。
|
||||
配置通过挂载 `setting.json` 提供(`APIBaseURL` / `IPLocationAPI` / `apiKeys` / `wsKeys` / `cors` / `rate-limit` / `remote-config-url` 等;`rate-limit` 为单 IP 每分钟限流次数,默认 120,0 表示不限流,可用环境变量 `RATE_LIMIT` 覆盖;`remote-config-url` 为远端配置地址,可用环境变量 `REMOTE_CONFIG_URL` 覆盖;**`apiKeys` / `wsKeys` 为敏感凭据,不随远端配置覆盖**——`apiKeys` 管 HTTP 转发鉴权,`wsKeys` 管 WS 注册校验,两者相互独立)。
|
||||
|
||||
## 方案二:二进制
|
||||
|
||||
@@ -37,7 +37,7 @@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o middleware-go-linux-amd64 .
|
||||
./middleware-go-linux-amd64 # 运行(与 setting.json 同目录)
|
||||
```
|
||||
|
||||
所有配置可用环境变量覆盖(`API_BASE_URLS` / `IP_LOCATION_APIS` / `CORS` / `APIKEYS` / `RATE_LIMIT` 等,数组/对象用 JSON 字符串),优先级:**环境变量 > setting.json > 默认值**(远端配置 `REMOTE_CONFIG_URL` 高于两者,详见 [配置文件 - 远端配置](/guide/config#远端配置remoteconfigurl))。
|
||||
所有配置可用环境变量覆盖(`API_BASE_URLS` / `IP_LOCATION_APIS` / `CORS` / `APIKEYS` / `WSKEYS` / `RATE_LIMIT` 等,数组/对象用 JSON 字符串),优先级:**环境变量 > setting.json > 默认值**(远端配置 `REMOTE_CONFIG_URL` 高于两者,详见 [配置文件 - 远端配置](/guide/config#远端配置remoteconfigurl))。
|
||||
|
||||
需要守护运行时,参考 [后端节点部署 - 方案五:一键安装](/guide/deploy-node#方案五一键安装installsh) 的 systemd 管理方式(`ExecStart` 指向中间件二进制,`WorkingDirectory` 指向 `middleware-go/setting.json` 所在目录)。
|
||||
|
||||
@@ -61,10 +61,11 @@ middleware-go 内置 WS 服务端,后端节点可作为 **WS 客户端**连入
|
||||
```json
|
||||
{
|
||||
"port": "8091",
|
||||
"apiBaseUrls": [
|
||||
{ "label": "上海 腾讯云 BGP", "id": "tencent-sh", "url": "", "ws": true }
|
||||
],
|
||||
"TCPing": { "DualStack": [], "IPv4": [], "IPv6": [ { "label": "河北 秦皇岛 联通", "id": "cn-hebei-qinhuangdao", "url": "https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/", "ws": true } ] }
|
||||
"APIBaseURL": {
|
||||
"DualStack": [ { "label": "上海 腾讯云 BGP", "id": "tencent-sh", "url": "", "ws": true } ],
|
||||
"IPv4": [],
|
||||
"IPv6": [ { "label": "河北 秦皇岛 联通", "id": "cn-hebei-qinhuangdao", "url": "https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/", "ws": true } ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,7 +77,7 @@ middleware-go 内置 WS 服务端,后端节点可作为 **WS 客户端**连入
|
||||
|
||||
| 方向 | type | data 说明 |
|
||||
|------|------|-----------|
|
||||
| 节点 → middleware | `register` | `{ "nodeId": "...", "key": "..." }`,连接后首条消息,同 id 新连接顶掉旧连接;`key` 为注册凭证,与 setting.json `apiKeys` 配置比对 |
|
||||
| 节点 → middleware | `register` | `{ "nodeId": "...", "key": "..." }`,连接后首条消息,同 id 新连接顶掉旧连接;`key` 为注册凭证,与 setting.json `wsKeys` 配置比对 |
|
||||
| middleware → 节点 | `register_ok` | `{ "heartbeatSeconds": 20 }` |
|
||||
| middleware → 节点 | `register_error` | `{ "code": 401, "command": "invalid key" }`(注册凭证错误时返回并断开连接) |
|
||||
| middleware → 节点 | `probe` | `{ "requestId": "...", "apiType": "tcping", "raw": "qq.com", "query": {"port":"443"} }`,拨测请求(HTTP/WS 双通道时前端请求仍走 HTTP,middleware 内部转 WS) |
|
||||
@@ -88,21 +89,21 @@ middleware-go 内置 WS 服务端,后端节点可作为 **WS 客户端**连入
|
||||
### 节点侧接入(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 的开放节点可不传)
|
||||
2. 首条消息发 `register { "nodeId": "<与中间件配置一致的节点 id>", "key": "<注册凭证>" }`(节点在 setting.json `wsKeys` 里配了 key 就必须传对;未配置 key 的开放节点可不传)
|
||||
3. 收到 `probe` → 执行拨测 → 回 `probe_result`(必须携带原 `requestId`,支持乱序返回,同一连接可并发多个拨测)
|
||||
4. 收到 `ping` → 回 `pong`;`status` 可忽略或记录
|
||||
5. 断线后重连并重新 `register` 即可
|
||||
|
||||
### 注册 key 校验
|
||||
|
||||
节点注册时中间件按 `apiKeys` 配置校验 key:
|
||||
节点注册时中间件按 `wsKeys` 配置校验 key(与 `apiKeys` HTTP 转发鉴权相互独立):
|
||||
|
||||
- 节点在 `apiKeys` 配置了 key(非空)→ `register` 必须携带正确 key,不传或传错返回 `register_error { "code": 401, "command": "invalid key" }` 并**断开连接**,不进入数据阶段
|
||||
- 节点未配置 key(`apiKeys` 无该项或为空)→ 开放注册,不传 key 即可
|
||||
- 节点在 `wsKeys` 配置了 key(非空)→ `register` 必须携带正确 key,不传或传错返回 `register_error { "code": 401, "command": "invalid key" }` 并**断开连接**,不进入数据阶段
|
||||
- 节点未配置 key(`wsKeys` 无该项或为空)→ 开放注册,不传 key 即可
|
||||
|
||||
与 HTTP 通道的鉴权语义一致(配置了 key 就必须带凭据,留空 = 开放)。
|
||||
|
||||
### 说明
|
||||
|
||||
- 注册阶段按节点 `apiKeys` 配置校验 key(配了 key 必须传对,否则 401 断开;未配置则开放,详见上文「注册 key 校验」);拨测请求经 WS 转发时不携带 `Authorization` 头(后续由节点侧自行决定是否需要额外鉴权)
|
||||
- 注册阶段按节点 `wsKeys` 配置校验 key(配了 key 必须传对,否则 401 断开;未配置则开放,详见上文「注册 key 校验」);拨测请求经 WS 转发时不携带 `Authorization` 头(后续由节点侧自行决定是否需要额外鉴权)
|
||||
- `ws:true` 且 WS 未启用(`WS_PORT=0`)时,自动回退原 HTTP 转发
|
||||
@@ -28,7 +28,7 @@ npx edgeone pages deploy -n ipw-cn -t $EDGEONE_API_TOKEN
|
||||
> **Serverless 节点均为 IPv4-only**(EdgeOne、Vercel 等平台的出站网络只提供 IPv4)。部署后需要:
|
||||
>
|
||||
> 1. 给节点配置 `SINGLE_STACK=ipv4`(声明节点为 IPv4 单栈),跳过 IPv6 相关测试,避免无意义的错误日志与超时
|
||||
> 2. 在前端配置 `config/index.ts` 中,**该节点只能填入 `TCPing.IPv4` / `SpeedTest.IPv4` 等 IPv4 分组**,不要放进 `DualStack` / `IPv6` 分组
|
||||
> 2. 在前端配置 `config/index.ts` 中,**该节点只能填入 `APIBaseURL.IPv4`**(拨测节点池的 IPv4 栈),不要放进 `DualStack` / `IPv6` 栈
|
||||
|
||||
## 方案三:Docker
|
||||
|
||||
@@ -60,6 +60,10 @@ go build -o lemonipw main.go
|
||||
仓库根目录提供 `install.sh` 一键安装脚本:自动检测架构 → 下载最新 release 二进制 → **交互式输入配置**(无需准备 setting.json,配置以环境变量注入)→ 生成 systemd 服务并守护进程。
|
||||
|
||||
```bash
|
||||
# 先下载脚本到本地(勿用 curl | bash 管道执行,避免下载中断导致语法解析错误)
|
||||
git clone https://github.com/nomdn/ipw-cn && cd ipw-cn # 或
|
||||
curl -fsSL https://raw.githubusercontent.com/nomdn/ipw-cn/main/install.sh -o install.sh
|
||||
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
@@ -69,7 +73,7 @@ sudo bash install.sh
|
||||
|--------|--------|------|
|
||||
| 安装目录 | `/opt/lemon-ipw` | 二进制与工作目录 |
|
||||
| 监听端口 | `8080` | `PORTS` |
|
||||
| access_token | 留空 | 留空 = 不启用鉴权 |
|
||||
| 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 |
|
||||
@@ -80,9 +84,9 @@ sudo bash install.sh
|
||||
|
||||
**WS 接入特殊规则**:
|
||||
|
||||
- `WS_URL` 留空时自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top`(可改填自己的中间件地址,逗号分隔多备)
|
||||
- `WS_URL` 留空时自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top/ws`;填写自己的中间件地址时须为**完整路径**(含 `wss://` 前缀与 `/ws` 路径,如 `wss://host:8092/ws`),逗号分隔多备
|
||||
- `NODE_ID` **强制自动生成 UUID**(无需输入,如 `4580ea9d-2a16-4b8e-8090-5f2e7f0a2229`),适合作为节点唯一标识
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS(中间件 `apiKeys` 必须包含该节点,否则注册被拒 401);安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 setting.json 的 `apiKeys`
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS(中间件 `wsKeys` 必须包含该节点,否则注册被拒 401);安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 setting.json 的 `wsKeys`
|
||||
|
||||
**安装完成后自动生成并启动 systemd 服务**(`lemon-ipw.service`),配置以 `Environment="K=V"` 注入,无需 setting.json。常用管理命令:
|
||||
|
||||
@@ -94,6 +98,7 @@ sudo systemctl restart lemon-ipw # 重启(改配置后)
|
||||
|
||||
修改配置:`sudo systemctl edit --full lemon-ipw` 编辑环境变量,保存后 `sudo systemctl daemon-reload && sudo systemctl restart lemon-ipw` 生效。
|
||||
|
||||
> [!WARNING]
|
||||
> **版本兼容提醒**: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_` 前缀。
|
||||
|
||||
## 配置
|
||||
@@ -108,14 +113,14 @@ sudo systemctl restart lemon-ipw # 重启(改配置后)
|
||||
|
||||
```bash
|
||||
WS_URL=ws://<中间件IP>:8092/ws \
|
||||
NODE_ID=<节点id,与中间件 apiBaseUrls 的节点 id 一致> \
|
||||
NODE_KEY=<注册key,与中间件 apiKeys[节点id] 一致;节点未配置 key 可留空> \
|
||||
NODE_ID=<节点id,与中间件 APIBaseURL / IPLocationAPI 池的节点 id 一致> \
|
||||
NODE_KEY=<注册key,与中间件 wsKeys[节点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_KEY`:中间件 `wsKeys` 里配了 key 就必须传对,否则返回 401 并断开;未配置 key 的节点开放注册
|
||||
|
||||
**注意**:`NODE_ID` 必须与中间件 `apiBaseUrls` / `TCPing` / `SpeedTest` 等配置中的节点 `id` 一致,且该节点需配置 `"ws": true` 才会走 WS 通道。未连接中间件时,`ws:true` 节点的拨测会返回 502。
|
||||
**注意**:`NODE_ID` 必须与中间件 `APIBaseURL` / `IPLocationAPI` 池中的节点 `id` 一致,且该节点需配置 `"ws": true` 才会走 WS 通道。未连接中间件时,`ws:true` 节点的拨测会返回 502。
|
||||
@@ -20,12 +20,15 @@
|
||||
```bash
|
||||
# 获取安装脚本(任选其一)
|
||||
git clone https://github.com/nomdn/ipw-cn && cd ipw-cn # 或
|
||||
curl -LO https://raw.githubusercontent.com/nomdn/ipw-cn/main/install.sh
|
||||
curl -fsSL https://raw.githubusercontent.com/nomdn/ipw-cn/main/install.sh -o install.sh
|
||||
|
||||
# 执行安装(root 权限)
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> 请勿使用 `curl ... | sudo bash` 管道方式执行:脚本经管道边下载边执行,网络慢/抖动时 bash 会读到不完整的脚本内容,报 `syntax error near unexpected token` 类错误(脚本本身无语法问题)。务必**先下载完整文件再执行**。
|
||||
|
||||
脚本会自动完成:检测架构 → 下载最新 release 二进制 → **交互式输入配置** → 生成并启动 systemd 守护进程。
|
||||
|
||||
**交互式配置项**(直接回车使用默认值):
|
||||
@@ -34,7 +37,7 @@ sudo bash install.sh
|
||||
|--------|--------|------|
|
||||
| 安装目录 | `/opt/lemon-ipw` | 二进制与工作目录 |
|
||||
| 监听端口 | `8080` | 后端 HTTP 端口 |
|
||||
| access_token | 留空 | 留空 = 不启用鉴权 |
|
||||
| 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 |
|
||||
@@ -45,9 +48,9 @@ sudo bash install.sh
|
||||
|
||||
**WS 通道接入规则**(选 `y` 时):
|
||||
|
||||
- `WS_URL` 留空自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top`,可改填自己的中间件地址
|
||||
- `WS_URL` 留空自动使用默认值 `wss://middleware-1.api-ipw.wsmdn.top/ws`;填写自己的中间件地址须为完整路径(`wss://host:8092/ws` 形式)
|
||||
- `NODE_ID` **强制自动生成 UUID**(无需输入)
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS;安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 `apiKeys`
|
||||
- `NODE_KEY` **必填**:不加 key 禁止启用 WS;安装完成后脚本会提示把 `"<NODE_ID>": "<NODE_KEY>"` 加入中间件 `wsKeys`
|
||||
|
||||
### 验证安装
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const result = ref<ASNResult | null>(null)
|
||||
const error = ref('')
|
||||
const userIP = ref('')
|
||||
|
||||
const apiList = config.IPLocationAPIs
|
||||
const apiList = config.IPLocationAPI
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
|
||||
|
||||
|
||||
@@ -72,7 +72,11 @@ const recordTypes = [
|
||||
{ value: 'ptr', label: 'PTR 记录' }
|
||||
]
|
||||
|
||||
const dnsServerFetches = config.NSLookup.map((server) => {
|
||||
const dnsServerFetches = [
|
||||
...config.APIBaseURL.DualStack,
|
||||
...config.APIBaseURL.IPv4,
|
||||
...config.APIBaseURL.IPv6
|
||||
].map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/dns/" + recordType.value + "/" + domain.value);
|
||||
const { data, error, execute } = useMiddlewareFetch(url, {
|
||||
immediate: false,
|
||||
|
||||
@@ -69,7 +69,11 @@ const loading = ref(false)
|
||||
const serverResults = ref<ServerResult[]>([])
|
||||
const userIP = ref('')
|
||||
|
||||
const apiList = config.NSLookup
|
||||
const apiList = [
|
||||
...config.APIBaseURL.DualStack,
|
||||
...config.APIBaseURL.IPv4,
|
||||
...config.APIBaseURL.IPv6
|
||||
]
|
||||
|
||||
const dnssecFetches = apiList.map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/dnssec/" + tmpDomain.value)
|
||||
|
||||
@@ -43,7 +43,7 @@ curl ${config.v6OnlyAPI}
|
||||
curl ${config.DualStackAPI}
|
||||
`.trim();
|
||||
const html = ref('');
|
||||
const apiList = config.IPLocationAPIs
|
||||
const apiList = config.IPLocationAPI
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]!.id)
|
||||
const ipAddress = ref('');
|
||||
|
||||
@@ -68,7 +68,7 @@ const serverResults = ref<ServerResult[]>([])
|
||||
// =============================================
|
||||
|
||||
/** 双栈服务器:同时支持 IPv4/IPv6 的节点 */
|
||||
const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
|
||||
const dualStackFetches = config.APIBaseURL.DualStack.map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/speed/v6/" + domain.value);
|
||||
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
|
||||
immediate: false,
|
||||
@@ -78,7 +78,7 @@ const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
|
||||
});
|
||||
|
||||
/** IPv6 专用服务器:仅支持 IPv6 的节点 */
|
||||
const ipv6Fetches = config.SpeedTest.IPv6.map((server) => {
|
||||
const ipv6Fetches = config.APIBaseURL.IPv6.map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/speed/v6/" + domain.value);
|
||||
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
|
||||
immediate: false,
|
||||
|
||||
@@ -79,8 +79,8 @@ const loading = ref(false)
|
||||
const serverResults = ref<ServerResult[]>([])
|
||||
|
||||
const allServers = [
|
||||
...config.TCPing.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
|
||||
...config.TCPing.IPv6.map((s: ServerConfig) => ({ ...s, type: 'IPv6' }))
|
||||
...config.APIBaseURL.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
|
||||
...config.APIBaseURL.IPv6.map((s: ServerConfig) => ({ ...s, type: 'IPv6' }))
|
||||
];
|
||||
|
||||
const tcpingFetches = allServers.map((server) => {
|
||||
@@ -95,7 +95,7 @@ const tcpingFetches = allServers.map((server) => {
|
||||
function initServerResults() {
|
||||
const results: ServerResult[] = []
|
||||
|
||||
config.TCPing.DualStack.forEach((server: ServerConfig) => {
|
||||
config.APIBaseURL.DualStack.forEach((server: ServerConfig) => {
|
||||
results.push({
|
||||
label: server.label,
|
||||
loading: false,
|
||||
@@ -103,7 +103,7 @@ function initServerResults() {
|
||||
})
|
||||
})
|
||||
|
||||
config.TCPing.IPv6.forEach((server: ServerConfig) => {
|
||||
config.APIBaseURL.IPv6.forEach((server: ServerConfig) => {
|
||||
results.push({
|
||||
label: server.label,
|
||||
loading: false,
|
||||
|
||||
@@ -63,7 +63,7 @@ interface PerformanceCheckItem {
|
||||
download_speed: number
|
||||
is_reachable: boolean
|
||||
}
|
||||
const apiList = config.apiBaseUrls
|
||||
const apiList = config.APIBaseURL.DualStack
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
|
||||
const tmpDomain = ref('https://www.zakoflare.com')
|
||||
|
||||
@@ -67,7 +67,7 @@ curl ${config.v6OnlyAPI}
|
||||
curl ${config.DualStackAPI}
|
||||
`.trim();
|
||||
const html = ref('');
|
||||
const apiList = config.IPLocationAPIs
|
||||
const apiList = config.IPLocationAPI
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
|
||||
const ipAddress = ref('');
|
||||
|
||||
@@ -68,7 +68,7 @@ const serverResults = ref<ServerResult[]>([])
|
||||
// =============================================
|
||||
|
||||
/** 双栈服务器:同时支持 IPv4/IPv6 的节点 */
|
||||
const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
|
||||
const dualStackFetches = config.APIBaseURL.DualStack.map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/speed/v4/" + domain.value);
|
||||
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
|
||||
immediate: false,
|
||||
@@ -78,7 +78,7 @@ const dualStackFetches = config.SpeedTest.DualStack.map((server) => {
|
||||
});
|
||||
|
||||
/** IPv4 专用服务器:仅支持 IPv4 的节点 */
|
||||
const ipv4Fetches = config.SpeedTest.IPv4.map((server) => {
|
||||
const ipv4Fetches = config.APIBaseURL.IPv4.map((server) => {
|
||||
const url = computed(() => "/middleware/" + server.id + "/speed/v4/" + domain.value);
|
||||
const { data, error: fetchError, execute } = useMiddlewareFetch(url, {
|
||||
immediate: false,
|
||||
|
||||
@@ -65,7 +65,7 @@ interface SSLCheckItem {
|
||||
is_expired: boolean
|
||||
is_reachable: boolean
|
||||
}
|
||||
const apiList = config.apiBaseUrls
|
||||
const apiList = config.APIBaseURL.DualStack
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
|
||||
const tmpDomain = ref('https://www.zakoflare.com')
|
||||
|
||||
@@ -78,8 +78,8 @@ const loading = ref(false)
|
||||
const serverResults = ref<ServerResult[]>([])
|
||||
|
||||
const allServers = [
|
||||
...config.TCPing.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
|
||||
...config.TCPing.IPv4.map((s: ServerConfig) => ({ ...s, type: 'IPv4' }))
|
||||
...config.APIBaseURL.DualStack.map((s: ServerConfig) => ({ ...s, type: 'DualStack' })),
|
||||
...config.APIBaseURL.IPv4.map((s: ServerConfig) => ({ ...s, type: 'IPv4' }))
|
||||
];
|
||||
|
||||
const tcpingFetches = allServers.map((server) => {
|
||||
@@ -94,7 +94,7 @@ const tcpingFetches = allServers.map((server) => {
|
||||
function initServerResults() {
|
||||
const results: ServerResult[] = []
|
||||
|
||||
config.TCPing.DualStack.forEach((server: ServerConfig) => {
|
||||
config.APIBaseURL.DualStack.forEach((server: ServerConfig) => {
|
||||
results.push({
|
||||
label: server.label,
|
||||
loading: false,
|
||||
@@ -102,7 +102,7 @@ function initServerResults() {
|
||||
})
|
||||
})
|
||||
|
||||
config.TCPing.IPv4.forEach((server: ServerConfig) => {
|
||||
config.APIBaseURL.IPv4.forEach((server: ServerConfig) => {
|
||||
results.push({
|
||||
label: server.label,
|
||||
loading: false,
|
||||
|
||||
@@ -64,7 +64,7 @@ const result = ref<any>(null)
|
||||
const error = ref('')
|
||||
const userIP = ref('')
|
||||
|
||||
const apiList = config.apiBaseUrls
|
||||
const apiList = config.APIBaseURL.DualStack
|
||||
const currentApiIndex = ref(0)
|
||||
const backendID = computed(() => apiList[currentApiIndex.value]?.id || '')
|
||||
const whoisUrl = computed(() => '/middleware/' + backendID.value + '/whois/' + domain.value)
|
||||
|
||||
+33
-231
@@ -35,25 +35,9 @@ const config = {
|
||||
v4OnlyAPI: "https://4.wsmdn.top",
|
||||
v6OnlyAPI: "https://6.wsmdn.top",
|
||||
DualStackAPI: "https://test.wsmdn.top",
|
||||
apiBaseUrls: [
|
||||
{
|
||||
label: "中国 江苏 移动",
|
||||
id: "cn-jiangsu",
|
||||
url: "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
|
||||
id: "cn-shenzhen",
|
||||
url: "https://cn-shenzhen.api-ipw.wsmdn.top/"
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "上海 腾讯云 BGP",
|
||||
id: "tencent-sh",
|
||||
url: ""
|
||||
}
|
||||
],
|
||||
IPLocationAPIs: [
|
||||
// ---- 节点池结构 ----
|
||||
// IPLocationAPI:IP 归属查询(location / asn)上游节点池(纯数组,无栈区分)
|
||||
IPLocationAPI: [
|
||||
{
|
||||
label: "中国 江苏 移动",
|
||||
id: "cn-jiangsu",
|
||||
@@ -65,283 +49,101 @@ const config = {
|
||||
url: "https://cn2-sichuan.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
TCPing:{
|
||||
DualStack: [
|
||||
// APIBaseURL:其余拨测(whois / ssl / detail / dns / dnssec / tcping / speed)上游节点池,含 { IPv6, IPv4, DualStack } 三栈
|
||||
APIBaseURL: {
|
||||
IPv6: [
|
||||
{
|
||||
label: "中国 江苏 移动",
|
||||
id: "cn-jiangsu",
|
||||
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
|
||||
label: "中国 四川 沙渠 电信[ZFC]",
|
||||
id: "cn2-sichuan",
|
||||
url: "https://cn2-sichuan.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
|
||||
id: "cn-shenzhen",
|
||||
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
|
||||
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
|
||||
id: "lntl-cn-hk-kowloon",
|
||||
url: "https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/"
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "上海 腾讯云 BGP",
|
||||
id: "tencent-sh",
|
||||
url :"",
|
||||
},
|
||||
label: "中国 河北 秦皇岛 联通",
|
||||
id: "cn-hebei-qinhuangdao",
|
||||
url: "https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
IPv4: [
|
||||
{
|
||||
label: "中国 广东 广州 腾讯云",
|
||||
id: "cn-guangzhou",
|
||||
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
|
||||
url: "https://cn-guangzhou.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "新加坡 腾讯云",
|
||||
id: "sg-1",
|
||||
url :"https://sg-1.api-ipw.wsmdn.top/",
|
||||
url: "https://sg-1.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
|
||||
id: "hk-shatin",
|
||||
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
|
||||
url: "https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
|
||||
id: "cn-xian",
|
||||
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
|
||||
url: "https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
|
||||
label: "美国/加利福尼亚州/洛杉矶/蒙特利奇帕克/USCD[ZFC]",
|
||||
id: "us-la",
|
||||
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
|
||||
url: "https://america-california-losangeles.ipwapi.zfap.wsmdn.top/"
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "山东 枣庄 双线",
|
||||
id: "zaozhuang",
|
||||
url :"",
|
||||
url: ""
|
||||
},
|
||||
{
|
||||
label: "湖北 十堰 电信",
|
||||
id: "shiyan",
|
||||
url :"",
|
||||
url: ""
|
||||
},
|
||||
{
|
||||
label: "香港 VpsQuan",
|
||||
id: "hongkong2",
|
||||
url :"",
|
||||
url: ""
|
||||
},
|
||||
{
|
||||
label: "北京 京东云 BGP",
|
||||
id: "jdcloud",
|
||||
url :"",
|
||||
url: ""
|
||||
},
|
||||
{
|
||||
label: "陕西 西安二 电信",
|
||||
id: "xian2",
|
||||
url :"",
|
||||
url: ""
|
||||
},
|
||||
{
|
||||
label: "香港 Cogent",
|
||||
id: "hongkong",
|
||||
url :"",
|
||||
url: ""
|
||||
}
|
||||
],
|
||||
IPv6: [
|
||||
{
|
||||
label: "中国 四川 沙渠 电信[ZFC]",
|
||||
id: "cn2-sichuan",
|
||||
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
|
||||
id: "lntl-cn-hk-kowloon",
|
||||
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 河北 秦皇岛 联通",
|
||||
id: "cn-hebei-qinhuangdao",
|
||||
url :"https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/",
|
||||
}
|
||||
]
|
||||
},
|
||||
SpeedTest:{
|
||||
DualStack: [
|
||||
{
|
||||
label: "中国 江苏 移动",
|
||||
id: "cn-jiangsu",
|
||||
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
|
||||
url: "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
},
|
||||
{
|
||||
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
|
||||
id: "cn-shenzhen",
|
||||
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
|
||||
url: "https://cn-shenzhen.api-ipw.wsmdn.top/"
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
// IP 直连的节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "上海 腾讯云 BGP",
|
||||
id: "tencent-sh",
|
||||
url :"",
|
||||
},
|
||||
],
|
||||
IPv4: [
|
||||
{
|
||||
label: "中国 广东 广州 腾讯云",
|
||||
id: "cn-guangzhou",
|
||||
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "新加坡 腾讯云",
|
||||
id: "sg-1",
|
||||
url :"https://sg-1.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
|
||||
id: "hk-shatin",
|
||||
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
|
||||
id: "cn-xian",
|
||||
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
|
||||
id: "us-la",
|
||||
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "山东 枣庄 双线",
|
||||
id: "zaozhuang",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "湖北 十堰 电信",
|
||||
id: "shiyan",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "香港 VpsQuan",
|
||||
id: "hongkong2",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "北京 京东云 BGP",
|
||||
id: "jdcloud",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "陕西 西安二 电信",
|
||||
id: "xian2",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "香港 Cogent",
|
||||
id: "hongkong",
|
||||
url :"",
|
||||
}
|
||||
],
|
||||
IPv6: [
|
||||
{
|
||||
label: "中国 四川 沙渠 电信[ZFC]",
|
||||
id: "cn2-sichuan",
|
||||
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
|
||||
id: "lntl-cn-hk-kowloon",
|
||||
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 河北 秦皇岛 联通",
|
||||
id: "cn-hebei-qinhuangdao",
|
||||
url :"https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/",
|
||||
url: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
NSLookup:[
|
||||
{
|
||||
label: "中国 江苏 移动",
|
||||
id: "cn-jiangsu",
|
||||
url :"https://cn-jiangsu.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 广州 腾讯云",
|
||||
id: "cn-guangzhou",
|
||||
url :"https://cn-guangzhou.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "新加坡 腾讯云",
|
||||
id: "sg-1",
|
||||
url :"https://sg-1.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 四川 沙渠 电信[ZFC]",
|
||||
id: "cn2-sichuan",
|
||||
url:"https://cn2-sichuan.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 陕西 西安 北经济技术开发区 未央区凤城 中国电信[ZFC]",
|
||||
id: "cn-xian",
|
||||
url :"https://cn-xian-shaanxiprovince.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 香港 新界 西贡区 将军澳 MICC[ZFC]",
|
||||
id: "hk-shatin",
|
||||
url :"https://china-hk-kowloon-shatindistrict-newcloud.ipwapi.zfap.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 香港 九龙城区 旺角东 Cloudie[ZFC]",
|
||||
id: "lntl-cn-hk-kowloon",
|
||||
url:"https://lntl-cn-hk-kowloon.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "中国 广东 深圳 龙岗 坪地街道 中国移动",
|
||||
id: "cn-shenzhen",
|
||||
url :"https://cn-shenzhen.api-ipw.wsmdn.top/",
|
||||
},
|
||||
{
|
||||
label: "美国/加利福尼亚州/洛杉矶/蒙特雷帕克/USCD[ZFC]",
|
||||
id: "us-la",
|
||||
url :"https://america-california-losangeles.ipwapi.zfap.wsmdn.top/",
|
||||
},
|
||||
// IP 直连新节点:URL 留空,仅经独立中间件转发
|
||||
{
|
||||
label: "上海 腾讯云 BGP",
|
||||
id: "tencent-sh",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "山东 枣庄 双线",
|
||||
id: "zaozhuang",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "湖北 十堰 电信",
|
||||
id: "shiyan",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "香港 VpsQuan",
|
||||
id: "hongkong2",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "北京 京东云 BGP",
|
||||
id: "jdcloud",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "陕西 西安二 电信",
|
||||
id: "xian2",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "香港 Cogent",
|
||||
id: "hongkong",
|
||||
url :"",
|
||||
},
|
||||
{
|
||||
label: "中国 河北 秦皇岛 联通",
|
||||
id: "cn-hebei-qinhuangdao",
|
||||
url :"https://cn-hebei-qinhuangdao.api-ipw.wsmdn.top/",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
export { config }
|
||||
@@ -98,25 +98,17 @@ export default defineEventHandler(async (event) => {
|
||||
let apiBaseUrls: Array<{ id: string, url: string ,label: string}> = []
|
||||
switch (apiType) {
|
||||
case 'whois':
|
||||
apiBaseUrls = [...config.apiBaseUrls]
|
||||
case 'ssl':
|
||||
case 'detail':
|
||||
apiBaseUrls = [...config.APIBaseURL.DualStack]
|
||||
break
|
||||
case 'dns':
|
||||
apiBaseUrls = [...config.NSLookup]
|
||||
case 'dnssec':
|
||||
apiBaseUrls = [...config.APIBaseURL.DualStack, ...config.APIBaseURL.IPv4, ...config.APIBaseURL.IPv6]
|
||||
break
|
||||
case 'location':
|
||||
apiBaseUrls = [...config.IPLocationAPIs]
|
||||
break
|
||||
case 'ssl':
|
||||
apiBaseUrls = [...config.apiBaseUrls]
|
||||
break
|
||||
case 'asn':
|
||||
apiBaseUrls = [...config.IPLocationAPIs]
|
||||
break
|
||||
case 'dnssec':
|
||||
apiBaseUrls = [...config.NSLookup]
|
||||
break
|
||||
case 'detail':
|
||||
apiBaseUrls = [...config.apiBaseUrls]
|
||||
apiBaseUrls = [...config.IPLocationAPI]
|
||||
break
|
||||
default:
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid API type' })
|
||||
@@ -151,21 +143,12 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
return data
|
||||
}else if (apiType === 'tcping' || apiType === 'udping' || apiType === 'speed') {
|
||||
let apiBaseUrls:Array<{ id: string, url: string ,label: string}> = []
|
||||
let apiTypeConfig: any = {}
|
||||
switch (apiType) {
|
||||
case 'tcping':
|
||||
apiTypeConfig = config.TCPing
|
||||
apiBaseUrls = [...apiTypeConfig.DualStack, ...apiTypeConfig.IPv4, ...apiTypeConfig.IPv6]
|
||||
break
|
||||
case 'speed':
|
||||
apiTypeConfig = config.SpeedTest
|
||||
apiBaseUrls = [...apiTypeConfig.DualStack, ...apiTypeConfig.IPv4, ...apiTypeConfig.IPv6]
|
||||
break
|
||||
default:
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid API type' })
|
||||
|
||||
}
|
||||
// tcping/udping/speed 统一走 APIBaseURL 节点池(平铺三栈)
|
||||
const apiBaseUrls: Array<{ id: string, url: string ,label: string}> = [
|
||||
...config.APIBaseURL.DualStack,
|
||||
...config.APIBaseURL.IPv4,
|
||||
...config.APIBaseURL.IPv6
|
||||
]
|
||||
const map = new Map<string, string>(apiBaseUrls.map(api => [api.id, api.url]))
|
||||
if (!map.has(backendID)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid backend ID' })
|
||||
|
||||
+14
-7
@@ -74,7 +74,9 @@ 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 "单栈模式 SINGLE_STACK(留空=双栈,ipv4 或 ipv6): " SINGLE_STACK
|
||||
|
||||
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}
|
||||
@@ -100,15 +102,15 @@ NODE_KEY=""
|
||||
WS_USED_DEFAULT=""
|
||||
case "${WS_CHOICE,,}" in
|
||||
y|yes)
|
||||
read -r -p " 中间件 WS 地址(留空=默认 wss://middleware-1.api-ipw.wsmdn.top,逗号分隔可多备): " WS_URL
|
||||
read -r -p " 中间件 WS 完整地址(含 wss:// 前缀与 /ws 路径,如 wss://host:8092/ws;留空=默认 wss://middleware-1.api-ipw.wsmdn.top/ws;逗号分隔可多备): " WS_URL
|
||||
if [ -z "$WS_URL" ]; then
|
||||
WS_URL="wss://middleware-1.api-ipw.wsmdn.top"
|
||||
WS_URL="wss://middleware-1.api-ipw.wsmdn.top/ws"
|
||||
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
|
||||
read -r -p " 注册 key(必填,中间件 wsKeys 必须包含此节点,否则注册被拒 401): " NODE_KEY
|
||||
if [ -z "$NODE_KEY" ]; then
|
||||
echo " 错误:不加 key 禁止启用 WS,必须提供注册 key"
|
||||
fi
|
||||
@@ -139,7 +141,8 @@ echo "========================================"
|
||||
echo "安装目录: $INSTALL_DIR"
|
||||
echo "服务名: lemon-ipw"
|
||||
echo "监听端口: $PORTS"
|
||||
echo "access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
|
||||
echo "单栈模式: ${SINGLE_STACK:-双栈}"
|
||||
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:-未启用}"
|
||||
@@ -182,6 +185,8 @@ SERVICE_FILE="/etc/systemd/system/lemon-ipw.service"
|
||||
ENV_LINES=""
|
||||
[ -n "$PORTS" ] && ENV_LINES="${ENV_LINES}Environment=\"PORTS=$PORTS\"
|
||||
"
|
||||
[ -n "$SINGLE_STACK" ] && ENV_LINES="${ENV_LINES}Environment=\"SINGLE_STACK=$SINGLE_STACK\"
|
||||
"
|
||||
[ -n "$ACCESS_TOKEN" ] && ENV_LINES="${ENV_LINES}Environment=\"ACCESS_TOKEN=$ACCESS_TOKEN\"
|
||||
"
|
||||
[ -n "$DNS_SERVER" ] && ENV_LINES="${ENV_LINES}Environment=\"DNS_SERVER=$DNS_SERVER\"
|
||||
@@ -243,7 +248,7 @@ 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 "WS 通道已启用,请把节点注册信息加入中间件 setting.json 的 wsKeys:"
|
||||
echo " \"$NODE_ID\": \"$NODE_KEY\""
|
||||
fi
|
||||
if [ -n "$WS_USED_DEFAULT" ]; then
|
||||
@@ -254,8 +259,10 @@ if [ -n "$WS_USED_DEFAULT" ]; then
|
||||
echo " 注册 key: $NODE_KEY"
|
||||
echo " WS 地址: $WS_URL"
|
||||
echo " 监听端口: $PORTS"
|
||||
echo " access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
|
||||
echo " 单栈模式: ${SINGLE_STACK:-双栈}"
|
||||
echo " access-token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
|
||||
echo " CORS: ${CORS:-不限}"
|
||||
echo " 地区-运营商 请自行填写"
|
||||
echo "========================================"
|
||||
fi
|
||||
echo "========================================"
|
||||
@@ -236,8 +236,8 @@ var (
|
||||
REMOTE_CONFIG_URL string
|
||||
REMOTE_INGORE_CONFIG []string // 不被远端配置覆盖的配置项列表(remote-ingore-config / REMOTE_INGORE_CONFIG)
|
||||
WS_URL string // WS 客户端:中间件 WS 地址(ws://host:port/ws)
|
||||
WS_NODE_ID string // WS 客户端:节点 id(与中间件 apiKeys 键一致)
|
||||
WS_NODE_KEY string // WS 客户端:注册 key(与中间件 apiKeys[节点id] 一致,可空)
|
||||
WS_NODE_ID string // WS 客户端:节点 id(与中间件 wsKeys 键一致)
|
||||
WS_NODE_KEY string // WS 客户端:注册 key(与中间件 wsKeys[节点id] 一致,可空)
|
||||
VERSION string
|
||||
COMMIT string
|
||||
BUILD_TIME string
|
||||
@@ -980,7 +980,7 @@ func configValue(CONFIG map[string]any, key string) string {
|
||||
// applyRemoteConfig 从远端配置 URL(REMOTE_CONFIG_URL,可由环境变量或 setting.json
|
||||
// 的 remote-config-url 提供)拉取配置并覆盖本地配置。
|
||||
// 优先级:远端配置 > 环境变量 > setting.json。
|
||||
// access_token 例外:保持原有优先级(环境变量 > setting.json),不随远端配置覆盖。
|
||||
// access-token 例外:保持原有优先级(环境变量 > setting.json),不随远端配置覆盖。
|
||||
func applyRemoteConfig() {
|
||||
url := REMOTE_CONFIG_URL
|
||||
if url == "" {
|
||||
@@ -1035,7 +1035,7 @@ func applyRemoteConfig() {
|
||||
if v := configValue(CONFIG, "node-key"); v != "" && !ignored("node-key") {
|
||||
WS_NODE_KEY = v
|
||||
}
|
||||
// access_token 不在此覆盖:保持原有优先级(环境变量 > setting.json)
|
||||
// access-token 不在此覆盖:保持原有优先级(环境变量 > setting.json)
|
||||
if CORS != "" {
|
||||
ACCEPT_DOMAINS = strings.Split(CORS, ",")
|
||||
}
|
||||
@@ -1092,7 +1092,7 @@ func readConfig() {
|
||||
ACCEPT_DOMAINS = strings.Split(CORS, ",")
|
||||
}
|
||||
if ACCESS_TOKEN == "" {
|
||||
ACCESS_TOKEN = viper.GetString("access_token")
|
||||
ACCESS_TOKEN = viper.GetString("access-token")
|
||||
}
|
||||
// REMOTE_CONFIG_URL 优先级:环境变量 > setting.json 的 remote-config-url
|
||||
REMOTE_CONFIG_URL = os.Getenv("REMOTE_CONFIG_URL")
|
||||
|
||||
+92
-98
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
// ==================== 全局变量 ====================
|
||||
|
||||
// apiInfo 对应 setting.json apiBaseUrls 等数组中的 { label, id, url } 配置项
|
||||
// apiInfo 对应 setting.json 节点池中的 { label, id, url } 配置项
|
||||
type apiInfo struct {
|
||||
Label string `json:"label"`
|
||||
ID string `json:"id"`
|
||||
@@ -49,7 +49,7 @@ func (w *wsFlag) UnmarshalJSON(b []byte) error {
|
||||
// UseWS 判断该节点是否走 WS 通道
|
||||
func (a *apiInfo) UseWS() bool { return a != nil && bool(a.WS) }
|
||||
|
||||
// stackConfig 对应 setting.json TCPing / SpeedTest
|
||||
// stackConfig 对应节点池中的 { DualStack, IPv4, IPv6 } 三栈
|
||||
type stackConfig struct {
|
||||
DualStack []apiInfo `json:"DualStack"`
|
||||
IPv4 []apiInfo `json:"IPv4"`
|
||||
@@ -57,20 +57,21 @@ type stackConfig struct {
|
||||
}
|
||||
|
||||
// middlewareConfig 仅用于解析 setting.json(JSON 键名统一用连接线,如 http-timeout-seconds)
|
||||
// 节点池结构:APIBaseURL 含 {IPv6,IPv4,DualStack} 三栈;IPLocationAPI 为纯数组(无栈区分)。
|
||||
// 原 apiBaseUrls/whois·ssl·detail、NSLookup/dns·dnssec、TCPing、SpeedTest 统一并入 APIBaseURL,
|
||||
// 原 IPLocationAPI/location·asn 并入 IPLocationAPI。
|
||||
type middlewareConfig struct {
|
||||
Port stringOrNumber `json:"port"`
|
||||
HTTPTimeoutSeconds int `json:"http-timeout-seconds"`
|
||||
RateLimit *int `json:"rate-limit"` // 单 IP 每分钟限流次数;缺省=默认120,0=不限流
|
||||
WSPort stringOrNumber `json:"ws-port"` // WS 服务端口:兼容 "8092" 与 8092;缺省=8092,"0"/0=关闭 WS 通道
|
||||
RemoteConfigURL string `json:"remote-config-url"`
|
||||
RemoteIngoreConfig []string `json:"remote-ingore-config"` // 不被远端覆盖的配置项列表
|
||||
Cors string `json:"cors"`
|
||||
APIBaseURLs []apiInfo `json:"apiBaseUrls"`
|
||||
IPLocationAPIs []apiInfo `json:"IPLocationAPIs"`
|
||||
TCPing stackConfig `json:"TCPing"`
|
||||
SpeedTest stackConfig `json:"SpeedTest"`
|
||||
NSLookup []apiInfo `json:"NSLookup"`
|
||||
APIKeys map[string]string `json:"apiKeys"`
|
||||
Port stringOrNumber `json:"port"`
|
||||
HTTPTimeoutSeconds int `json:"http-timeout-seconds"`
|
||||
RateLimit *int `json:"rate-limit"` // 单 IP 每分钟限流次数;缺省=默认120,0=不限流
|
||||
WSPort stringOrNumber `json:"ws-port"` // WS 端口:兼容 "8092" 与 8092;缺省=8092,"0"/0=关闭 WS 通道
|
||||
RemoteConfigURL string `json:"remote-config-url"`
|
||||
RemoteIngoreConfig []string `json:"remote-ingore-config"` // 不被远端覆盖的配置项列表
|
||||
Cors string `json:"cors"`
|
||||
APIBaseURL stackConfig `json:"APIBaseURL"` // whois/ssl/detail/dns/dnssec/tcping/speed 上游节点池
|
||||
IPLocationAPI []apiInfo `json:"IPLocationAPI"` // location/asn 上游节点池(纯数组,无栈)
|
||||
APIKeys map[string]string `json:"apiKeys"` // HTTP 转发鉴权:backendID → token(注入 Authorization: Bearer)
|
||||
WSKeys map[string]string `json:"wsKeys"` // WS 注册校验:nodeID → key(节点 register 的 key 须匹配)
|
||||
}
|
||||
|
||||
// stringOrNumber 兼容 JSON 中的字符串与数字(如 "8080" 或 8080)
|
||||
@@ -85,12 +86,10 @@ var (
|
||||
HTTP_TIMEOUT int // 上游请求超时(秒)
|
||||
CORS string // CORS 配置原始字符串(逗号分隔的允许域名,可空)
|
||||
ACCEPT_DOMAINS []string // 允许跨域的域名列表(由 CORS 拆分而来,空则允许所有)
|
||||
API_BASE_URLS []apiInfo // whois/ssl/detail 上游列表
|
||||
IP_LOCATION_APIS []apiInfo // location/asn 上游列表
|
||||
TCPING stackConfig // tcping 上游列表(DualStack/IPv4/IPv6)
|
||||
SPEED_TEST stackConfig // speed 上游列表(DualStack/IPv4/IPv6)
|
||||
NS_LOOKUP []apiInfo // dns/dnssec 上游列表
|
||||
API_KEYS map[string]string // backendID → token
|
||||
API_BASE_URLS stackConfig // whois/ssl/detail/dns/dnssec/tcping/speed 上游节点池(DualStack/IPv4/IPv6)
|
||||
IP_LOCATION_APIS []apiInfo // location/asn 上游节点池(纯数组,无栈)
|
||||
API_KEYS map[string]string // backendID → HTTP 转发 token(注入 Authorization: Bearer)
|
||||
WS_KEYS map[string]string // backendID → WS 注册校验 key
|
||||
CONFIG_SOURCE string // 配置文件路径
|
||||
RATE_LIMIT int // 单 IP 每分钟限流次数(0 表示不限流),默认 120
|
||||
WS_PORT int // WS 服务端口(0 = 关闭),缺省 8092,由 readConfig 统一解析
|
||||
@@ -174,6 +173,20 @@ func findNode(list []apiInfo, id string) *apiInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
// flattenStack 将节点池三栈展开为一个节点列表(DualStack + IPv4 + IPv6)
|
||||
func flattenStack(s stackConfig) []apiInfo {
|
||||
out := make([]apiInfo, 0, len(s.DualStack)+len(s.IPv4)+len(s.IPv6))
|
||||
out = append(out, s.DualStack...)
|
||||
out = append(out, s.IPv4...)
|
||||
out = append(out, s.IPv6...)
|
||||
return out
|
||||
}
|
||||
|
||||
// stackNonEmpty 判断节点池三栈是否至少有一项
|
||||
func stackNonEmpty(s stackConfig) bool {
|
||||
return len(s.DualStack) > 0 || len(s.IPv4) > 0 || len(s.IPv6) > 0
|
||||
}
|
||||
|
||||
func keysOf(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
@@ -242,6 +255,26 @@ func lookupAPIKey(backendID string) string {
|
||||
return apiKey
|
||||
}
|
||||
|
||||
// lookupWSKey 查找 nodeID 对应的 WS 注册校验 key。
|
||||
// 优先级: setting.json wsKeys > 环境变量 WSKEYS (JSON 字符串)
|
||||
// 与 lookupAPIKey 相互独立:apiKeys 用于 HTTP 转发鉴权,wsKeys 用于 WS 注册校验。
|
||||
func lookupWSKey(nodeID string) string {
|
||||
var wsKeys map[string]string
|
||||
if len(WS_KEYS) > 0 {
|
||||
wsKeys = WS_KEYS
|
||||
} else if raw := os.Getenv("WSKEYS"); raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &wsKeys); err != nil {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
wsKey := wsKeys[nodeID]
|
||||
|
||||
return wsKey
|
||||
}
|
||||
|
||||
// ==================== 业务接口 ====================
|
||||
|
||||
// middlewareHandler 对应前端 server/routes/middleware/[...slug].get.ts
|
||||
@@ -299,14 +332,13 @@ func middlewareHandler(c fiber.Ctx) error {
|
||||
|
||||
switch apiType {
|
||||
case "whois", "dns", "location", "ssl", "asn", "dnssec", "detail":
|
||||
// 节点池结构:location/asn 走 IPLocationAPI(纯数组,无栈),其余走 APIBaseURL(三栈平铺)
|
||||
var apiBaseUrls []apiInfo
|
||||
switch apiType {
|
||||
case "whois", "ssl", "detail":
|
||||
apiBaseUrls = API_BASE_URLS
|
||||
case "dns", "dnssec":
|
||||
apiBaseUrls = NS_LOOKUP
|
||||
case "location", "asn":
|
||||
apiBaseUrls = IP_LOCATION_APIS
|
||||
default:
|
||||
apiBaseUrls = flattenStack(API_BASE_URLS)
|
||||
}
|
||||
|
||||
node := findNode(apiBaseUrls, backendID)
|
||||
@@ -319,7 +351,8 @@ func middlewareHandler(c fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).SendString("WS probe failed: " + err.Error())
|
||||
}
|
||||
return c.Status(status).Send(body)
|
||||
// 节点上报的 body 为 JSON,显式指定类型(fiber 默认 text/plain)
|
||||
return c.Status(status).Type("json").Send(body)
|
||||
}
|
||||
apiBaseUrl := node.URL
|
||||
if !strings.HasSuffix(apiBaseUrl, "/") {
|
||||
@@ -330,20 +363,8 @@ func middlewareHandler(c fiber.Ctx) error {
|
||||
return forwardUpstream(c, apiBaseUrl, apiBaseUrl+"v1/"+apiType+"/"+raw, authHeaders, nil)
|
||||
|
||||
case "tcping", "udping", "speed":
|
||||
var apiTypeConfig stackConfig
|
||||
switch apiType {
|
||||
case "tcping":
|
||||
apiTypeConfig = TCPING
|
||||
case "speed":
|
||||
apiTypeConfig = SPEED_TEST
|
||||
default:
|
||||
return badRequest(c, "Invalid API type")
|
||||
}
|
||||
|
||||
apiBaseUrls := make([]apiInfo, 0, len(apiTypeConfig.DualStack)+len(apiTypeConfig.IPv4)+len(apiTypeConfig.IPv6))
|
||||
apiBaseUrls = append(apiBaseUrls, apiTypeConfig.DualStack...)
|
||||
apiBaseUrls = append(apiBaseUrls, apiTypeConfig.IPv4...)
|
||||
apiBaseUrls = append(apiBaseUrls, apiTypeConfig.IPv6...)
|
||||
// tcping/udping/speed 统一走 APIBaseURL 节点池
|
||||
apiBaseUrls := flattenStack(API_BASE_URLS)
|
||||
|
||||
// 转发 query 参数(过滤空值,等价于 TS 中 URLSearchParams 过滤 undefined)
|
||||
queryString := url.Values{}
|
||||
@@ -365,7 +386,8 @@ func middlewareHandler(c fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).SendString("WS probe failed: " + err.Error())
|
||||
}
|
||||
return c.Status(status).Send(body)
|
||||
// 节点上报的 body 为 JSON,显式指定类型(fiber 默认 text/plain)
|
||||
return c.Status(status).Type("json").Send(body)
|
||||
}
|
||||
apiBaseUrl := node.URL
|
||||
if !strings.HasSuffix(apiBaseUrl, "/") {
|
||||
@@ -456,22 +478,14 @@ func applyRemoteConfig(mw *middlewareConfig) error {
|
||||
if !ignored("cors") && remote.Cors != "" {
|
||||
mw.Cors = remote.Cors
|
||||
}
|
||||
if !ignored("apiBaseUrls") && len(remote.APIBaseURLs) > 0 {
|
||||
mw.APIBaseURLs = remote.APIBaseURLs
|
||||
// 节点池结构:APIBaseURL 与 IPLocationAPI(远端非空栈时覆盖本地)
|
||||
if !ignored("APIBaseURL") && stackNonEmpty(remote.APIBaseURL) {
|
||||
mw.APIBaseURL = remote.APIBaseURL
|
||||
}
|
||||
if !ignored("IPLocationAPIs") && len(remote.IPLocationAPIs) > 0 {
|
||||
mw.IPLocationAPIs = remote.IPLocationAPIs
|
||||
if !ignored("IPLocationAPI") && len(remote.IPLocationAPI) > 0 {
|
||||
mw.IPLocationAPI = remote.IPLocationAPI
|
||||
}
|
||||
if !ignored("TCPing") && (len(remote.TCPing.DualStack) > 0 || len(remote.TCPing.IPv4) > 0 || len(remote.TCPing.IPv6) > 0) {
|
||||
mw.TCPing = remote.TCPing
|
||||
}
|
||||
if !ignored("SpeedTest") && (len(remote.SpeedTest.DualStack) > 0 || len(remote.SpeedTest.IPv4) > 0 || len(remote.SpeedTest.IPv6) > 0) {
|
||||
mw.SpeedTest = remote.SpeedTest
|
||||
}
|
||||
if !ignored("NSLookup") && len(remote.NSLookup) > 0 {
|
||||
mw.NSLookup = remote.NSLookup
|
||||
}
|
||||
// apiKeys 强制忽略:密钥凭据不随远端配置覆盖(与后端 access_token 一致),只从本地 setting.json / env 读取
|
||||
// apiKeys / wsKeys 强制忽略:密钥凭据不随远端配置覆盖(与后端 access-token 一致),只从本地 setting.json / env 读取
|
||||
log.Printf("[middleware] remote config applied from %s", url)
|
||||
return nil
|
||||
}
|
||||
@@ -551,45 +565,21 @@ func readConfig() error {
|
||||
mw.RemoteIngoreConfig = viper.GetStringSlice("remote-ingore-config")
|
||||
}
|
||||
|
||||
// 数组/对象类配置:先 Getenv(JSON 字符串),再 viper.Get(经 JSON 中转反序列化到 typed 结构)
|
||||
if err := envJSON("API_BASE_URLS", &mw.APIBaseURLs); err != nil {
|
||||
// 节点池结构:APIBaseURL 与 IPLocationAPI(env JSON > setting.json)
|
||||
if err := envJSON("API_BASE_URLS", &mw.APIBaseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(mw.APIBaseURLs) == 0 {
|
||||
if err := viperValue("apiBaseUrls", &mw.APIBaseURLs); err != nil {
|
||||
return fmt.Errorf("parse apiBaseUrls: %w", err)
|
||||
if !stackNonEmpty(mw.APIBaseURL) && viper.Get("APIBaseURL") != nil {
|
||||
if err := viperValue("APIBaseURL", &mw.APIBaseURL); err != nil {
|
||||
return fmt.Errorf("parse APIBaseURL: %w", err)
|
||||
}
|
||||
}
|
||||
if err := envJSON("IP_LOCATION_APIS", &mw.IPLocationAPIs); err != nil {
|
||||
if err := envJSON("IP_LOCATION_APIS", &mw.IPLocationAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(mw.IPLocationAPIs) == 0 {
|
||||
if err := viperValue("IPLocationAPIs", &mw.IPLocationAPIs); err != nil {
|
||||
return fmt.Errorf("parse IPLocationAPIs: %w", err)
|
||||
}
|
||||
}
|
||||
if err := envJSON("TCPING", &mw.TCPing); err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.Get("TCPing") != nil && len(mw.TCPing.DualStack)+len(mw.TCPing.IPv4)+len(mw.TCPing.IPv6) == 0 {
|
||||
if err := viperValue("TCPing", &mw.TCPing); err != nil {
|
||||
return fmt.Errorf("parse TCPing: %w", err)
|
||||
}
|
||||
}
|
||||
if err := envJSON("SPEED_TEST", &mw.SpeedTest); err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.Get("SpeedTest") != nil && len(mw.SpeedTest.DualStack)+len(mw.SpeedTest.IPv4)+len(mw.SpeedTest.IPv6) == 0 {
|
||||
if err := viperValue("SpeedTest", &mw.SpeedTest); err != nil {
|
||||
return fmt.Errorf("parse SpeedTest: %w", err)
|
||||
}
|
||||
}
|
||||
if err := envJSON("NS_LOOKUP", &mw.NSLookup); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(mw.NSLookup) == 0 {
|
||||
if err := viperValue("NSLookup", &mw.NSLookup); err != nil {
|
||||
return fmt.Errorf("parse NSLookup: %w", err)
|
||||
if len(mw.IPLocationAPI) == 0 && viper.Get("IPLocationAPI") != nil {
|
||||
if err := viperValue("IPLocationAPI", &mw.IPLocationAPI); err != nil {
|
||||
return fmt.Errorf("parse IPLocationAPI: %w", err)
|
||||
}
|
||||
}
|
||||
if err := envJSON("APIKEYS", &mw.APIKeys); err != nil {
|
||||
@@ -600,6 +590,15 @@ func readConfig() error {
|
||||
return fmt.Errorf("parse apiKeys: %w", err)
|
||||
}
|
||||
}
|
||||
// wsKeys:WS 注册校验表(nodeID → key),优先级 env WSKEYS > setting.json wsKeys,与 apiKeys 相互独立
|
||||
if err := envJSON("WSKEYS", &mw.WSKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(mw.WSKeys) == 0 {
|
||||
if err := viperValue("wsKeys", &mw.WSKeys); err != nil {
|
||||
return fmt.Errorf("parse wsKeys: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 远端配置(最高优先级):REMOTE_CONFIG_URL 拉取后覆盖(优先级:远端 > 环境变量 > setting.json)
|
||||
if err := applyRemoteConfig(&mw); err != nil {
|
||||
@@ -607,11 +606,8 @@ func readConfig() error {
|
||||
}
|
||||
|
||||
// 校验配置存在(防止在错误的文件上静默空跑)
|
||||
hasEndpoints := len(mw.APIBaseURLs) > 0 || len(mw.NSLookup) > 0 || len(mw.IPLocationAPIs) > 0 ||
|
||||
len(mw.TCPing.DualStack) > 0 || len(mw.TCPing.IPv4) > 0 || len(mw.TCPing.IPv6) > 0 ||
|
||||
len(mw.SpeedTest.DualStack) > 0 || len(mw.SpeedTest.IPv4) > 0 || len(mw.SpeedTest.IPv6) > 0
|
||||
if !hasEndpoints {
|
||||
return fmt.Errorf("invalid config in %s: missing endpoint lists (expected flat middleware config)", path)
|
||||
if !stackNonEmpty(mw.APIBaseURL) && len(mw.IPLocationAPI) == 0 {
|
||||
return fmt.Errorf("invalid config in %s: missing endpoint pools (expected APIBaseURL / IPLocationAPI node pools)", path)
|
||||
}
|
||||
|
||||
// 将配置内容写入全局变量
|
||||
@@ -639,12 +635,10 @@ func readConfig() error {
|
||||
if CORS != "" {
|
||||
ACCEPT_DOMAINS = strings.Split(CORS, ",")
|
||||
}
|
||||
API_BASE_URLS = mw.APIBaseURLs
|
||||
IP_LOCATION_APIS = mw.IPLocationAPIs
|
||||
TCPING = mw.TCPing
|
||||
SPEED_TEST = mw.SpeedTest
|
||||
NS_LOOKUP = mw.NSLookup
|
||||
API_BASE_URLS = mw.APIBaseURL
|
||||
IP_LOCATION_APIS = mw.IPLocationAPI
|
||||
API_KEYS = mw.APIKeys
|
||||
WS_KEYS = mw.WSKeys
|
||||
CONFIG_SOURCE = viper.ConfigFileUsed()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,21 +6,7 @@
|
||||
"remote-config-url": "",
|
||||
"remote-ingore-config": [],
|
||||
"cors": "",
|
||||
"apiBaseUrls": [
|
||||
{
|
||||
"label": "中国 江苏 移动",
|
||||
"id": "cn-jiangsu",
|
||||
"url": "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
"IPLocationAPIs": [
|
||||
{
|
||||
"label": "中国 江苏 移动",
|
||||
"id": "cn-jiangsu",
|
||||
"url": "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
"TCPing": {
|
||||
"APIBaseURL": {
|
||||
"DualStack": [
|
||||
{
|
||||
"label": "中国 江苏 移动",
|
||||
@@ -31,23 +17,13 @@
|
||||
"IPv4": [],
|
||||
"IPv6": []
|
||||
},
|
||||
"SpeedTest": {
|
||||
"DualStack": [
|
||||
{
|
||||
"label": "中国 江苏 移动",
|
||||
"id": "cn-jiangsu",
|
||||
"url": "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
"IPv4": [],
|
||||
"IPv6": []
|
||||
},
|
||||
"NSLookup": [
|
||||
"IPLocationAPI": [
|
||||
{
|
||||
"label": "中国 江苏 移动",
|
||||
"id": "cn-jiangsu",
|
||||
"url": "https://cn-jiangsu.api-ipw.wsmdn.top/"
|
||||
}
|
||||
],
|
||||
"apiKeys": {}
|
||||
}
|
||||
"apiKeys": {},
|
||||
"wsKeys": {}
|
||||
}
|
||||
+3
-3
@@ -117,7 +117,7 @@ func (s *wsServer) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
case "register":
|
||||
var reg struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
Key string `json:"key"` // 注册凭证:与 setting.json apiKeys 配置比对
|
||||
Key string `json:"key"` // 注册凭证:与 setting.json wsKeys 配置比对
|
||||
}
|
||||
_ = json.Unmarshal(msg.Data, ®)
|
||||
if reg.NodeID == "" {
|
||||
@@ -128,9 +128,9 @@ func (s *wsServer) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
s.sendJSON(c, wsMessage{Type: "register_error", Data: mustRaw(wsCommand{Command: "already registered"})})
|
||||
continue
|
||||
}
|
||||
// 注册 key 校验:节点配置了 apiKeys[节点id] 就必须传对 key,否则 401 并断开;
|
||||
// 注册 key 校验:节点配置了 wsKeys[节点id] 就必须传对 key,否则 401 并断开;
|
||||
// 未配置 key 的节点(开放)无需传 key
|
||||
if expected := lookupAPIKey(reg.NodeID); expected != "" && reg.Key != expected {
|
||||
if expected := lookupWSKey(reg.NodeID); expected != "" && reg.Key != expected {
|
||||
log.Printf("[ws] AUTH FAILED for node %s: invalid key", reg.NodeID)
|
||||
s.sendJSON(c, wsMessage{Type: "register_error", Data: mustRaw(struct {
|
||||
Code int `json:"code"`
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
"dns-server": "",
|
||||
"dnssec-server": "",
|
||||
"block-private-ips": true,
|
||||
"ipdb": true,
|
||||
"ipdb": "false",
|
||||
"cors": "",
|
||||
"remote-config-url": "",
|
||||
"remote-ingore-config": [],
|
||||
"access_token": "",
|
||||
"access-token": "",
|
||||
"ws-url": "",
|
||||
"node-id": "test"
|
||||
"node-id": "",
|
||||
"node-key": ""
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// 配置(readConfig 解析,环境变量优先,其次 setting.json,远端配置可覆盖):
|
||||
//
|
||||
// WS_URL ws://<中间件>:8092/ws,ws://<备中间件>:8092/ws (逗号分隔多个中间件,主失败自动切换下一个;不配置 = 不启用)
|
||||
// NODE_ID 节点 id(与中间件 apiKeys 键 / 前端配置 id 一致)
|
||||
// NODE_KEY 注册 key(与中间件 apiKeys[节点id] 一致;中间件未配置该节点 key 时留空)
|
||||
// NODE_ID 节点 id(与中间件 wsKeys 键 / 前端配置 id 一致)
|
||||
// NODE_KEY 注册 key(与中间件 wsKeys[节点id] 一致;中间件未配置该节点 key 时留空)
|
||||
//
|
||||
// 与 HTTP handler 共用同一批 webtest 探针函数和缓存:收到 probe 后按 apiType 取数(带缓存),
|
||||
// 返回处理在本文件内完成,结果经 probe_result 上报。节点 HTTP 接口完全不变。
|
||||
|
||||
Reference in new issue
Block a user