Compare commits

...
5 Commits
Author SHA1 Message Date
nomdn 6512a46596 fix: WS 通道转发响应 Content-Type 缺失(text/plain → application/json)
## 问题

中间件经 WS 通道转发拨测结果时,成功路径用 fiber 的 `c.Send(body)`
直接返回节点上报的 JSON 字节,未指定 Content-Type——fiber 默认
`text/plain; charset=utf-8`,即使 body 是合法 JSON。

仅 WS 通道受影响:HTTP 直连通道(forwardUpstream)本就 `.Type("json")`。

## 修复

middleware-go/main.go 两处 WS 转发成功路径(detail/whois/ssl 等 322 行、
tcping/speed 368 行)加 `.Type("json")`:

    return c.Status(status).Type("json").Send(body)

失败路径(502 文本)保持不变。

## 冒烟验证

- 复现:/v1/test/tcping/1.1.1.1?count=1 → Content-Type: text/plain(body 为 JSON)
- 修复后:tcping(368 分支)与 whois(322 分支)均返回
  application/json; charset=utf-8
- go build / go vet 通过
2026-08-22 12:47:35 +08:00
nomdn e261666efc fix:补全SINGLE_STACK 2026-08-22 11:28:59 +08:00
nomdn 0d1a9e5bbd fix:修正一键安装脚本 2026-08-22 11:24:20 +08:00
nomdn 279f663694 fix:测试配置误上传 2026-08-21 22:40:52 +08:00
nomdn 3af1076492 docs:清理README的过时内容 2026-08-21 22:25:37 +08:00
6 changed files with 46 additions and 267 deletions

No files matched your search

+30 -260
View File
@@ -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 -o- https://cdn.jsdelivr.net/gh/nomdn/ipw-cn@main/install.sh | bash
```
完整部署(后端节点 / 独立中间件 / 前端)与配置说明见 [文档](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,自托管)
├── middleware-go/ # 独立转发中间件(Fiber v3,多节点候选)
├── frontend-ssr/ # Nuxt 4 SSR 前端
├── edgeone/ # 腾讯 EdgeOne 边缘函数后端
├── webtest/ # 网络测试工具库
├── ipdb/ # IP 数据库模块
├── ssrf/ # SSRF 防护
├── 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 一键部署前端:
[![使用 EdgeOne Makers 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](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 |
## 许可证
+1 -1
View File
@@ -80,7 +80,7 @@ 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`
+1 -1
View File
@@ -45,7 +45,7 @@ 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`
+9 -2
View File
@@ -74,6 +74,8 @@ INSTALL_DIR=${INSTALL_DIR:-/opt/lemon-ipw}
read -r -p "监听端口 [8080]: " PORTS
PORTS=${PORTS:-8080}
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
@@ -100,9 +102,9 @@ 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)
@@ -139,6 +141,7 @@ echo "========================================"
echo "安装目录: $INSTALL_DIR"
echo "服务名: lemon-ipw"
echo "监听端口: $PORTS"
echo "单栈模式: ${SINGLE_STACK:-双栈}"
echo "access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
echo "DNS: $DNS_SERVER"
echo "ipdb: $IPDB"
@@ -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\"
@@ -254,8 +259,10 @@ if [ -n "$WS_USED_DEFAULT" ]; then
echo " 注册 key: $NODE_KEY"
echo " WS 地址: $WS_URL"
echo " 监听端口: $PORTS"
echo " 单栈模式: ${SINGLE_STACK:-双栈}"
echo " access_token: ${ACCESS_TOKEN:+已设置 (隐藏)}"
echo " CORS: ${CORS:-不限}"
echo " 地区-运营商 请自行填写"
echo "========================================"
fi
echo "========================================"
+4 -2
View File
@@ -319,7 +319,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, "/") {
@@ -365,7 +366,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, "/") {
+1 -1
View File
@@ -11,5 +11,5 @@
"remote-ingore-config": [],
"access_token": "",
"ws-url": "",
"node-id": "test"
"node-id": ""
}