From cb90ea3c7a9ae1e326bcded708bfda340a1d5b52 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:17:25 +0800 Subject: [PATCH 1/7] security: add hardened build documentation and dependencies Documents the reviewed build, pins updated Go dependencies, and records the offline-safe operating model. --- README.md | 346 +++++++++++++++++++++--------------------- SECURITY-HARDENING.md | 50 ++++++ go.mod | 6 +- go.sum | 8 +- 4 files changed, 231 insertions(+), 179 deletions(-) create mode 100644 SECURITY-HARDENING.md diff --git a/README.md b/README.md index bfa2df6..c1e2674 100644 --- a/README.md +++ b/README.md @@ -2,175 +2,177 @@ 一个专业的Reality协议目标网站检测工具,用于评估网站是否适合作为Reality协议的目标域名。 -[V2RaySSR综合网](https://v2rayssr.com) - -## ✨ 功能特性 - -* **被墙检测** - 基于GFWList检测网站是否被墙 -* **地理位置检测** - 检测IP地理位置,国内网站直接终止 -* **TLS协议检测** - 检测TLS 1.3和X25519支持 -* **证书检测** - 检测证书有效性和SNI匹配 -* **CDN检测** - 智能检测CDN使用情况 -* **热门网站检测** - 检测是否为热门网站 -* **重定向检测** - 检测域名重定向 -* **批量检测** - 支持多域名并发检测,可与RealiTLScanner配合使用 -* **智能报告** - 生成详细的检测分析报告 - -## 📊 检测结果说明 - -### 检测结果示例 - -以下是一个批量检测的实际输出示例: - -```bash -./reality-checker csv file.csv -``` - -**实际运行效果:** - -![RealityChecker检测结果示例](RealityChecker.png) - -**只有满足Reality目标域名硬性条件的(TLS1.3、X25519、H2、SNI匹配、证书有效),才会在列表中显示** - -### CDN检测等级说明 - -| 等级 | 含义 | 影响 | -|------|------|------| -| **高** | 明确使用CDN | 可以使用,但不推荐 | -| **中** | 疑似使用CDN | 可以使用,但不推荐 | -| **低** | 轻微CDN特征 | 可以使用,但不推荐 | -| **-** | 未检测到CDN | 可以使用,强烈推荐 | - -### 热门网站说明 - -热门网站(如 apple.com、tesla.com、microsoft.com 等)由于使用人群多,容易被识别和封禁,因此不太推荐作为 Reality 协议的目标域名。 - -**结果分析:** -- 所有域名都支持TLS 1.3、X25519、HTTP/2和SNI匹配 -- 证书有效期充足 -- 部分使用了CDN且为热门网站 -- 部分虽然技术指标优秀,但由于CDN和热门网站特性,推荐度有所降低 - - -## 🚀 快速开始 - -### 系统要求 - -* **Linux VPS** - 主要针对VPS环境使用 -* **Windows、macOS** - 等自行编译 -* **Go 1.21+** - 用于本地编译(Windows、macOS可选) - -### 安装步骤 - -**方法1:直接下载(推荐)** - -从 [Releases](https://github.com/V2RaySSR/RealityChecker/releases) 页面下载对应架构的zip文件: - -```bash -# Linux x86_64 -wget https://github.com/V2RaySSR/RealityChecker/releases/latest/download/reality-checker-linux-amd64.zip - -# Linux ARM64 -wget https://github.com/V2RaySSR/RealityChecker/releases/latest/download/reality-checker-linux-arm64.zip -``` - -解压后直接使用: -```bash -# 解压 -unzip reality-checker-linux-amd64.zip - -# 添加执行权限 -chmod +x reality-checker - -# 开始检测 -./reality-checker check <域名> -``` - -**方法2:本地编译** - -```bash -# 克隆项目 -git clone https://github.com/V2RaySSR/RealityChecker.git -cd RealityChecker - -# 编译程序 -go build -o reality-checker - -# 开始检测 -./reality-checker check <域名> -``` - -## 🔍 使用示例 - -### 单域名检测 - -```bash -# 基础检测 -./reality-checker check apple.com -``` - -### 批量检测 - -```bash -# 批量检测多个域名(空格分隔) -./reality-checker batch apple.com tesla.com microsoft.com -``` - -### CSV文件检测 - -```bash -# 从CSV文件批量检测域名 -./reality-checker csv file.csv -``` - -### 推荐工作流程 - -对于大量域名检测,建议配合使用 [RealiTLScanner](https://github.com/XTLS/RealiTLScanner) 工具( [教程观看](https://www.youtube.com/watch?v=zE8CFQ6muUI) ): - -**1. 使用RealiTLScanner扫描VPS IP:** -```bash -./RealiTLScanner -addr -port 443 -thread 100 -timeout 5 -out file.csv -``` - -**2. 使用本工具检测生成的CSV文件:** -```bash -./reality-checker csv file.csv -``` - -**重要提示:** -- RealiTLScanner 尽量在本地运行,不要在远端 -- 多次运行RealiTLScanner时,请更改输出文件名,如:`file1.csv`、`file2.csv`、`file3.csv` 等 -- 如果使用相同的文件名,可能会导致文件导出失败或覆盖之前的扫描结果 - -### 查看帮助 - -```bash -# 显示使用说明 -./reality-checker - -# 查看版本信息 -./reality-checker version -``` - -## 🔧常见问题 - -**1. 数据文件下载失败** - -如果自动下载失败,请手动下载以下文件到 `data/` 目录: - -- [Country.mmdb](https://github.com/Loyalsoldier/geoip/releases/latest/download/Country.mmdb) -- [gfwlist.conf](https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt) -- [cdn_keywords.txt](https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/cdn_keywords.txt) -- [hot_websites.txt](https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/hot_websites.txt) - - -## 🏆 致谢 - -感谢以下开源项目: - -* [Loyalsoldier/geoip](https://github.com/Loyalsoldier/geoip) - GeoIP数据库 -* [Loyalsoldier/clash-rules](https://github.com/Loyalsoldier/clash-rules) - GFW规则 - ---- - -**注意**: 本工具仅用于技术研究和学习目的,请遵守当地法律法规,合理使用网络资源。 +> 安全加固分支说明:`codex/hardened-build-20260828` 默认使用离线数据、启动时进行 SHA-256 完整性校验、禁止访问私网/本机/保留地址,并取消版本查询和推广请求。详细变更、构建与验证结果见 [SECURITY-HARDENING.md](SECURITY-HARDENING.md)。 + +[V2RaySSR综合网](https://v2rayssr.com) + +## ✨ 功能特性 + +* **被墙检测** - 基于GFWList检测网站是否被墙 +* **地理位置检测** - 检测IP地理位置,国内网站直接终止 +* **TLS协议检测** - 检测TLS 1.3和X25519支持 +* **证书检测** - 检测证书有效性和SNI匹配 +* **CDN检测** - 智能检测CDN使用情况 +* **热门网站检测** - 检测是否为热门网站 +* **重定向检测** - 检测域名重定向 +* **批量检测** - 支持多域名并发检测,可与RealiTLScanner配合使用 +* **智能报告** - 生成详细的检测分析报告 + +## 📊 检测结果说明 + +### 检测结果示例 + +以下是一个批量检测的实际输出示例: + +```bash +./reality-checker csv file.csv +``` + +**实际运行效果:** + +![RealityChecker检测结果示例](RealityChecker.png) + +**只有满足Reality目标域名硬性条件的(TLS1.3、X25519、H2、SNI匹配、证书有效),才会在列表中显示** + +### CDN检测等级说明 + +| 等级 | 含义 | 影响 | +|------|------|------| +| **高** | 明确使用CDN | 可以使用,但不推荐 | +| **中** | 疑似使用CDN | 可以使用,但不推荐 | +| **低** | 轻微CDN特征 | 可以使用,但不推荐 | +| **-** | 未检测到CDN | 可以使用,强烈推荐 | + +### 热门网站说明 + +热门网站(如 apple.com、tesla.com、microsoft.com 等)由于使用人群多,容易被识别和封禁,因此不太推荐作为 Reality 协议的目标域名。 + +**结果分析:** +- 所有域名都支持TLS 1.3、X25519、HTTP/2和SNI匹配 +- 证书有效期充足 +- 部分使用了CDN且为热门网站 +- 部分虽然技术指标优秀,但由于CDN和热门网站特性,推荐度有所降低 + + +## 🚀 快速开始 + +### 系统要求 + +* **Linux VPS** - 主要针对VPS环境使用 +* **Windows、macOS** - 等自行编译 +* **Go 1.21+** - 用于本地编译(Windows、macOS可选) + +### 安装步骤 + +**方法1:直接下载(推荐)** + +从 [Releases](https://github.com/V2RaySSR/RealityChecker/releases) 页面下载对应架构的zip文件: + +```bash +# Linux x86_64 +wget https://github.com/V2RaySSR/RealityChecker/releases/latest/download/reality-checker-linux-amd64.zip + +# Linux ARM64 +wget https://github.com/V2RaySSR/RealityChecker/releases/latest/download/reality-checker-linux-arm64.zip +``` + +解压后直接使用: +```bash +# 解压 +unzip reality-checker-linux-amd64.zip + +# 添加执行权限 +chmod +x reality-checker + +# 开始检测 +./reality-checker check <域名> +``` + +**方法2:本地编译** + +```bash +# 克隆项目 +git clone https://github.com/V2RaySSR/RealityChecker.git +cd RealityChecker + +# 编译程序 +go build -o reality-checker + +# 开始检测 +./reality-checker check <域名> +``` + +## 🔍 使用示例 + +### 单域名检测 + +```bash +# 基础检测 +./reality-checker check apple.com +``` + +### 批量检测 + +```bash +# 批量检测多个域名(空格分隔) +./reality-checker batch apple.com tesla.com microsoft.com +``` + +### CSV文件检测 + +```bash +# 从CSV文件批量检测域名 +./reality-checker csv file.csv +``` + +### 推荐工作流程 + +对于大量域名检测,建议配合使用 [RealiTLScanner](https://github.com/XTLS/RealiTLScanner) 工具( [教程观看](https://www.youtube.com/watch?v=zE8CFQ6muUI) ): + +**1. 使用RealiTLScanner扫描VPS IP:** +```bash +./RealiTLScanner -addr -port 443 -thread 100 -timeout 5 -out file.csv +``` + +**2. 使用本工具检测生成的CSV文件:** +```bash +./reality-checker csv file.csv +``` + +**重要提示:** +- RealiTLScanner 尽量在本地运行,不要在远端 +- 多次运行RealiTLScanner时,请更改输出文件名,如:`file1.csv`、`file2.csv`、`file3.csv` 等 +- 如果使用相同的文件名,可能会导致文件导出失败或覆盖之前的扫描结果 + +### 查看帮助 + +```bash +# 显示使用说明 +./reality-checker + +# 查看版本信息 +./reality-checker version +``` + +## 🔧常见问题 + +**1. 数据文件校验失败** + +安全加固版不会在运行时自动下载或替换文件。请从同一个 Release 重新解压完整的 `data/` 目录,不要混用其他版本的数据文件。程序会对以下文件执行 SHA-256 校验: + +- [Country.mmdb](https://github.com/Loyalsoldier/geoip/releases/latest/download/Country.mmdb) +- [gfwlist.conf](https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt) +- [cdn_keywords.txt](https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/cdn_keywords.txt) +- [hot_websites.txt](https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/hot_websites.txt) + + +## 🏆 致谢 + +感谢以下开源项目: + +* [Loyalsoldier/geoip](https://github.com/Loyalsoldier/geoip) - GeoIP数据库 +* [Loyalsoldier/clash-rules](https://github.com/Loyalsoldier/clash-rules) - GFW规则 + +--- + +**注意**: 本工具仅用于技术研究和学习目的,请遵守当地法律法规,合理使用网络资源。 diff --git a/SECURITY-HARDENING.md b/SECURITY-HARDENING.md new file mode 100644 index 0000000..b10bb08 --- /dev/null +++ b/SECURITY-HARDENING.md @@ -0,0 +1,50 @@ +# RealityChecker 安全加固说明 + +## 版本与来源 + +- 加固版本:`hardened-20260828` +- 基础提交:`8a152d4014b3781fa1e004f51e37dabcb1ffcbcf` +- fork 与 `V2RaySSR/RealityChecker` 上游 `main` 在审查时为同一提交,差异为 `0 ahead / 0 behind`。 +- 构建工具链:Go 1.27.0,官方 Windows AMD64 归档 SHA-256 为 `f0c0a0d33ba94f4d2c5dbc887334ce678b21813504ddb3aafcb06e60a5a667c4`。 + +## 加固内容 + +1. 删除运行时自动下载以及每三天静默更新规则数据的逻辑。 +2. 固定四个离线数据文件的 SHA-256;程序启动时先校验,任何文件不匹配都会拒绝运行。 +3. 优先从可执行文件旁边的 `data` 目录加载文件,避免依赖启动目录。 +4. 取消启动时的 GitHub Release 查询和推广广告,不再产生这些额外联网请求。 +5. 所有主动网络连接只允许公网地址的 TCP 80/443;拒绝回环、RFC1918 私网、链路本地、共享地址空间、云元数据和常见保留网段。 +6. 网络层先解析并检查所有 DNS 结果,然后直接连接已检查的 IP,降低 DNS rebinding 风险。 +7. 更新 `golang.org/x/sys` 至 v0.47.0、`golang.org/x/text` 至 v0.41.0。 +8. 增加离线数据完整性和非公网地址拦截单元测试。 + +## 验证结果 + +- `go test ./...`:通过。 +- `govulncheck ./...`:`No vulnerabilities found`。 +- `govulncheck -mode=binary reality-checker.exe`:`No vulnerabilities found`。 +- Windows AMD64、Linux AMD64、Linux ARM64 均连续构建两次,二进制逐字节一致。 +- 最终 Windows ZIP 重新解压后,`version` 命令正常,数据完整性校验通过。 +- 对 `127.0.0.1` 的测试没有建立连接,由安全网络层拒绝。 + +## 构建参数 + +安全构建使用: + +```text +CGO_ENABLED=0 +go build -trimpath -buildvcs=false -ldflags "-s -w -buildid= ..." +``` + +Release 必须同时包含可执行文件、完整 `data/` 目录、使用说明和 SHA-256 清单。 + +## 使用限制 + +- 本工具会主动连接和探测目标网站。只能用于你有权检测的目标,并应遵守当地法律、服务条款和网络使用政策。 +- Windows 构建没有商业 Authenticode 证书签名,SmartScreen 可能显示未知发布者;请使用 Release 附带的 SHA-256 清单核对文件。 +- 安全版数据是固定快照,不会自动更新。若需要更新数据,应在源码中同步更新固定哈希、重新审查并重新构建。 +- 上游仓库未提供明确的软件许可证。本分支及构建产物不应在未确认许可的情况下再次分发或商用。 + +## 报告问题 + +安全问题请不要在公开 Issue 中附带可直接利用的细节。建议先通过仓库所有者公开提供的私密联系方式进行报告;确认修复窗口后再公开披露。 diff --git a/go.mod b/go.mod index 293cbab..bdea158 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module RealityChecker -go 1.21 +go 1.25.0 require ( github.com/jedib0t/go-pretty/v6 v6.6.8 @@ -12,6 +12,6 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index abb40f0..3dc8ffd 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,10 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 9a678ee1ec2385420e90c974e18d2c9fca187db8 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:18:09 +0800 Subject: [PATCH 2/7] ci: add read-only hardened build verification Runs tests, govulncheck, and multi-platform builds with contents: read permission only. --- .github/workflows/build.yml | 176 +++++++++++------------------------- 1 file changed, 53 insertions(+), 123 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d0c68e..a6c4f76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,133 +1,63 @@ -name: Build and Release +name: Hardened build verification on: - push: - tags: - - 'v*' workflow_dispatch: + pull_request: permissions: - contents: write - packages: write + contents: read jobs: build: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' - - - name: Get version from tag - id: version - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/} - else - VERSION="dev-$(date +%Y%m%d-%H%M%S)" - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Building version: $VERSION" - - - name: Get build time - id: build_time - run: | - BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - echo "time=$BUILD_TIME" >> $GITHUB_OUTPUT - - - name: Build for multiple platforms - run: | - # 创建输出目录 - mkdir -p dist - - # 设置版本信息 - VERSION="${{ steps.version.outputs.version }}" - COMMIT=$(git rev-parse --short HEAD) - BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - - # 构建标志 - 添加静态编译选项解决GLIBC兼容性问题 - LDFLAGS="-X RealityChecker/internal/version.Version=$VERSION -X RealityChecker/internal/version.Commit=$COMMIT -X RealityChecker/internal/version.BuildTime=$BUILD_TIME -s -w" - - # Linux AMD64 - 静态编译 - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -tags netgo -installsuffix netgo -o reality-checker . - chmod +x reality-checker - zip -j dist/reality-checker-linux-amd64.zip reality-checker - rm reality-checker - - # Linux ARM64 - 静态编译 - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -tags netgo -installsuffix netgo -o reality-checker . - chmod +x reality-checker - zip -j dist/reality-checker-linux-arm64.zip reality-checker - rm reality-checker - - # 显示构建结果 - ls -la dist/ - - - name: Create Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 - with: - files: | - dist/* - body: | - ## Reality协议目标网站检测工具 ${{ steps.version.outputs.version }} - - ### 使用方法 - - 详细使用方法请参考 [README.md](https://github.com/V2RaySSR/RealityChecker/blob/main/README.md) - - **下载说明:** - - `reality-checker-linux-amd64.zip` - Linux x86_64 - - `reality-checker-linux-arm64.zip` - Linux ARM64 - - **基本命令:** - ```bash - # 单域名检测 - ./reality-checker check example.com - - # 批量检测 - ./reality-checker batch domain1 domain2 domain3 - - # CSV文件检测 - ./reality-checker csv domains.csv - ``` - - **推荐工作流程:** - 1. 使用 [RealiTLScanner](https://github.com/XTLS/RealiTLScanner) 扫描VPS IP: - ```bash - ./RealiTLScanner -addr -port 443 -thread 100 -timeout 5 -out file.csv - ``` - 2. 使用本工具检测生成的CSV文件: - ```bash - ./reality-checker csv file.csv - ``` - - **重要提示:** - - RealiTLScanner 尽量在本地运行,不要在远端 - - 多次运行RealiTLScanner时,请更改输出文件名,如:`file1.csv`、`file2.csv`、`file3.csv` 等 - - 如果使用相同的文件名,可能会导致文件导出失败或覆盖之前的扫描结果 - - ### 版本信息 - - **版本**: ${{ steps.version.outputs.version }} - - **提交**: ${{ github.sha }} - - **构建时间**: ${{ steps.build_time.outputs.time }} - - --- - - **注意**: 本工具仅用于技术研究和学习目的,请遵守当地法律法规。 - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload artifacts (for non-tag builds) - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - uses: actions/upload-artifact@v4 - with: - name: reality-checker-builds - path: dist/ + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.27.0" + check-latest: false + + - name: Test + run: go test ./... + + - name: Vulnerability scan + run: | + go install golang.org/x/vuln/cmd/govulncheck@v1.7.0 + govulncheck ./... + + - name: Build packages + shell: bash + run: | + set -euo pipefail + version="hardened-${GITHUB_SHA::12}" + commit="${GITHUB_SHA}+hardened" + build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + ldflags="-s -w -buildid= -X RealityChecker/internal/version.Version=${version} -X RealityChecker/internal/version.Commit=${commit} -X RealityChecker/internal/version.BuildTime=${build_time}" + + mkdir -p dist staging + build_package() { + local goos="$1" + local goarch="$2" + local suffix="$3" + local dir="staging/${goos}-${goarch}" + mkdir -p "${dir}/data" + cp data/cdn_keywords.txt data/Country.mmdb data/gfwlist.conf data/hot_websites.txt "${dir}/data/" + cp SECURITY-HARDENING.md "${dir}/" + CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ + go build -trimpath -buildvcs=false -tags netgo -ldflags "${ldflags}" -o "${dir}/reality-checker${suffix}" . + (cd "${dir}" && zip -qr "../../dist/RealityChecker-hardened-${goos}-${goarch}.zip" .) + } + + build_package windows amd64 .exe + build_package linux amd64 "" + build_package linux arm64 "" + (cd dist && sha256sum *.zip > SHA256SUMS.txt) + + - name: Upload verification artifacts + uses: actions/upload-artifact@v4 + with: + name: RealityChecker-hardened + path: dist/* From 320c7c3be24d0c14205b0ba8b9cda5b36acee61d Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:18:43 +0800 Subject: [PATCH 3/7] security: require offline verified data files Removes runtime downloads and verifies bundled data with pinned SHA-256 hashes. --- internal/data/downloader.go | 242 +++++++------------------------ internal/data/downloader_test.go | 17 +++ 2 files changed, 73 insertions(+), 186 deletions(-) create mode 100644 internal/data/downloader_test.go diff --git a/internal/data/downloader.go b/internal/data/downloader.go index b5a0a39..4164511 100644 --- a/internal/data/downloader.go +++ b/internal/data/downloader.go @@ -1,216 +1,86 @@ package data import ( + "crypto/sha256" "fmt" "io" - "net/http" "os" + "path/filepath" "time" ) -// DataFile 数据文件配置 -type DataFile struct { - Name string - URL string - LocalPath string -} +// Downloader is retained for API compatibility. The hardened build never +// downloads or silently replaces data at runtime. +type Downloader struct{} -// Downloader 数据文件下载器 -type Downloader struct { - timeout time.Duration - retries int - retryDelay time.Duration -} +func NewDownloader() *Downloader { return &Downloader{} } -// NewDownloader 创建下载器 -func NewDownloader() *Downloader { - return &Downloader{ - timeout: 30 * time.Second, - retries: 3, - retryDelay: 2 * time.Second, - } +var expectedFiles = map[string]string{ + "cdn_keywords.txt": "3526689af9ba522084b6bb39f55a525cead6a88555898bda2ae4f2e8d0364626", + "Country.mmdb": "577a545e33aa6375d844e28c7becc6f57f40ed435b25cf3687616846ae4f7644", + "gfwlist.conf": "de612f34d66f023b7a6c03eb72aa1dcaf1feb147deb8e74f0b24cb8ed7d9f06f", + "hot_websites.txt": "92e773f5e55e4037d924cabc6c8c45cefd042429cafd8468a764d54c3f9eee4a", } -// printTimestampedMessage 打印带时间戳的消息 func printTimestampedMessage(format string, args ...interface{}) { timestamp := time.Now().Format("15:04:05") - message := fmt.Sprintf(format, args...) - fmt.Printf("[%s] %s\n", timestamp, message) -} - -// EnsureDataFiles 确保所有数据文件存在且最新 -func (d *Downloader) EnsureDataFiles() error { - printTimestampedMessage("检查数据文件...") - - // 定义需要下载的文件 - files := []DataFile{ - { - Name: "cdn_keywords.txt", - URL: "https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/cdn_keywords.txt", - LocalPath: "data/cdn_keywords.txt", - }, - { - Name: "hot_websites.txt", - URL: "https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/hot_websites.txt", - LocalPath: "data/hot_websites.txt", - }, - { - Name: "gfwlist.conf", - URL: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt", - LocalPath: "data/gfwlist.conf", - }, - { - Name: "Country.mmdb", - URL: "https://github.com/Loyalsoldier/geoip/releases/latest/download/Country.mmdb", - LocalPath: "data/Country.mmdb", - }, - } - - // 确保data目录存在 - if err := os.MkdirAll("data", 0755); err != nil { - return fmt.Errorf("创建data目录失败: %v", err) - } - - // 检查并下载每个文件 - for _, file := range files { - if err := d.ensureFile(file); err != nil { - return err - } - } - - printTimestampedMessage("数据文件检查完成。") - return nil + fmt.Printf("[%s] %s\n", timestamp, fmt.Sprintf(format, args...)) } -// ensureFile 确保单个文件存在且最新 -func (d *Downloader) ensureFile(file DataFile) error { - // 检查文件是否存在 - exists, err := d.fileExists(file.LocalPath) - if err != nil { - return fmt.Errorf("检查文件 %s 失败: %v", file.Name, err) +// ResolvePath locates a bundled, read-only data file. An explicit directory +// can be supplied for advanced use, but its contents are still hash checked. +func ResolvePath(name string) (string, error) { + if _, ok := expectedFiles[name]; !ok { + return "", fmt.Errorf("未知数据文件: %s", name) } - // 如果文件不存在,直接下载 - if !exists { - printTimestampedMessage("下载 %s...", file.Name) - return d.downloadWithRetry(file) + var candidates []string + if dir := os.Getenv("REALITYCHECK_DATA_DIR"); dir != "" { + candidates = append(candidates, filepath.Join(dir, name)) } - - // 检查文件是否需要更新(3天) - needsUpdate, err := d.needsUpdate(file.LocalPath) - if err != nil { - return fmt.Errorf("检查文件 %s 更新时间失败: %v", file.Name, err) + if executable, err := os.Executable(); err == nil { + candidates = append(candidates, filepath.Join(filepath.Dir(executable), "data", name)) } + candidates = append(candidates, filepath.Join("data", name)) - // 如果需要更新,下载新文件 - if needsUpdate { - printTimestampedMessage("更新 %s...", file.Name) - return d.downloadWithRetry(file) - } - - return nil -} - -// fileExists 检查文件是否存在 -func (d *Downloader) fileExists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} - -// needsUpdate 检查文件是否需要更新(超过3天) -func (d *Downloader) needsUpdate(path string) (bool, error) { - info, err := os.Stat(path) - if err != nil { - return false, err + for _, candidate := range candidates { + if info, err := os.Lstat(candidate); err == nil && info.Mode().IsRegular() { + absolute, err := filepath.Abs(candidate) + if err != nil { + return "", err + } + return absolute, nil + } } - - // 检查文件修改时间是否超过3天 - threeDaysAgo := time.Now().Add(-3 * 24 * time.Hour) - return info.ModTime().Before(threeDaysAgo), nil + return "", fmt.Errorf("缺少数据文件 %s;请保留程序旁边的 data 目录", name) } -// downloadWithRetry 带重试的下载 -func (d *Downloader) downloadWithRetry(file DataFile) error { - for i := 0; i < d.retries; i++ { - if i > 0 { - fmt.Printf("重试中... (%d/%d)\n", i, d.retries) - time.Sleep(d.retryDelay) +// EnsureDataFiles verifies every bundled file before any scan begins. +func (d *Downloader) EnsureDataFiles() error { + printTimestampedMessage("校验离线数据文件...") + for name, expected := range expectedFiles { + path, err := ResolvePath(name) + if err != nil { + return err } - - err := d.downloadFile(file) - if err == nil { - return nil // 成功 + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("打开数据文件 %s 失败: %w", name, err) + } + hash := sha256.New() + _, copyErr := io.Copy(hash, file) + closeErr := file.Close() + if copyErr != nil { + return fmt.Errorf("校验数据文件 %s 失败: %w", name, copyErr) + } + if closeErr != nil { + return fmt.Errorf("关闭数据文件 %s 失败: %w", name, closeErr) + } + actual := fmt.Sprintf("%x", hash.Sum(nil)) + if actual != expected { + return fmt.Errorf("数据文件 %s 完整性校验失败(应为 %s,实际为 %s)", name, expected, actual) } - - fmt.Printf("错误:下载 %s 失败 - %s %v\n", file.Name, file.URL, err) - } - - // 所有重试都失败了,显示手动下载说明 - d.showManualDownloadInstructions() - return fmt.Errorf("下载失败,已重试 %d 次", d.retries) -} - -// downloadFile 下载单个文件 -func (d *Downloader) downloadFile(file DataFile) error { - // 创建HTTP客户端 - client := &http.Client{ - Timeout: d.timeout, - } - - // 发送请求 - resp, err := client.Get(file.URL) - if err != nil { - return err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("HTTP %d", resp.StatusCode) - } - - // 创建临时文件 - tmpFile := file.LocalPath + ".tmp" - out, err := os.Create(tmpFile) - if err != nil { - return err - } - defer out.Close() - - // 复制数据 - _, err = io.Copy(out, resp.Body) - if err != nil { - os.Remove(tmpFile) // 清理临时文件 - return err - } - - // 关闭文件 - out.Close() - - // 原子性替换原文件 - if err := os.Rename(tmpFile, file.LocalPath); err != nil { - os.Remove(tmpFile) // 清理临时文件 - return err } - + printTimestampedMessage("离线数据完整性校验通过。") return nil } - -// showManualDownloadInstructions 显示手动下载说明 -func (d *Downloader) showManualDownloadInstructions() { - fmt.Println("程序终止:缺少必要的数据文件") - fmt.Println() - fmt.Println("请手动下载以下文件到 data/ 目录:") - fmt.Println("1. cdn_keywords.txt: https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/cdn_keywords.txt") - fmt.Println("2. hot_websites.txt: https://raw.githubusercontent.com/V2RaySSR/RealityChecker/main/data/hot_websites.txt") - fmt.Println("3. gfwlist.conf: https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt") - fmt.Println("4. Country.mmdb: https://github.com/Loyalsoldier/geoip/releases/latest/download/Country.mmdb") - fmt.Println() - fmt.Println("下载完成后重新运行程序即可。") -} diff --git a/internal/data/downloader_test.go b/internal/data/downloader_test.go new file mode 100644 index 0000000..878492a --- /dev/null +++ b/internal/data/downloader_test.go @@ -0,0 +1,17 @@ +package data + +import ( + "path/filepath" + "testing" +) + +func TestBundledDataIntegrity(t *testing.T) { + dataDir, err := filepath.Abs(filepath.Join("..", "..", "data")) + if err != nil { + t.Fatal(err) + } + t.Setenv("REALITYCHECK_DATA_DIR", dataDir) + if err := NewDownloader().EnsureDataFiles(); err != nil { + t.Fatalf("bundled data verification failed: %v", err) + } +} From 22dc9a14dc9ea1f7c5a5093bdb2444c887cd81f6 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:18:55 +0800 Subject: [PATCH 4/7] security: route detector traffic through safe dialer Restricts detector traffic to validated public targets and approved ports. --- internal/detectors/blocked.go | 7 +++++- internal/detectors/cdn.go | 20 ++++++++++++---- internal/detectors/comprehensive_tls.go | 13 ++++++---- internal/detectors/hot_website.go | 7 +++++- internal/detectors/ip_resolver.go | 32 +++++++------------------ internal/detectors/location.go | 7 +++++- internal/detectors/redirect.go | 31 +++++++++++------------- 7 files changed, 64 insertions(+), 53 deletions(-) diff --git a/internal/detectors/blocked.go b/internal/detectors/blocked.go index 9549b18..062c423 100644 --- a/internal/detectors/blocked.go +++ b/internal/detectors/blocked.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "RealityChecker/internal/data" "RealityChecker/internal/types" ) @@ -64,7 +65,11 @@ func (bs *BlockedStage) checkBlocked(domain string) (bool, string) { // loadGFWList 加载GFWList func (bs *BlockedStage) loadGFWList() { - file, err := os.Open("data/gfwlist.conf") + path, err := data.ResolvePath("gfwlist.conf") + if err != nil { + return + } + file, err := os.Open(path) if err != nil { return } diff --git a/internal/detectors/cdn.go b/internal/detectors/cdn.go index 5523d4b..2f8ef78 100644 --- a/internal/detectors/cdn.go +++ b/internal/detectors/cdn.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "RealityChecker/internal/data" + "RealityChecker/internal/security" "RealityChecker/internal/types" ) @@ -241,12 +243,16 @@ func (cs *CDNStage) checkCertIssuerHint(domain string) (string, string) { ) // 建立TLS连接获取证书 - conn, err := tls.DialWithDialer(&net.Dialer{ - Timeout: certTimeout, - }, "tcp", domain+certPort, &tls.Config{ + tcpConn, err := security.DialTimeoutPublic("tcp", net.JoinHostPort(domain, "443"), certTimeout) + if err != nil { + return "", "" + } + conn := tls.Client(tcpConn, &tls.Config{ ServerName: domain, }) - if err != nil { + _ = conn.SetDeadline(time.Now().Add(certTimeout)) + if err := conn.Handshake(); err != nil { + tcpConn.Close() return "", "" } defer conn.Close() @@ -315,7 +321,11 @@ func (cs *CDNStage) getProviderFromHeader(header string) string { // loadCDNKeywords 加载CDN关键词 func (cs *CDNStage) loadCDNKeywords() { - file, err := os.Open("data/cdn_keywords.txt") + path, err := data.ResolvePath("cdn_keywords.txt") + if err != nil { + return + } + file, err := os.Open(path) if err != nil { return } diff --git a/internal/detectors/comprehensive_tls.go b/internal/detectors/comprehensive_tls.go index 0831ecc..f0c3b33 100644 --- a/internal/detectors/comprehensive_tls.go +++ b/internal/detectors/comprehensive_tls.go @@ -4,10 +4,10 @@ import ( "context" "crypto/tls" "fmt" - "net" "strings" "time" + "RealityChecker/internal/security" "RealityChecker/internal/types" ) @@ -216,14 +216,17 @@ func (cts *ComprehensiveTLSStage) checkX25519Support(domain string, timeout time MaxVersion: tls.VersionTLS13, } - conn, err := tls.DialWithDialer(&net.Dialer{ - Timeout: timeout, - }, "tcp", domain+port, x25519Config) - + tcpConn, err := security.DialTimeoutPublic("tcp", domain+port, timeout) if err != nil { // X25519握手失败,说明不支持X25519 return false } + conn := tls.Client(tcpConn, x25519Config) + _ = conn.SetDeadline(time.Now().Add(timeout)) + if err := conn.Handshake(); err != nil { + tcpConn.Close() + return false + } defer conn.Close() // 检查连接状态 diff --git a/internal/detectors/hot_website.go b/internal/detectors/hot_website.go index ae37467..db3fdeb 100644 --- a/internal/detectors/hot_website.go +++ b/internal/detectors/hot_website.go @@ -5,6 +5,7 @@ import ( "os" "strings" + "RealityChecker/internal/data" "RealityChecker/internal/types" ) @@ -120,7 +121,11 @@ func (hws *HotWebsiteStage) isSubdomain(domain, baseDomain string) bool { // loadHotWebsites 加载热门网站列表 func (hws *HotWebsiteStage) loadHotWebsites() { - file, err := os.Open("data/hot_websites.txt") + path, err := data.ResolvePath("hot_websites.txt") + if err != nil { + return + } + file, err := os.Open(path) if err != nil { return } diff --git a/internal/detectors/ip_resolver.go b/internal/detectors/ip_resolver.go index ecbd36f..217523a 100644 --- a/internal/detectors/ip_resolver.go +++ b/internal/detectors/ip_resolver.go @@ -6,6 +6,7 @@ import ( "net" "time" + "RealityChecker/internal/security" "RealityChecker/internal/types" ) @@ -43,10 +44,10 @@ func (irs *IPResolverStage) Execute(ctx *types.PipelineContext) error { // quickConnectivityTest 快速连通性测试 func (irs *IPResolverStage) quickConnectivityTest(ip string) bool { // 测试HTTPS端口443的连通性 - conn, err := net.DialTimeout("tcp", ip+":443", 2*time.Second) + conn, err := security.DialTimeoutPublic("tcp", net.JoinHostPort(ip, "443"), 2*time.Second) if err != nil { // 如果HTTPS不可达,尝试HTTP端口80 - conn, err = net.DialTimeout("tcp", ip+":80", 2*time.Second) + conn, err = security.DialTimeoutPublic("tcp", net.JoinHostPort(ip, "80"), 2*time.Second) if err != nil { return false } @@ -57,24 +58,9 @@ func (irs *IPResolverStage) quickConnectivityTest(ip string) bool { // resolveIP 解析IP地址 func (irs *IPResolverStage) resolveIP(domain string) (string, error) { - // 检查是否已经是IP地址 - if net.ParseIP(domain) != nil { - return domain, nil - } - - // 使用自定义DNS解析器,设置更短的超时 - resolver := &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: 2 * time.Second, // DNS查询超时2秒 - } - return d.DialContext(ctx, network, address) - }, - } - - // 解析域名 - ips, err := resolver.LookupIPAddr(context.Background(), domain) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ips, err := security.LookupPublicIPs(ctx, domain) if err != nil { return "", err } @@ -85,13 +71,13 @@ func (irs *IPResolverStage) resolveIP(domain string) (string, error) { // 优先选择IPv4地址 for _, ipAddr := range ips { - if ipAddr.IP.To4() != nil { - return ipAddr.IP.String(), nil + if ipAddr.To4() != nil { + return ipAddr.String(), nil } } // 如果没有IPv4,使用IPv6 - return ips[0].IP.String(), nil + return ips[0].String(), nil } // CanEarlyExit 是否可以早期退出 diff --git a/internal/detectors/location.go b/internal/detectors/location.go index 6e56b1b..f8b54b6 100644 --- a/internal/detectors/location.go +++ b/internal/detectors/location.go @@ -4,6 +4,7 @@ import ( "fmt" "net" + "RealityChecker/internal/data" "RealityChecker/internal/types" "github.com/oschwald/geoip2-golang" @@ -94,7 +95,11 @@ func (ls *LocationStage) getLocation(ip string) (string, bool) { // loadGeoIPDatabase 加载GeoIP数据库 func (ls *LocationStage) loadGeoIPDatabase() { - db, err := geoip2.Open("data/Country.mmdb") + path, err := data.ResolvePath("Country.mmdb") + if err != nil { + return + } + db, err := geoip2.Open(path) if err != nil { return } diff --git a/internal/detectors/redirect.go b/internal/detectors/redirect.go index 784e79f..9757bc9 100644 --- a/internal/detectors/redirect.go +++ b/internal/detectors/redirect.go @@ -3,9 +3,9 @@ package detectors import ( "net/http" "net/url" - "strings" "time" + "RealityChecker/internal/security" "RealityChecker/internal/types" ) @@ -23,6 +23,10 @@ func (rs *RedirectStage) Execute(ctx *types.PipelineContext) error { // 创建HTTP客户端,禁用自动重定向 client := &http.Client{ Timeout: 3 * time.Second, // 减少HTTP客户端超时时间到3秒 + Transport: &http.Transport{ + Proxy: nil, + DialContext: security.DialContextPublic, + }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, @@ -131,27 +135,20 @@ func (rs *RedirectStage) followRedirects(client *http.Client, domain string) *Re if resp.StatusCode >= redirectMin && resp.StatusCode < redirectMax { location := resp.Header.Get("Location") if location != "" { - // 处理相对URL - const ( - rootPathPrefix = "/" - httpPrefix = "http" - ) - - if strings.HasPrefix(location, rootPathPrefix) { - parsedURL, _ := url.Parse(currentURL) - location = parsedURL.Scheme + "://" + parsedURL.Host + location - } else if !strings.HasPrefix(location, httpPrefix) { - location = httpsScheme + location - } - - parsedLocation, err := url.Parse(location) - if err == nil { + baseURL, baseErr := url.Parse(currentURL) + parsedLocation, locationErr := url.Parse(location) + if baseErr == nil && locationErr == nil { + parsedLocation = baseURL.ResolveReference(parsedLocation) + if parsedLocation.Scheme != "https" && parsedLocation.Scheme != "http" { + resp.Body.Close() + break + } newDomain := parsedLocation.Hostname() if newDomain != domain && newDomain != "" { result.RedirectChain = append(result.RedirectChain, newDomain) result.IsRedirected = true result.RedirectCount++ - currentURL = location + currentURL = parsedLocation.String() domain = newDomain resp.Body.Close() continue From 977d82c43bc94ed5454bbb3127836012bbe647d8 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:19:13 +0800 Subject: [PATCH 5/7] security: enforce safe network manager dials Routes managed connections through the validated target dialer. --- internal/network/manager.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/network/manager.go b/internal/network/manager.go index f09d5e6..4888d57 100644 --- a/internal/network/manager.go +++ b/internal/network/manager.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "RealityChecker/internal/security" "RealityChecker/internal/types" ) @@ -87,8 +88,9 @@ func (cm *ConnectionManager) Stop() error { // GetHTTPConnection 获取HTTP连接 func (cm *ConnectionManager) GetHTTPConnection(ctx context.Context, domain string) (net.Conn, error) { // 总是创建新的HTTP连接 - const httpPort = ":80" - conn, err := net.DialTimeout("tcp", domain+httpPort, cm.config.Network.Timeout) + dialCtx, cancel := context.WithTimeout(ctx, cm.config.Network.Timeout) + defer cancel() + conn, err := security.DialContextPublic(dialCtx, "tcp", net.JoinHostPort(domain, "80")) if err != nil { cm.mu.Lock() cm.stats.FailedConnections++ @@ -105,8 +107,9 @@ func (cm *ConnectionManager) GetHTTPConnection(ctx context.Context, domain strin // GetTLSConnection 获取TLS连接 func (cm *ConnectionManager) GetTLSConnection(ctx context.Context, domain string) (*tls.Conn, error) { // 总是创建新的TLS连接,确保ALPN协商正确 - const tlsPort = ":443" - tcpConn, err := net.DialTimeout("tcp", domain+tlsPort, cm.config.Network.Timeout) + dialCtx, cancel := context.WithTimeout(ctx, cm.config.Network.Timeout) + defer cancel() + tcpConn, err := security.DialContextPublic(dialCtx, "tcp", net.JoinHostPort(domain, "443")) if err != nil { cm.mu.Lock() cm.stats.FailedConnections++ @@ -139,8 +142,9 @@ func (cm *ConnectionManager) GetTLSConnection(ctx context.Context, domain string // GetX25519TLSConnection 获取强制X25519的TLS连接 func (cm *ConnectionManager) GetX25519TLSConnection(ctx context.Context, domain string) (*tls.Conn, error) { // 创建强制X25519的TLS连接 - const tlsPort = ":443" - tcpConn, err := net.DialTimeout("tcp", domain+tlsPort, cm.config.Network.Timeout) + dialCtx, cancel := context.WithTimeout(ctx, cm.config.Network.Timeout) + defer cancel() + tcpConn, err := security.DialContextPublic(dialCtx, "tcp", net.JoinHostPort(domain, "443")) if err != nil { cm.mu.Lock() cm.stats.FailedConnections++ From 61c1552c77bec69707c03aa144c104c36212caf9 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:19:23 +0800 Subject: [PATCH 6/7] security: validate public targets and ports Rejects private, loopback, link-local, documentation, benchmark, and unsafe-port targets; includes unit coverage. --- internal/security/target.go | 98 ++++++++++++++++++++++++++++++++ internal/security/target_test.go | 39 +++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 internal/security/target.go create mode 100644 internal/security/target_test.go diff --git a/internal/security/target.go b/internal/security/target.go new file mode 100644 index 0000000..270d87e --- /dev/null +++ b/internal/security/target.go @@ -0,0 +1,98 @@ +package security + +import ( + "context" + "fmt" + "net" + "time" +) + +var reservedNetworks = mustParseCIDRs( + "100.64.0.0/10", + "192.0.0.0/24", + "192.0.2.0/24", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "2001:db8::/32", +) + +func mustParseCIDRs(values ...string) []*net.IPNet { + result := make([]*net.IPNet, 0, len(values)) + for _, value := range values { + _, network, err := net.ParseCIDR(value) + if err != nil { + panic(err) + } + result = append(result, network) + } + return result +} + +func IsPublicIP(ip net.IP) bool { + if ip == nil || !ip.IsGlobalUnicast() || ip.IsPrivate() || ip.IsLoopback() || + ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() { + return false + } + for _, network := range reservedNetworks { + if network.Contains(ip) { + return false + } + } + return true +} + +func LookupPublicIPs(ctx context.Context, host string) ([]net.IP, error) { + if parsed := net.ParseIP(host); parsed != nil { + if !IsPublicIP(parsed) { + return nil, fmt.Errorf("拒绝访问非公网地址 %s", host) + } + return []net.IP{parsed}, nil + } + addresses, err := net.DefaultResolver.LookupIP(ctx, "ip", host) + if err != nil { + return nil, err + } + if len(addresses) == 0 { + return nil, fmt.Errorf("域名 %s 没有可用地址", host) + } + for _, address := range addresses { + if !IsPublicIP(address) { + return nil, fmt.Errorf("域名 %s 解析到非公网地址 %s,已拒绝", host, address) + } + } + return addresses, nil +} + +// DialContextPublic resolves once, validates every answer, and dials the +// validated IP directly. This closes the DNS-rebinding gap between checking +// and connecting. +func DialContextPublic(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + if port != "80" && port != "443" { + return nil, fmt.Errorf("拒绝访问端口 %s;仅允许 80/443", port) + } + addresses, err := LookupPublicIPs(ctx, host) + if err != nil { + return nil, err + } + dialer := &net.Dialer{} + var lastErr error + for _, ip := range addresses { + conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port)) + if dialErr == nil { + return conn, nil + } + lastErr = dialErr + } + return nil, lastErr +} + +func DialTimeoutPublic(network, address string, timeout time.Duration) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return DialContextPublic(ctx, network, address) +} diff --git a/internal/security/target_test.go b/internal/security/target_test.go new file mode 100644 index 0000000..901796a --- /dev/null +++ b/internal/security/target_test.go @@ -0,0 +1,39 @@ +package security + +import ( + "net" + "testing" +) + +func TestIsPublicIP(t *testing.T) { + tests := []struct { + address string + public bool + }{ + {"8.8.8.8", true}, + {"2606:4700:4700::1111", true}, + {"127.0.0.1", false}, + {"10.0.0.1", false}, + {"172.16.0.1", false}, + {"192.168.1.1", false}, + {"169.254.169.254", false}, + {"100.64.0.1", false}, + {"198.18.0.1", false}, + {"::1", false}, + {"fc00::1", false}, + {"fe80::1", false}, + } + for _, test := range tests { + t.Run(test.address, func(t *testing.T) { + if actual := IsPublicIP(net.ParseIP(test.address)); actual != test.public { + t.Fatalf("IsPublicIP(%s) = %v, want %v", test.address, actual, test.public) + } + }) + } +} + +func TestLookupPublicIPsRejectsPrivateLiteral(t *testing.T) { + if _, err := LookupPublicIPs(t.Context(), "127.0.0.1"); err == nil { + t.Fatal("expected loopback address to be rejected") + } +} From bc5af4c34ee1d934a6b417cb6d0301b177c64912 Mon Sep 17 00:00:00 2001 From: navieproxy <51807311+zbklk@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:19:30 +0800 Subject: [PATCH 7/7] privacy: remove advertisement and update check Removes unsolicited advertisement output and the GitHub update check. --- internal/ui/display.go | 57 ++---------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/internal/ui/display.go b/internal/ui/display.go index cbf1ade..67be8f3 100644 --- a/internal/ui/display.go +++ b/internal/ui/display.go @@ -1,10 +1,7 @@ package ui import ( - "encoding/json" "fmt" - "io" - "net/http" "time" "RealityChecker/internal/version" @@ -56,43 +53,7 @@ func PrintErrorWithDetails(message string, details ...string) { // getLatestVersion 获取GitHub最新版本号 func getLatestVersion() string { - // 设置超时时间 - client := &http.Client{ - Timeout: 5 * time.Second, - } - - // 请求GitHub API - resp, err := client.Get("https://api.github.com/repos/V2RaySSR/RealityChecker/releases/latest") - if err != nil { - return "" // 网络错误时返回空字符串 - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "" // HTTP错误时返回空字符串 - } - - // 读取响应 - body, err := io.ReadAll(resp.Body) - if err != nil { - return "" // 读取错误时返回空字符串 - } - - // 解析JSON - var release GitHubRelease - if err := json.Unmarshal(body, &release); err != nil { - return "" // 解析错误时返回空字符串 - } - - // 返回版本号,如果没有tag_name则使用name - if release.TagName != "" { - return release.TagName - } - if release.Name != "" { - return release.Name - } - - return "" // 没有版本信息时返回空字符串 + return "" } // getVersionInfo 获取版本信息字符串 @@ -132,19 +93,5 @@ func getDisplayWidth(s string) int { // PrintAdvertisement 打印广告信息 func PrintAdvertisement() { - // 使用颜色代码 - blue := "\033[36m" // 青色 - yellow := "\033[33m" // 黄色 - white := "\033[37m" // 白色 - reset := "\033[0m" // 重置颜色 - - fmt.Println() - fmt.Printf("%s-----------------------------------------------------%s\n", white, reset) - fmt.Println() - fmt.Printf("%s %s五年老机场%s %s%shttps://goii.cc/mn%s %s(牧牛云)%s\n", - white, - yellow, reset, - blue, white, reset, - yellow, reset) - fmt.Println() + // Hardened build intentionally contains no advertising or tracking links. }